- ScanScheduler: tier-based scheduling (BASIC=24h, PLUS=6h, PREMIUM=1h) - WebhookHandler: HMAC-verified webhook ingestion with SCAN_TRIGGER support - API routes: /scheduler and /webhooks endpoints under /api/v1/darkwatch - Jobs: scheduled scan checker + webhook retry processor via BullMQ - Schema: ScanSchedule, WebhookEvent models; ScanJob.scheduledBy field - Types: ScheduleStatus, WebhookEventType, WebhookTriggerInput - Tests: scheduler lifecycle + webhook signature/processing tests Co-Authored-By: Paperclip <noreply@paperclip.ing>
153 lines
4.0 KiB
TypeScript
153 lines
4.0 KiB
TypeScript
import Stripe from 'stripe';
|
|
import { loadBillingConfig, SubscriptionTier } from '../config/billing.config';
|
|
import type { Subscription, SubscriptionCreateSchema, SubscriptionUpdateSchema } from '../models/subscription.model';
|
|
|
|
const config = loadBillingConfig();
|
|
const stripe = new Stripe(config.stripe.apiKey, { apiVersion: '2024-04-10' });
|
|
|
|
export class BillingService {
|
|
private static instance: BillingService;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): BillingService {
|
|
if (!BillingService.instance) {
|
|
BillingService.instance = new BillingService();
|
|
}
|
|
return BillingService.instance;
|
|
}
|
|
|
|
async createCustomer(email: string, userId: string): Promise<Stripe.Customer> {
|
|
const customer = await stripe.customers.create({
|
|
email,
|
|
metadata: { userId },
|
|
});
|
|
return customer;
|
|
}
|
|
|
|
async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
|
|
try {
|
|
const customer = await stripe.customers.retrieve(customerId);
|
|
return customer as Stripe.Customer;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async createSubscription(
|
|
userId: string,
|
|
tier: SubscriptionTier,
|
|
customerId: string
|
|
): Promise<{ subscription: Stripe.Subscription; customer: Stripe.Customer }> {
|
|
const tierConfig = config.tiers[tier];
|
|
|
|
const subscription = await stripe.subscriptions.create({
|
|
customer: customerId,
|
|
items: [{ price: tierConfig.priceId }],
|
|
metadata: { userId, tier },
|
|
});
|
|
|
|
const customer = await this.getCustomer(customerId);
|
|
|
|
return { subscription, customer: customer! };
|
|
}
|
|
|
|
async cancelSubscription(
|
|
subscriptionId: string,
|
|
cancelAtPeriodEnd: boolean = false
|
|
): Promise<Stripe.Subscription> {
|
|
if (cancelAtPeriodEnd) {
|
|
return await stripe.subscriptions.update(subscriptionId, {
|
|
cancel_at_period_end: true,
|
|
});
|
|
}
|
|
return await stripe.subscriptions.cancel(subscriptionId);
|
|
}
|
|
|
|
async updateSubscription(
|
|
subscriptionId: string,
|
|
newTier: SubscriptionTier
|
|
): Promise<Stripe.Subscription> {
|
|
const newTierConfig = config.tiers[newTier];
|
|
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
|
|
const updated = await stripe.subscriptions.update(subscriptionId, {
|
|
proration_behavior: 'create_prorations',
|
|
items: [
|
|
{
|
|
id: subscription.items.data[0]?.id,
|
|
price: newTierConfig.priceId,
|
|
},
|
|
],
|
|
});
|
|
|
|
return updated;
|
|
}
|
|
|
|
async createCustomerPortalSession(
|
|
customerId: string,
|
|
returnUrl: string
|
|
): Promise<Stripe.BillingPortal.Session> {
|
|
return await stripe.billingPortal.sessions.create({
|
|
customer: customerId,
|
|
return_url: returnUrl,
|
|
});
|
|
}
|
|
|
|
async getSubscription(subscriptionId: string): Promise<Stripe.Subscription | null> {
|
|
try {
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
return subscription;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getTierLimits(tier: SubscriptionTier) {
|
|
return config.tiers[tier];
|
|
}
|
|
|
|
async checkUsageAgainstLimit(
|
|
userId: string,
|
|
tier: SubscriptionTier,
|
|
currentUsage: number
|
|
): Promise<{ withinLimit: boolean; remaining: number; limit: number }> {
|
|
const tierConfig = config.tiers[tier];
|
|
const limit = tierConfig.callMinutesLimit;
|
|
const remaining = Math.max(0, limit - currentUsage);
|
|
|
|
return {
|
|
withinLimit: currentUsage <= limit,
|
|
remaining,
|
|
limit,
|
|
};
|
|
}
|
|
|
|
async createInvoice(
|
|
customerId: string,
|
|
amount: number,
|
|
description: string,
|
|
metadata?: Record<string, string>
|
|
): Promise<Stripe.Invoice> {
|
|
return await stripe.invoices.create({
|
|
customer: customerId,
|
|
metadata: { ...metadata, description },
|
|
});
|
|
}
|
|
|
|
async handleWebhook(
|
|
sig: string,
|
|
body: Buffer
|
|
): Promise<Stripe.Event> {
|
|
return stripe.webhooks.constructEvent(body, sig, config.stripe.webhookSecret);
|
|
}
|
|
|
|
async getInvoiceHistory(customerId: string): Promise<Stripe.ApiList<Stripe.Invoice>> {
|
|
return await stripe.invoices.list({
|
|
customer: customerId,
|
|
limit: 100,
|
|
});
|
|
}
|
|
}
|