kybernus 2.2.1 → 2.3.0

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 (49) hide show
  1. package/package.json +1 -1
  2. package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +89 -0
  3. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/web/payment/PaymentController.java.hbs +78 -0
  4. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +7 -0
  5. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/inbound/web/PaymentController.java.hbs +78 -0
  6. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/stripe/StripeAdapter.java.hbs +76 -0
  7. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +90 -0
  8. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +7 -0
  9. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +42 -53
  10. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +105 -23
  11. package/templates/nestjs/clean/src/app.module.ts.hbs +3 -1
  12. package/templates/nestjs/clean/src/application/payment.service.ts.hbs +90 -0
  13. package/templates/nestjs/clean/src/infrastructure/http/payment.controller.ts.hbs +46 -0
  14. package/templates/nestjs/clean/src/infrastructure/stripe.provider.ts.hbs +51 -0
  15. package/templates/nestjs/clean/src/main.ts.hbs +13 -4
  16. package/templates/nestjs/clean/src/payment.module.ts.hbs +23 -0
  17. package/templates/nestjs/hexagonal/src/adapters/inbound/payment.controller.ts.hbs +46 -0
  18. package/templates/nestjs/hexagonal/src/adapters/outbound/stripe.adapter.ts.hbs +54 -0
  19. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -0
  20. package/templates/nestjs/hexagonal/src/core/payment.service.ts.hbs +90 -0
  21. package/templates/nestjs/hexagonal/src/main.ts.hbs +13 -4
  22. package/templates/nestjs/hexagonal/src/payment.module.ts.hbs +23 -0
  23. package/templates/nestjs/mvc/src/main.ts.hbs +6 -3
  24. package/templates/nestjs/mvc/src/payments/payments.controller.ts.hbs +33 -8
  25. package/templates/nestjs/mvc/src/payments/payments.service.ts.hbs +66 -22
  26. package/templates/nextjs/mvc/src/app/api/checkout/route.ts.hbs +42 -13
  27. package/templates/nextjs/mvc/src/app/api/portal/route.ts.hbs +36 -0
  28. package/templates/nextjs/mvc/src/app/api/webhook/route.ts.hbs +32 -20
  29. package/templates/nodejs-express/clean/src/application/services/PaymentService.ts.hbs +98 -0
  30. package/templates/nodejs-express/clean/src/index.ts.hbs +29 -8
  31. package/templates/nodejs-express/clean/src/infrastructure/http/controllers/PaymentController.ts.hbs +57 -0
  32. package/templates/nodejs-express/clean/src/infrastructure/providers/StripeProvider.ts.hbs +45 -0
  33. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +57 -0
  34. package/templates/nodejs-express/hexagonal/src/adapters/outbound/StripeAdapter.ts.hbs +48 -0
  35. package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +89 -0
  36. package/templates/nodejs-express/hexagonal/src/index.ts.hbs +28 -8
  37. package/templates/nodejs-express/mvc/src/app.ts.hbs +11 -2
  38. package/templates/nodejs-express/mvc/src/controllers/payments.controller.ts.hbs +31 -47
  39. package/templates/nodejs-express/mvc/src/services/stripe.service.ts.hbs +66 -49
  40. package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +85 -0
  41. package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +64 -0
  42. package/templates/python-fastapi/clean/app/infrastructure/stripe_provider.py.hbs +44 -0
  43. package/templates/python-fastapi/clean/app/main.py.hbs +8 -5
  44. package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +64 -0
  45. package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +44 -0
  46. package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +81 -0
  47. package/templates/python-fastapi/hexagonal/app/main.py.hbs +9 -3
  48. package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +70 -35
  49. package/templates/python-fastapi/mvc/app/services/stripe_service.py.hbs +58 -0
