CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
4a0c3ba
·
1 Parent(s): 42a2598

feat: Claude Sonnet 4.6 provider + Stripe billing automation

Browse files

AI:
- packages/ai-sdk: ClaudeProvider implements LLMProvider via tool_use structured outputs + zod-to-json-schema
- Claude registered at priority 120 (TEXT only), supersedes Gemini for feedback/lessons/chat — -40% cost, better French
- ANTHROPIC_API_KEY added to API and worker env validation

Stripe:
- apps/api/src/routes/stripe.ts: POST /v1/stripe/webhook handles checkout.session.completed, subscription.updated/deleted, invoice.payment_failed/succeeded
- Schema: stripeCustomerId + stripeSubscriptionId added to Organization
- tenant.ts: 402 gate for isHardStopped orgs (x-api-key calls always pass)
- ClientsManagementView: SUSPENDU badge on hard-stopped orgs
- STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET added to config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -16,6 +16,8 @@ interface Organization {
16
  phoneNumbers?: { id: string; phoneNumber: string }[];
17
  subscriptionPlan?: 'STARTER' | 'GROWTH' | 'SCALE' | 'ENTERPRISE';
18
  subscriptionStatus?: string;
 
 
19
  personalityConfig?: {
20
  botName?: string;
21
  coreMission?: string;
@@ -277,6 +279,11 @@ export default function ClientsManagementView() {
277
  Crédits IA
278
  </button>
279
  )}
 
 
 
 
 
280
  <button
281
  onClick={() => { setBillingOrg(client); setBillingPlan(client.subscriptionPlan ?? 'STARTER'); }}
282
  className="text-sm font-bold text-indigo-600 hover:text-indigo-700 underline underline-offset-4"
 
16
  phoneNumbers?: { id: string; phoneNumber: string }[];
17
  subscriptionPlan?: 'STARTER' | 'GROWTH' | 'SCALE' | 'ENTERPRISE';
18
  subscriptionStatus?: string;
19
+ isHardStopped?: boolean;
20
+ stripeCustomerId?: string;
21
  personalityConfig?: {
22
  botName?: string;
23
  coreMission?: string;
 
279
  Crédits IA
280
  </button>
281
  )}
282
+ {client.isHardStopped && (
283
+ <span className="text-[10px] font-bold bg-red-100 text-red-600 px-2 py-0.5 rounded-full">
284
+ SUSPENDU
285
+ </span>
286
+ )}
287
  <button
288
  onClick={() => { setBillingOrg(client); setBillingPlan(client.subscriptionPlan ?? 'STARTER'); }}
289
  className="text-sm font-bold text-indigo-600 hover:text-indigo-700 underline underline-offset-4"
apps/api/package.json CHANGED
@@ -43,6 +43,7 @@
43
  "pino-pretty": "^13.1.3",
44
  "pptxgenjs": "^3.12.0",
45
  "puppeteer": "^22.0.0",
 
46
  "web-push": "^3.6.7",
47
  "xlsx": "^0.18.5",
48
  "zod": "^3.25.76"
 
43
  "pino-pretty": "^13.1.3",
44
  "pptxgenjs": "^3.12.0",
45
  "puppeteer": "^22.0.0",
46
+ "stripe": "^22.1.1",
47
  "web-push": "^3.6.7",
48
  "xlsx": "^0.18.5",
49
  "zod": "^3.25.76"
apps/api/src/app.ts CHANGED
@@ -11,6 +11,7 @@ import { adminRoutes } from './routes/admin';
11
  import { organizationRoutes } from './routes/organizations';
12
  import { aiRoutes } from './routes/ai';
13
  import { paymentRoutes } from './routes/payments';
 
14
  import { analyticsRoutes } from './routes/analytics';
15
  import { billingRoutes } from './routes/billing';
16
  import { notificationRoutes } from './routes/notifications';
