kybernus 2.2.0 → 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.
- package/package.json +1 -1
- package/templates/java-spring/clean/infra/main.tf.hbs +42 -18
- package/templates/java-spring/clean/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/java-spring/clean/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/java-spring/clean/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +89 -0
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/web/payment/PaymentController.java.hbs +78 -0
- package/templates/java-spring/clean/src/main/resources/application.properties.hbs +7 -0
- package/templates/java-spring/hexagonal/infra/main.tf.hbs +42 -18
- package/templates/java-spring/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/java-spring/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/java-spring/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/inbound/web/PaymentController.java.hbs +78 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/stripe/StripeAdapter.java.hbs +76 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +90 -0
- package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +7 -0
- package/templates/java-spring/mvc/infra/main.tf.hbs +42 -18
- package/templates/java-spring/mvc/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/java-spring/mvc/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/java-spring/mvc/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +42 -53
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +105 -23
- package/templates/nestjs/clean/infra/main.tf.hbs +42 -18
- package/templates/nestjs/clean/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nestjs/clean/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nestjs/clean/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nestjs/clean/src/app.module.ts.hbs +3 -1
- package/templates/nestjs/clean/src/application/payment.service.ts.hbs +90 -0
- package/templates/nestjs/clean/src/infrastructure/http/payment.controller.ts.hbs +46 -0
- package/templates/nestjs/clean/src/infrastructure/stripe.provider.ts.hbs +51 -0
- package/templates/nestjs/clean/src/main.ts.hbs +13 -4
- package/templates/nestjs/clean/src/payment.module.ts.hbs +23 -0
- package/templates/nestjs/hexagonal/infra/main.tf.hbs +42 -18
- package/templates/nestjs/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nestjs/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nestjs/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nestjs/hexagonal/src/adapters/inbound/payment.controller.ts.hbs +46 -0
- package/templates/nestjs/hexagonal/src/adapters/outbound/stripe.adapter.ts.hbs +54 -0
- package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -0
- package/templates/nestjs/hexagonal/src/core/payment.service.ts.hbs +90 -0
- package/templates/nestjs/hexagonal/src/main.ts.hbs +13 -4
- package/templates/nestjs/hexagonal/src/payment.module.ts.hbs +23 -0
- package/templates/nestjs/mvc/infra/main.tf.hbs +42 -18
- package/templates/nestjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nestjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nestjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nestjs/mvc/src/main.ts.hbs +6 -3
- package/templates/nestjs/mvc/src/payments/payments.controller.ts.hbs +33 -8
- package/templates/nestjs/mvc/src/payments/payments.service.ts.hbs +66 -22
- package/templates/nextjs/mvc/infra/main.tf.hbs +42 -18
- package/templates/nextjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nextjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nextjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nextjs/mvc/src/app/api/checkout/route.ts.hbs +42 -13
- package/templates/nextjs/mvc/src/app/api/portal/route.ts.hbs +36 -0
- package/templates/nextjs/mvc/src/app/api/webhook/route.ts.hbs +32 -20
- package/templates/nodejs-express/clean/infra/main.tf.hbs +42 -18
- package/templates/nodejs-express/clean/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nodejs-express/clean/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nodejs-express/clean/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nodejs-express/clean/src/application/services/PaymentService.ts.hbs +98 -0
- package/templates/nodejs-express/clean/src/index.ts.hbs +29 -8
- package/templates/nodejs-express/clean/src/infrastructure/http/controllers/PaymentController.ts.hbs +57 -0
- package/templates/nodejs-express/clean/src/infrastructure/providers/StripeProvider.ts.hbs +45 -0
- package/templates/nodejs-express/hexagonal/infra/main.tf.hbs +42 -18
- package/templates/nodejs-express/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nodejs-express/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nodejs-express/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +57 -0
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/StripeAdapter.ts.hbs +48 -0
- package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +89 -0
- package/templates/nodejs-express/hexagonal/src/index.ts.hbs +28 -8
- package/templates/nodejs-express/mvc/infra/main.tf.hbs +42 -18
- package/templates/nodejs-express/mvc/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/nodejs-express/mvc/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/nodejs-express/mvc/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/nodejs-express/mvc/src/app.ts.hbs +11 -2
- package/templates/nodejs-express/mvc/src/controllers/payments.controller.ts.hbs +31 -47
- package/templates/nodejs-express/mvc/src/services/stripe.service.ts.hbs +66 -49
- package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +85 -0
- package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +64 -0
- package/templates/python-fastapi/clean/app/infrastructure/stripe_provider.py.hbs +44 -0
- package/templates/python-fastapi/clean/app/main.py.hbs +8 -5
- package/templates/python-fastapi/clean/infra/main.tf.hbs +42 -18
- package/templates/python-fastapi/clean/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/python-fastapi/clean/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/python-fastapi/clean/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +64 -0
- package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +44 -0
- package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +81 -0
- package/templates/python-fastapi/hexagonal/app/main.py.hbs +9 -3
- package/templates/python-fastapi/hexagonal/infra/main.tf.hbs +42 -18
- package/templates/python-fastapi/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/python-fastapi/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/python-fastapi/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
- package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +70 -35
- package/templates/python-fastapi/mvc/app/services/stripe_service.py.hbs +58 -0
- package/templates/python-fastapi/mvc/infra/main.tf.hbs +42 -18
- package/templates/python-fastapi/mvc/infra/modules/ecs/main.tf.hbs +217 -6
- package/templates/python-fastapi/mvc/infra/modules/rds/main.tf.hbs +15 -15
- package/templates/python-fastapi/mvc/infra/modules/vpc/main.tf.hbs +170 -30
|
@@ -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
|
|
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(
|
|
31
|
+
public ResponseEntity<?> createCheckout(
|
|
32
|
+
@AuthenticationPrincipal UserDetails userDetails,
|
|
33
|
+
@RequestBody Map<String, String> body) {
|
|
38
34
|
try {
|
|
39
|
-
String
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
63
|
-
|
|
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
|
}
|
package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs
CHANGED
|
@@ -3,65 +3,147 @@ package {{packageName}}.service;
|
|
|
3
3
|
import com.stripe.Stripe;
|
|
4
4
|
import com.stripe.exception.StripeException;
|
|
5
5
|
import com.stripe.model.Customer;
|
|
6
|
+
import com.stripe.model.Event;
|
|
6
7
|
import com.stripe.model.checkout.Session;
|
|
8
|
+
import com.stripe.net.Webhook;
|
|
7
9
|
import com.stripe.param.CustomerCreateParams;
|
|
8
10
|
import com.stripe.param.checkout.SessionCreateParams;
|
|
11
|
+
import {{packageName}}.model.User;
|
|
12
|
+
import {{packageName}}.repository.UserRepository;
|
|
9
13
|
import jakarta.annotation.PostConstruct;
|
|
14
|
+
import org.slf4j.Logger;
|
|
15
|
+
import org.slf4j.LoggerFactory;
|
|
10
16
|
import org.springframework.beans.factory.annotation.Value;
|
|
11
17
|
import org.springframework.stereotype.Service;
|
|
12
18
|
|
|
13
19
|
@Service
|
|
14
20
|
public class StripeService {
|
|
15
21
|
|
|
22
|
+
private static final Logger logger = LoggerFactory.getLogger(StripeService.class);
|
|
23
|
+
|
|
16
24
|
@Value("${stripe.secret-key}")
|
|
17
25
|
private String stripeSecretKey;
|
|
18
26
|
|
|
19
|
-
@Value("${stripe.
|
|
20
|
-
private String
|
|
27
|
+
@Value("${stripe.webhook-secret}")
|
|
28
|
+
private String webhookSecret;
|
|
21
29
|
|
|
22
30
|
@Value("${frontend.url}")
|
|
23
31
|
private String frontendUrl;
|
|
24
32
|
|
|
33
|
+
private final UserRepository userRepository;
|
|
34
|
+
|
|
35
|
+
public StripeService(UserRepository userRepository) {
|
|
36
|
+
this.userRepository = userRepository;
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
@PostConstruct
|
|
26
40
|
public void init() {
|
|
27
41
|
Stripe.apiKey = stripeSecretKey;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Retrieve or create a Stripe customer for the given user, then open a checkout session.
|
|
46
|
+
*/
|
|
47
|
+
public Session createCheckoutSession(String userId, String priceId) throws StripeException {
|
|
48
|
+
User user = userRepository.findById(java.util.UUID.fromString(userId))
|
|
49
|
+
.orElseThrow(() -> new RuntimeException("User not found"));
|
|
50
|
+
|
|
51
|
+
String customerId = user.getStripeCustomerId();
|
|
52
|
+
|
|
53
|
+
if (customerId == null || customerId.isEmpty()) {
|
|
54
|
+
CustomerCreateParams customerParams = CustomerCreateParams.builder()
|
|
55
|
+
.setEmail(user.getEmail())
|
|
56
|
+
.putMetadata("userId", userId)
|
|
57
|
+
.build();
|
|
58
|
+
Customer customer = Customer.create(customerParams);
|
|
59
|
+
customerId = customer.getId();
|
|
60
|
+
user.setStripeCustomerId(customerId);
|
|
61
|
+
userRepository.save(user);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
SessionCreateParams params = SessionCreateParams.builder()
|
|
32
65
|
.setMode(SessionCreateParams.Mode.SUBSCRIPTION)
|
|
66
|
+
.addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD)
|
|
67
|
+
.addLineItem(SessionCreateParams.LineItem.builder()
|
|
68
|
+
.setPrice(priceId)
|
|
69
|
+
.setQuantity(1L)
|
|
70
|
+
.build())
|
|
71
|
+
.setCustomer(customerId)
|
|
33
72
|
.setSuccessUrl(frontendUrl + "/success?session_id={CHECKOUT_SESSION_ID}")
|
|
34
73
|
.setCancelUrl(frontendUrl + "/cancel")
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
.setPrice(priceId)
|
|
38
|
-
.setQuantity(1L)
|
|
39
|
-
.build()
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
if (customerId != null && !customerId.isEmpty()) {
|
|
43
|
-
paramsBuilder.setCustomer(customerId);
|
|
44
|
-
}
|
|
74
|
+
.setClientReferenceId(userId)
|
|
75
|
+
.build();
|
|
45
76
|
|
|
46
|
-
return Session.create(
|
|
77
|
+
return Session.create(params);
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Open the Stripe Billing Portal for the given customer.
|
|
82
|
+
*/
|
|
83
|
+
public com.stripe.model.billingportal.Session createPortalSession(String userId) throws StripeException {
|
|
84
|
+
User user = userRepository.findById(java.util.UUID.fromString(userId))
|
|
85
|
+
.orElseThrow(() -> new RuntimeException("User not found"));
|
|
86
|
+
|
|
87
|
+
if (user.getStripeCustomerId() == null) {
|
|
88
|
+
throw new RuntimeException("No Stripe customer found for this user");
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
com.stripe.param.billingportal.SessionCreateParams params =
|
|
51
92
|
com.stripe.param.billingportal.SessionCreateParams.builder()
|
|
52
|
-
.setCustomer(
|
|
93
|
+
.setCustomer(user.getStripeCustomerId())
|
|
53
94
|
.setReturnUrl(frontendUrl + "/dashboard")
|
|
54
95
|
.build();
|
|
55
96
|
|
|
56
97
|
return com.stripe.model.billingportal.Session.create(params);
|
|
57
98
|
}
|
|
58
99
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Validate and handle a Stripe webhook event.
|
|
102
|
+
*/
|
|
103
|
+
public void handleWebhook(String payload, String sigHeader) throws Exception {
|
|
104
|
+
Event event;
|
|
105
|
+
try {
|
|
106
|
+
event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
|
|
107
|
+
} catch (Exception e) {
|
|
108
|
+
throw new RuntimeException("Webhook signature verification failed: " + e.getMessage());
|
|
109
|
+
}
|
|
64
110
|
|
|
65
|
-
|
|
111
|
+
switch (event.getType()) {
|
|
112
|
+
case "checkout.session.completed": {
|
|
113
|
+
Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
|
|
114
|
+
if (session != null && session.getClientReferenceId() != null) {
|
|
115
|
+
String userId = session.getClientReferenceId();
|
|
116
|
+
userRepository.findById(java.util.UUID.fromString(userId)).ifPresent(user -> {
|
|
117
|
+
user.setStripeCustomerId(session.getCustomer());
|
|
118
|
+
userRepository.save(user);
|
|
119
|
+
});
|
|
120
|
+
logger.info("Checkout completed for user: {}", userId);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "customer.subscription.updated": {
|
|
125
|
+
com.stripe.model.Subscription sub =
|
|
126
|
+
(com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
|
|
127
|
+
if (sub != null) logger.info("Subscription updated: {} | Status: {}", sub.getId(), sub.getStatus());
|
|
128
|
+
// TODO: update subscriptionStatus field on user
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "customer.subscription.deleted": {
|
|
132
|
+
com.stripe.model.Subscription sub =
|
|
133
|
+
(com.stripe.model.Subscription) event.getDataObjectDeserializer().getObject().orElse(null);
|
|
134
|
+
if (sub != null) logger.info("Subscription deleted: {}", sub.getId());
|
|
135
|
+
// TODO: mark user as unsubscribed
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "invoice.payment_failed": {
|
|
139
|
+
com.stripe.model.Invoice invoice =
|
|
140
|
+
(com.stripe.model.Invoice) event.getDataObjectDeserializer().getObject().orElse(null);
|
|
141
|
+
if (invoice != null) logger.info("Payment failed for invoice: {}", invoice.getId());
|
|
142
|
+
// TODO: notify user via email
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
logger.info("Unhandled Stripe event: {}", event.getType());
|
|
147
|
+
}
|
|
66
148
|
}
|
|
67
149
|
}
|
|
@@ -5,9 +5,13 @@ terraform {
|
|
|
5
5
|
|
|
6
6
|
required_providers {
|
|
7
7
|
aws = {
|
|
8
|
-
source
|
|
8
|
+
source = "hashicorp/aws"
|
|
9
9
|
version = "~> 5.0"
|
|
10
10
|
}
|
|
11
|
+
random = {
|
|
12
|
+
source = "hashicorp/random"
|
|
13
|
+
version = "~> 3.5"
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
# Uncomment for remote state (recommended for production)
|
|
@@ -27,27 +31,27 @@ provider "aws" {
|
|
|
27
31
|
# Variables
|
|
28
32
|
variable "aws_region" {
|
|
29
33
|
description = "AWS region"
|
|
30
|
-
type
|
|
31
|
-
default
|
|
34
|
+
type = string
|
|
35
|
+
default = "us-east-1"
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
variable "environment" {
|
|
35
39
|
description = "Environment name (dev, staging, prod)"
|
|
36
|
-
type
|
|
37
|
-
default
|
|
40
|
+
type = string
|
|
41
|
+
default = "dev"
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
variable "app_name" {
|
|
41
45
|
description = "Application name"
|
|
42
|
-
type
|
|
43
|
-
default
|
|
46
|
+
type = string
|
|
47
|
+
default = "{{projectNameKebabCase}}"
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
# VPC
|
|
47
51
|
module "vpc" {
|
|
48
52
|
source = "./modules/vpc"
|
|
49
53
|
|
|
50
|
-
app_name
|
|
54
|
+
app_name = var.app_name
|
|
51
55
|
environment = var.environment
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -55,29 +59,49 @@ module "vpc" {
|
|
|
55
59
|
module "ecs" {
|
|
56
60
|
source = "./modules/ecs"
|
|
57
61
|
|
|
58
|
-
app_name
|
|
59
|
-
environment
|
|
60
|
-
vpc_id
|
|
61
|
-
|
|
62
|
+
app_name = var.app_name
|
|
63
|
+
environment = var.environment
|
|
64
|
+
vpc_id = module.vpc.vpc_id
|
|
65
|
+
public_subnet_ids = module.vpc.public_subnet_ids
|
|
66
|
+
private_subnet_ids = module.vpc.private_subnet_ids
|
|
67
|
+
alb_security_group_id = module.vpc.alb_security_group_id
|
|
68
|
+
ecs_tasks_security_group_id = module.vpc.ecs_tasks_security_group_id
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
# RDS PostgreSQL
|
|
65
72
|
module "rds" {
|
|
66
73
|
source = "./modules/rds"
|
|
67
74
|
|
|
68
|
-
app_name
|
|
69
|
-
environment
|
|
70
|
-
vpc_id
|
|
71
|
-
subnet_ids
|
|
75
|
+
app_name = var.app_name
|
|
76
|
+
environment = var.environment
|
|
77
|
+
vpc_id = module.vpc.vpc_id
|
|
78
|
+
subnet_ids = module.vpc.private_subnet_ids
|
|
72
79
|
security_group_id = module.vpc.db_security_group_id
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
# Outputs
|
|
83
|
+
output "vpc_id" {
|
|
84
|
+
value = module.vpc.vpc_id
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
output "ecs_cluster_name" {
|
|
77
88
|
value = module.ecs.cluster_name
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
output "ecr_repository_url" {
|
|
92
|
+
value = module.ecs.ecr_repository_url
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
output "alb_dns_name" {
|
|
96
|
+
value = module.ecs.alb_dns_name
|
|
97
|
+
description = "The DNS name of the ALB to access the application"
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
output "rds_endpoint" {
|
|
81
|
-
value
|
|
101
|
+
value = module.rds.endpoint
|
|
82
102
|
sensitive = true
|
|
83
|
-
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
output "db_name" {
|
|
106
|
+
value = module.rds.db_name
|
|
107
|
+
}
|
|
@@ -12,21 +12,54 @@ variable "vpc_id" {
|
|
|
12
12
|
type = string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
variable "
|
|
15
|
+
variable "public_subnet_ids" {
|
|
16
16
|
type = list(string)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
variable "private_subnet_ids" {
|
|
20
|
+
type = list(string)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
variable "alb_security_group_id" {
|
|
24
|
+
type = string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
variable "ecs_tasks_security_group_id" {
|
|
28
|
+
type = string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
variable "container_port" {
|
|
32
|
+
type = number
|
|
33
|
+
default = 3000
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# ECR Repository
|
|
37
|
+
resource "aws_ecr_repository" "app" {
|
|
38
|
+
name = "${var.app_name}-${var.environment}"
|
|
39
|
+
image_tag_mutability = "MUTABLE"
|
|
40
|
+
force_delete = true
|
|
41
|
+
|
|
42
|
+
image_scanning_configuration {
|
|
43
|
+
scan_on_push = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
tags = {
|
|
47
|
+
Name = "${var.app_name}-${var.environment}"
|
|
48
|
+
Environment = var.environment
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
19
52
|
# ECS Cluster
|
|
20
53
|
resource "aws_ecs_cluster" "main" {
|
|
21
54
|
name = "${var.app_name}-${var.environment}"
|
|
22
55
|
|
|
23
56
|
setting {
|
|
24
|
-
name
|
|
57
|
+
name = "containerInsights"
|
|
25
58
|
value = "enabled"
|
|
26
59
|
}
|
|
27
60
|
|
|
28
61
|
tags = {
|
|
29
|
-
Name
|
|
62
|
+
Name = "${var.app_name}-${var.environment}"
|
|
30
63
|
Environment = var.environment
|
|
31
64
|
}
|
|
32
65
|
}
|
|
@@ -38,12 +71,182 @@ resource "aws_ecs_cluster_capacity_providers" "main" {
|
|
|
38
71
|
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
|
|
39
72
|
|
|
40
73
|
default_capacity_provider_strategy {
|
|
41
|
-
base
|
|
42
|
-
weight
|
|
74
|
+
base = 1
|
|
75
|
+
weight = 100
|
|
43
76
|
capacity_provider = "FARGATE"
|
|
44
77
|
}
|
|
45
78
|
}
|
|
46
79
|
|
|
80
|
+
# Application Load Balancer
|
|
81
|
+
resource "aws_lb" "main" {
|
|
82
|
+
name = "${var.app_name}-${var.environment}-alb"
|
|
83
|
+
internal = false
|
|
84
|
+
load_balancer_type = "application"
|
|
85
|
+
security_groups = [var.alb_security_group_id]
|
|
86
|
+
subnets = var.public_subnet_ids
|
|
87
|
+
|
|
88
|
+
enable_deletion_protection = false
|
|
89
|
+
|
|
90
|
+
tags = {
|
|
91
|
+
Name = "${var.app_name}-${var.environment}-alb"
|
|
92
|
+
Environment = var.environment
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ALB Target Group
|
|
97
|
+
resource "aws_lb_target_group" "app" {
|
|
98
|
+
name = "${var.app_name}-${var.environment}-tg"
|
|
99
|
+
port = var.container_port
|
|
100
|
+
protocol = "HTTP"
|
|
101
|
+
vpc_id = var.vpc_id
|
|
102
|
+
target_type = "ip"
|
|
103
|
+
|
|
104
|
+
health_check {
|
|
105
|
+
path = "/health"
|
|
106
|
+
healthy_threshold = 2
|
|
107
|
+
unhealthy_threshold = 10
|
|
108
|
+
timeout = 30
|
|
109
|
+
interval = 40
|
|
110
|
+
matcher = "200-399"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tags = {
|
|
114
|
+
Name = "${var.app_name}-${var.environment}-tg"
|
|
115
|
+
Environment = var.environment
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ALB Listener (HTTP)
|
|
120
|
+
resource "aws_lb_listener" "http" {
|
|
121
|
+
load_balancer_arn = aws_lb.main.arn
|
|
122
|
+
port = "80"
|
|
123
|
+
protocol = "HTTP"
|
|
124
|
+
|
|
125
|
+
default_action {
|
|
126
|
+
type = "forward"
|
|
127
|
+
target_group_arn = aws_lb_target_group.app.arn
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# CloudWatch Log Group for ECS
|
|
132
|
+
resource "aws_cloudwatch_log_group" "ecs" {
|
|
133
|
+
name = "/ecs/${var.app_name}-${var.environment}"
|
|
134
|
+
retention_in_days = 14
|
|
135
|
+
|
|
136
|
+
tags = {
|
|
137
|
+
Environment = var.environment
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# IAM Role for ECS Task Execution
|
|
142
|
+
resource "aws_iam_role" "ecs_task_execution_role" {
|
|
143
|
+
name = "${var.app_name}-${var.environment}-ecs-execution-role"
|
|
144
|
+
|
|
145
|
+
assume_role_policy = jsonencode({
|
|
146
|
+
Version = "2012-10-17"
|
|
147
|
+
Statement = [
|
|
148
|
+
{
|
|
149
|
+
Action = "sts:AssumeRole"
|
|
150
|
+
Effect = "Allow"
|
|
151
|
+
Principal = {
|
|
152
|
+
Service = "ecs-tasks.amazonaws.com"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" {
|
|
160
|
+
role = aws_iam_role.ecs_task_execution_role.name
|
|
161
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# IAM Role for ECS Task (the application itself)
|
|
165
|
+
resource "aws_iam_role" "ecs_task_role" {
|
|
166
|
+
name = "${var.app_name}-${var.environment}-ecs-task-role"
|
|
167
|
+
|
|
168
|
+
assume_role_policy = jsonencode({
|
|
169
|
+
Version = "2012-10-17"
|
|
170
|
+
Statement = [
|
|
171
|
+
{
|
|
172
|
+
Action = "sts:AssumeRole"
|
|
173
|
+
Effect = "Allow"
|
|
174
|
+
Principal = {
|
|
175
|
+
Service = "ecs-tasks.amazonaws.com"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ECS Task Definition
|
|
183
|
+
resource "aws_ecs_task_definition" "app" {
|
|
184
|
+
family = "${var.app_name}-${var.environment}"
|
|
185
|
+
requires_compatibilities = ["FARGATE"]
|
|
186
|
+
network_mode = "awsvpc"
|
|
187
|
+
cpu = 256
|
|
188
|
+
memory = 512
|
|
189
|
+
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
|
|
190
|
+
task_role_arn = aws_iam_role.ecs_task_role.arn
|
|
191
|
+
|
|
192
|
+
container_definitions = jsonencode([
|
|
193
|
+
{
|
|
194
|
+
name = "${var.app_name}"
|
|
195
|
+
image = "${aws_ecr_repository.app.repository_url}:latest"
|
|
196
|
+
essential = true
|
|
197
|
+
|
|
198
|
+
portMappings = [
|
|
199
|
+
{
|
|
200
|
+
containerPort = var.container_port
|
|
201
|
+
hostPort = var.container_port
|
|
202
|
+
protocol = "tcp"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
environment = [
|
|
207
|
+
{ name = "NODE_ENV", value = var.environment == "prod" ? "production" : "development" },
|
|
208
|
+
{ name = "PORT", value = tostring(var.container_port) }
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
# Note: Add Secrets (like DB passwords) via SSM Parameter Store dynamically in a real deployment
|
|
212
|
+
|
|
213
|
+
logConfiguration = {
|
|
214
|
+
logDriver = "awslogs"
|
|
215
|
+
options = {
|
|
216
|
+
"awslogs-group" = aws_cloudwatch_log_group.ecs.name
|
|
217
|
+
"awslogs-region" = "us-east-1"
|
|
218
|
+
"awslogs-stream-prefix" = "ecs"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
])
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# ECS Service
|
|
226
|
+
resource "aws_ecs_service" "app" {
|
|
227
|
+
name = "${var.app_name}-${var.environment}"
|
|
228
|
+
cluster = aws_ecs_cluster.main.id
|
|
229
|
+
task_definition = aws_ecs_task_definition.app.arn
|
|
230
|
+
desired_count = 1
|
|
231
|
+
launch_type = "FARGATE"
|
|
232
|
+
|
|
233
|
+
network_configuration {
|
|
234
|
+
subnets = var.private_subnet_ids
|
|
235
|
+
security_groups = [var.ecs_tasks_security_group_id]
|
|
236
|
+
assign_public_ip = false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
load_balancer {
|
|
240
|
+
target_group_arn = aws_lb_target_group.app.arn
|
|
241
|
+
container_name = "${var.app_name}"
|
|
242
|
+
container_port = var.container_port
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
depends_on = [
|
|
246
|
+
aws_lb_listener.http
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
|
|
47
250
|
# Outputs
|
|
48
251
|
output "cluster_name" {
|
|
49
252
|
value = aws_ecs_cluster.main.name
|
|
@@ -51,4 +254,12 @@ output "cluster_name" {
|
|
|
51
254
|
|
|
52
255
|
output "cluster_arn" {
|
|
53
256
|
value = aws_ecs_cluster.main.arn
|
|
54
|
-
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
output "ecr_repository_url" {
|
|
260
|
+
value = aws_ecr_repository.app.repository_url
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
output "alb_dns_name" {
|
|
264
|
+
value = aws_lb.main.dns_name
|
|
265
|
+
}
|