@@ -0,0 +1,23 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { PaymentController } from './adapters/inbound/payment.controller';
4
+ import { PaymentService } from './core/payment.service';
5
+ import { StripeAdapter } from './adapters/outbound/stripe.adapter';
6
+ import { UserPort } from './core/ports/user.port';
7
+ import { PrismaUserAdapter } from './adapters/outbound/persistence/prisma.user.adapter';
8
+ import { PrismaService } from './adapters/outbound/persistence/prisma.service';
9
+
10
+ @Module({
11
+ imports: [ConfigModule],
12
+ controllers: [PaymentController],
13
+ providers: [
14
+ PaymentService,
15
+ StripeAdapter,
16
+ PrismaService,
17
+ {
18
+ provide: UserPort,
19
+ useClass: PrismaUserAdapter,
20
+ },
21
+ ],
22
+ })
23
+ export class PaymentModule {}
@@ -3,7 +3,10 @@ import { AppModule } from './app.module';
3
3
  import { ValidationPipe } from '@nestjs/common';
4
4
 
5
5
  async function bootstrap() {
6
- const app = await NestFactory.create(AppModule);
6
+ const app = await NestFactory.create(AppModule, {
7
+ // rawBody is required so that the Stripe webhook can verify the signature
8
+ rawBody: true,
9
+ });
7
10
 
8
11
  app.enableCors();
9
12
  app.setGlobalPrefix('api');
@@ -12,6 +15,6 @@ async function bootstrap() {
12
15
  const port = process.env.PORT || 3000;
13
16
  await app.listen(port);
14
17
 
15
- console.log(`🚀 {{pascalCase projectName}} running on port ${port}`);
18
+ console.log(`{{pascalCase projectName}} running on port ${port}`);
16
19
  }
17
- bootstrap();
20
+ bootstrap();
@@ -1,26 +1,51 @@
1
- import { Controller, Post, Body, Headers, UseGuards, RawBodyRequest, Req } from '@nestjs/common';
1
+ import { Controller, Post, Body, Headers, UseGuards, Req, Request, BadRequestException } from '@nestjs/common';
2
2
  import { AuthGuard } from '@nestjs/passport';
3
+ import { IsString } from 'class-validator';
3
4
  import { PaymentsService } from './payments.service';
4
- import { Request } from 'express';
5
5
 
6
6
  class CreateCheckoutDto {
7
+ @IsString()
7
8
  priceId: string;
8
- customerId?: string;
9
9
  }
10
10
 
11
11
  @Controller('payments')
12
12
  export class PaymentsController {
13
13
  constructor(private paymentsService: PaymentsService) {}
14
14
 
15
+ /**
16
+ * POST /api/payments/checkout
17
+ * Requires JWT authentication.
18
+ */
15
19
  @Post('checkout')
16
20
  @UseGuards(AuthGuard('jwt'))
17
- async createCheckout(@Body() dto: CreateCheckoutDto) {
18
- const session = await this.paymentsService.createCheckoutSession(dto.priceId, dto.customerId);
21
+ async createCheckout(@Req() req: Request, @Body() dto: CreateCheckoutDto) {
22
+ const userId = (req as any).user?.id;
23
+ const session = await this.paymentsService.createCheckoutSession(userId, dto.priceId);
19
24
  return { url: session.url };
20
25
  }
21
26
 
27
+ /**
28
+ * POST /api/payments/portal
29
+ * Requires JWT authentication.
30
+ */
31
+ @Post('portal')
32
+ @UseGuards(AuthGuard('jwt'))
33
+ async createPortal(@Req() req: Request) {
34
+ const userId = (req as any).user?.id;
35
+ const session = await this.paymentsService.createPortalSession(userId);
36
+ return { url: session.url };
37
+ }
38
+
39
+ /**
40
+ * POST /api/payments/webhook
41
+ * No authentication. Stripe sends a raw body – NestFactory must have rawBody: true.
42
+ */
22
43
  @Post('webhook')
23
- async webhook(@Req() req: RawBodyRequest<Request>, @Headers('stripe-signature') signature: string) {
24
- return this.paymentsService.handleWebhook(req.rawBody!, signature);
44
+ async webhook(
45
+ @Req() req: any,
46
+ @Headers('stripe-signature') signature: string,
47
+ ) {
48
+ if (!signature) throw new BadRequestException('Missing stripe-signature header');
49
+ return this.paymentsService.handleWebhook(req.rawBody, signature);
25
50
  }
26
- }
51
+ }
@@ -12,50 +12,94 @@ export class PaymentsService {
12
12
  private prisma: PrismaService,
13
13
  ) {
14
14
  this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY', ''), {
15
- apiVersion: '2024-12-18.acacia',
15
+ apiVersion: '2026-02-25.clover',
16
16
  });
17
17
  }
