Transferred ShieldAI-related files mistakenly placed in ~/code/FrenoCorp:
- Services: spamshield (feature-flags, audit-logger, error-handler), voiceprint (config, service, feature-flags), darkwatch (pipeline, scan, scheduler, watchlist, webhook)
- Packages: shared-analytics, shared-auth, shared-ui, shared-utils (new); shared-billing, jobs supplemented with unique FC files
- Server: alerts (FC version newer), routes (spamshield, darkwatch, voiceprint)
- Config: turbo.json, tsconfig.base.json, vite/vitest configs, drizzle, Dockerfile
- VoicePrint ML service
- Examples
Pending: apps/{api,web,mobile}/ structured merge, shared-db/db mapping
Co-Authored-By: Paperclip <noreply@paperclip.ing>
224 lines
5.5 KiB
TypeScript
224 lines
5.5 KiB
TypeScript
import { stripe, SubscriptionTier, tierConfig } from '../config/billing.config';
|
|
import { z } from 'zod';
|
|
|
|
// Subscription service
|
|
export class SubscriptionService {
|
|
/**
|
|
* Create a new subscription for a customer
|
|
*/
|
|
async createSubscription(
|
|
customerId: string,
|
|
tier: SubscriptionTier,
|
|
metadata?: Record<string, string>
|
|
): Promise<Stripe.Subscription> {
|
|
const priceId = tierConfig[tier].priceId;
|
|
|
|
const subscription = await stripe.subscriptions.create({
|
|
customer: customerId,
|
|
items: [{ price: priceId }],
|
|
metadata: metadata,
|
|
proration_behavior: 'create_prorations',
|
|
});
|
|
|
|
return subscription;
|
|
}
|
|
|
|
/**
|
|
* Update a customer's subscription tier
|
|
*/
|
|
async updateSubscriptionTier(
|
|
subscriptionId: string,
|
|
newTier: SubscriptionTier
|
|
): Promise<Stripe.Subscription> {
|
|
const newPriceId = tierConfig[newTier].priceId;
|
|
|
|
const subscription = await stripe.subscriptions.update(subscriptionId, {
|
|
items: [
|
|
{
|
|
price: newPriceId,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
proration_behavior: 'create_prorations',
|
|
});
|
|
|
|
return subscription;
|
|
}
|
|
|
|
/**
|
|
* Cancel a subscription
|
|
*/
|
|
async cancelSubscription(
|
|
subscriptionId: string,
|
|
atPeriodEnd: boolean = true
|
|
): Promise<Stripe.Subscription> {
|
|
const subscription = await stripe.subscriptions.update(subscriptionId, {
|
|
cancel_at_period_end: atPeriodEnd,
|
|
});
|
|
|
|
return subscription;
|
|
}
|
|
|
|
/**
|
|
* Get subscription by ID
|
|
*/
|
|
async getSubscription(subscriptionId: string): Promise<Stripe.Subscription | null> {
|
|
try {
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
return subscription;
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeInvalidRequestError) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get customer's current subscription
|
|
*/
|
|
async getCustomerSubscription(customerId: string): Promise<Stripe.Subscription | null> {
|
|
const subscriptions = await stripe.subscriptions.list({
|
|
customer: customerId,
|
|
status: 'active',
|
|
limit: 1,
|
|
});
|
|
|
|
return subscriptions.data[0] || null;
|
|
}
|
|
}
|
|
|
|
// Customer service
|
|
export class CustomerService {
|
|
/**
|
|
* Create a new Stripe customer
|
|
*/
|
|
async createCustomer(
|
|
email: string,
|
|
name?: string,
|
|
metadata?: Record<string, string>
|
|
): Promise<Stripe.Customer> {
|
|
const customer = await stripe.customers.create({
|
|
email,
|
|
name,
|
|
metadata,
|
|
});
|
|
|
|
return customer;
|
|
}
|
|
|
|
/**
|
|
* Get or create customer by email
|
|
*/
|
|
async getOrCreateCustomer(
|
|
email: string,
|
|
name?: string
|
|
): Promise<Stripe.Customer> {
|
|
const existingCustomers = await stripe.customers.list({
|
|
email,
|
|
limit: 1,
|
|
});
|
|
|
|
if (existingCustomers.data.length > 0) {
|
|
return existingCustomers.data[0];
|
|
}
|
|
|
|
return this.createCustomer(email, name);
|
|
}
|
|
|
|
/**
|
|
* Create a billing portal session
|
|
*/
|
|
async createBillingPortalSession(
|
|
customerId: string,
|
|
returnUrl: string
|
|
): Promise<Stripe.BillingPortal.Session> {
|
|
const session = await stripe.billingPortal.sessions.create({
|
|
customer: customerId,
|
|
return_url: returnUrl,
|
|
});
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Get customer by ID
|
|
*/
|
|
async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
|
|
try {
|
|
const customer = await stripe.customers.retrieve(customerId);
|
|
return customer as Stripe.Customer;
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeInvalidRequestError) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Webhook service
|
|
export class WebhookService {
|
|
/**
|
|
* Construct webhook event from raw body
|
|
*/
|
|
constructEvent(
|
|
rawBody: Buffer | string,
|
|
signature: string
|
|
): Stripe.Event {
|
|
return stripe.webhooks.constructEvent(
|
|
rawBody,
|
|
signature,
|
|
process.env.STRIPE_WEBHOOK_SECRET!
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle webhook event
|
|
*/
|
|
async handleWebhook(event: Stripe.Event): Promise<void> {
|
|
switch (event.type) {
|
|
case 'customer.subscription.created':
|
|
case 'customer.subscription.updated':
|
|
await this.handleSubscriptionChange(event.data.object);
|
|
break;
|
|
case 'customer.subscription.deleted':
|
|
await this.handleSubscriptionDeleted(event.data.object);
|
|
break;
|
|
case 'invoice.payment_succeeded':
|
|
await this.handlePaymentSucceeded(event.data.object);
|
|
break;
|
|
case 'invoice.payment_failed':
|
|
await this.handlePaymentFailed(event.data.object);
|
|
break;
|
|
default:
|
|
console.log(`Unhandled event type: ${event.type}`);
|
|
}
|
|
}
|
|
|
|
private async handleSubscriptionChange(subscription: Stripe.Subscription) {
|
|
console.log(`Subscription ${subscription.id} changed to ${subscription.status}`);
|
|
// TODO: Update local database
|
|
}
|
|
|
|
private async handleSubscriptionDeleted(subscription: Stripe.Subscription) {
|
|
console.log(`Subscription ${subscription.id} deleted`);
|
|
// TODO: Update local database
|
|
}
|
|
|
|
private async handlePaymentSucceeded(invoice: Stripe.Invoice) {
|
|
console.log(`Payment succeeded for invoice ${invoice.id}`);
|
|
// TODO: Update usage tracking
|
|
}
|
|
|
|
private async handlePaymentFailed(invoice: Stripe.Invoice) {
|
|
console.log(`Payment failed for invoice ${invoice.id}`);
|
|
// TODO: Send notification to customer
|
|
}
|
|
}
|
|
|
|
// Export instances
|
|
export const subscriptionService = new SubscriptionService();
|
|
export const customerService = new CustomerService();
|
|
export const webhookService = new WebhookService();
|