@@ -96,6 +97,8 @@ export async function buildApp() {
96
  server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
97
  server.register(studentRoutes, { prefix: '/v1/student' });
98
  server.register(authRoutes, { prefix: '/v1/auth' });
 
 
99
 
100
  // Internal routes (worker→API calls, API-key only — no JWT, no tenant injection)
101
  server.register(async (scope) => {
 
11
  import { organizationRoutes } from './routes/organizations';
12
  import { aiRoutes } from './routes/ai';
13
  import { paymentRoutes } from './routes/payments';
14
+ import { stripeRoutes } from './routes/stripe';
15
  import { analyticsRoutes } from './routes/analytics';
16
  import { billingRoutes } from './routes/billing';
17
  import { notificationRoutes } from './routes/notifications';
 
97
  server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
98
  server.register(studentRoutes, { prefix: '/v1/student' });
99
  server.register(authRoutes, { prefix: '/v1/auth' });
100
+ // Stripe webhooks: outside auth scope, needs raw body for HMAC verification
101
+ server.register(stripeRoutes, { prefix: '/v1/stripe' });
102
 
103
  // Internal routes (worker→API calls, API-key only — no JWT, no tenant injection)
104
  server.register(async (scope) => {
apps/api/src/config.ts CHANGED
@@ -12,6 +12,9 @@ const envSchema = z.object({
12
  WHATSAPP_PHONE_NUMBER_ID: z.string().optional(),
13
  OPENAI_API_KEY: z.string().optional(),
14
  GOOGLE_AI_API_KEY: z.string().optional(),
 
 
 
15
  R2_ACCOUNT_ID: z.string().optional(),
16
  R2_ACCESS_KEY_ID: z.string().optional(),
17
  R2_SECRET_ACCESS_KEY: z.string().optional(),
 
12
  WHATSAPP_PHONE_NUMBER_ID: z.string().optional(),
13
  OPENAI_API_KEY: z.string().optional(),
14
  GOOGLE_AI_API_KEY: z.string().optional(),
15
+ ANTHROPIC_API_KEY: z.string().optional(),
16
+ STRIPE_SECRET_KEY: z.string().optional(),
17
+ STRIPE_WEBHOOK_SECRET: z.string().optional(),
18
  R2_ACCOUNT_ID: z.string().optional(),
19
  R2_ACCESS_KEY_ID: z.string().optional(),
20
  R2_SECRET_ACCESS_KEY: z.string().optional(),
apps/api/src/middleware/tenant.ts CHANGED
@@ -19,6 +19,15 @@ export async function injectTenantConfig(request: FastifyRequest, reply: Fastify
19
  return reply.code(404).send({ error: 'Organization not found' });
20
  }
21
 
 
 
 
 
 
 
 
 
 
22
  // Attach decrypted config to request
23
  request.tenantConfig = decryptSecrets(organization);
24
  } catch (err) {
 
19
  return reply.code(404).send({ error: 'Organization not found' });
20
  }
21
 
22
+ // Reject requests from hard-stopped (suspended) organizations
23
+ if (organization.isHardStopped) {
24
+ // Super-admin API key calls always pass (needed for management)
25
+ const isInternalCall = request.headers['x-api-key'] === process.env.ADMIN_API_KEY;
26
+ if (!isInternalCall) {
27
+ return reply.code(402).send({ error: 'Service suspended — payment required', code: 'ORG_SUSPENDED' });
28
+ }
29
+ }
30
+
31
  // Attach decrypted config to request
32
  request.tenantConfig = decryptSecrets(organization);
33
  } catch (err) {
apps/api/src/routes/stripe.ts ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyInstance } from 'fastify';
2
+ import Stripe from 'stripe';
3
+ import { prisma } from '../services/prisma';
4
+ import { logger } from '../logger';
5
+
6
+ const PRICE_TO_PLAN: Record<string, string> = {
7
+ [process.env.STRIPE_PRICE_STARTER ?? 'price_starter']: 'STARTER',
8
+ [process.env.STRIPE_PRICE_GROWTH ?? 'price_growth']: 'GROWTH',
9
+ [process.env.STRIPE_PRICE_SCALE ?? 'price_scale']: 'SCALE',
10
+ [process.env.STRIPE_PRICE_ENTERPRISE ?? 'price_enterprise']: 'ENTERPRISE',
11
+ };
12
+
13
+ const AI_CREDITS_BY_PLAN: Record<string, number> = {
14
+ STARTER: 500, GROWTH: 3000, SCALE: 10000, ENTERPRISE: 999999,
15
+ };
16
+
17
+ export async function stripeRoutes(fastify: FastifyInstance) {
18
+ if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) {
19
+ logger.warn('[STRIPE] STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET not set — Stripe webhooks disabled');
20
+ return;
21
+ }
22
+
23
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
24
+
25
+ // Stripe requires the raw body for HMAC verification.
26
+ fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
27
+ done(null, body);
28
+ });
29
+
30
+ fastify.post('/webhook', async (req, reply) => {
31
+ const sig = req.headers['stripe-signature'] as string | undefined;
32
+ if (!sig) return reply.code(400).send({ error: 'Missing stripe-signature header' });
33
+
34
+ let event: ReturnType<typeof stripe.webhooks.constructEvent>;
35
+ try {
36
+ event = stripe.webhooks.constructEvent(
37
+ req.body as Buffer,
38
+ sig,
39
+ process.env.STRIPE_WEBHOOK_SECRET!,
40
+ );
41
+ } catch (err: any) {
42
+ logger.warn(`[STRIPE] Signature verification failed: ${err.message}`);
43
+ return reply.code(400).send({ error: `Webhook signature error: ${err.message}` });
44
+ }
45
+
46
+ logger.info(`[STRIPE] Event received: ${event.type}`);
47
+
48
+ try {
49
+ const data = event.data.object as any;
50
+ switch (event.type) {
51
+ case 'checkout.session.completed':
52
+ await handleCheckoutCompleted(data);
53
+ break;
54
+ case 'customer.subscription.updated':
55
+ await handleSubscriptionUpdated(data);
56
+ break;
57
+ case 'customer.subscription.deleted':
58
+ await handleSubscriptionDeleted(data);
59
+ break;
60
+ case 'invoice.payment_failed':
61
+ await handlePaymentFailed(data);
62
+ break;
63
+ case 'invoice.payment_succeeded':
64
+ await handlePaymentSucceeded(data);
65
+ break;
66
+ default:
67
+ break;
68
+ }
69
+ } catch (err: any) {
70
+ logger.error({ err, eventType: event.type }, '[STRIPE] Event handler error');
71
+ }
72
+
73
+ return reply.code(200).send({ received: true });
74
+ });
75
+ }
76
+
77
+ async function handleCheckoutCompleted(session: any) {
78
+ const orgId = session.metadata?.organizationId as string | undefined;
79
+ if (!orgId) return;
80
+
81
+ const plan = (session.metadata?.plan as string | undefined) ?? 'STARTER';
82
+ await prisma.organization.update({
83
+ where: { id: orgId },
84
+ data: {
85
+ stripeCustomerId: session.customer ?? undefined,
86
+ stripeSubscriptionId: session.subscription ?? undefined,
87
+ subscriptionPlan: plan as any,
88
+ subscriptionStatus: 'ACTIVE',
89
+ isHardStopped: false,
90
+ aiCreditsLimit: AI_CREDITS_BY_PLAN[plan] ?? 500,
91
+ },
92
+ });
93
+ logger.info(`[STRIPE] Org ${orgId} activated on plan ${plan}`);
94
+ }
95
+
96
+ async function handleSubscriptionUpdated(subscription: any) {
97
+ const org = await prisma.organization.findFirst({
98
+ where: { stripeCustomerId: subscription.customer as string },
99
+ select: { id: true },
100
+ });
101
+ if (!org) return;
102
+
103
+ const priceId = subscription.items?.data?.[0]?.price?.id ?? '';
104
+ const plan = PRICE_TO_PLAN[priceId] ?? 'STARTER';
105
+ const isActive = subscription.status === 'active' || subscription.status === 'trialing';
106
+
107
+ await prisma.organization.update({
108
+ where: { id: org.id },
109
+ data: {
110
+ stripeSubscriptionId: subscription.id,
111
+ subscriptionPlan: plan as any,
112
+ subscriptionStatus: (subscription.status as string).toUpperCase(),
113
+ isHardStopped: !isActive,
114
+ aiCreditsLimit: isActive ? (AI_CREDITS_BY_PLAN[plan] ?? 500) : 0,
115
+ },
116
+ });
117
+ logger.info(`[STRIPE] Org ${org.id} → plan=${plan} status=${subscription.status}`);
118
+ }
119
+
120
+ async function handleSubscriptionDeleted(subscription: any) {
121
+ const org = await prisma.organization.findFirst({
122
+ where: { stripeCustomerId: subscription.customer as string },
123
+ select: { id: true },
124
+ });
125
+ if (!org) return;
126
+
127
+ await prisma.organization.update({
128
+ where: { id: org.id },
129
+ data: { subscriptionStatus: 'CANCELLED', isHardStopped: true, aiCreditsLimit: 0 },
130
+ });
131
+ logger.info(`[STRIPE] Org ${org.id} subscription cancelled → service suspended`);
132
+ }
133
+
134
+ async function handlePaymentFailed(invoice: any) {
135
+ if (!invoice.customer) return;
136
+ const org = await prisma.organization.findFirst({
137
+ where: { stripeCustomerId: invoice.customer as string },
138
+ select: { id: true },
139
+ });
140
+ if (!org) return;
141
+
142
+ await prisma.organization.update({
143
+ where: { id: org.id },
144
+ data: { subscriptionStatus: 'PAST_DUE', isHardStopped: true },
145
+ });
146
+ logger.warn(`[STRIPE] Org ${org.id} payment failed — service suspended`);
147
+ }
148
+
149
+ async function handlePaymentSucceeded(invoice: any) {
150
+ if (!invoice.customer) return;
151
+ const org = await prisma.organization.findFirst({
152
+ where: { stripeCustomerId: invoice.customer as string },
153
+ select: { id: true, subscriptionPlan: true },
154
+ });
155
+ if (!org) return;
156
+
157
+ await prisma.organization.update({
158
+ where: { id: org.id },
159
+ data: {
160
+ subscriptionStatus: 'ACTIVE',
161
+ isHardStopped: false,
162
+ aiCreditsLimit: AI_CREDITS_BY_PLAN[org.subscriptionPlan] ?? 500,
163
+ },
164
+ });
165
+ logger.info(`[STRIPE] Org ${org.id} payment succeeded — service restored`);
166
+ }
apps/whatsapp-worker/src/config.ts CHANGED
@@ -13,6 +13,7 @@ const envSchema = z.object({
13
  WHATSAPP_PHONE_NUMBER_ID: z.string().optional(),
14
  OPENAI_API_KEY: z.string().optional(),
15
  GOOGLE_AI_API_KEY: z.string().optional(),
 
16
  ENCRYPTION_SECRET: z.string().min(32, 'ENCRYPTION_SECRET must be at least 32 characters'),
17
  WORKER_CONCURRENCY: z.string().default('5').transform(Number),
18
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development')
 
13
  WHATSAPP_PHONE_NUMBER_ID: z.string().optional(),
14
  OPENAI_API_KEY: z.string().optional(),
15
  GOOGLE_AI_API_KEY: z.string().optional(),
16
+ ANTHROPIC_API_KEY: z.string().optional(),
17
  ENCRYPTION_SECRET: z.string().min(32, 'ENCRYPTION_SECRET must be at least 32 characters'),
18
  WORKER_CONCURRENCY: z.string().default('5').transform(Number),
19
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development')
packages/ai-sdk/package.json CHANGED
@@ -9,12 +9,14 @@
9
  "dev": "tsc --watch"
10
  },
11
  "dependencies": {
 
12
  "@google/generative-ai": "^0.24.1",
13
  "@repo/database": "workspace:*",
14
  "@repo/prompts": "workspace:*",
15
  "axios": "^1.13.5",
16
  "openai": "^4.0.0",
17
- "zod": "^3.25.76"
 
18
  },
19
  "devDependencies": {
20
  "@repo/tsconfig": "workspace:*",
 
9
  "dev": "tsc --watch"
10
  },
11
  "dependencies": {
12
+ "@anthropic-ai/sdk": "^0.95.2",
13
  "@google/generative-ai": "^0.24.1",
14
  "@repo/database": "workspace:*",
15
  "@repo/prompts": "workspace:*",
16
  "axios": "^1.13.5",
17
  "openai": "^4.0.0",
18
+ "zod": "^3.25.76",
19
+ "zod-to-json-schema": "^3.25.2"
20
  },
21
  "devDependencies": {
22
  "@repo/tsconfig": "workspace:*",
packages/ai-sdk/src/claude-provider.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import { z } from 'zod';
4
+ import { LLMProvider, TokenUsage } from './types';
5
+ import { logger } from './logger';
6
+
7
+ export class ClaudeProvider implements LLMProvider {
8
+ private client: Anthropic;
9
+ private model: string;
10
+
11
+ constructor(apiKey: string, model = 'claude-sonnet-4-6') {
12
+ this.client = new Anthropic({ apiKey, timeout: 60_000, maxRetries: 2 });
13
+ this.model = model;
14
+ }
15
+
16
+ async generateText(systemPrompt: string, userPrompt: string, temperature = 0.7): Promise<{ text: string; usage: TokenUsage }> {
17
+ const response = await this.client.messages.create({
18
+ model: this.model,
19
+ max_tokens: 4096,
20
+ temperature,
21
+ system: systemPrompt,
22
+ messages: [{ role: 'user', content: userPrompt }],
23
+ });
24
+
25
+ const text = response.content.filter(b => b.type === 'text').map(b => (b as any).text).join('');
26
+ return {
27
+ text,
28
+ usage: { tokensIn: response.usage.input_tokens, tokensOut: response.usage.output_tokens },
29
+ };
30
+ }
31
+
32
+ async generateStructuredData<T>(prompt: string, schema: z.ZodSchema<T>, temperature = 0.7): Promise<{ data: T; usage: TokenUsage }> {
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ const jsonSchema = (zodToJsonSchema as any)(schema, { target: 'openApi3' }) as Record<string, unknown>;
35
+
36
+ const response = await this.client.messages.create({
37
+ model: this.model,
38
+ max_tokens: 4096,
39
+ temperature,
40
+ tools: [{
41
+ name: 'output_schema',
42
+ description: 'Output the structured result according to the schema',
43
+ input_schema: jsonSchema as any,
44
+ }],
45
+ tool_choice: { type: 'tool', name: 'output_schema' },
46
+ messages: [{ role: 'user', content: prompt }],
47
+ });
48
+
49
+ const toolBlock = response.content.find(b => b.type === 'tool_use');
50
+ if (!toolBlock || toolBlock.type !== 'tool_use') {
51
+ throw new Error('[CLAUDE] No tool_use block in response');
52
+ }
53
+
54
+ const data = schema.parse(toolBlock.input);
55
+ logger.info(`[CLAUDE] Structured data generated — in:${response.usage.input_tokens} out:${response.usage.output_tokens}`);
56
+ return {
57
+ data,
58
+ usage: { tokensIn: response.usage.input_tokens, tokensOut: response.usage.output_tokens },
59
+ };
60
+ }
61
+
62
+ // Not implemented — Claude not registered for these capabilities
63
+ async transcribeAudio(): Promise<never> { throw new Error('Claude does not support audio transcription'); }
64
+ async generateSpeech(): Promise<never> { throw new Error('Claude does not support speech generation'); }
65
+ async generateImage(): Promise<never> { throw new Error('Claude does not support image generation'); }
66
+ }
packages/ai-sdk/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
  } from './types';
10
  import { MockLLMProvider } from './mock-provider';
11
  import { OpenAIProvider } from './openai-provider';
 
12
  import { searchService } from './search';
13
  import { GeminiProvider } from './gemini-provider';
14
  import { PromptLoader, PersonalityConfig } from '@repo/prompts';
@@ -39,9 +40,17 @@ export class AIService {
39
  }
40
 
41
  private initializeProviders() {
 
42
  const geminiApiKey = process.env.GOOGLE_AI_API_KEY;
43
  const openAiApiKey = process.env.OPENAI_API_KEY;
44
 
 
 
 
 
 
 
 
45
  if (geminiApiKey) {
46
  this.registry.register('GEMINI', new GeminiProvider(geminiApiKey), 100, [
47
  ProviderCapability.TEXT,
 
9
  } from './types';
10
  import { MockLLMProvider } from './mock-provider';
11
  import { OpenAIProvider } from './openai-provider';
12
+ import { ClaudeProvider } from './claude-provider';
13
  import { searchService } from './search';
14
  import { GeminiProvider } from './gemini-provider';
15
  import { PromptLoader, PersonalityConfig } from '@repo/prompts';
 
40
  }
41
 
42
  private initializeProviders() {
43
+ const claudeApiKey = process.env.ANTHROPIC_API_KEY;
44
  const geminiApiKey = process.env.GOOGLE_AI_API_KEY;
45
  const openAiApiKey = process.env.OPENAI_API_KEY;
46
 
47
+ // Claude: priority 120 for TEXT only (feedback, lessons, chat)
48
+ if (claudeApiKey) {
49
+ this.registry.register('CLAUDE', new ClaudeProvider(claudeApiKey), 120, [
50
+ ProviderCapability.TEXT,
51
+ ]);
52
+ }
53
+
54
  if (geminiApiKey) {
55
  this.registry.register('GEMINI', new GeminiProvider(geminiApiKey), 100, [
56
  ProviderCapability.TEXT,
packages/database/prisma/migrations/20260513000002_add_stripe_fields/migration.sql ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ALTER TABLE "Organization" ADD COLUMN IF NOT EXISTS "stripeCustomerId" TEXT;
2
+ ALTER TABLE "Organization" ADD COLUMN IF NOT EXISTS "stripeSubscriptionId" TEXT;
3
+
4
+ CREATE UNIQUE INDEX IF NOT EXISTS "Organization_stripeCustomerId_key" ON "Organization"("stripeCustomerId");
5
+ CREATE UNIQUE INDEX IF NOT EXISTS "Organization_stripeSubscriptionId_key" ON "Organization"("stripeSubscriptionId");
packages/database/prisma/schema.prisma CHANGED
@@ -32,6 +32,8 @@ model Organization {
32
  googleAiApiKey String?
33
  subscriptionStatus String? @default("ACTIVE")
34
  subscriptionPlan SubscriptionPlan @default(STARTER)
 
 
35
  aiCreditsLimit Int @default(500)
36
  aiCreditsUsed Int @default(0)
37
  whatsappMessagesSent Int @default(0)
 
32
  googleAiApiKey String?
33
  subscriptionStatus String? @default("ACTIVE")
34
  subscriptionPlan SubscriptionPlan @default(STARTER)
35
+ stripeCustomerId String? @unique
36
+ stripeSubscriptionId String? @unique
37
  aiCreditsLimit Int @default(500)
38
  aiCreditsUsed Int @default(0)
39
  whatsappMessagesSent Int @default(0)
pnpm-lock.yaml CHANGED
@@ -198,6 +198,9 @@ importers:
198
  puppeteer:
199
  specifier: ^22.0.0
200
  version: 22.15.0(typescript@5.9.3)
 
 
 
201
  web-push:
202
  specifier: ^3.6.7
203
  version: 3.6.7
@@ -375,6 +378,9 @@ importers:
375
 
376
  packages/ai-sdk:
377
  dependencies:
 
 
 
378
  '@google/generative-ai':
379
  specifier: ^0.24.1
380
  version: 0.24.1
@@ -393,6 +399,9 @@ importers:
393
  zod:
394
  specifier: ^3.25.76
395
  version: 3.25.76
 
 
 
396
  devDependencies:
397
  '@repo/tsconfig':
398
  specifier: workspace:*
@@ -475,6 +484,15 @@ packages:
475
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
476
  engines: {node: '>=10'}
477
 
 
 
 
 
 
 
 
 
 
478
  '@aws-crypto/crc32@5.2.0':
479
  resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
480
  engines: {node: '>=16.0.0'}
@@ -2217,6 +2235,9 @@ packages:
2217
  resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
2218
  engines: {node: '>=18.0.0'}
2219
 
 
 
 
2220
  '@standard-schema/spec@1.1.0':
2221
  resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
2222
 
@@ -3189,6 +3210,9 @@ packages:
3189
  fast-safe-stringify@2.1.1:
3190
  resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
3191
 
 
 
 
3192
  fast-uri@2.4.0:
3193
  resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==}
3194
 
@@ -3581,6 +3605,10 @@ packages:
3581
  json-schema-ref-resolver@1.0.1:
3582
  resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
3583
 
 
 
 
 
3584
  json-schema-traverse@1.0.0:
3585
  resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
3586
 
@@ -4470,6 +4498,9 @@ packages:
4470
  standard-as-callback@2.1.0:
4471
  resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
4472
 
 
 
 
4473
  statuses@2.0.1:
4474
  resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
4475
  engines: {node: '>= 0.8'}
@@ -4520,6 +4551,15 @@ packages:
4520
  strip-literal@2.1.1:
4521
  resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
4522
 
 
 
 
 
 
 
 
 
 
4523
  strnum@2.1.2:
4524
  resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==}
4525
 
@@ -4621,6 +4661,9 @@ packages:
4621
  tr46@0.0.3:
4622
  resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
4623
 
 
 
 
4624
  ts-interface-checker@0.1.13:
4625
  resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
4626
 
@@ -5006,6 +5049,11 @@ packages:
5006
  resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
5007
  engines: {node: '>=12.20'}
5008
 
 
 
 
 
 
5009
  zod@3.23.8:
5010
  resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
5011
 
@@ -5019,6 +5067,13 @@ snapshots:
5019
 
5020
  '@alloc/quick-lru@5.2.0': {}
5021
 
 
 
 
 
 
 
 
5022
  '@aws-crypto/crc32@5.2.0':
5023
  dependencies:
5024
  '@aws-crypto/util': 5.2.0
@@ -7073,6 +7128,8 @@ snapshots:
7073
  dependencies:
7074
  tslib: 2.8.1
7075
 
 
 
7076
  '@standard-schema/spec@1.1.0': {}
7077
 
7078
  '@standard-schema/utils@0.3.0': {}
@@ -8093,6 +8150,8 @@ snapshots:
8093
 
8094
  fast-safe-stringify@2.1.1: {}
8095
 
 
 
8096
  fast-uri@2.4.0: {}
8097
 
8098
  fast-uri@3.1.0: {}
@@ -8487,6 +8546,11 @@ snapshots:
8487
  dependencies:
8488
  fast-deep-equal: 3.1.3
8489
 
 
 
 
 
 
8490
  json-schema-traverse@1.0.0: {}
8491
 
8492
  json-stringify-safe@5.0.1:
@@ -9500,6 +9564,11 @@ snapshots:
9500
 
9501
  standard-as-callback@2.1.0: {}
9502
 
 
 
 
 
 
9503
  statuses@2.0.1: {}
9504
 
9505
  std-env@3.10.0: {}
@@ -9559,6 +9628,10 @@ snapshots:
9559
  dependencies:
9560
  js-tokens: 9.0.1
9561
 
 
 
 
 
9562
  strnum@2.1.2: {}
9563
 
9564
  sucrase@3.35.1:
@@ -9690,6 +9763,8 @@ snapshots:
9690
 
9691
  tr46@0.0.3: {}
9692
 
 
 
9693
  ts-interface-checker@0.1.13: {}
9694
 
9695
  ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3):
@@ -10041,6 +10116,10 @@ snapshots:
10041
 
10042
  yocto-queue@1.2.2: {}
10043
 
 
 
 
 
10044
  zod@3.23.8: {}
10045
 
10046
  zod@3.25.76: {}
 
198
  puppeteer:
199
  specifier: ^22.0.0
200
  version: 22.15.0(typescript@5.9.3)
201
+ stripe:
202
+ specifier: ^22.1.1
203
+ version: 22.1.1(@types/node@20.19.33)
204
  web-push:
205
  specifier: ^3.6.7
206
  version: 3.6.7
 
378
 
379
  packages/ai-sdk:
380
  dependencies:
381
+ '@anthropic-ai/sdk':
382
+ specifier: ^0.95.2
383
+ version: 0.95.2(zod@3.25.76)
384
  '@google/generative-ai':
385
  specifier: ^0.24.1
386
  version: 0.24.1
 
399
  zod:
400
  specifier: ^3.25.76
401
  version: 3.25.76
402
+ zod-to-json-schema:
403
+ specifier: ^3.25.2
404
+ version: 3.25.2(zod@3.25.76)
405
  devDependencies:
406
  '@repo/tsconfig':
407
  specifier: workspace:*
 
484
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
485
  engines: {node: '>=10'}
486
 
487
+ '@anthropic-ai/sdk@0.95.2':
488
+ resolution: {integrity: sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==}
489
+ hasBin: true
490
+ peerDependencies:
491
+ zod: ^3.25.0 || ^4.0.0
492
+ peerDependenciesMeta:
493
+ zod:
494
+ optional: true
495
+
496
  '@aws-crypto/crc32@5.2.0':
497
  resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
498
  engines: {node: '>=16.0.0'}
 
2235
  resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
2236
  engines: {node: '>=18.0.0'}
2237
 
2238
+ '@stablelib/base64@1.0.1':
2239
+ resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
2240
+
2241
  '@standard-schema/spec@1.1.0':
2242
  resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
2243
 
 
3210
  fast-safe-stringify@2.1.1:
3211
  resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
3212
 
3213
+ fast-sha256@1.3.0:
3214
+ resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
3215
+
3216
  fast-uri@2.4.0:
3217
  resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==}
3218
 
 
3605
  json-schema-ref-resolver@1.0.1:
3606
  resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
3607
 
3608
+ json-schema-to-ts@3.1.1:
3609
+ resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
3610
+ engines: {node: '>=16'}
3611
+
3612
  json-schema-traverse@1.0.0:
3613
  resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
3614
 
 
4498
  standard-as-callback@2.1.0:
4499
  resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
4500
 
4501
+ standardwebhooks@1.0.0:
4502
+ resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
4503
+
4504
  statuses@2.0.1:
4505
  resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
4506
  engines: {node: '>= 0.8'}
 
4551
  strip-literal@2.1.1:
4552
  resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
4553
 
4554
+ stripe@22.1.1:
4555
+ resolution: {integrity: sha512-cmodIYP27tBkJ8G7DuGgWw0PFuemlFZbuF3Wwr1TrjFjUa3T7NIgCe6TVwX8BO2ynu+xtTuDGfHafNDCPt9lXA==}
4556
+ engines: {node: '>=18'}
4557
+ peerDependencies:
4558
+ '@types/node': '>=18'
4559
+ peerDependenciesMeta:
4560
+ '@types/node':
4561
+ optional: true
4562
+
4563
  strnum@2.1.2:
4564
  resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==}
4565
 
 
4661
  tr46@0.0.3:
4662
  resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
4663
 
4664
+ ts-algebra@2.0.0:
4665
+ resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
4666
+
4667
  ts-interface-checker@0.1.13:
4668
  resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
4669
 
 
5049
  resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
5050
  engines: {node: '>=12.20'}
5051
 
5052
+ zod-to-json-schema@3.25.2:
5053
+ resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
5054
+ peerDependencies:
5055
+ zod: ^3.25.28 || ^4
5056
+
5057
  zod@3.23.8:
5058
  resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
5059
 
 
5067
 
5068
  '@alloc/quick-lru@5.2.0': {}
5069
 
5070
+ '@anthropic-ai/sdk@0.95.2(zod@3.25.76)':
5071
+ dependencies:
5072
+ json-schema-to-ts: 3.1.1
5073
+ standardwebhooks: 1.0.0
5074
+ optionalDependencies:
5075
+ zod: 3.25.76
5076
+
5077
  '@aws-crypto/crc32@5.2.0':
5078
  dependencies:
5079
  '@aws-crypto/util': 5.2.0
 
7128
  dependencies:
7129
  tslib: 2.8.1
7130
 
7131
+ '@stablelib/base64@1.0.1': {}
7132
+
7133
  '@standard-schema/spec@1.1.0': {}
7134
 
7135
  '@standard-schema/utils@0.3.0': {}
 
8150
 
8151
  fast-safe-stringify@2.1.1: {}
8152
 
8153
+ fast-sha256@1.3.0: {}
8154
+
8155
  fast-uri@2.4.0: {}
8156
 
8157
  fast-uri@3.1.0: {}
 
8546
  dependencies:
8547
  fast-deep-equal: 3.1.3
8548
 
8549
+ json-schema-to-ts@3.1.1:
8550
+ dependencies:
8551
+ '@babel/runtime': 7.29.2
8552
+ ts-algebra: 2.0.0
8553
+
8554
  json-schema-traverse@1.0.0: {}
8555
 
8556
  json-stringify-safe@5.0.1:
 
9564
 
9565
  standard-as-callback@2.1.0: {}
9566
 
9567
+ standardwebhooks@1.0.0:
9568
+ dependencies:
9569
+ '@stablelib/base64': 1.0.1
9570
+ fast-sha256: 1.3.0
9571
+
9572
  statuses@2.0.1: {}
9573
 
9574
  std-env@3.10.0: {}
 
9628
  dependencies:
9629
  js-tokens: 9.0.1
9630
 
9631
+ stripe@22.1.1(@types/node@20.19.33):
9632
+ optionalDependencies:
9633
+ '@types/node': 20.19.33
9634
+
9635
  strnum@2.1.2: {}
9636
 
9637
  sucrase@3.35.1:
 
9763
 
9764
  tr46@0.0.3: {}
9765
 
9766
+ ts-algebra@2.0.0: {}
9767
+
9768
  ts-interface-checker@0.1.13: {}
9769
 
9770
  ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3):
 
10116
 
10117
  yocto-queue@1.2.2: {}
10118
 
10119
+ zod-to-json-schema@3.25.2(zod@3.25.76):
10120
+ dependencies:
10121
+ zod: 3.25.76
10122
+
10123
  zod@3.23.8: {}
10124
 
10125
  zod@3.25.76: {}