18
18
 
19
- async createCheckoutSession(priceId: string, customerId?: string) {
19
+ async createCheckoutSession(userId: string, priceId: string) {
20
+ const user = await this.prisma.user.findUnique({ where: { id: userId } });
21
+ if (!user) throw new Error('User not found');
22
+
23
+ let customerId = user.stripeCustomerId;
24
+
25
+ if (!customerId) {
26
+ const customer = await this.stripe.customers.create({
27
+ email: user.email,
28
+ metadata: { userId },
29
+ });
30
+ customerId = customer.id;
31
+ await this.prisma.user.update({
32
+ where: { id: userId },
33
+ data: { stripeCustomerId: customerId },
34
+ });
35
+ }
36
+
20
37
  const session = await this.stripe.checkout.sessions.create({
21
38
  mode: 'subscription',
22
39
  payment_method_types: ['card'],
23
40
  line_items: [{ price: priceId, quantity: 1 }],
24
- customer: customerId || undefined,
41
+ customer: customerId,
25
42
  success_url: `${this.configService.get('FRONTEND_URL')}/success?session_id={CHECKOUT_SESSION_ID}`,
26
43
  cancel_url: `${this.configService.get('FRONTEND_URL')}/cancel`,
44
+ client_reference_id: userId,
27
45
  });
28
46
 
29
47
  return session;
30
48
  }
31
49
 
50
+ async createPortalSession(userId: string) {
51
+ const user = await this.prisma.user.findUnique({ where: { id: userId } });
52
+ if (!user?.stripeCustomerId) throw new Error('No Stripe customer found for this user');
53
+
54
+ return this.stripe.billingPortal.sessions.create({
55
+ customer: user.stripeCustomerId,
56
+ return_url: `${this.configService.get('FRONTEND_URL')}/dashboard`,
57
+ });
58
+ }
59
+
32
60
  async handleWebhook(payload: Buffer, signature: string) {
33
61
  const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET', '');
34
- const event = this.stripe.webhooks.constructEvent(payload, signature, webhookSecret);
62
+
63
+ let event: Stripe.Event;
64
+ try {
65
+ event = this.stripe.webhooks.constructEvent(payload, signature, webhookSecret);
66
+ } catch (err: any) {
67
+ throw new Error(`Webhook signature verification failed: ${err.message}`);
68
+ }
35
69
 
36
70
  switch (event.type) {
37
- case 'checkout.session.completed':
38
- {
39
- const session = event.data.object as Stripe.Checkout.Session;
40
- console.log('Checkout completed:', session.id);
41
- // Match the session back to the user via client_reference_id or customer email
42
- // await this.prisma.user.update({...});
71
+ case 'checkout.session.completed': {
72
+ const session = event.data.object as Stripe.Checkout.Session;
73
+ const userId = session.client_reference_id;
74
+ if (userId && session.customer) {
75
+ await this.prisma.user.update({
76
+ where: { id: userId },
77
+ data: { stripeCustomerId: session.customer as string },
78
+ });
43
79
  }
80
+ console.log('Checkout completed for user:', userId);
44
81
  break;
45
- case 'customer.subscription.updated':
46
- {
47
- const subscription = event.data.object as Stripe.Subscription;
48
- console.log('Subscription updated:', subscription.id);
49
- // Update user's subscription status in DB
50
- }
82
+ }
83
+ case 'customer.subscription.updated': {
84
+ const sub = event.data.object as Stripe.Subscription;
85
+ console.log('Subscription updated:', sub.id, '| Status:', sub.status);
86
+ // TODO: Update subscriptionStatus in DB
51
87
  break;
52
- case 'customer.subscription.deleted':
53
- {
54
- const subscription = event.data.object as Stripe.Subscription;
55
- console.log('Subscription deleted:', subscription.id);
56
- // Cancel user's subscription in DB
57
- }
88
+ }
89
+ case 'customer.subscription.deleted': {
90
+ const sub = event.data.object as Stripe.Subscription;
91
+ console.log('Subscription deleted:', sub.id);
92
+ // TODO: Mark user as unsubscribed in DB
93
+ break;
94
+ }
95
+ case 'invoice.payment_failed': {
96
+ const invoice = event.data.object as Stripe.Invoice;
97
+ console.log('Payment failed for invoice:', invoice.id);
98
+ // TODO: Notify user via email
58
99
  break;
100
+ }
101
+ default:
102
+ console.log('Unhandled Stripe event:', event.type);
59
103
  }
60
104
 
61
105
  return { received: true };
@@ -1,30 +1,59 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import Stripe from "stripe";
3
+ import { getServerSession } from "next-auth";
4
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
+ import { prisma } from "@/lib/prisma";
3
6
 
4
7
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
5
- apiVersion: "2025-12-15.clover",
8
+ apiVersion: "2026-02-25.clover",
6
9
  });
7
10
 
8
11
  export async function POST(request: NextRequest) {
9
12
  try {
10
- const { priceId, customerId } = await request.json();
13
+ const session = await getServerSession(authOptions);
14
+ if (!session?.user?.email) {
15
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
16
+ }
11
17
 
12
- const session = await stripe.checkout.sessions.create({
18
+ const { priceId } = await request.json();
19
+ if (!priceId) {
20
+ return NextResponse.json({ error: "priceId is required" }, { status: 400 });
21
+ }
22
+
23
+ // Get or create Stripe customer
24
+ const user = await prisma.user.findUnique({ where: { email: session.user.email } });
25
+ if (!user) {
26
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
27
+ }
28
+
29
+ let customerId = user.stripeCustomerId;
30
+
31
+ if (!customerId) {
32
+ const customer = await stripe.customers.create({
33
+ email: user.email,
34
+ name: user.name ?? undefined,
35
+ metadata: { userId: user.id },
36
+ });
37
+ customerId = customer.id;
38
+ await prisma.user.update({
39
+ where: { id: user.id },
40
+ data: { stripeCustomerId: customerId },
41
+ });
42
+ }
43
+
44
+ const checkoutSession = await stripe.checkout.sessions.create({
13
45
  mode: "subscription",
14
46
  payment_method_types: ["card"],
15
- line_items: [
16
- {
17
- price: priceId || process.env.STRIPE_PRICE_ID,
18
- quantity: 1,
19
- },
20
- ],
21
- customer: customerId || undefined,
22
- success_url: ,
23
- cancel_url: ,
47
+ line_items: [{ price: priceId, quantity: 1 }],
48
+ customer: customerId,
49
+ success_url: `${process.env.NEXTAUTH_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
50
+ cancel_url: `${process.env.NEXTAUTH_URL}/dashboard`,
51
+ client_reference_id: user.id,
24
52
  });
25
53
 
26
- return NextResponse.json({ url: session.url });
54
+ return NextResponse.json({ url: checkoutSession.url });
27
55
  } catch (error: any) {
56
+ console.error("Checkout error:", error.message);
28
57
  return NextResponse.json({ error: error.message }, { status: 500 });
29
58
  }
30
59
  }
@@ -0,0 +1,36 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import Stripe from "stripe";
3
+ import { getServerSession } from "next-auth";
4
+ import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
+ import { prisma } from "@/lib/prisma";
6
+
7
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
8
+ apiVersion: "2026-02-25.clover",
9
+ });
10
+
11
+ export async function POST(request: NextRequest) {
12
+ try {
13
+ const session = await getServerSession(authOptions);
14
+ if (!session?.user?.email) {
15
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
16
+ }
17
+
18
+ const user = await prisma.user.findUnique({ where: { email: session.user.email } });
19
+ if (!user?.stripeCustomerId) {
20
+ return NextResponse.json(
21
+ { error: "No Stripe customer found for this user" },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ const portalSession = await stripe.billingPortal.sessions.create({
27
+ customer: user.stripeCustomerId,
28
+ return_url: `${process.env.NEXTAUTH_URL}/dashboard`,
29
+ });
30
+
31
+ return NextResponse.json({ url: portalSession.url });
32
+ } catch (error: any) {
33
+ console.error("Portal error:", error.message);
34
+ return NextResponse.json({ error: error.message }, { status: 500 });
35
+ }
36
+ }
@@ -3,7 +3,7 @@ import Stripe from "stripe";
3
3
  import { prisma } from "@/lib/prisma";
4
4
 
5
5
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
6
- apiVersion: "2025-12-15.clover",
6
+ apiVersion: "2026-02-25.clover",
7
7
  });
8
8
 
9
9
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
@@ -12,44 +12,56 @@ export async function POST(request: NextRequest) {
12
12
  const body = await request.text();
13
13
  const signature = request.headers.get("stripe-signature") || "";
14
14
 
15
+ if (!signature) {
16
+ return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 });
17
+ }
18
+
15
19
  let event: Stripe.Event;
16
20
 
17
21
  try {
18
22
  event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
19
23
  } catch (err: any) {
20
- console.error("Webhook signature verification failed.", err.message);
24
+ console.error("Webhook signature verification failed:", err.message);
21
25
  return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
22
26
  }
23
27
 
24
28
  switch (event.type) {
25
- case "checkout.session.completed":
29
+ case "checkout.session.completed": {
26
30
  const session = event.data.object as Stripe.Checkout.Session;
27
- console.log("Checkout completed:", session.id);
28
- // Example: Update user's stripeCustomerId
29
- // if (session.client_reference_id) {
30
- // await prisma.user.update({
31
- // where: { id: session.client_reference_id },
32
- // data: { stripeCustomerId: session.customer as string }
33
- // });
34
- // }
31
+ const userId = session.client_reference_id;
32
+ if (userId && session.customer) {
33
+ await prisma.user.update({
34
+ where: { id: userId },
35
+ data: { stripeCustomerId: session.customer as string },
36
+ });
37
+ }
38
+ console.log("Checkout completed for user:", userId);
35
39
  break;
40
+ }
36
41
 
37
- case "customer.subscription.updated":
42
+ case "customer.subscription.updated": {
38
43
  const subscription = event.data.object as Stripe.Subscription;
39
- console.log("Subscription updated:", subscription.id);
40
- // TODO: Update user subscription status in database
44
+ console.log("Subscription updated:", subscription.id, "| Status:", subscription.status);
45
+ // TODO: update subscriptionStatus field on user
41
46
  break;
47
+ }
42
48
 
43
- case "customer.subscription.deleted":
44
- const deletedSubscription = event.data.object as Stripe.Subscription;
45
- console.log("Subscription deleted:", deletedSubscription.id);
46
- // TODO: Handle subscription cancellation in database
49
+ case "customer.subscription.deleted": {
50
+ const subscription = event.data.object as Stripe.Subscription;
51
+ console.log("Subscription deleted:", subscription.id);
52
+ // TODO: mark user as unsubscribed
47
53
  break;
54
+ }
48
55
 
49
- case "invoice.payment_failed":
56
+ case "invoice.payment_failed": {
50
57
  const invoice = event.data.object as Stripe.Invoice;
51
- console.log("Payment failed:", invoice.id);
58
+ console.log("Payment failed for invoice:", invoice.id);
59
+ // TODO: notify user via email
52
60
  break;
61
+ }
62
+
63
+ default:
64
+ console.log("Unhandled Stripe event:", event.type);
53
65
  }
54
66
 
55
67
  return NextResponse.json({ received: true });
@@ -0,0 +1,98 @@
1
+ import { IUserRepository } from '../../domain/repositories/IUserRepository';
2
+ import { StripeProvider } from '../../infrastructure/providers/StripeProvider';
3
+
4
+ export class PaymentService {
5
+ constructor(
6
+ private readonly userRepository: IUserRepository,
7
+ private readonly stripeProvider: StripeProvider,
8
+ ) {}
9
+
10
+ async createCheckoutSession(userId: string, priceId: string) {
11
+ const user = await this.userRepository.findById(userId);
12
+ if (!user) throw new Error('User not found');
13
+
14
+ let customerId = user.stripeCustomerId;
15
+
16
+ if (!customerId) {
17
+ const customer = await this.stripeProvider.createCustomer(
18
+ user.email,
19
+ user.name,
20
+ user.id,
21
+ );
22
+ customerId = customer.id;
23
+ user.stripeCustomerId = customerId;
24
+ await this.userRepository.save(user);
25
+ }
26
+
27
+ const session = await this.stripeProvider.createCheckoutSession(
28
+ customerId,
29
+ priceId,
30
+ `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
31
+ `${process.env.FRONTEND_URL}/cancel`,
32
+ userId,
33
+ );
34
+
35
+ return session;
36
+ }
37
+
38
+ async createPortalSession(userId: string) {
39
+ const user = await this.userRepository.findById(userId);
40
+ if (!user?.stripeCustomerId) throw new Error('No Stripe customer found for this user');
41
+
42
+ return this.stripeProvider.createPortalSession(
43
+ user.stripeCustomerId,
44
+ `${process.env.FRONTEND_URL}/dashboard`,
45
+ );
46
+ }
47
+
48
+ async handleWebhook(rawBody: Buffer, signature: string) {
49
+ let event: ReturnType<typeof this.stripeProvider.constructEvent>;
50
+ try {
51
+ event = this.stripeProvider.constructEvent(rawBody, signature);
52
+ } catch (err: any) {
53
+ throw new Error(`Webhook signature verification failed: ${err.message}`);
54
+ }
55
+
56
+ switch (event.type) {
57
+ case 'checkout.session.completed': {
58
+ const session = event.data.object as any;
59
+ const userId = session.client_reference_id as string;
60
+ if (userId && session.customer) {
61
+ const user = await this.userRepository.findById(userId);
62
+ if (user) {
63
+ user.stripeCustomerId = session.customer as string;
64
+ await this.userRepository.save(user);
65
+ }
66
+ }
67
+ console.log('Checkout completed for user:', userId);
68
+ break;
69
+ }
70
+
71
+ case 'customer.subscription.updated': {
72
+ const subscription = event.data.object as any;
73
+ console.log('Subscription updated:', subscription.id, '| Status:', subscription.status);
74
+ // TODO: Update subscriptionStatus field in user model
75
+ break;
76
+ }
77
+
78
+ case 'customer.subscription.deleted': {
79
+ const subscription = event.data.object as any;
80
+ console.log('Subscription deleted:', subscription.id);
81
+ // TODO: Mark user as unsubscribed in DB
82
+ break;
83
+ }
84
+
85
+ case 'invoice.payment_failed': {
86
+ const invoice = event.data.object as any;
87
+ console.log('Payment failed for invoice:', invoice.id);
88
+ // TODO: Notify user via email
89
+ break;
90
+ }
91
+
92
+ default:
93
+ console.log('Unhandled Stripe event:', event.type);
94
+ }
95
+
96
+ return { received: true };
97
+ }
98
+ }
@@ -5,32 +5,53 @@ import helmet from 'helmet';
5
5
  import morgan from 'morgan';
6
6
  import { config } from './config';
7
7
  import { PrismaUserRepository } from './infrastructure/database/PrismaUserRepository';
8
+ import { jwtTokenGenerator } from './infrastructure/providers/TokenGenerator';
9
+ import { PasswordHasher } from './infrastructure/providers/PasswordHasher';
10
+ import { StripeProvider } from './infrastructure/providers/StripeProvider';
8
11
  import { AuthController } from './infrastructure/http/controllers/AuthController';
12
+ import { PaymentController } from './infrastructure/http/controllers/PaymentController';
13
+ import { authMiddleware } from './infrastructure/http/middlewares/authMiddleware';
14
+ import { PaymentService } from './application/services/PaymentService';
9
15
  import { errorHandler } from './infrastructure/http/middlewares/errorHandler';
10
16
 
11
17
  const app = express();
12
18
 
13
- // Middlewares
14
- app.use(express.json());
15
- app.use(cors());
19
+ // Security middleware
16
20
  app.use(helmet());
21
+ app.use(cors());
17
22
  app.use(morgan('dev'));
18
23
 
19
- // Dependency Injection
24
+ // === IMPORTANT: raw body for Stripe webhook must come BEFORE express.json() ===
25
+ app.use('/api/payments/webhook', express.raw({ type: 'application/json' }));
26
+
27
+ // JSON parsing for all other routes
28
+ app.use(express.json());
29
+
30
+ // --- Dependency Injection ---
20
31
  const userRepository = new PrismaUserRepository();
21
- const authController = new AuthController(userRepository);
32
+ const stripeProvider = new StripeProvider();
22
33
 
23
- // Routes
34
+ const authController = new AuthController(userRepository, jwtTokenGenerator, new PasswordHasher());
35
+ const paymentService = new PaymentService(userRepository, stripeProvider);
36
+ const paymentController = new PaymentController(paymentService);
37
+
38
+ // --- Auth Routes ---
24
39
  app.post('/api/auth/register', authController.register);
25
40
  app.post('/api/auth/login', authController.login);
26
41
 
42
+ // --- Payment Routes ---
43
+ app.post('/api/payments/checkout', authMiddleware, paymentController.createCheckout);
44
+ app.post('/api/payments/portal', authMiddleware, paymentController.createPortal);
45
+ app.post('/api/payments/webhook', paymentController.handleWebhook);
46
+
47
+ // --- Health Check ---
27
48
  app.get('/health', (req, res) => {
28
49
  res.json({ status: 'ok', architecture: 'clean' });
29
50
  });
30
51
 
31
- // Error Handler
52
+ // --- Error Handler (must be last) ---
32
53
  app.use(errorHandler);
33
54
 
34
55
  app.listen(config.port, () => {
35
- console.log(`🚀 Server running on port ${config.port}`);
56
+ console.log(`Server running on port ${config.port}`);
36
57
  });
@@ -0,0 +1,57 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { PaymentService } from '../../../application/services/PaymentService';
3
+
4
+ export class PaymentController {
5
+ constructor(private readonly paymentService: PaymentService) {}
6
+
7
+ /**
8
+ * POST /api/payments/checkout
9
+ * Requires authentication (attach authMiddleware in index.ts)
10
+ */
11
+ createCheckout = async (req: Request, res: Response, next: NextFunction) => {
12
+ try {
13
+ const userId = (req as any).userId as string;
14
+ const { priceId } = req.body;
15
+
16
+ if (!priceId) {
17
+ return res.status(400).json({ error: 'priceId is required' });
18
+ }
19
+
20
+ const session = await this.paymentService.createCheckoutSession(userId, priceId);
21
+ res.json({ url: session.url });
22
+ } catch (err) {
23
+ next(err);
24
+ }
25
+ };
26
+
27
+ /**
28
+ * POST /api/payments/portal
29
+ * Requires authentication (attach authMiddleware in index.ts)
30
+ */
31
+ createPortal = async (req: Request, res: Response, next: NextFunction) => {
32
+ try {
33
+ const userId = (req as any).userId as string;
34
+ const session = await this.paymentService.createPortalSession(userId);
35
+ res.json({ url: session.url });
36
+ } catch (err) {
37
+ next(err);
38
+ }
39
+ };
40
+
41
+ /**
42
+ * POST /api/payments/webhook
43
+ * No authentication – Stripe sends the raw body here.
44
+ */
45
+ handleWebhook = async (req: Request, res: Response, next: NextFunction) => {
46
+ try {
47
+ const signature = req.headers['stripe-signature'] as string;
48
+ if (!signature) {
49
+ return res.status(400).json({ error: 'Missing stripe-signature header' });
50
+ }
51
+ const result = await this.paymentService.handleWebhook(req.body, signature);
52
+ res.json(result);
53
+ } catch (err) {
54
+ next(err);
55
+ }
56
+ };
57
+ }