Add tier-based scan scheduler and webhook triggers (FRE-4498)
- 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>
This commit is contained in:
152
packages/shared-billing/src/services/billing.service.ts
Normal file
152
packages/shared-billing/src/services/billing.service.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user