99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import signale from 'signale';
|
|
|
|
import {STRIPE_ENABLED, STRIPE_METER_EVENT_NAME} from '../app/constants.js';
|
|
import {stripe} from '../app/stripe.js';
|
|
|
|
/**
|
|
* Meter Service
|
|
* Handles recording usage events to Stripe billing meters for pay-as-you-go pricing
|
|
*/
|
|
export class MeterService {
|
|
/**
|
|
* Record an email sent event to Stripe meter
|
|
* This is used for usage-based billing where customers pay per email sent
|
|
*
|
|
* @param customerId - Stripe customer ID
|
|
* @param value - Number of emails sent (default: 1)
|
|
* @param idempotencyKey - Optional unique identifier to prevent duplicate recording
|
|
*/
|
|
public static async recordEmailSent(customerId: string, value = 1, idempotencyKey?: string): Promise<void> {
|
|
// Skip if billing is disabled (self-hosted mode)
|
|
if (!STRIPE_ENABLED || !stripe) {
|
|
return;
|
|
}
|
|
|
|
// Skip if no customer ID (not subscribed)
|
|
if (!customerId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await stripe.billing.meterEvents.create({
|
|
event_name: STRIPE_METER_EVENT_NAME,
|
|
payload: {
|
|
stripe_customer_id: customerId,
|
|
value: value.toString(), // Must be a string
|
|
},
|
|
...(idempotencyKey && {identifier: idempotencyKey}),
|
|
});
|
|
|
|
signale.debug(`[METER] Recorded ${value} email(s) sent for customer ${customerId}`);
|
|
} catch (error) {
|
|
// Log error but don't throw - we don't want billing issues to break email sending
|
|
signale.error('[METER] Failed to record email sent event:', error);
|
|
|
|
// If it's a rate limit error, we might want to retry
|
|
if (error instanceof Error && error.message.includes('429')) {
|
|
signale.warn('[METER] Rate limited - consider implementing queue-based meter recording');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record multiple emails in a single batch (for campaign sending)
|
|
* More efficient than individual calls when sending bulk emails
|
|
*
|
|
* @param customerId - Stripe customer ID
|
|
* @param count - Number of emails sent in this batch
|
|
* @param batchId - Unique identifier for this batch
|
|
*/
|
|
public static async recordEmailBatch(customerId: string, count: number, batchId: string): Promise<void> {
|
|
if (count <= 0) return;
|
|
|
|
await this.recordEmailSent(customerId, count, `batch_${batchId}`);
|
|
}
|
|
|
|
/**
|
|
* Get current usage for a customer in the current billing period
|
|
* Useful for displaying usage dashboards or implementing soft limits
|
|
*
|
|
* @param customerId - Stripe customer ID
|
|
* @param meterId - The meter ID from Stripe dashboard
|
|
* @param startTime - Start of period (Unix timestamp)
|
|
* @param endTime - End of period (Unix timestamp)
|
|
*/
|
|
public static async getUsageSummary(
|
|
meterId: string,
|
|
customerId: string,
|
|
startTime: number,
|
|
endTime: number,
|
|
): Promise<unknown> {
|
|
if (!STRIPE_ENABLED || !stripe) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const summaries = await stripe.billing.meters.listEventSummaries(meterId, {
|
|
customer: customerId,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
});
|
|
|
|
return summaries;
|
|
} catch (error) {
|
|
signale.error('[METER] Failed to get usage summary:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|