@primstack/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
- package/dist/generators/crud/templates/handlers.ts.template +136 -0
- package/dist/generators/crud/templates/routes.ts.template +21 -0
- package/dist/generators/crud/templates/schema.ts.template +20 -0
- package/dist/index.js +9618 -0
- package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
- package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
- package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
- package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
- package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
- package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
- package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
- package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
- package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
- package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
- package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
- package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
- package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
- package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
- package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
- package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
- package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
- package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
- package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
- package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
- package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
- package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
- package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
- package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
- package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
- package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
- package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
- package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
- package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
- package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
- package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
- package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
- package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
- package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
- package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
- package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
- package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
- package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
- package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
- package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
- package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
- package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
- package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
- package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
- package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
- package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
- package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
- package/dist/templates/api/fastify/.env.example.template +7 -0
- package/dist/templates/api/fastify/.gitignore.template +24 -0
- package/dist/templates/api/fastify/package.json.template +23 -0
- package/dist/templates/api/fastify/src/index.ts.template +52 -0
- package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
- package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
- package/dist/templates/api/fastify/tsconfig.json.template +18 -0
- package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
- package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-postgres/package.json.template +16 -0
- package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
- package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
- package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
- package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
- package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
- package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-supabase/package.json.template +15 -0
- package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
- package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-turso/.env.example.template +11 -0
- package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
- package/dist/templates/api/fastify-turso/package.json.template +15 -0
- package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
- package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
- package/dist/templates/fullstack/api/.env.example.template +10 -0
- package/dist/templates/fullstack/api/.gitignore.template +4 -0
- package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
- package/dist/templates/fullstack/api/package.json.template +33 -0
- package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
- package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
- package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
- package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
- package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
- package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
- package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
- package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
- package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
- package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
- package/dist/templates/fullstack/api/src/index.ts.template +54 -0
- package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
- package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
- package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
- package/dist/templates/fullstack/root/.gitignore.template +26 -0
- package/dist/templates/fullstack/root/package.json.template +15 -0
- package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/fullstack/root/turbo.json.template +17 -0
- package/dist/templates/fullstack/shared/package.json.template +36 -0
- package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
- package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
- package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
- package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
- package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
- package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
- package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
- package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
- package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
- package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
- package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
- package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
- package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
- package/dist/templates/fullstack/web/.gitignore.template +3 -0
- package/dist/templates/fullstack/web/index.html.template +13 -0
- package/dist/templates/fullstack/web/package.json.template +23 -0
- package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
- package/dist/templates/fullstack/web/src/index.css.template +54 -0
- package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
- package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
- package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
- package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
- package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
- package/dist/templates/hosted/root/.env.local.template +13 -0
- package/dist/templates/hosted/root/.gitignore.template +32 -0
- package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
- package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
- package/dist/templates/hosted/root/next.config.ts.template +15 -0
- package/dist/templates/hosted/root/package.json.template +40 -0
- package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
- package/dist/templates/hosted/root/primstack.config.json.template +5 -0
- package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
- package/dist/templates/hosted/root/tsconfig.json.template +25 -0
- package/dist/templates/hosted/root/wrangler.toml.template +9 -0
- package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
- package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
- package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
- package/dist/templates/hosted/src/app/globals.css.template +59 -0
- package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
- package/dist/templates/hosted/src/app/page.tsx.template +34 -0
- package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
- package/dist/templates/hosted/src/db/schema.ts.template +52 -0
- package/dist/templates/hosted/src/env.d.ts.template +10 -0
- package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
- package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
- package/dist/templates/hosted/src/lib/db.ts.template +17 -0
- package/dist/templates/hosted/src/middleware.ts.template +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Webhook Handler
|
|
3
|
+
*
|
|
4
|
+
* This route handles incoming Stripe webhooks for subscription events.
|
|
5
|
+
*
|
|
6
|
+
* Setup:
|
|
7
|
+
* 1. Register this route in your Fastify app
|
|
8
|
+
* 2. Configure webhook endpoint in Stripe Dashboard: /webhooks/stripe
|
|
9
|
+
* 3. For local development: stripe listen --forward-to localhost:3001/webhooks/stripe
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
13
|
+
import { constructWebhookEvent } from '../lib/stripe.js';
|
|
14
|
+
import type Stripe from 'stripe';
|
|
15
|
+
|
|
16
|
+
// Import your database client
|
|
17
|
+
// import { db } from '../db/index.js';
|
|
18
|
+
// import { subscriptions, organizations } from '../db/schema/index.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom error class for permanent webhook failures
|
|
22
|
+
* (should not be retried by Stripe)
|
|
23
|
+
*/
|
|
24
|
+
class PermanentWebhookError extends Error {
|
|
25
|
+
constructor(message: string) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'PermanentWebhookError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register Stripe webhook routes
|
|
33
|
+
*/
|
|
34
|
+
export async function stripeWebhookRoutes(fastify: FastifyInstance): Promise<void> {
|
|
35
|
+
// Register a raw body parser ONLY for this route
|
|
36
|
+
// This doesn't affect other routes since we're using a route-specific hook
|
|
37
|
+
fastify.post('/webhooks/stripe', {
|
|
38
|
+
config: {
|
|
39
|
+
// Skip auth for webhooks - we verify via Stripe signature
|
|
40
|
+
skipAuth: true,
|
|
41
|
+
rawBody: true, // Request raw body for signature verification
|
|
42
|
+
},
|
|
43
|
+
// Use preValidation to get raw body before parsing
|
|
44
|
+
preValidation: async (request) => {
|
|
45
|
+
// Store raw body for webhook verification
|
|
46
|
+
// @ts-ignore - Adding custom property
|
|
47
|
+
request.rawBody = request.body;
|
|
48
|
+
},
|
|
49
|
+
handler: async (request: FastifyRequest, reply: FastifyReply) => {
|
|
50
|
+
// Handle stripe-signature header (could be string or array)
|
|
51
|
+
const signatureHeader = request.headers['stripe-signature'];
|
|
52
|
+
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
53
|
+
|
|
54
|
+
if (!signature) {
|
|
55
|
+
return reply.status(400).send({ error: 'Missing stripe-signature header' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let event: Stripe.Event;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// @ts-ignore - Using rawBody property
|
|
62
|
+
const body = request.rawBody || request.body;
|
|
63
|
+
event = constructWebhookEvent(body as Buffer, signature);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
66
|
+
fastify.log.error(`Webhook signature verification failed: ${message}`);
|
|
67
|
+
return reply.status(400).send({ error: `Webhook Error: ${message}` });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle the event
|
|
71
|
+
try {
|
|
72
|
+
await handleStripeEvent(event, fastify);
|
|
73
|
+
return reply.status(200).send({ received: true });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
fastify.log.error(`Error handling webhook ${event.type}: ${err}`);
|
|
76
|
+
|
|
77
|
+
// Permanent errors should return 200 to prevent retries
|
|
78
|
+
// Transient errors (DB down, network issues) should return 500 for retry
|
|
79
|
+
if (err instanceof PermanentWebhookError) {
|
|
80
|
+
fastify.log.warn(`Permanent webhook error (no retry): ${err.message}`);
|
|
81
|
+
return reply.status(200).send({ received: true, error: err.message });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Return 500 for transient errors so Stripe retries
|
|
85
|
+
return reply.status(500).send({ error: 'Webhook handler failed, will retry' });
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle Stripe webhook events
|
|
93
|
+
*/
|
|
94
|
+
async function handleStripeEvent(
|
|
95
|
+
event: Stripe.Event,
|
|
96
|
+
fastify: FastifyInstance
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
fastify.log.info(`Processing Stripe event: ${event.type}`);
|
|
99
|
+
|
|
100
|
+
switch (event.type) {
|
|
101
|
+
case 'checkout.session.completed':
|
|
102
|
+
await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session, fastify);
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'customer.subscription.created':
|
|
106
|
+
await handleSubscriptionCreated(event.data.object as Stripe.Subscription, fastify);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'customer.subscription.updated':
|
|
110
|
+
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription, fastify);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'customer.subscription.deleted':
|
|
114
|
+
await handleSubscriptionDeleted(event.data.object as Stripe.Subscription, fastify);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'invoice.payment_succeeded':
|
|
118
|
+
await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice, fastify);
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'invoice.payment_failed':
|
|
122
|
+
await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice, fastify);
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
fastify.log.info(`Unhandled event type: ${event.type}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle checkout.session.completed
|
|
132
|
+
* Called when a customer completes checkout
|
|
133
|
+
*/
|
|
134
|
+
async function handleCheckoutCompleted(
|
|
135
|
+
session: Stripe.Checkout.Session,
|
|
136
|
+
fastify: FastifyInstance
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
fastify.log.info(`Checkout completed: ${session.id}`);
|
|
139
|
+
|
|
140
|
+
// Get metadata from the session
|
|
141
|
+
const organizationId = session.metadata?.organizationId;
|
|
142
|
+
const userId = session.metadata?.userId;
|
|
143
|
+
|
|
144
|
+
if (!organizationId) {
|
|
145
|
+
fastify.log.warn('Checkout session missing organizationId in metadata');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// TODO: Update your database
|
|
150
|
+
// Example:
|
|
151
|
+
// await db.update(organizations)
|
|
152
|
+
// .set({
|
|
153
|
+
// stripeCustomerId: session.customer as string,
|
|
154
|
+
// stripeSubscriptionId: session.subscription as string,
|
|
155
|
+
// })
|
|
156
|
+
// .where(eq(organizations.id, organizationId));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle customer.subscription.created
|
|
161
|
+
*/
|
|
162
|
+
async function handleSubscriptionCreated(
|
|
163
|
+
subscription: Stripe.Subscription,
|
|
164
|
+
fastify: FastifyInstance
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
fastify.log.info(`Subscription created: ${subscription.id}`);
|
|
167
|
+
|
|
168
|
+
const customerId = subscription.customer as string;
|
|
169
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
170
|
+
const lookupKey = subscription.items.data[0]?.price.lookup_key;
|
|
171
|
+
|
|
172
|
+
// TODO: Create subscription record in your database
|
|
173
|
+
// Example:
|
|
174
|
+
// await db.insert(subscriptions).values({
|
|
175
|
+
// stripeSubscriptionId: subscription.id,
|
|
176
|
+
// stripeCustomerId: customerId,
|
|
177
|
+
// status: subscription.status,
|
|
178
|
+
// priceLookupKey: lookupKey,
|
|
179
|
+
// currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
180
|
+
// currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
181
|
+
// });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle customer.subscription.updated
|
|
186
|
+
*/
|
|
187
|
+
async function handleSubscriptionUpdated(
|
|
188
|
+
subscription: Stripe.Subscription,
|
|
189
|
+
fastify: FastifyInstance
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
fastify.log.info(`Subscription updated: ${subscription.id}`);
|
|
192
|
+
|
|
193
|
+
// Common updates:
|
|
194
|
+
// - Status change (active, past_due, canceled, etc.)
|
|
195
|
+
// - Plan change (different price)
|
|
196
|
+
// - Cancel at period end
|
|
197
|
+
|
|
198
|
+
// TODO: Update subscription in your database
|
|
199
|
+
// Example:
|
|
200
|
+
// await db.update(subscriptions)
|
|
201
|
+
// .set({
|
|
202
|
+
// status: subscription.status,
|
|
203
|
+
// priceLookupKey: subscription.items.data[0]?.price.lookup_key,
|
|
204
|
+
// cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
205
|
+
// currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
206
|
+
// })
|
|
207
|
+
// .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Handle customer.subscription.deleted
|
|
212
|
+
*/
|
|
213
|
+
async function handleSubscriptionDeleted(
|
|
214
|
+
subscription: Stripe.Subscription,
|
|
215
|
+
fastify: FastifyInstance
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
fastify.log.info(`Subscription deleted: ${subscription.id}`);
|
|
218
|
+
|
|
219
|
+
// TODO: Update subscription status in your database
|
|
220
|
+
// Example:
|
|
221
|
+
// await db.update(subscriptions)
|
|
222
|
+
// .set({
|
|
223
|
+
// status: 'canceled',
|
|
224
|
+
// canceledAt: new Date(),
|
|
225
|
+
// })
|
|
226
|
+
// .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
|
|
227
|
+
|
|
228
|
+
// You might want to:
|
|
229
|
+
// - Downgrade the organization to free plan
|
|
230
|
+
// - Send a cancellation email
|
|
231
|
+
// - Revoke premium features
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle invoice.payment_succeeded
|
|
236
|
+
*/
|
|
237
|
+
async function handleInvoicePaymentSucceeded(
|
|
238
|
+
invoice: Stripe.Invoice,
|
|
239
|
+
fastify: FastifyInstance
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
fastify.log.info(`Invoice payment succeeded: ${invoice.id}`);
|
|
242
|
+
|
|
243
|
+
// This is called for:
|
|
244
|
+
// - Successful subscription renewals
|
|
245
|
+
// - Successful payment after failed payment
|
|
246
|
+
|
|
247
|
+
// TODO: You might want to:
|
|
248
|
+
// - Update subscription status if it was past_due
|
|
249
|
+
// - Send a receipt email
|
|
250
|
+
// - Log the payment for accounting
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle invoice.payment_failed
|
|
255
|
+
*/
|
|
256
|
+
async function handleInvoicePaymentFailed(
|
|
257
|
+
invoice: Stripe.Invoice,
|
|
258
|
+
fastify: FastifyInstance
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
fastify.log.info(`Invoice payment failed: ${invoice.id}`);
|
|
261
|
+
|
|
262
|
+
const customerId = invoice.customer as string;
|
|
263
|
+
const subscriptionId = invoice.subscription as string;
|
|
264
|
+
|
|
265
|
+
// TODO: Handle failed payment
|
|
266
|
+
// - Update subscription status to past_due
|
|
267
|
+
// - Send a failed payment email
|
|
268
|
+
// - Notify admin
|
|
269
|
+
|
|
270
|
+
// Example:
|
|
271
|
+
// if (subscriptionId) {
|
|
272
|
+
// await db.update(subscriptions)
|
|
273
|
+
// .set({ status: 'past_due' })
|
|
274
|
+
// .where(eq(subscriptions.stripeSubscriptionId, subscriptionId));
|
|
275
|
+
// }
|
|
276
|
+
|
|
277
|
+
// Get customer email for notification
|
|
278
|
+
// const customer = await stripe.customers.retrieve(customerId);
|
|
279
|
+
// if (customer && !customer.deleted) {
|
|
280
|
+
// await sendPaymentFailedEmail(customer.email);
|
|
281
|
+
// }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export default stripeWebhookRoutes;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file contains all Stripe-related configuration including:
|
|
5
|
+
* - API credentials (per environment)
|
|
6
|
+
* - Product definitions with versioned pricing
|
|
7
|
+
* - Usage meters for metered billing
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: This file contains sensitive information.
|
|
10
|
+
* Never commit API keys - use environment variables instead.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { StripeConfig } from '@{{PROJECT_NAME}}/shared/stripe-types';
|
|
14
|
+
|
|
15
|
+
export const stripeConfig: StripeConfig = {
|
|
16
|
+
/**
|
|
17
|
+
* Environment credentials
|
|
18
|
+
* Keys are loaded from environment variables
|
|
19
|
+
*/
|
|
20
|
+
environments: {
|
|
21
|
+
test: {
|
|
22
|
+
secretKey: process.env.STRIPE_TEST_SECRET_KEY || '',
|
|
23
|
+
publishableKey: process.env.STRIPE_TEST_PUBLISHABLE_KEY || '',
|
|
24
|
+
webhookSecret: process.env.STRIPE_TEST_WEBHOOK_SECRET || '',
|
|
25
|
+
},
|
|
26
|
+
live: {
|
|
27
|
+
secretKey: process.env.STRIPE_LIVE_SECRET_KEY || '',
|
|
28
|
+
publishableKey: process.env.STRIPE_LIVE_PUBLISHABLE_KEY || '',
|
|
29
|
+
webhookSecret: process.env.STRIPE_LIVE_WEBHOOK_SECRET || '',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Product definitions
|
|
35
|
+
*
|
|
36
|
+
* Each product has:
|
|
37
|
+
* - Versioned pricing (for grandfathering existing customers)
|
|
38
|
+
* - Feature flags and limits
|
|
39
|
+
* - Stripe product IDs (populated by `prim stripe sync`)
|
|
40
|
+
*
|
|
41
|
+
* Price lookup keys follow the format: {slug}_{interval}_v{version}
|
|
42
|
+
* Example: pro_monthly_v1, pro_yearly_v2
|
|
43
|
+
*/
|
|
44
|
+
products: {
|
|
45
|
+
free: {
|
|
46
|
+
name: 'Free',
|
|
47
|
+
slug: 'free',
|
|
48
|
+
description: 'Perfect for getting started',
|
|
49
|
+
features: [
|
|
50
|
+
'1 project',
|
|
51
|
+
'100 API calls/month',
|
|
52
|
+
'Community support',
|
|
53
|
+
],
|
|
54
|
+
limits: {
|
|
55
|
+
projects: 1,
|
|
56
|
+
apiCalls: 100,
|
|
57
|
+
storage: 100_000_000, // 100 MB
|
|
58
|
+
seats: 1,
|
|
59
|
+
},
|
|
60
|
+
prices: {
|
|
61
|
+
monthly: [
|
|
62
|
+
{
|
|
63
|
+
version: 1,
|
|
64
|
+
lookupKey: 'free_monthly_v1',
|
|
65
|
+
amountCents: 0,
|
|
66
|
+
isDefault: true,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
yearly: [
|
|
70
|
+
{
|
|
71
|
+
version: 1,
|
|
72
|
+
lookupKey: 'free_yearly_v1',
|
|
73
|
+
amountCents: 0,
|
|
74
|
+
isDefault: true,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
pro: {
|
|
81
|
+
name: 'Pro',
|
|
82
|
+
slug: 'pro',
|
|
83
|
+
description: 'For professionals and small teams',
|
|
84
|
+
features: [
|
|
85
|
+
'Unlimited projects',
|
|
86
|
+
'10,000 API calls/month',
|
|
87
|
+
'Priority support',
|
|
88
|
+
'10 team members',
|
|
89
|
+
],
|
|
90
|
+
limits: {
|
|
91
|
+
projects: -1, // unlimited
|
|
92
|
+
apiCalls: 10_000,
|
|
93
|
+
storage: 10_000_000_000, // 10 GB
|
|
94
|
+
seats: 10,
|
|
95
|
+
},
|
|
96
|
+
highlighted: true,
|
|
97
|
+
prices: {
|
|
98
|
+
monthly: [
|
|
99
|
+
{
|
|
100
|
+
version: 1,
|
|
101
|
+
lookupKey: 'pro_monthly_v1',
|
|
102
|
+
amountCents: 2900, // $29/mo
|
|
103
|
+
isDefault: true,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
yearly: [
|
|
107
|
+
{
|
|
108
|
+
version: 1,
|
|
109
|
+
lookupKey: 'pro_yearly_v1',
|
|
110
|
+
amountCents: 29000, // $290/yr (2 months free)
|
|
111
|
+
isDefault: true,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
enterprise: {
|
|
118
|
+
name: 'Enterprise',
|
|
119
|
+
slug: 'enterprise',
|
|
120
|
+
description: 'For large organizations',
|
|
121
|
+
features: [
|
|
122
|
+
'Everything in Pro',
|
|
123
|
+
'Unlimited API calls',
|
|
124
|
+
'Dedicated support',
|
|
125
|
+
'Unlimited team members',
|
|
126
|
+
'Custom integrations',
|
|
127
|
+
'SLA guarantee',
|
|
128
|
+
],
|
|
129
|
+
limits: {
|
|
130
|
+
projects: -1,
|
|
131
|
+
apiCalls: -1,
|
|
132
|
+
storage: -1,
|
|
133
|
+
seats: -1,
|
|
134
|
+
},
|
|
135
|
+
prices: {
|
|
136
|
+
monthly: [
|
|
137
|
+
{
|
|
138
|
+
version: 1,
|
|
139
|
+
lookupKey: 'enterprise_monthly_v1',
|
|
140
|
+
amountCents: 9900, // $99/mo
|
|
141
|
+
isDefault: true,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
yearly: [
|
|
145
|
+
{
|
|
146
|
+
version: 1,
|
|
147
|
+
lookupKey: 'enterprise_yearly_v1',
|
|
148
|
+
amountCents: 99000, // $990/yr
|
|
149
|
+
isDefault: true,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Usage meters (for usage-based billing)
|
|
158
|
+
* These are optional and require Stripe Billing Meters
|
|
159
|
+
*/
|
|
160
|
+
meters: {
|
|
161
|
+
// apiCalls: {
|
|
162
|
+
// displayName: 'API Calls',
|
|
163
|
+
// stripeMeterEventName: 'api_call',
|
|
164
|
+
// aggregation: 'sum',
|
|
165
|
+
// },
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Customer portal configuration
|
|
170
|
+
*/
|
|
171
|
+
customerPortal: {
|
|
172
|
+
allowCancellations: true,
|
|
173
|
+
allowPlanChanges: true,
|
|
174
|
+
allowPaymentMethodUpdates: true,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default stripeConfig;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Pricing Data
|
|
3
|
+
*
|
|
4
|
+
* This file contains ONLY public pricing information that is safe to expose
|
|
5
|
+
* to the frontend. It does NOT include:
|
|
6
|
+
* - Stripe IDs
|
|
7
|
+
* - Price versions (only current default prices)
|
|
8
|
+
* - Internal limits
|
|
9
|
+
*
|
|
10
|
+
* For the full config with Stripe IDs and versioning, see apps/api/stripe.config.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PublicPlan {
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
priceMonthly: number; // cents
|
|
18
|
+
priceYearly: number; // cents
|
|
19
|
+
features: string[];
|
|
20
|
+
highlighted?: boolean;
|
|
21
|
+
cta?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Public pricing plans for display
|
|
26
|
+
*
|
|
27
|
+
* This is the SINGLE SOURCE OF TRUTH for pricing display.
|
|
28
|
+
* Update this when prices change.
|
|
29
|
+
*/
|
|
30
|
+
export const PRICING_PLANS: PublicPlan[] = [
|
|
31
|
+
{
|
|
32
|
+
slug: 'free',
|
|
33
|
+
name: 'Free',
|
|
34
|
+
description: 'Perfect for getting started',
|
|
35
|
+
priceMonthly: 0,
|
|
36
|
+
priceYearly: 0,
|
|
37
|
+
features: [
|
|
38
|
+
'1 project',
|
|
39
|
+
'100 API calls/month',
|
|
40
|
+
'Community support',
|
|
41
|
+
],
|
|
42
|
+
cta: 'Get Started',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
slug: 'pro',
|
|
46
|
+
name: 'Pro',
|
|
47
|
+
description: 'For professionals and small teams',
|
|
48
|
+
priceMonthly: 2900, // $29
|
|
49
|
+
priceYearly: 29000, // $290 (2 months free)
|
|
50
|
+
features: [
|
|
51
|
+
'Unlimited projects',
|
|
52
|
+
'10,000 API calls/month',
|
|
53
|
+
'Priority support',
|
|
54
|
+
'10 team members',
|
|
55
|
+
],
|
|
56
|
+
highlighted: true,
|
|
57
|
+
cta: 'Start Free Trial',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
slug: 'enterprise',
|
|
61
|
+
name: 'Enterprise',
|
|
62
|
+
description: 'For large organizations',
|
|
63
|
+
priceMonthly: 9900, // $99
|
|
64
|
+
priceYearly: 99000, // $990
|
|
65
|
+
features: [
|
|
66
|
+
'Everything in Pro',
|
|
67
|
+
'Unlimited API calls',
|
|
68
|
+
'Dedicated support',
|
|
69
|
+
'Unlimited team members',
|
|
70
|
+
'Custom integrations',
|
|
71
|
+
'SLA guarantee',
|
|
72
|
+
],
|
|
73
|
+
cta: 'Contact Sales',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format a price in cents as a currency string
|
|
79
|
+
*/
|
|
80
|
+
export function formatPrice(cents: number, currency = 'USD'): string {
|
|
81
|
+
if (cents === 0) return 'Free';
|
|
82
|
+
|
|
83
|
+
return new Intl.NumberFormat('en-US', {
|
|
84
|
+
style: 'currency',
|
|
85
|
+
currency,
|
|
86
|
+
minimumFractionDigits: 0,
|
|
87
|
+
maximumFractionDigits: 0,
|
|
88
|
+
}).format(cents / 100);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a plan by slug
|
|
93
|
+
*/
|
|
94
|
+
export function getPlanBySlug(slug: string): PublicPlan | undefined {
|
|
95
|
+
return PRICING_PLANS.find((plan) => plan.slug === slug);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the monthly/yearly savings percentage
|
|
100
|
+
*/
|
|
101
|
+
export function getYearlySavingsPercent(plan: PublicPlan): number {
|
|
102
|
+
if (plan.priceMonthly === 0) return 0;
|
|
103
|
+
const yearlyFromMonthly = plan.priceMonthly * 12;
|
|
104
|
+
const savings = yearlyFromMonthly - plan.priceYearly;
|
|
105
|
+
return Math.round((savings / yearlyFromMonthly) * 100);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate lookup key for checkout
|
|
110
|
+
*/
|
|
111
|
+
export function getPriceLookupKey(
|
|
112
|
+
slug: string,
|
|
113
|
+
interval: 'monthly' | 'yearly',
|
|
114
|
+
version = 1
|
|
115
|
+
): string {
|
|
116
|
+
return `${slug}_${interval}_v${version}`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Types
|
|
3
|
+
*
|
|
4
|
+
* Shared type definitions for Stripe configuration and billing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type StripeEnvironment = 'test' | 'live';
|
|
8
|
+
export type BillingInterval = 'monthly' | 'yearly';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stripe credentials for an environment
|
|
12
|
+
*/
|
|
13
|
+
export interface StripeCredentials {
|
|
14
|
+
secretKey: string;
|
|
15
|
+
publishableKey: string;
|
|
16
|
+
webhookSecret: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Price version for versioned pricing
|
|
21
|
+
*
|
|
22
|
+
* When you change prices, existing customers keep their old prices.
|
|
23
|
+
* New customers get the latest (isDefault: true) price.
|
|
24
|
+
*/
|
|
25
|
+
export interface PriceVersion {
|
|
26
|
+
/** Version number (increment for each price change) */
|
|
27
|
+
version: number;
|
|
28
|
+
|
|
29
|
+
/** Lookup key for Stripe (format: {slug}_{interval}_v{version}) */
|
|
30
|
+
lookupKey: string;
|
|
31
|
+
|
|
32
|
+
/** Price in cents */
|
|
33
|
+
amountCents: number;
|
|
34
|
+
|
|
35
|
+
/** Whether this is the current default price for new customers */
|
|
36
|
+
isDefault?: boolean;
|
|
37
|
+
|
|
38
|
+
/** Date when this price was deprecated */
|
|
39
|
+
deprecatedAt?: string;
|
|
40
|
+
|
|
41
|
+
/** Stripe price ID (populated by `prim stripe sync`) */
|
|
42
|
+
stripePriceId?: Record<StripeEnvironment, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Plan/feature limits
|
|
47
|
+
* Use -1 for unlimited
|
|
48
|
+
*/
|
|
49
|
+
export interface PlanLimits {
|
|
50
|
+
projects: number;
|
|
51
|
+
apiCalls: number;
|
|
52
|
+
storage: number;
|
|
53
|
+
seats: number;
|
|
54
|
+
[key: string]: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Product configuration
|
|
59
|
+
*/
|
|
60
|
+
export interface ProductConfig {
|
|
61
|
+
name: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
description: string;
|
|
64
|
+
features: string[];
|
|
65
|
+
limits: PlanLimits;
|
|
66
|
+
highlighted?: boolean;
|
|
67
|
+
|
|
68
|
+
/** Stripe product ID (populated by `prim stripe sync`) */
|
|
69
|
+
stripeProductId?: Record<StripeEnvironment, string>;
|
|
70
|
+
|
|
71
|
+
/** Versioned prices per billing interval */
|
|
72
|
+
prices: {
|
|
73
|
+
monthly: PriceVersion[];
|
|
74
|
+
yearly: PriceVersion[];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Usage meter configuration
|
|
80
|
+
*/
|
|
81
|
+
export interface MeterConfig {
|
|
82
|
+
displayName: string;
|
|
83
|
+
stripeMeterEventName: string;
|
|
84
|
+
aggregation: 'sum' | 'max' | 'last';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Customer portal configuration
|
|
89
|
+
*/
|
|
90
|
+
export interface CustomerPortalConfig {
|
|
91
|
+
allowCancellations: boolean;
|
|
92
|
+
allowPlanChanges: boolean;
|
|
93
|
+
allowPaymentMethodUpdates: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Full Stripe configuration
|
|
98
|
+
*/
|
|
99
|
+
export interface StripeConfig {
|
|
100
|
+
environments: Record<StripeEnvironment, StripeCredentials>;
|
|
101
|
+
products: Record<string, ProductConfig>;
|
|
102
|
+
meters?: Record<string, MeterConfig>;
|
|
103
|
+
customerPortal?: CustomerPortalConfig;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the default price for a product and interval
|
|
108
|
+
*/
|
|
109
|
+
export function getDefaultPrice(
|
|
110
|
+
product: ProductConfig,
|
|
111
|
+
interval: BillingInterval
|
|
112
|
+
): PriceVersion | undefined {
|
|
113
|
+
const prices = product.prices[interval];
|
|
114
|
+
return prices.find((p) => p.isDefault) || prices[prices.length - 1];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get price by lookup key from all products
|
|
119
|
+
*/
|
|
120
|
+
export function findPriceByLookupKey(
|
|
121
|
+
config: StripeConfig,
|
|
122
|
+
lookupKey: string
|
|
123
|
+
): { product: ProductConfig; price: PriceVersion; interval: BillingInterval } | undefined {
|
|
124
|
+
for (const product of Object.values(config.products)) {
|
|
125
|
+
for (const interval of ['monthly', 'yearly'] as const) {
|
|
126
|
+
const price = product.prices[interval].find((p) => p.lookupKey === lookupKey);
|
|
127
|
+
if (price) {
|
|
128
|
+
return { product, price, interval };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|