specweave 0.24.0 → 0.24.6

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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +55 -0
  2. package/CLAUDE.md +42 -0
  3. package/dist/src/cli/commands/init.d.ts.map +1 -1
  4. package/dist/src/cli/commands/init.js +80 -41
  5. package/dist/src/cli/commands/init.js.map +1 -1
  6. package/dist/src/cli/helpers/issue-tracker/types.d.ts +1 -1
  7. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  8. package/dist/src/config/types.d.ts +24 -24
  9. package/dist/src/core/config/types.d.ts +25 -0
  10. package/dist/src/core/config/types.d.ts.map +1 -1
  11. package/dist/src/core/config/types.js +6 -0
  12. package/dist/src/core/config/types.js.map +1 -1
  13. package/dist/src/core/repo-structure/repo-bulk-discovery.d.ts +33 -0
  14. package/dist/src/core/repo-structure/repo-bulk-discovery.d.ts.map +1 -0
  15. package/dist/src/core/repo-structure/repo-bulk-discovery.js +275 -0
  16. package/dist/src/core/repo-structure/repo-bulk-discovery.js.map +1 -0
  17. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +9 -0
  18. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  19. package/dist/src/core/repo-structure/repo-structure-manager.js +255 -87
  20. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  21. package/dist/src/init/architecture/types.d.ts +6 -6
  22. package/dist/src/utils/plugin-validator.d.ts.map +1 -1
  23. package/dist/src/utils/plugin-validator.js +15 -14
  24. package/dist/src/utils/plugin-validator.js.map +1 -1
  25. package/package.json +4 -4
  26. package/plugins/specweave/.claude-plugin/plugin.json +4 -4
  27. package/plugins/specweave/agents/pm/AGENT.md +2 -0
  28. package/plugins/specweave/commands/specweave-do.md +0 -47
  29. package/plugins/specweave/commands/specweave-increment.md +0 -82
  30. package/plugins/specweave/commands/specweave-next.md +0 -47
  31. package/plugins/specweave/hooks/post-task-completion.sh +67 -6
  32. package/plugins/specweave/hooks/pre-edit-spec.sh +11 -0
  33. package/plugins/specweave/hooks/pre-task-completion.sh +69 -2
  34. package/plugins/specweave/hooks/pre-write-spec.sh +11 -0
  35. package/plugins/specweave/skills/increment-planner/SKILL.md +124 -4
  36. package/plugins/specweave-frontend/agents/frontend-architect/AGENT.md +21 -0
  37. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +150 -0
  38. package/plugins/specweave-payments/commands/stripe-setup.md +931 -0
  39. package/plugins/specweave-payments/commands/subscription-flow.md +1193 -0
  40. package/plugins/specweave-payments/commands/subscription-manage.md +386 -0
  41. package/plugins/specweave-payments/commands/webhook-setup.md +295 -0
  42. package/plugins/specweave-testing/agents/qa-engineer/AGENT.md +21 -0
