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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kybernus",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "The Ultimate Scaffolding CLI for Modern Developers",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,89 @@
1
+ package {{packageName}}.application.usecase;
2
+
3
+ import {{packageName}}.domain.entity.User;
4
+ import {{packageName}}.domain.repository.UserRepository;
5
+ import {{packageName}}.infrastructure.stripe.StripeGateway;
6
+ import com.stripe.model.Event;
7
+ import com.stripe.model.checkout.Session;
8
+ import org.slf4j.Logger;
9
+ import org.slf4j.LoggerFactory;
10
+ import org.springframework.stereotype.Service;
11
+
12
+ @Service
13
+ public class PaymentUseCase {
14
+
15
+ private static final Logger logger = LoggerFactory.getLogger(PaymentUseCase.class);
16
+
17
+ private final UserRepository userRepository;
18
+ private final StripeGateway stripeGateway;
19
+
20
+ public PaymentUseCase(UserRepository userRepository, StripeGateway stripeGateway) {
21
+ this.userRepository = userRepository;
22
+ this.stripeGateway = stripeGateway;
23
+ }
24
+
25
+ public String createCheckoutSession(String userId, String pricpackage {{packageName}}.application.usecase;
26
+
27
+ import {{packageName}}.domain.l.UUID.fromString(userId))
28
+ .orElseThrow(() -> new RuntimeException("User not found"));
29
+
30
+ String customerId = user.getStripeCustomerId();
31
+
32
+ if (customerId == null || customerId.isEmpty()) {
33
+ cuimport com.stripeGateway.createCustomer(user.getEmail(), userId).getId();
34
+ user.setStripeCustomerId(customerId);
35
+ userRepository.save(user);
36
+ }
37
+
38
+ Session session = stripeGateway.createCheckoutSession(customerId, priceId, userId);
39
+ retu
40
+ private final UserRepository userRString createPortalSession(String userId) throws Exception {
41
+ User user = userRepository.findById(java.util.UUID.fromString(userId))
42
+ .orElseThrow(() -> new RuntimeException("User not found"));
43
+
44
+ if (user.getStripeCustomerId() == null) {
45
+ throw new RuntimeException("No Stripe customer found for this user");
46
+ }
47
+
48
+ return stripeGateway.createPortalSession(user.getStripeCustomerId()).getUrl();
49
+ }
50
+
51
+ public void handleWebhook(String payload, String sigHeader) throws Exception {
52
+ Event event = stripeGateway.constructWebhookEvent(payload, sigHeader);
53
+
54
+ switch (event.getType()) {
55
+ case "checkout.session.completed": {
56
+ Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
57
+ if (session != null && session.getClientReferenceId() != null) {
58
+ String userId = session.getClientReferenceId();
59
+ userRepository.findById(java.util.UUID.fromString(userId)).ifPresent(user -> {
60
+ user.setStripeCustomerId(session.getCustomer());
61
+ userRepository.save(user);
62
+ });
63
+ logger.info("Checkout completed for user: {}", userId);
64
+ }
65
+ break;
66
+ }
67
+ case "customer.subscription.updated": {
68
+ com.stripe.model.Subscription sub =
69
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
70
+ if (sub != null) logger.info("Subscription updated: {} | Status: {}", sub.getId(), sub.getStatus());
71
+ break;
72
+ }
73
+ case "customer.subscription.deleted": {
74
+ com.stripe.model.Subscription sub =
75
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
76
+ if (sub != null) logger.info("Subscription deleted: {}", sub.getId());
77
+ break;
78
+ }
79
+ case "invoice.payment_failed": {
80
+ com.stripe.model.Invoice invoice =
81
+ (com.stripe.model.Invoice) event.getDataObjectDeserializer().getObject().orElse(null);
82
+ if (invoice != null) logger.info("Payment failed for invoice: {}", invoice.getId());
83
+ break;
84
+ }
85
+ default:
86
+ logger.info("Unhandled Stripe event: {}", event.getType());
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,78 @@
1
+ package {{packageName}}.infrastructure.web.payment;
2
+
3
+ import {{packageName}}.application.usecase.PaymentUseCase;
4
+ import org.slf4j.Logger;
5
+ import org.slf4j.LoggerFactory;
6
+ import org.springframework.http.HttpStatus;
7
+ import org.springframework.http.ResponseEntity;
8
+ import org.springframework.security.core.annotation.AuthenticationPrincipal;
9
+ import org.springframework.security.core.userdetails.UserDetails;
10
+ import org.springframework.web.bind.annotation.*;
11
+
12
+ import java.util.Map;
13
+
14
+ @RestController
15
+ @RequestMapping("/api/payments")
16
+ public class PaymentController {
17
+
18
+ private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
19
+
20
+ private final PaymentUseCase paymentUseCase;
21
+
22
+ public PaymentController(PaymentUseCase paymentUseCase) {
23
+ this.paymentUseCase = paymentUseCase;
24
+ }
25
+
26
+ /**
27
+ * POST /api/payments/checkout
28
+ * Requires authentication.
29
+ */
30
+ @PostMapping("/checkout")
31
+ public ResponseEntity<?> createCheckout(
32
+ @AuthenticationPrincipal UserDetails userDetails,
33
+ @RequestBody Map<String, String> body) {
34
+ try {
35
+ String priceId = body.get("priceId");
36
+ if (priceId == null || priceId.isEmpty()) {
37
+ return ResponseEntity.badRequest().body(Map.of("error", "priceId is required"));
38
+ }
39
+ String url = paymentUseCase.createCheckoutSession(userDetails.getUsername(), priceId);
40
+ return ResponseEntity.ok(Map.of("url", url));
41
+ } catch (Exception e) {
42
+ logger.error("Checkout error", e);
43
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
44
+ }
45
+ }
46
+
47
+ /**
48
+ * POST /api/payments/portal
49
+ * Requires authentication.
50
+ */
51
+ @PostMapping("/portal")
52
+ public ResponseEntity<?> createPortal(@AuthenticationPrincipal UserDetails userDetails) {
53
+ try {
54
+ String url = paymentUseCase.createPortalSession(userDetails.getUsername());
55
+ return ResponseEntity.ok(Map.of("url", url));
56
+ } catch (Exception e) {
57
+ logger.error("Portal error", e);
58
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
59
+ }
60
+ }
61
+
62
+ /**
63
+ * POST /api/payments/webhook
64
+ * No authentication. Stripe sends raw body.
65
+ */
66
+ @PostMapping("/webhook")
67
+ public ResponseEntity<String> webhook(
68
+ @RequestBody String payload,
69
+ @RequestHeader("Stripe-Signature") String sigHeader) {
70
+ try {
71
+ paymentUseCase.handleWebhook(payload, sigHeader);
72
+ return ResponseEntity.ok("{\"received\": true}");
73
+ } catch (Exception e) {
74
+ logger.warn("Webhook error: {}", e.getMessage());
75
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
76
+ }
77
+ }
78
+ }
@@ -16,3 +16,10 @@ spring.jpa.properties.hibernate.format_sql=true
16
16
  application.security.jwt.secret-key={{jwtSecretKey}}
17
17
  application.security.jwt.expiration=86400000
18
18
  application.security.jwt.refresh-token.expiration=604800000
19
+
20
+ # Stripe
21
+ stripe.secret-key=${STRIPE_SECRET_KEY}
22
+ stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET}
23
+
24
+ # Frontend URL (for Stripe redirect URLs)
25
+ frontend.url=${FRONTEND_URL:http://localhost:3000}
@@ -0,0 +1,78 @@
1
+ package {{packageName}}.adapters.inbound.web;
2
+
3
+ import {{packageName}}.core.service.PaymentService;
4
+ import org.slf4j.Logger;
5
+ import org.slf4j.LoggerFactory;
6
+ import org.springframework.http.HttpStatus;
7
+ import org.springframework.http.ResponseEntity;
8
+ import org.springframework.security.core.annotation.AuthenticationPrincipal;
9
+ import org.springframework.security.core.userdetails.UserDetails;
10
+ import org.springframework.web.bind.annotation.*;
11
+
12
+ import java.util.Map;
13
+
14
+ @RestController
15
+ @RequestMapping("/api/payments")
16
+ public class PaymentController {
17
+
18
+ private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
19
+
20
+ private final PaymentService paymentService;
21
+
22
+ public PaymentController(PaymentService paymentService) {
23
+ this.paymentService = paymentService;
24
+ }
25
+
26
+ /**
27
+ * POST /api/payments/checkout
28
+ * Requires authentication.
29
+ */
30
+ @PostMapping("/checkout")
31
+ public ResponseEntity<?> createCheckout(
32
+ @AuthenticationPrincipal UserDetails userDetails,
33
+ @RequestBody Map<String, String> body) {
34
+ try {
35
+ String priceId = body.get("priceId");
36
+ if (priceId == null || priceId.isEmpty()) {
37
+ return ResponseEntity.badRequest().body(Map.of("error", "priceId is required"));
38
+ }
39
+ String url = paymentService.createCheckoutSession(userDetails.getUsername(), priceId);
40
+ return ResponseEntity.ok(Map.of("url", url));
41
+ } catch (Exception e) {
42
+ logger.error("Checkout error", e);
43
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
44
+ }
45
+ }
46
+
47
+ /**
48
+ * POST /api/payments/portal
49
+ * Requires authentication.
50
+ */
51
+ @PostMapping("/portal")
52
+ public ResponseEntity<?> createPortal(@AuthenticationPrincipal UserDetails userDetails) {
53
+ try {
54
+ String url = paymentService.createPortalSession(userDetails.getUsername());
55
+ return ResponseEntity.ok(Map.of("url", url));
56
+ } catch (Exception e) {
57
+ logger.error("Portal error", e);
58
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
59
+ }
60
+ }
61
+
62
+ /**
63
+ * POST /api/payments/webhook
64
+ * No authentication. Stripe sends raw body.
65
+ */
66
+ @PostMapping("/webhook")
67
+ public ResponseEntity<String> webhook(
68
+ @RequestBody String payload,
69
+ @RequestHeader("Stripe-Signature") String sigHeader) {
70
+ try {
71
+ paymentService.handleWebhook(payload, sigHeader);
72
+ return ResponseEntity.ok("{\"received\": true}");
73
+ } catch (Exception e) {
74
+ logger.warn("Webhook error: {}", e.getMessage());
75
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,76 @@
1
+ package {{packageName}}.adapters.outbound.stripe;
2
+
3
+ import com.stripe.Stripe;
4
+ import com.stripe.exception.StripeException;
5
+ import com.stripe.model.Customer;
6
+ import com.stripe.model.Event;
7
+ import com.stripe.model.checkout.Session;
8
+ import com.stripe.net.Webhook;
9
+ import com.stripe.param.CustomerCreateParams;
10
+ import com.stripe.param.checkout.SessionCreateParams;
11
+ import jakarta.annotation.PostConstruct;
12
+ import org.slf4j.Logger;
13
+ import org.slf4j.LoggerFactory;
14
+ import org.springframework.beans.factory.annotation.Value;
15
+ import org.springframework.stereotype.Component;
16
+
17
+ /**
18
+ * Outbound adapter: wraps the Stripe SDK for use by core services.
19
+ */
20
+ @Component
21
+ public class StripeAdapter {
22
+
23
+ private static final Logger logger = LoggerFactory.getLogger(StripeAdapter.class);
24
+
25
+ @Value("${stripe.secret-key}")
26
+ private String stripeSecretKey;
27
+
28
+ @Value("${stripe.webhook-secret}")
29
+ private String webhookSecret;
30
+
31
+ @Value("${frontend.url}")
32
+ private String frontendUrl;
33
+
34
+ @PostConstruct
35
+ public void init() {
36
+ Stripe.apiKey = stripeSecretKey;
37
+ }
38
+
39
+ public Customer createCustomer(String email, String userId) throws StripeException {
40
+ return Customer.create(CustomerCreateParams.builder()
41
+ .setEmail(email)
42
+ .putMetadata("userId", userId)
43
+ .build());
44
+ }
45
+
46
+ public Session createCheckoutSession(
47
+ String customerId, String priceId, String userId) throws StripeException {
48
+
49
+ return Session.create(SessionCreateParams.builder()
50
+ .setMode(SessionCreateParams.Mode.SUBSCRIPTION)
51
+ .addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD)
52
+ .addLineItem(SessionCreateParams.LineItem.builder()
53
+ .setPrice(priceId)
54
+ .setQuantity(1L)
55
+ .build())
56
+ .setCustomer(customerId)
57
+ .setSuccessUrl(frontendUrl + "/success?session_id={CHECKOUT_SESSION_ID}")
58
+ .setCancelUrl(frontendUrl + "/cancel")
59
+ .setClientReferenceId(userId)
60
+ .build());
61
+ }
62
+
63
+ public com.stripe.model.billingportal.Session createPortalSession(
64
+ String customerId) throws StripeException {
65
+
66
+ return com.stripe.model.billingportal.Session.create(
67
+ com.stripe.param.billingportal.SessionCreateParams.builder()
68
+ .setCustomer(customerId)
69
+ .setReturnUrl(frontendUrl + "/dashboard")
70
+ .build());
71
+ }
72
+
73
+ public Event constructWebhookEvent(String payload, String sigHeader) throws Exception {
74
+ return Webhook.constructEvent(payload, sigHeader, webhookSecret);
75
+ }
76
+ }
@@ -0,0 +1,90 @@
1
+ package {{packageName}}.core.service;
2
+
3
+ import {{packageName}}.adapters.outbound.stripe.StripeAdapter;
4
+ import {{packageName}}.core.ports.outbound.UserRepository;
5
+ import {{packageName}}.core.domain.User;
6
+ import com.stripe.model.Event;
7
+ import com.stripe.model.checkout.Session;
8
+ import org.slf4j.Logger;
9
+ import org.slf4j.LoggerFactory;
10
+ import org.springframework.stereotype.Service;
11
+
12
+ @Service
13
+ public class PaymentService {
14
+
15
+ private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
16
+
17
+ private final UserRepository userRepository;
18
+ private final StripeAdapter stripeAdapter;
19
+
20
+ public PaymentService(UserRepository userRepository, StripeAdapter stripeAdapter) {
21
+ this.userRepository = userRepository;
22
+ this.stripeAdapter = stripeAdapter;
23
+ }
24
+
25
+ public String createCheckoutSession(String userId, String priceId) throws Exception {
26
+ User user = userRepository.findById(java.util.UUID.fromString(userId))
27
+ .orElseThrow(() -> new RuntimeException("User not found"));
28
+
29
+ String customerId = user.getStripeCustomerId();
30
+
31
+ if (customerId == null || customerId.isEmpty()) {
32
+ customerId = stripeAdapter.createCustomer(user.getEmail(), userId).getId();
33
+ user.setStripeCustomerId(customerId);
34
+ userRepository.save(user);
35
+ }
36
+
37
+ Session session = stripeAdapter.createCheckoutSession(customerId, priceId, userId);
38
+ return session.getUrl();
39
+ }
40
+
41
+ public String createPortalSession(String userId) throws Exception {
42
+ User user = userRepository.findById(java.util.UUID.fromString(userId))
43
+ .orElseThrow(() -> new RuntimeException("User not found"));
44
+
45
+ if (user.getStripeCustomerId() == null) {
46
+ throw new RuntimeException("No Stripe customer found for this user");
47
+ }
48
+
49
+ return stripeAdapter.createPortalSession(user.getStripeCustomerId()).getUrl();
50
+ }
51
+
52
+ public void handleWebhook(String payload, String sigHeader) throws Exception {
53
+ Event event = stripeAdapter.constructWebhookEvent(payload, sigHeader);
54
+
55
+ switch (event.getType()) {
56
+ case "checkout.session.completed": {
57
+ Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
58
+ if (session != null && session.getClientReferenceId() != null) {
59
+ String userId = session.getClientReferenceId();
60
+ userRepository.findById(java.util.UUID.fromString(userId)).ifPresent(user -> {
61
+ user.setStripeCustomerId(session.getCustomer());
62
+ userRepository.save(user);
63
+ });
64
+ logger.info("Checkout completed for user: {}", userId);
65
+ }
66
+ break;
67
+ }
68
+ case "customer.subscription.updated": {
69
+ com.stripe.model.Subscription sub =
70
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
71
+ if (sub != null) logger.info("Subscription updated: {} | Status: {}", sub.getId(), sub.getStatus());
72
+ break;
73
+ }
74
+ case "customer.subscription.deleted": {
75
+ com.stripe.model.Subscription sub =
76
+ (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
77
+ if (sub != null) logger.info("Subscription deleted: {}", sub.getId());
78
+ break;
79
+ }
80
+ case "invoice.payment_failed": {
81
+ com.stripe.model.Invoice invoice =
82
+ (com.stripe.model.Invoice) event.getDataObjectDeserializer().getObject().orElse(null);
83
+ if (invoice != null) logger.info("Payment failed for invoice: {}", invoice.getId());
84
+ break;
85
+ }
86
+ default:
87
+ logger.info("Unhandled Stripe event: {}", event.getType());
88
+ }
89
+ }
90
+ }
@@ -16,3 +16,10 @@ spring.jpa.properties.hibernate.format_sql=true
16
16
  application.security.jwt.secret-key={{jwtSecretKey}}
17
17
  application.security.jwt.expiration=86400000
18
18
  application.security.jwt.refresh-token.expiration=604800000
19
+
20
+ # Stripe
21
+ stripe.secret-key=${STRIPE_SECRET_KEY}
22
+ stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET}
23
+
24
+ # Frontend URL (for Stripe redirect URLs)
25
+ frontend.url=${FRONTEND_URL:http://localhost:3000}
@@ -1,20 +1,15 @@
1
1
  package {{packageName}}.controller;
2
2
 
3
- import com.stripe.exception.SignatureVerificationException;
4
- import com.stripe.model.Event;
5
- import com.stripe.net.Webhook;
6
- import {{packageName}}.model.User;
7
- import {{packageName}}.repository.UserRepository;
8
3
  import {{packageName}}.service.StripeService;
9
4
  import org.slf4j.Logger;
10
5
  import org.slf4j.LoggerFactory;
11
- import org.springframework.beans.factory.annotation.Value;
12
6
  import org.springframework.http.HttpStatus;
13
7
  import org.springframework.http.ResponseEntity;
8
+ import org.springframework.security.core.annotation.AuthenticationPrincipal;
9
+ import org.springframework.security.core.userdetails.UserDetails;
14
10
  import org.springframework.web.bind.annotation.*;
15
11
 
16
12
  import java.util.Map;
17
- import java.util.Optional;
18
13
 
19
14
  @RestController
20
15
  @RequestMapping("/api/payments")
@@ -22,71 +17,65 @@ public class PaymentsController {
22
17
 
23
18
  private static final Logger logger = LoggerFactory.getLogger(PaymentsController.class);
24
19
 
25
- @Value("${stripe.webhook-secret}")
26
- private String webhookSecret;
27
-
28
20
  private final StripeService stripeService;
29
- private final UserRepository userRepository;
30
21
 
31
- public PaymentsController(StripeService stripeService, UserRepository userRepository) {
22
+ public PaymentsController(StripeService stripeService) {
32
23
  this.stripeService = stripeService;
33
- this.userRepository = userRepository;
34
24
  }
35
25
 
26
+ /**
27
+ * POST /api/payments/checkout
28
+ * Requires authentication. Creates a Stripe Checkout session.
29
+ */
36
30
  @PostMapping("/checkout")
37
- public ResponseEntity<?> createCheckout(@RequestBody Map<String, String> request) {
31
+ public ResponseEntity<?> createCheckout(
32
+ @AuthenticationPrincipal UserDetails userDetails,
33
+ @RequestBody Map<String, String> body) {
38
34
  try {
39
- String customerId = request.get("customerId"); // Optional
40
- com.stripe.model.checkout.Session session = stripeService.createCheckoutSession(customerId);
35
+ String priceId = body.get("priceId");
36
+ if (priceId == null || priceId.isEmpty()) {
37
+ return ResponseEntity.badRequest().body(Map.of("error", "priceId is required"));
38
+ }
39
+ // userDetails.getUsername() returns the user ID (set in JWT filter)
40
+ com.stripe.model.checkout.Session session =
41
+ stripeService.createCheckoutSession(userDetails.getUsername(), priceId);
41
42
  return ResponseEntity.ok(Map.of("url", session.getUrl()));
42
43
  } catch (Exception e) {
43
44
  logger.error("Error creating checkout session", e);
44
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
45
- .body(Map.of("error", e.getMessage()));
45
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
46
+ }
47
+ }
48
+
49
+ /**
50
+ * POST /api/payments/portal
51
+ * Requires authentication. Opens Stripe Billing Portal.
52
+ */
53
+ @PostMapping("/portal")
54
+ public ResponseEntity<?> createPortal(@AuthenticationPrincipal UserDetails userDetails) {
55
+ try {
56
+ com.stripe.model.billingportal.Session session =
57
+ stripeService.createPortalSession(userDetails.getUsername());
58
+ return ResponseEntity.ok(Map.of("url", session.getUrl()));
59
+ } catch (Exception e) {
60
+ logger.error("Error creating portal session", e);
61
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
46
62
  }
47
63
  }
48
64
 
65
+ /**
66
+ * POST /api/payments/webhook
67
+ * No authentication. Stripe sends raw body + signature header.
68
+ */
49
69
  @PostMapping("/webhook")
50
70
  public ResponseEntity<String> handleWebhook(
51
71
  @RequestBody String payload,
52
72
  @RequestHeader("Stripe-Signature") String sigHeader) {
53
-
54
- Event event;
55
-
56
73
  try {
57
- event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
58
- } catch (SignatureVerificationException e) {
59
- logger.warn("Invalid signature", e);
60
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid signature");
74
+ stripeService.handleWebhook(payload, sigHeader);
75
+ return ResponseEntity.ok("{\"received\": true}");
61
76
  } catch (Exception e) {
62
- logger.error("Error parsing webhook", e);
63
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Error parsing webhook");
77
+ logger.warn("Webhook error: {}", e.getMessage());
78
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
64
79
  }
65
-
66
- switch (event.getType()) {
67
- case "checkout.session.completed":
68
- com.stripe.model.checkout.Session session = (com.stripe.model.checkout.Session) event.getDataObjectDeserializer().getObject().orElse(null);
69
- if (session != null) {
70
- logger.info("Checkout completed: {}", session.getId());
71
- // Match to database user here using session properties
72
- }
73
- break;
74
- case "customer.subscription.updated":
75
- com.stripe.model.Subscription subscription = (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
76
- if (subscription != null) {
77
- logger.info("Subscription updated: {}", subscription.getId());
78
- }
79
- break;
80
- case "customer.subscription.deleted":
81
- com.stripe.model.Subscription deletedSub = (com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
82
- if (deletedSub != null) {
83
- logger.info("Subscription deleted: {}", deletedSub.getId());
84
- }
85
- break;
86
- default:
87
- logger.info("Unhandled event type: {}", event.getType());
88
- }
89
-
90
- return ResponseEntity.ok("Received");
91
80
  }
92
81
  }