@@ -0,0 +1,1193 @@
1
+ # /specweave-payments:subscription-flow
2
+
3
+ Complete subscription billing implementation guide with pricing tiers, trials, upgrades/downgrades, and lifecycle management.
4
+
5
+ You are a subscription billing expert who designs and implements SaaS recurring revenue systems.
6
+
7
+ ## Your Task
8
+
9
+ Implement complete subscription billing with multiple tiers, trial periods, proration, cancellation handling, and customer portal.
10
+
11
+ ### 1. Subscription Architecture
12
+
13
+ **Subscription Components**:
14
+
15
+ ```
16
+ Product (e.g., "Pro Plan")
17
+ ├─ Price (Monthly: $29)
18
+ ├─ Price (Yearly: $290, 16% discount)
19
+ └─ Features (API access, 10 users, Priority support)
20
+
21
+ Customer
22
+ ├─ Subscription (Active)
23
+ │ ├─ Items (Price ID, Quantity)
24
+ │ ├─ Current Period (Start/End)
25
+ │ └─ Payment Method (Card)
26
+ └─ Invoices (History)
27
+ ```
28
+
29
+ **Subscription States**:
30
+ ```
31
+ trialing → active → past_due → canceled
32
+
33
+ paused → resumed
34
+
35
+ incomplete → incomplete_expired
36
+ ```
37
+
38
+ ### 2. Product and Pricing Setup
39
+
40
+ **Define Pricing Tiers**:
41
+
42
+ ```typescript
43
+ // src/config/subscription-plans.ts
44
+ export interface SubscriptionPlan {
45
+ id: string;
46
+ name: string;
47
+ description: string;
48
+ features: string[];
49
+ stripePriceIds: {
50
+ monthly: string;
51
+ yearly: string;
52
+ };
53
+ prices: {
54
+ monthly: number; // in cents
55
+ yearly: number;
56
+ };
57
+ limits: {
58
+ users?: number;
59
+ apiCalls?: number;
60
+ storage?: number; // in GB
61
+ };
62
+ popular?: boolean;
63
+ }
64
+
65
+ export const SUBSCRIPTION_PLANS: SubscriptionPlan[] = [
66
+ {
67
+ id: 'free',
68
+ name: 'Free',
69
+ description: 'Perfect for trying out our service',
70
+ features: [
71
+ 'Up to 2 users',
72
+ '1,000 API calls/month',
73
+ '1 GB storage',
74
+ 'Community support',
75
+ ],
76
+ stripePriceIds: {
77
+ monthly: '', // No Stripe price for free tier
78
+ yearly: '',
79
+ },
80
+ prices: {
81
+ monthly: 0,
82
+ yearly: 0,
83
+ },
84
+ limits: {
85
+ users: 2,
86
+ apiCalls: 1000,
87
+ storage: 1,
88
+ },
89
+ },
90
+ {
91
+ id: 'starter',
92
+ name: 'Starter',
93
+ description: 'Great for small teams getting started',
94
+ features: [
95
+ 'Up to 5 users',
96
+ '10,000 API calls/month',
97
+ '10 GB storage',
98
+ 'Email support',
99
+ 'Basic analytics',
100
+ ],
101
+ stripePriceIds: {
102
+ monthly: 'price_starter_monthly_xxx',
103
+ yearly: 'price_starter_yearly_xxx',
104
+ },
105
+ prices: {
106
+ monthly: 2900, // $29
107
+ yearly: 29000, // $290 (16% discount)
108
+ },
109
+ limits: {
110
+ users: 5,
111
+ apiCalls: 10000,
112
+ storage: 10,
113
+ },
114
+ },
115
+ {
116
+ id: 'pro',
117
+ name: 'Pro',
118
+ description: 'For growing teams with advanced needs',
119
+ features: [
120
+ 'Up to 20 users',
121
+ '100,000 API calls/month',
122
+ '100 GB storage',
123
+ 'Priority support',
124
+ 'Advanced analytics',
125
+ 'Custom integrations',
126
+ ],
127
+ stripePriceIds: {
128
+ monthly: 'price_pro_monthly_xxx',
129
+ yearly: 'price_pro_yearly_xxx',
130
+ },
131
+ prices: {
132
+ monthly: 9900, // $99
133
+ yearly: 99000, // $990 (16% discount)
134
+ },
135
+ limits: {
136
+ users: 20,
137
+ apiCalls: 100000,
138
+ storage: 100,
139
+ },
140
+ popular: true,
141
+ },
142
+ {
143
+ id: 'enterprise',
144
+ name: 'Enterprise',
145
+ description: 'Custom solutions for large organizations',
146
+ features: [
147
+ 'Unlimited users',
148
+ 'Unlimited API calls',
149
+ 'Unlimited storage',
150
+ 'Dedicated support',
151
+ 'SLA guarantees',
152
+ 'Custom contracts',
153
+ 'On-premise deployment',
154
+ ],
155
+ stripePriceIds: {
156
+ monthly: 'price_enterprise_monthly_xxx',
157
+ yearly: 'price_enterprise_yearly_xxx',
158
+ },
159
+ prices: {
160
+ monthly: 49900, // $499
161
+ yearly: 499000, // $4,990 (16% discount)
162
+ },
163
+ limits: {
164
+ users: undefined, // unlimited
165
+ apiCalls: undefined,
166
+ storage: undefined,
167
+ },
168
+ },
169
+ ];
170
+
171
+ export function getPlanById(planId: string): SubscriptionPlan | undefined {
172
+ return SUBSCRIPTION_PLANS.find((plan) => plan.id === planId);
173
+ }
174
+
175
+ export function getPlanByPriceId(priceId: string): SubscriptionPlan | undefined {
176
+ return SUBSCRIPTION_PLANS.find(
177
+ (plan) =>
178
+ plan.stripePriceIds.monthly === priceId ||
179
+ plan.stripePriceIds.yearly === priceId
180
+ );
181
+ }
182
+ ```
183
+
184
+ ### 3. Subscription Service
185
+
186
+ **Subscription Management**:
187
+
188
+ ```typescript
189
+ // src/services/subscription.service.ts
190
+ import { stripe } from '../config/stripe';
191
+ import type Stripe from 'stripe';
192
+ import { getPlanById } from '../config/subscription-plans';
193
+
194
+ export class SubscriptionService {
195
+ /**
196
+ * Create a subscription with trial period
197
+ */
198
+ async createSubscription(params: {
199
+ customerId: string;
200
+ priceId: string;
201
+ trialDays?: number;
202
+ quantity?: number;
203
+ couponId?: string;
204
+ metadata?: Record<string, string>;
205
+ }): Promise<Stripe.Subscription> {
206
+ try {
207
+ const subscriptionParams: Stripe.SubscriptionCreateParams = {
208
+ customer: params.customerId,
209
+ items: [
210
+ {
211
+ price: params.priceId,
212
+ quantity: params.quantity || 1,
213
+ },
214
+ ],
215
+ payment_behavior: 'default_incomplete',
216
+ payment_settings: {
217
+ save_default_payment_method: 'on_subscription',
218
+ },
219
+ expand: ['latest_invoice.payment_intent'],
220
+ metadata: params.metadata,
221
+ };
222
+
223
+ // Add trial period if specified
224
+ if (params.trialDays && params.trialDays > 0) {
225
+ subscriptionParams.trial_period_days = params.trialDays;
226
+ }
227
+
228
+ // Add coupon if specified
229
+ if (params.couponId) {
230
+ subscriptionParams.coupon = params.couponId;
231
+ }
232
+
233
+ const subscription = await stripe.subscriptions.create(subscriptionParams);
234
+
235
+ return subscription;
236
+ } catch (error) {
237
+ console.error('Failed to create subscription:', error);
238
+ throw new Error('Subscription creation failed');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Create subscription with checkout session
244
+ */
245
+ async createSubscriptionCheckout(params: {
246
+ customerId?: string;
247
+ customerEmail?: string;
248
+ priceId: string;
249
+ trialDays?: number;
250
+ successUrl: string;
251
+ cancelUrl: string;
252
+ metadata?: Record<string, string>;
253
+ }): Promise<Stripe.Checkout.Session> {
254
+ try {
255
+ const sessionParams: Stripe.Checkout.SessionCreateParams = {
256
+ mode: 'subscription',
257
+ line_items: [
258
+ {
259
+ price: params.priceId,
260
+ quantity: 1,
261
+ },
262
+ ],
263
+ success_url: params.successUrl,
264
+ cancel_url: params.cancelUrl,
265
+ metadata: params.metadata,
266
+ };
267
+
268
+ // Customer reference
269
+ if (params.customerId) {
270
+ sessionParams.customer = params.customerId;
271
+ } else if (params.customerEmail) {
272
+ sessionParams.customer_email = params.customerEmail;
273
+ }
274
+
275
+ // Trial period
276
+ if (params.trialDays && params.trialDays > 0) {
277
+ sessionParams.subscription_data = {
278
+ trial_period_days: params.trialDays,
279
+ };
280
+ }
281
+
282
+ const session = await stripe.checkout.sessions.create(sessionParams);
283
+
284
+ return session;
285
+ } catch (error) {
286
+ console.error('Failed to create subscription checkout:', error);
287
+ throw new Error('Checkout creation failed');
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Retrieve subscription details
293
+ */
294
+ async getSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
295
+ try {
296
+ return await stripe.subscriptions.retrieve(subscriptionId, {
297
+ expand: ['customer', 'default_payment_method', 'latest_invoice'],
298
+ });
299
+ } catch (error) {
300
+ console.error('Failed to retrieve subscription:', error);
301
+ throw new Error('Subscription retrieval failed');
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Update subscription (upgrade/downgrade)
307
+ */
308
+ async updateSubscription(params: {
309
+ subscriptionId: string;
310
+ newPriceId: string;
311
+ prorationBehavior?: 'create_prorations' | 'none' | 'always_invoice';
312
+ quantity?: number;
313
+ }): Promise<Stripe.Subscription> {
314
+ try {
315
+ // Get current subscription
316
+ const subscription = await stripe.subscriptions.retrieve(params.subscriptionId);
317
+
318
+ // Update subscription
319
+ const updated = await stripe.subscriptions.update(params.subscriptionId, {
320
+ items: [
321
+ {
322
+ id: subscription.items.data[0].id,
323
+ price: params.newPriceId,
324
+ quantity: params.quantity,
325
+ },
326
+ ],
327
+ proration_behavior: params.prorationBehavior || 'create_prorations',
328
+ });
329
+
330
+ return updated;
331
+ } catch (error) {
332
+ console.error('Failed to update subscription:', error);
333
+ throw new Error('Subscription update failed');
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Cancel subscription (immediate or at period end)
339
+ */
340
+ async cancelSubscription(params: {
341
+ subscriptionId: string;
342
+ immediately?: boolean;
343
+ cancellationReason?: string;
344
+ }): Promise<Stripe.Subscription> {
345
+ try {
346
+ if (params.immediately) {
347
+ // Cancel immediately
348
+ return await stripe.subscriptions.cancel(params.subscriptionId, {
349
+ cancellation_details: {
350
+ comment: params.cancellationReason,
351
+ },
352
+ });
353
+ } else {
354
+ // Cancel at period end
355
+ return await stripe.subscriptions.update(params.subscriptionId, {
356
+ cancel_at_period_end: true,
357
+ cancellation_details: {
358
+ comment: params.cancellationReason,
359
+ },
360
+ });
361
+ }
362
+ } catch (error) {
363
+ console.error('Failed to cancel subscription:', error);
364
+ throw new Error('Subscription cancellation failed');
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Resume a canceled subscription
370
+ */
371
+ async resumeSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
372
+ try {
373
+ return await stripe.subscriptions.update(subscriptionId, {
374
+ cancel_at_period_end: false,
375
+ });
376
+ } catch (error) {
377
+ console.error('Failed to resume subscription:', error);
378
+ throw new Error('Subscription resume failed');
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Pause subscription
384
+ */
385
+ async pauseSubscription(params: {
386
+ subscriptionId: string;
387
+ resumeAt?: number; // Unix timestamp
388
+ }): Promise<Stripe.Subscription> {
389
+ try {
390
+ return await stripe.subscriptions.update(params.subscriptionId, {
391
+ pause_collection: {
392
+ behavior: 'void',
393
+ resumes_at: params.resumeAt,
394
+ },
395
+ });
396
+ } catch (error) {
397
+ console.error('Failed to pause subscription:', error);
398
+ throw new Error('Subscription pause failed');
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Resume paused subscription
404
+ */
405
+ async unpauseSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
406
+ try {
407
+ return await stripe.subscriptions.update(subscriptionId, {
408
+ pause_collection: null as any,
409
+ });
410
+ } catch (error) {
411
+ console.error('Failed to unpause subscription:', error);
412
+ throw new Error('Subscription unpause failed');
413
+ }
414
+ }
415
+
416
+ /**
417
+ * List customer subscriptions
418
+ */
419
+ async listCustomerSubscriptions(
420
+ customerId: string
421
+ ): Promise<Stripe.Subscription[]> {
422
+ try {
423
+ const subscriptions = await stripe.subscriptions.list({
424
+ customer: customerId,
425
+ status: 'all',
426
+ expand: ['data.default_payment_method'],
427
+ });
428
+
429
+ return subscriptions.data;
430
+ } catch (error) {
431
+ console.error('Failed to list subscriptions:', error);
432
+ throw new Error('Subscription listing failed');
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Get upcoming invoice (preview charges)
438
+ */
439
+ async getUpcomingInvoice(params: {
440
+ customerId: string;
441
+ subscriptionId: string;
442
+ newPriceId?: string;
443
+ }): Promise<Stripe.Invoice> {
444
+ try {
445
+ const invoiceParams: Stripe.InvoiceRetrieveUpcomingParams = {
446
+ customer: params.customerId,
447
+ subscription: params.subscriptionId,
448
+ };
449
+
450
+ // Preview plan change
451
+ if (params.newPriceId) {
452
+ const subscription = await stripe.subscriptions.retrieve(params.subscriptionId);
453
+ invoiceParams.subscription_items = [
454
+ {
455
+ id: subscription.items.data[0].id,
456
+ price: params.newPriceId,
457
+ },
458
+ ];
459
+ }
460
+
461
+ return await stripe.invoices.retrieveUpcoming(invoiceParams);
462
+ } catch (error) {
463
+ console.error('Failed to retrieve upcoming invoice:', error);
464
+ throw new Error('Invoice preview failed');
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Create customer portal session
470
+ */
471
+ async createPortalSession(params: {
472
+ customerId: string;
473
+ returnUrl: string;
474
+ }): Promise<Stripe.BillingPortal.Session> {
475
+ try {
476
+ return await stripe.billingPortal.sessions.create({
477
+ customer: params.customerId,
478
+ return_url: params.returnUrl,
479
+ });
480
+ } catch (error) {
481
+ console.error('Failed to create portal session:', error);
482
+ throw new Error('Portal session creation failed');
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Apply coupon to subscription
488
+ */
489
+ async applyCoupon(
490
+ subscriptionId: string,
491
+ couponId: string
492
+ ): Promise<Stripe.Subscription> {
493
+ try {
494
+ return await stripe.subscriptions.update(subscriptionId, {
495
+ coupon: couponId,
496
+ });
497
+ } catch (error) {
498
+ console.error('Failed to apply coupon:', error);
499
+ throw new Error('Coupon application failed');
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Remove coupon from subscription
505
+ */
506
+ async removeCoupon(subscriptionId: string): Promise<Stripe.Subscription> {
507
+ try {
508
+ return await stripe.subscriptions.update(subscriptionId, {
509
+ coupon: '',
510
+ });
511
+ } catch (error) {
512
+ console.error('Failed to remove coupon:', error);
513
+ throw new Error('Coupon removal failed');
514
+ }
515
+ }
516
+ }
517
+
518
+ export const subscriptionService = new SubscriptionService();
519
+ ```
520
+
521
+ ### 4. API Routes
522
+
523
+ **Subscription Endpoints**:
524
+
525
+ ```typescript
526
+ // src/routes/subscription.routes.ts
527
+ import { Router, Request, Response } from 'express';
528
+ import { subscriptionService } from '../services/subscription.service';
529
+
530
+ const router = Router();
531
+
532
+ /**
533
+ * POST /api/subscriptions
534
+ * Create a subscription
535
+ */
536
+ router.post('/', async (req: Request, res: Response) => {
537
+ try {
538
+ const { customerId, priceId, trialDays, quantity, couponId, metadata } = req.body;
539
+
540
+ if (!customerId || !priceId) {
541
+ return res.status(400).json({ error: 'Customer ID and Price ID required' });
542
+ }
543
+
544
+ const subscription = await subscriptionService.createSubscription({
545
+ customerId,
546
+ priceId,
547
+ trialDays,
548
+ quantity,
549
+ couponId,
550
+ metadata,
551
+ });
552
+
553
+ res.json({
554
+ subscriptionId: subscription.id,
555
+ status: subscription.status,
556
+ clientSecret: (subscription.latest_invoice as any)?.payment_intent
557
+ ?.client_secret,
558
+ });
559
+ } catch (error) {
560
+ console.error('Subscription creation error:', error);
561
+ res.status(500).json({ error: 'Failed to create subscription' });
562
+ }
563
+ });
564
+
565
+ /**
566
+ * POST /api/subscriptions/checkout
567
+ * Create subscription checkout session
568
+ */
569
+ router.post('/checkout', async (req: Request, res: Response) => {
570
+ try {
571
+ const { customerId, customerEmail, priceId, trialDays, metadata } = req.body;
572
+
573
+ if (!priceId) {
574
+ return res.status(400).json({ error: 'Price ID required' });
575
+ }
576
+
577
+ const session = await subscriptionService.createSubscriptionCheckout({
578
+ customerId,
579
+ customerEmail,
580
+ priceId,
581
+ trialDays,
582
+ successUrl: `${req.headers.origin}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
583
+ cancelUrl: `${req.headers.origin}/subscription/cancel`,
584
+ metadata,
585
+ });
586
+
587
+ res.json({ sessionId: session.id, url: session.url });
588
+ } catch (error) {
589
+ console.error('Checkout creation error:', error);
590
+ res.status(500).json({ error: 'Failed to create checkout' });
591
+ }
592
+ });
593
+
594
+ /**
595
+ * GET /api/subscriptions/:id
596
+ * Get subscription details
597
+ */
598
+ router.get('/:id', async (req: Request, res: Response) => {
599
+ try {
600
+ const subscription = await subscriptionService.getSubscription(req.params.id);
601
+ res.json(subscription);
602
+ } catch (error) {
603
+ console.error('Subscription retrieval error:', error);
604
+ res.status(500).json({ error: 'Failed to retrieve subscription' });
605
+ }
606
+ });
607
+
608
+ /**
609
+ * PATCH /api/subscriptions/:id
610
+ * Update subscription (upgrade/downgrade)
611
+ */
612
+ router.patch('/:id', async (req: Request, res: Response) => {
613
+ try {
614
+ const { newPriceId, quantity, prorationBehavior } = req.body;
615
+
616
+ if (!newPriceId) {
617
+ return res.status(400).json({ error: 'New price ID required' });
618
+ }
619
+
620
+ const subscription = await subscriptionService.updateSubscription({
621
+ subscriptionId: req.params.id,
622
+ newPriceId,
623
+ quantity,
624
+ prorationBehavior,
625
+ });
626
+
627
+ res.json(subscription);
628
+ } catch (error) {
629
+ console.error('Subscription update error:', error);
630
+ res.status(500).json({ error: 'Failed to update subscription' });
631
+ }
632
+ });
633
+
634
+ /**
635
+ * DELETE /api/subscriptions/:id
636
+ * Cancel subscription
637
+ */
638
+ router.delete('/:id', async (req: Request, res: Response) => {
639
+ try {
640
+ const { immediately, reason } = req.body;
641
+
642
+ const subscription = await subscriptionService.cancelSubscription({
643
+ subscriptionId: req.params.id,
644
+ immediately,
645
+ cancellationReason: reason,
646
+ });
647
+
648
+ res.json(subscription);
649
+ } catch (error) {
650
+ console.error('Subscription cancellation error:', error);
651
+ res.status(500).json({ error: 'Failed to cancel subscription' });
652
+ }
653
+ });
654
+
655
+ /**
656
+ * POST /api/subscriptions/:id/resume
657
+ * Resume canceled subscription
658
+ */
659
+ router.post('/:id/resume', async (req: Request, res: Response) => {
660
+ try {
661
+ const subscription = await subscriptionService.resumeSubscription(req.params.id);
662
+ res.json(subscription);
663
+ } catch (error) {
664
+ console.error('Subscription resume error:', error);
665
+ res.status(500).json({ error: 'Failed to resume subscription' });
666
+ }
667
+ });
668
+
669
+ /**
670
+ * POST /api/subscriptions/:id/pause
671
+ * Pause subscription
672
+ */
673
+ router.post('/:id/pause', async (req: Request, res: Response) => {
674
+ try {
675
+ const { resumeAt } = req.body;
676
+
677
+ const subscription = await subscriptionService.pauseSubscription({
678
+ subscriptionId: req.params.id,
679
+ resumeAt,
680
+ });
681
+
682
+ res.json(subscription);
683
+ } catch (error) {
684
+ console.error('Subscription pause error:', error);
685
+ res.status(500).json({ error: 'Failed to pause subscription' });
686
+ }
687
+ });
688
+
689
+ /**
690
+ * POST /api/subscriptions/:id/unpause
691
+ * Resume paused subscription
692
+ */
693
+ router.post('/:id/unpause', async (req: Request, res: Response) => {
694
+ try {
695
+ const subscription = await subscriptionService.unpauseSubscription(req.params.id);
696
+ res.json(subscription);
697
+ } catch (error) {
698
+ console.error('Subscription unpause error:', error);
699
+ res.status(500).json({ error: 'Failed to unpause subscription' });
700
+ }
701
+ });
702
+
703
+ /**
704
+ * GET /api/subscriptions/customer/:customerId
705
+ * List customer subscriptions
706
+ */
707
+ router.get('/customer/:customerId', async (req: Request, res: Response) => {
708
+ try {
709
+ const subscriptions = await subscriptionService.listCustomerSubscriptions(
710
+ req.params.customerId
711
+ );
712
+ res.json(subscriptions);
713
+ } catch (error) {
714
+ console.error('Subscription listing error:', error);
715
+ res.status(500).json({ error: 'Failed to list subscriptions' });
716
+ }
717
+ });
718
+
719
+ /**
720
+ * GET /api/subscriptions/:id/upcoming-invoice
721
+ * Preview upcoming invoice
722
+ */
723
+ router.get('/:id/upcoming-invoice', async (req: Request, res: Response) => {
724
+ try {
725
+ const { customerId, newPriceId } = req.query;
726
+
727
+ if (!customerId) {
728
+ return res.status(400).json({ error: 'Customer ID required' });
729
+ }
730
+
731
+ const invoice = await subscriptionService.getUpcomingInvoice({
732
+ customerId: customerId as string,
733
+ subscriptionId: req.params.id,
734
+ newPriceId: newPriceId as string,
735
+ });
736
+
737
+ res.json(invoice);
738
+ } catch (error) {
739
+ console.error('Invoice preview error:', error);
740
+ res.status(500).json({ error: 'Failed to preview invoice' });
741
+ }
742
+ });
743
+
744
+ /**
745
+ * POST /api/subscriptions/portal
746
+ * Create customer portal session
747
+ */
748
+ router.post('/portal', async (req: Request, res: Response) => {
749
+ try {
750
+ const { customerId } = req.body;
751
+
752
+ if (!customerId) {
753
+ return res.status(400).json({ error: 'Customer ID required' });
754
+ }
755
+
756
+ const session = await subscriptionService.createPortalSession({
757
+ customerId,
758
+ returnUrl: `${req.headers.origin}/account`,
759
+ });
760
+
761
+ res.json({ url: session.url });
762
+ } catch (error) {
763
+ console.error('Portal session error:', error);
764
+ res.status(500).json({ error: 'Failed to create portal session' });
765
+ }
766
+ });
767
+
768
+ /**
769
+ * POST /api/subscriptions/:id/coupon
770
+ * Apply coupon to subscription
771
+ */
772
+ router.post('/:id/coupon', async (req: Request, res: Response) => {
773
+ try {
774
+ const { couponId } = req.body;
775
+
776
+ if (!couponId) {
777
+ return res.status(400).json({ error: 'Coupon ID required' });
778
+ }
779
+
780
+ const subscription = await subscriptionService.applyCoupon(
781
+ req.params.id,
782
+ couponId
783
+ );
784
+
785
+ res.json(subscription);
786
+ } catch (error) {
787
+ console.error('Coupon application error:', error);
788
+ res.status(500).json({ error: 'Failed to apply coupon' });
789
+ }
790
+ });
791
+
792
+ /**
793
+ * DELETE /api/subscriptions/:id/coupon
794
+ * Remove coupon from subscription
795
+ */
796
+ router.delete('/:id/coupon', async (req: Request, res: Response) => {
797
+ try {
798
+ const subscription = await subscriptionService.removeCoupon(req.params.id);
799
+ res.json(subscription);
800
+ } catch (error) {
801
+ console.error('Coupon removal error:', error);
802
+ res.status(500).json({ error: 'Failed to remove coupon' });
803
+ }
804
+ });
805
+
806
+ export default router;
807
+ ```
808
+
809
+ ### 5. Frontend Components
810
+
811
+ **Pricing Table**:
812
+
813
+ ```typescript
814
+ // src/components/PricingTable.tsx
815
+ import React from 'react';
816
+ import { SUBSCRIPTION_PLANS } from '../config/subscription-plans';
817
+
818
+ interface PricingTableProps {
819
+ billingCycle: 'monthly' | 'yearly';
820
+ onSelectPlan: (planId: string, priceId: string) => void;
821
+ }
822
+
823
+ export const PricingTable: React.FC<PricingTableProps> = ({
824
+ billingCycle,
825
+ onSelectPlan,
826
+ }) => {
827
+ return (
828
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 p-6">
829
+ {SUBSCRIPTION_PLANS.map((plan) => (
830
+ <div
831
+ key={plan.id}
832
+ className={`
833
+ relative border rounded-lg p-6 flex flex-col
834
+ ${plan.popular ? 'border-blue-500 shadow-lg' : 'border-gray-200'}
835
+ `}
836
+ >
837
+ {plan.popular && (
838
+ <span className="absolute top-0 right-0 bg-blue-500 text-white text-xs px-3 py-1 rounded-bl-lg rounded-tr-lg">
839
+ Popular
840
+ </span>
841
+ )}
842
+
843
+ <h3 className="text-2xl font-bold text-gray-900">{plan.name}</h3>
844
+ <p className="mt-2 text-gray-600 text-sm">{plan.description}</p>
845
+
846
+ <div className="mt-6">
847
+ <span className="text-4xl font-bold text-gray-900">
848
+ ${plan.prices[billingCycle] / 100}
849
+ </span>
850
+ <span className="text-gray-600">/{billingCycle === 'yearly' ? 'year' : 'month'}</span>
851
+ {billingCycle === 'yearly' && plan.prices.yearly > 0 && (
852
+ <p className="text-sm text-green-600 mt-1">
853
+ Save ${(plan.prices.monthly * 12 - plan.prices.yearly) / 100}/year
854
+ </p>
855
+ )}
856
+ </div>
857
+
858
+ <ul className="mt-6 space-y-3 flex-grow">
859
+ {plan.features.map((feature, index) => (
860
+ <li key={index} className="flex items-start">
861
+ <svg
862
+ className="w-5 h-5 text-green-500 mr-2 flex-shrink-0"
863
+ fill="currentColor"
864
+ viewBox="0 0 20 20"
865
+ >
866
+ <path
867
+ fillRule="evenodd"
868
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
869
+ clipRule="evenodd"
870
+ />
871
+ </svg>
872
+ <span className="text-gray-700 text-sm">{feature}</span>
873
+ </li>
874
+ ))}
875
+ </ul>
876
+
877
+ <button
878
+ onClick={() =>
879
+ onSelectPlan(plan.id, plan.stripePriceIds[billingCycle])
880
+ }
881
+ disabled={plan.id === 'free'}
882
+ className={`
883
+ mt-6 w-full py-3 px-4 rounded-lg font-medium transition-colors
884
+ ${
885
+ plan.popular
886
+ ? 'bg-blue-600 text-white hover:bg-blue-700'
887
+ : 'bg-gray-100 text-gray-900 hover:bg-gray-200'
888
+ }
889
+ ${plan.id === 'free' ? 'opacity-50 cursor-not-allowed' : ''}
890
+ `}
891
+ >
892
+ {plan.id === 'free' ? 'Current Plan' : 'Get Started'}
893
+ </button>
894
+ </div>
895
+ ))}
896
+ </div>
897
+ );
898
+ };
899
+ ```
900
+
901
+ **Subscription Management**:
902
+
903
+ ```typescript
904
+ // src/components/SubscriptionManager.tsx
905
+ import React, { useState, useEffect } from 'react';
906
+ import type Stripe from 'stripe';
907
+
908
+ interface SubscriptionManagerProps {
909
+ customerId: string;
910
+ }
911
+
912
+ export const SubscriptionManager: React.FC<SubscriptionManagerProps> = ({
913
+ customerId,
914
+ }) => {
915
+ const [subscriptions, setSubscriptions] = useState<Stripe.Subscription[]>([]);
916
+ const [loading, setLoading] = useState(true);
917
+
918
+ useEffect(() => {
919
+ loadSubscriptions();
920
+ }, [customerId]);
921
+
922
+ const loadSubscriptions = async () => {
923
+ try {
924
+ const response = await fetch(`/api/subscriptions/customer/${customerId}`);
925
+ const data = await response.json();
926
+ setSubscriptions(data);
927
+ } catch (error) {
928
+ console.error('Failed to load subscriptions:', error);
929
+ } finally {
930
+ setLoading(false);
931
+ }
932
+ };
933
+
934
+ const handleCancelSubscription = async (subscriptionId: string) => {
935
+ if (!confirm('Are you sure you want to cancel this subscription?')) {
936
+ return;
937
+ }
938
+
939
+ try {
940
+ await fetch(`/api/subscriptions/${subscriptionId}`, {
941
+ method: 'DELETE',
942
+ headers: { 'Content-Type': 'application/json' },
943
+ body: JSON.stringify({ immediately: false }),
944
+ });
945
+
946
+ await loadSubscriptions();
947
+ alert('Subscription will be canceled at the end of the billing period');
948
+ } catch (error) {
949
+ console.error('Failed to cancel subscription:', error);
950
+ alert('Failed to cancel subscription');
951
+ }
952
+ };
953
+
954
+ const handleResumeSubscription = async (subscriptionId: string) => {
955
+ try {
956
+ await fetch(`/api/subscriptions/${subscriptionId}/resume`, {
957
+ method: 'POST',
958
+ });
959
+
960
+ await loadSubscriptions();
961
+ alert('Subscription resumed successfully');
962
+ } catch (error) {
963
+ console.error('Failed to resume subscription:', error);
964
+ alert('Failed to resume subscription');
965
+ }
966
+ };
967
+
968
+ const handleManageBilling = async () => {
969
+ try {
970
+ const response = await fetch('/api/subscriptions/portal', {
971
+ method: 'POST',
972
+ headers: { 'Content-Type': 'application/json' },
973
+ body: JSON.stringify({ customerId }),
974
+ });
975
+
976
+ const { url } = await response.json();
977
+ window.location.href = url;
978
+ } catch (error) {
979
+ console.error('Failed to open billing portal:', error);
980
+ alert('Failed to open billing portal');
981
+ }
982
+ };
983
+
984
+ if (loading) {
985
+ return <div>Loading subscriptions...</div>;
986
+ }
987
+
988
+ return (
989
+ <div className="max-w-4xl mx-auto p-6">
990
+ <div className="flex justify-between items-center mb-6">
991
+ <h2 className="text-2xl font-bold">Your Subscriptions</h2>
992
+ <button
993
+ onClick={handleManageBilling}
994
+ className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
995
+ >
996
+ Manage Billing
997
+ </button>
998
+ </div>
999
+
1000
+ <div className="space-y-4">
1001
+ {subscriptions.map((subscription) => (
1002
+ <div
1003
+ key={subscription.id}
1004
+ className="border border-gray-200 rounded-lg p-6"
1005
+ >
1006
+ <div className="flex justify-between items-start">
1007
+ <div>
1008
+ <h3 className="text-lg font-semibold">
1009
+ {subscription.items.data[0].price.product as string}
1010
+ </h3>
1011
+ <p className="text-gray-600 mt-1">
1012
+ ${subscription.items.data[0].price.unit_amount! / 100}/
1013
+ {subscription.items.data[0].price.recurring?.interval}
1014
+ </p>
1015
+ <p className="text-sm text-gray-500 mt-2">
1016
+ Status:{' '}
1017
+ <span
1018
+ className={`
1019
+ font-medium
1020
+ ${subscription.status === 'active' ? 'text-green-600' : ''}
1021
+ ${subscription.status === 'trialing' ? 'text-blue-600' : ''}
1022
+ ${subscription.status === 'past_due' ? 'text-red-600' : ''}
1023
+ `}
1024
+ >
1025
+ {subscription.status}
1026
+ </span>
1027
+ </p>
1028
+ {subscription.cancel_at_period_end && (
1029
+ <p className="text-sm text-orange-600 mt-1">
1030
+ Cancels on{' '}
1031
+ {new Date(subscription.current_period_end * 1000).toLocaleDateString()}
1032
+ </p>
1033
+ )}
1034
+ </div>
1035
+
1036
+ <div className="flex gap-2">
1037
+ {subscription.cancel_at_period_end ? (
1038
+ <button
1039
+ onClick={() => handleResumeSubscription(subscription.id)}
1040
+ className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
1041
+ >
1042
+ Resume
1043
+ </button>
1044
+ ) : (
1045
+ <button
1046
+ onClick={() => handleCancelSubscription(subscription.id)}
1047
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
1048
+ >
1049
+ Cancel
1050
+ </button>
1051
+ )}
1052
+ </div>
1053
+ </div>
1054
+ </div>
1055
+ ))}
1056
+
1057
+ {subscriptions.length === 0 && (
1058
+ <p className="text-gray-600 text-center py-8">
1059
+ You don't have any active subscriptions
1060
+ </p>
1061
+ )}
1062
+ </div>
1063
+ </div>
1064
+ );
1065
+ };
1066
+ ```
1067
+
1068
+ ### 6. Webhook Handling
1069
+
1070
+ **Subscription Events**:
1071
+
1072
+ ```typescript
1073
+ // src/webhooks/subscription.webhook.ts
1074
+ import type Stripe from 'stripe';
1075
+
1076
+ export async function handleSubscriptionCreated(
1077
+ subscription: Stripe.Subscription
1078
+ ): Promise<void> {
1079
+ console.log('Subscription created:', subscription.id);
1080
+
1081
+ // Update database
1082
+ // await db.subscriptions.create({
1083
+ // stripeSubscriptionId: subscription.id,
1084
+ // customerId: subscription.customer,
1085
+ // status: subscription.status,
1086
+ // currentPeriodEnd: new Date(subscription.current_period_end * 1000),
1087
+ // });
1088
+
1089
+ // Send welcome email
1090
+ }
1091
+
1092
+ export async function handleSubscriptionUpdated(
1093
+ subscription: Stripe.Subscription
1094
+ ): Promise<void> {
1095
+ console.log('Subscription updated:', subscription.id);
1096
+
1097
+ // Update database
1098
+ // await db.subscriptions.update({
1099
+ // where: { stripeSubscriptionId: subscription.id },
1100
+ // data: {
1101
+ // status: subscription.status,
1102
+ // currentPeriodEnd: new Date(subscription.current_period_end * 1000),
1103
+ // },
1104
+ // });
1105
+
1106
+ // Handle status changes
1107
+ if (subscription.status === 'past_due') {
1108
+ // Send payment failed email
1109
+ }
1110
+ }
1111
+
1112
+ export async function handleSubscriptionDeleted(
1113
+ subscription: Stripe.Subscription
1114
+ ): Promise<void> {
1115
+ console.log('Subscription deleted:', subscription.id);
1116
+
1117
+ // Update database
1118
+ // await db.subscriptions.update({
1119
+ // where: { stripeSubscriptionId: subscription.id },
1120
+ // data: {
1121
+ // status: 'canceled',
1122
+ // canceledAt: new Date(),
1123
+ // },
1124
+ // });
1125
+
1126
+ // Revoke access
1127
+ // Send cancellation confirmation email
1128
+ }
1129
+
1130
+ export async function handleInvoicePaymentSucceeded(
1131
+ invoice: Stripe.Invoice
1132
+ ): Promise<void> {
1133
+ console.log('Invoice payment succeeded:', invoice.id);
1134
+
1135
+ // Record payment
1136
+ // Send receipt
1137
+ }
1138
+
1139
+ export async function handleInvoicePaymentFailed(
1140
+ invoice: Stripe.Invoice
1141
+ ): Promise<void> {
1142
+ console.log('Invoice payment failed:', invoice.id);
1143
+
1144
+ // Send payment failed notification
1145
+ // Implement dunning management
1146
+ }
1147
+ ```
1148
+
1149
+ ## Output Deliverables
1150
+
1151
+ When you complete this implementation, provide:
1152
+
1153
+ 1. **Configuration**:
1154
+ - Subscription plans with pricing tiers
1155
+ - Stripe product and price IDs
1156
+ - Trial period settings
1157
+
1158
+ 2. **Backend Services**:
1159
+ - Subscription service with all operations
1160
+ - API routes for subscription management
1161
+ - Webhook handlers for subscription events
1162
+
1163
+ 3. **Frontend Components**:
1164
+ - Pricing table with plan comparison
1165
+ - Subscription management dashboard
1166
+ - Plan upgrade/downgrade UI
1167
+
1168
+ 4. **Documentation**:
1169
+ - Subscription lifecycle diagram
1170
+ - Upgrade/downgrade flow
1171
+ - Proration explanation
1172
+ - Cancellation policy
1173
+
1174
+ 5. **Testing**:
1175
+ - Subscription creation tests
1176
+ - Plan change tests
1177
+ - Cancellation tests
1178
+ - Trial period tests
1179
+
1180
+ ## Best Practices
1181
+
1182
+ 1. **Always use proration** for mid-cycle changes
1183
+ 2. **Implement trials** to reduce friction
1184
+ 3. **Allow cancellation at period end** (not immediately)
1185
+ 4. **Use customer portal** for self-service
1186
+ 5. **Send clear email notifications** for all subscription events
1187
+ 6. **Handle failed payments gracefully** with retry logic
1188
+ 7. **Preview charges** before plan changes
1189
+ 8. **Track subscription metrics** (MRR, churn, LTV)
1190
+ 9. **Offer annual discounts** to improve retention
1191
+ 10. **Make downgrades easy** to reduce immediate cancellations
1192
+
1193
+ Subscriptions are the foundation of SaaS revenue. Implement them robustly with clear communication and excellent UX.