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.
Files changed (101) hide show
  1. package/package.json +1 -1
  2. package/templates/java-spring/clean/infra/main.tf.hbs +42 -18
  3. package/templates/java-spring/clean/infra/modules/ecs/main.tf.hbs +217 -6
  4. package/templates/java-spring/clean/infra/modules/rds/main.tf.hbs +15 -15
  5. package/templates/java-spring/clean/infra/modules/vpc/main.tf.hbs +170 -30
  6. package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +89 -0
  7. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/web/payment/PaymentController.java.hbs +78 -0
  8. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +7 -0
  9. package/templates/java-spring/hexagonal/infra/main.tf.hbs +42 -18
  10. package/templates/java-spring/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  11. package/templates/java-spring/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  12. package/templates/java-spring/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  13. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/inbound/web/PaymentController.java.hbs +78 -0
  14. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/stripe/StripeAdapter.java.hbs +76 -0
  15. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +90 -0
  16. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +7 -0
  17. package/templates/java-spring/mvc/infra/main.tf.hbs +42 -18
  18. package/templates/java-spring/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  19. package/templates/java-spring/mvc/infra/modules/rds/main.tf.hbs +15 -15
  20. package/templates/java-spring/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  21. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +42 -53
  22. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +105 -23
  23. package/templates/nestjs/clean/infra/main.tf.hbs +42 -18
  24. package/templates/nestjs/clean/infra/modules/ecs/main.tf.hbs +217 -6
  25. package/templates/nestjs/clean/infra/modules/rds/main.tf.hbs +15 -15
  26. package/templates/nestjs/clean/infra/modules/vpc/main.tf.hbs +170 -30
  27. package/templates/nestjs/clean/src/app.module.ts.hbs +3 -1
  28. package/templates/nestjs/clean/src/application/payment.service.ts.hbs +90 -0
  29. package/templates/nestjs/clean/src/infrastructure/http/payment.controller.ts.hbs +46 -0
  30. package/templates/nestjs/clean/src/infrastructure/stripe.provider.ts.hbs +51 -0
  31. package/templates/nestjs/clean/src/main.ts.hbs +13 -4
  32. package/templates/nestjs/clean/src/payment.module.ts.hbs +23 -0
  33. package/templates/nestjs/hexagonal/infra/main.tf.hbs +42 -18
  34. package/templates/nestjs/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  35. package/templates/nestjs/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  36. package/templates/nestjs/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  37. package/templates/nestjs/hexagonal/src/adapters/inbound/payment.controller.ts.hbs +46 -0
  38. package/templates/nestjs/hexagonal/src/adapters/outbound/stripe.adapter.ts.hbs +54 -0
  39. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -0
  40. package/templates/nestjs/hexagonal/src/core/payment.service.ts.hbs +90 -0
  41. package/templates/nestjs/hexagonal/src/main.ts.hbs +13 -4
  42. package/templates/nestjs/hexagonal/src/payment.module.ts.hbs +23 -0
  43. package/templates/nestjs/mvc/infra/main.tf.hbs +42 -18
  44. package/templates/nestjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  45. package/templates/nestjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
  46. package/templates/nestjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  47. package/templates/nestjs/mvc/src/main.ts.hbs +6 -3
  48. package/templates/nestjs/mvc/src/payments/payments.controller.ts.hbs +33 -8
  49. package/templates/nestjs/mvc/src/payments/payments.service.ts.hbs +66 -22
  50. package/templates/nextjs/mvc/infra/main.tf.hbs +42 -18
  51. package/templates/nextjs/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  52. package/templates/nextjs/mvc/infra/modules/rds/main.tf.hbs +15 -15
  53. package/templates/nextjs/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  54. package/templates/nextjs/mvc/src/app/api/checkout/route.ts.hbs +42 -13
  55. package/templates/nextjs/mvc/src/app/api/portal/route.ts.hbs +36 -0
  56. package/templates/nextjs/mvc/src/app/api/webhook/route.ts.hbs +32 -20
  57. package/templates/nodejs-express/clean/infra/main.tf.hbs +42 -18
  58. package/templates/nodejs-express/clean/infra/modules/ecs/main.tf.hbs +217 -6
  59. package/templates/nodejs-express/clean/infra/modules/rds/main.tf.hbs +15 -15
  60. package/templates/nodejs-express/clean/infra/modules/vpc/main.tf.hbs +170 -30
  61. package/templates/nodejs-express/clean/src/application/services/PaymentService.ts.hbs +98 -0
  62. package/templates/nodejs-express/clean/src/index.ts.hbs +29 -8
  63. package/templates/nodejs-express/clean/src/infrastructure/http/controllers/PaymentController.ts.hbs +57 -0
  64. package/templates/nodejs-express/clean/src/infrastructure/providers/StripeProvider.ts.hbs +45 -0
  65. package/templates/nodejs-express/hexagonal/infra/main.tf.hbs +42 -18
  66. package/templates/nodejs-express/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  67. package/templates/nodejs-express/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  68. package/templates/nodejs-express/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  69. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +57 -0
  70. package/templates/nodejs-express/hexagonal/src/adapters/outbound/StripeAdapter.ts.hbs +48 -0
  71. package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +89 -0
  72. package/templates/nodejs-express/hexagonal/src/index.ts.hbs +28 -8
  73. package/templates/nodejs-express/mvc/infra/main.tf.hbs +42 -18
  74. package/templates/nodejs-express/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  75. package/templates/nodejs-express/mvc/infra/modules/rds/main.tf.hbs +15 -15
  76. package/templates/nodejs-express/mvc/infra/modules/vpc/main.tf.hbs +170 -30
  77. package/templates/nodejs-express/mvc/src/app.ts.hbs +11 -2
  78. package/templates/nodejs-express/mvc/src/controllers/payments.controller.ts.hbs +31 -47
  79. package/templates/nodejs-express/mvc/src/services/stripe.service.ts.hbs +66 -49
  80. package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +85 -0
  81. package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +64 -0
  82. package/templates/python-fastapi/clean/app/infrastructure/stripe_provider.py.hbs +44 -0
  83. package/templates/python-fastapi/clean/app/main.py.hbs +8 -5
  84. package/templates/python-fastapi/clean/infra/main.tf.hbs +42 -18
  85. package/templates/python-fastapi/clean/infra/modules/ecs/main.tf.hbs +217 -6
  86. package/templates/python-fastapi/clean/infra/modules/rds/main.tf.hbs +15 -15
  87. package/templates/python-fastapi/clean/infra/modules/vpc/main.tf.hbs +170 -30
  88. package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +64 -0
  89. package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +44 -0
  90. package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +81 -0
  91. package/templates/python-fastapi/hexagonal/app/main.py.hbs +9 -3
  92. package/templates/python-fastapi/hexagonal/infra/main.tf.hbs +42 -18
  93. package/templates/python-fastapi/hexagonal/infra/modules/ecs/main.tf.hbs +217 -6
  94. package/templates/python-fastapi/hexagonal/infra/modules/rds/main.tf.hbs +15 -15
  95. package/templates/python-fastapi/hexagonal/infra/modules/vpc/main.tf.hbs +170 -30
  96. package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +70 -35
  97. package/templates/python-fastapi/mvc/app/services/stripe_service.py.hbs +58 -0
  98. package/templates/python-fastapi/mvc/infra/main.tf.hbs +42 -18
  99. package/templates/python-fastapi/mvc/infra/modules/ecs/main.tf.hbs +217 -6
  100. package/templates/python-fastapi/mvc/infra/modules/rds/main.tf.hbs +15 -15
  101. package/templates/python-fastapi/mvc/infra/modules/vpc/main.tf.hbs +170 -30
@@ -10,81 +10,213 @@ variable "environment" {
10
10
 
11
11
  # VPC
12
12
  resource "aws_vpc" "main" {
13
- cidr_block = "10.0.0.0/16"
13
+ cidr_block = "10.0.0.0/16"
14
14
  enable_dns_hostnames = true
15
- enable_dns_support = true
15
+ enable_dns_support = true
16
16
 
17
17
  tags = {
18
- Name = "${var.app_name}-${var.environment}-vpc"
18
+ Name = "${var.app_name}-${var.environment}-vpc"
19
19
  Environment = var.environment
20
20
  }
21
21
  }
22
22
 
23
+ # Internet Gateway
24
+ resource "aws_internet_gateway" "main" {
25
+ vpc_id = aws_vpc.main.id
26
+
27
+ tags = {
28
+ Name = "${var.app_name}-${var.environment}-igw"
29
+ Environment = var.environment
30
+ }
31
+ }
32
+
33
+ # Data source for AZs
34
+ data "aws_availability_zones" "available" {
35
+ state = "available"
36
+ }
37
+
23
38
  # Public Subnets
24
39
  resource "aws_subnet" "public" {
25
- count = 2
40
+ count = 2
41
+ vpc_id = aws_vpc.main.id
42
+ cidr_block = "10.0.${count.index + 1}.0/24"
43
+ availability_zone = data.aws_availability_zones.available.names[count.index]
44
+ map_public_ip_on_launch = true
45
+
46
+ tags = {
47
+ Name = "${var.app_name}-${var.environment}-public-${count.index + 1}"
48
+ Environment = var.environment
49
+ }
50
+ }
51
+
52
+ # Route Table for Public Subnets
53
+ resource "aws_route_table" "public" {
26
54
  vpc_id = aws_vpc.main.id
27
- cidr_block = "10.0.${count.index + 1}.0/24"
28
- availability_zone = data.aws_availability_zones.available.names[count.index]
29
55
 
30
- map_public_ip_on_launch = true
56
+ route {
57
+ cidr_block = "0.0.0.0/0"
58
+ gateway_id = aws_internet_gateway.main.id
59
+ }
60
+
61
+ tags = {
62
+ Name = "${var.app_name}-${var.environment}-public-rt"
63
+ Environment = var.environment
64
+ }
65
+ }
66
+
67
+ # Association for Public Subnets
68
+ resource "aws_route_table_association" "public" {
69
+ count = 2
70
+ subnet_id = aws_subnet.public[count.index].id
71
+ route_table_id = aws_route_table.public.id
72
+ }
73
+
74
+ # Elastic IP for NAT Gateway
75
+ resource "aws_eip" "nat" {
76
+ count = 1
77
+ domain = "vpc"
31
78
 
32
79
  tags = {
33
- Name = "${var.app_name}-${var.environment}-public-${count.index + 1}"
80
+ Name = "${var.app_name}-${var.environment}-nat-eip"
81
+ Environment = var.environment
82
+ }
83
+ }
84
+
85
+ # NAT Gateway (single NAT for cost savings, can change to 1 per AZ for production if needed)
86
+ resource "aws_nat_gateway" "main" {
87
+ count = 1
88
+ allocation_id = aws_eip.nat[0].id
89
+ subnet_id = aws_subnet.public[0].id
90
+
91
+ depends_on = [aws_internet_gateway.main]
92
+
93
+ tags = {
94
+ Name = "${var.app_name}-${var.environment}-nat"
34
95
  Environment = var.environment
35
96
  }
36
97
  }
37
98
 
38
99
  # Private Subnets
39
100
  resource "aws_subnet" "private" {
40
- count = 2
41
- vpc_id = aws_vpc.main.id
42
- cidr_block = "10.0.${count.index + 10}.0/24"
101
+ count = 2
102
+ vpc_id = aws_vpc.main.id
103
+ cidr_block = "10.0.${count.index + 10}.0/24"
43
104
  availability_zone = data.aws_availability_zones.available.names[count.index]
44
105
 
45
106
  tags = {
46
- Name = "${var.app_name}-${var.environment}-private-${count.index + 1}"
107
+ Name = "${var.app_name}-${var.environment}-private-${count.index + 1}"
47
108
  Environment = var.environment
48
109
  }
49
110
  }
50
111
 
51
- # Internet Gateway
52
- resource "aws_internet_gateway" "main" {
112
+ # Route Table for Private Subnets
113
+ resource "aws_route_table" "private" {
53
114
  vpc_id = aws_vpc.main.id
54
115
 
116
+ route {
117
+ cidr_block = "0.0.0.0/0"
118
+ nat_gateway_id = aws_nat_gateway.main[0].id
119
+ }
120
+
55
121
  tags = {
56
- Name = "${var.app_name}-${var.environment}-igw"
122
+ Name = "${var.app_name}-${var.environment}-private-rt"
57
123
  Environment = var.environment
58
124
  }
59
125
  }
60
126
 
61
- # Data source for AZs
62
- data "aws_availability_zones" "available" {
63
- state = "available"
127
+ # Association for Private Subnets
128
+ resource "aws_route_table_association" "private" {
129
+ count = 2
130
+ subnet_id = aws_subnet.private[count.index].id
131
+ route_table_id = aws_route_table.private.id
132
+ }
133
+
134
+ # Security Group for Load Balancer (ALB)
135
+ resource "aws_security_group" "alb" {
136
+ name = "${var.app_name}-${var.environment}-alb-sg"
137
+ description = "Security group for ALB"
138
+ vpc_id = aws_vpc.main.id
139
+
140
+ ingress {
141
+ from_port = 80
142
+ to_port = 80
143
+ protocol = "tcp"
144
+ cidr_blocks = ["0.0.0.0/0"]
145
+ description = "Allow HTTP from anywhere"
146
+ }
147
+
148
+ ingress {
149
+ from_port = 443
150
+ to_port = 443
151
+ protocol = "tcp"
152
+ cidr_blocks = ["0.0.0.0/0"]
153
+ description = "Allow HTTPS from anywhere"
154
+ }
155
+
156
+ egress {
157
+ from_port = 0
158
+ to_port = 0
159
+ protocol = "-1"
160
+ cidr_blocks = ["0.0.0.0/0"]
161
+ }
162
+
163
+ tags = {
164
+ Name = "${var.app_name}-${var.environment}-alb-sg"
165
+ Environment = var.environment
166
+ }
64
167
  }
65
168
 
66
- # Security Group for DB
169
+ # Security Group for ECS Tasks
170
+ resource "aws_security_group" "ecs_tasks" {
171
+ name = "${var.app_name}-${var.environment}-ecs-tasks-sg"
172
+ description = "Security group for ECS tasks"
173
+ vpc_id = aws_vpc.main.id
174
+
175
+ ingress {
176
+ from_port = 0
177
+ to_port = 0
178
+ protocol = "-1"
179
+ security_groups = [aws_security_group.alb.id]
180
+ description = "Allow all traffic from ALB"
181
+ }
182
+
183
+ egress {
184
+ from_port = 0
185
+ to_port = 0
186
+ protocol = "-1"
187
+ cidr_blocks = ["0.0.0.0/0"]
188
+ description = "Allow all outbound traffic"
189
+ }
190
+
191
+ tags = {
192
+ Name = "${var.app_name}-${var.environment}-ecs-tasks-sg"
193
+ Environment = var.environment
194
+ }
195
+ }
196
+
197
+ # Security Group for Database (RDS)
67
198
  resource "aws_security_group" "db" {
68
- name = "${var.app_name}-${var.environment}-db-sg"
199
+ name = "${var.app_name}-${var.environment}-db-sg"
69
200
  description = "Security group for database"
70
- vpc_id = aws_vpc.main.id
201
+ vpc_id = aws_vpc.main.id
71
202
 
72
203
  ingress {
73
- from_port = 5432
74
- to_port = 5432
75
- protocol = "tcp"
76
- cidr_blocks = ["10.0.0.0/16"]
204
+ from_port = 5432
205
+ to_port = 5432
206
+ protocol = "tcp"
207
+ security_groups = [aws_security_group.ecs_tasks.id]
208
+ description = "Allow PostgreSQL access from ECS tasks"
77
209
  }
78
210
 
79
211
  egress {
80
- from_port = 0
81
- to_port = 0
82
- protocol = "-1"
212
+ from_port = 0
213
+ to_port = 0
214
+ protocol = "-1"
83
215
  cidr_blocks = ["0.0.0.0/0"]
84
216
  }
85
217
 
86
218
  tags = {
87
- Name = "${var.app_name}-${var.environment}-db-sg"
219
+ Name = "${var.app_name}-${var.environment}-db-sg"
88
220
  Environment = var.environment
89
221
  }
90
222
  }
@@ -104,4 +236,12 @@ output "private_subnet_ids" {
104
236
 
105
237
  output "db_security_group_id" {
106
238
  value = aws_security_group.db.id
107
- }
239
+ }
240
+
241
+ output "alb_security_group_id" {
242
+ value = aws_security_group.alb.id
243
+ }
244
+
245
+ output "ecs_tasks_security_group_id" {
246
+ value = aws_security_group.ecs_tasks.id
247
+ }
@@ -0,0 +1,64 @@
1
+ from fastapi import APIRouter, HTTPException, Header, Request, Depends
2
+ from pydantic import BaseModel
3
+ from app.core.payment_service import PaymentService
4
+ from app.adapters.outbound.stripe_adapter import StripeAdapter
5
+ from app.infrastructure.database.session import AsyncSessionLocal
6
+ from app.infrastructure.database.user_repository import SQLAlchemyUserRepository
7
+ from app.infrastructure.security.jwt import get_current_user_id
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ def get_payment_service() -> PaymentService:
13
+ repo = SQLAlchemyUserRepository(AsyncSessionLocal())
14
+ adapter = StripeAdapter()
15
+ return PaymentService(user_repository=repo, stripe_adapter=adapter)
16
+
17
+
18
+ class CheckoutRequest(BaseModel):
19
+ price_id: str
20
+
21
+
22
+ @router.post("/checkout")
23
+ async def create_checkout(
24
+ data: CheckoutRequest,
25
+ user_id: str = Depends(get_current_user_id),
26
+ service: PaymentService = Depends(get_payment_service),
27
+ ):
28
+ """Create a Stripe Checkout Session (authenticated)."""
29
+ try:
30
+ url = await service.create_checkout_session(user_id=user_id, price_id=data.price_id)
31
+ return {"url": url}
32
+ except ValueError as e:
33
+ raise HTTPException(status_code=400, detail=str(e))
34
+
35
+
36
+ @router.post("/portal")
37
+ async def create_portal(
38
+ user_id: str = Depends(get_current_user_id),
39
+ service: PaymentService = Depends(get_payment_service),
40
+ ):
41
+ """Open Stripe Billing Portal (authenticated)."""
42
+ try:
43
+ url = await service.create_portal_session(user_id=user_id)
44
+ return {"url": url}
45
+ except ValueError as e:
46
+ raise HTTPException(status_code=400, detail=str(e))
47
+
48
+
49
+ @router.post("/webhook")
50
+ async def stripe_webhook(
51
+ request: Request,
52
+ stripe_signature: str = Header(None, alias="stripe-signature"),
53
+ service: PaymentService = Depends(get_payment_service),
54
+ ):
55
+ """Handle Stripe webhook events. No auth required – raw body."""
56
+ if not stripe_signature:
57
+ raise HTTPException(status_code=400, detail="Missing stripe-signature header")
58
+
59
+ payload = await request.body()
60
+ try:
61
+ result = await service.handle_webhook(payload=payload, sig_header=stripe_signature)
62
+ return result
63
+ except ValueError as e:
64
+ raise HTTPException(status_code=400, detail=str(e))
@@ -0,0 +1,44 @@
1
+ import stripe
2
+ import os
3
+
4
+ stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
5
+ stripe.api_version = "2026-02-25.clover"
6
+
7
+
8
+ class StripeAdapter:
9
+ """Outbound adapter: wraps the Stripe SDK for use by the core domain."""
10
+
11
+ def create_customer(self, email: str, name: str | None = None, user_id: str | None = None):
12
+ return stripe.Customer.create(
13
+ email=email,
14
+ name=name,
15
+ metadata={"userId": user_id} if user_id else {},
16
+ )
17
+
18
+ def create_checkout_session(
19
+ self,
20
+ customer_id: str,
21
+ price_id: str,
22
+ user_id: str,
23
+ success_url: str,
24
+ cancel_url: str,
25
+ ):
26
+ return stripe.checkout.Session.create(
27
+ mode="subscription",
28
+ payment_method_types=["card"],
29
+ line_items=[{"price": price_id, "quantity": 1}],
30
+ customer=customer_id,
31
+ success_url=success_url,
32
+ cancel_url=cancel_url,
33
+ client_reference_id=str(user_id),
34
+ )
35
+
36
+ def create_portal_session(self, customer_id: str, return_url: str):
37
+ return stripe.billing_portal.Session.create(
38
+ customer=customer_id,
39
+ return_url=return_url,
40
+ )
41
+
42
+ def construct_event(self, payload: bytes, sig_header: str):
43
+ webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET", "")
44
+ return stripe.Webhook.construct_event(payload, sig_header, webhook_secret)
@@ -0,0 +1,81 @@
1
+ import os
2
+ from app.core.ports.user_repository import UserRepository
3
+ from app.adapters.outbound.stripe_adapter import StripeAdapter
4
+
5
+
6
+ class PaymentService:
7
+ """Core domain service for payment operations (Hexagonal Architecture)."""
8
+
9
+ def __init__(self, user_repository: UserRepository, stripe_adapter: StripeAdapter):
10
+ self.user_repository = user_repository
11
+ self.stripe_adapter = stripe_adapter
12
+
13
+ async def create_checkout_session(self, user_id: str, price_id: str) -> str:
14
+ user = await self.user_repository.find_by_id(user_id)
15
+ if not user:
16
+ raise ValueError("User not found")
17
+
18
+ customer_id = user.stripe_customer_id
19
+
20
+ if not customer_id:
21
+ customer = self.stripe_adapter.create_customer(
22
+ email=user.email,
23
+ name=getattr(user, "name", None),
24
+ user_id=str(user.id),
25
+ )
26
+ customer_id = customer.id
27
+ user.stripe_customer_id = customer_id
28
+ await self.user_repository.save(user)
29
+
30
+ session = self.stripe_adapter.create_checkout_session(
31
+ customer_id=customer_id,
32
+ price_id=price_id,
33
+ user_id=str(user_id),
34
+ success_url=f"{os.getenv('FRONTEND_URL')}/success?session_id={{CHECKOUT_SESSION_ID}}",
35
+ cancel_url=f"{os.getenv('FRONTEND_URL')}/cancel",
36
+ )
37
+ return session.url
38
+
39
+ async def create_portal_session(self, user_id: str) -> str:
40
+ user = await self.user_repository.find_by_id(user_id)
41
+ if not user or not user.stripe_customer_id:
42
+ raise ValueError("No Stripe customer found for this user")
43
+
44
+ session = self.stripe_adapter.create_portal_session(
45
+ customer_id=user.stripe_customer_id,
46
+ return_url=f"{os.getenv('FRONTEND_URL')}/dashboard",
47
+ )
48
+ return session.url
49
+
50
+ async def handle_webhook(self, payload: bytes, sig_header: str) -> dict:
51
+ try:
52
+ event = self.stripe_adapter.construct_event(payload, sig_header)
53
+ except Exception as e:
54
+ raise ValueError(f"Webhook signature verification failed: {e}")
55
+
56
+ event_type = event["type"]
57
+ data_object = event["data"]["object"]
58
+
59
+ if event_type == "checkout.session.completed":
60
+ user_id = data_object.get("client_reference_id")
61
+ customer_id = data_object.get("customer")
62
+ if user_id and customer_id:
63
+ user = await self.user_repository.find_by_id(user_id)
64
+ if user:
65
+ user.stripe_customer_id = customer_id
66
+ await self.user_repository.save(user)
67
+ print(f"Checkout completed for user: {user_id}")
68
+
69
+ elif event_type == "customer.subscription.updated":
70
+ print(f"Subscription updated: {data_object.get('id')} | Status: {data_object.get('status')}")
71
+
72
+ elif event_type == "customer.subscription.deleted":
73
+ print(f"Subscription deleted: {data_object.get('id')}")
74
+
75
+ elif event_type == "invoice.payment_failed":
76
+ print(f"Payment failed for invoice: {data_object.get('id')}")
77
+
78
+ else:
79
+ print(f"Unhandled Stripe event: {event_type}")
80
+
81
+ return {"received": True}
@@ -1,30 +1,36 @@
1
1
  from contextlib import asynccontextmanager
2
2
  from fastapi import FastAPI
3
3
  from app.adapters.inbound.http_adapter import router as auth_router
4
+ from app.adapters.inbound.payment_http_adapter import router as payment_router
4
5
  from app.infrastructure.database.session import engine, Base
5
6
  from app.config import get_settings
6
7
 
7
8
  settings = get_settings()
8
9
 
10
+
9
11
  @asynccontextmanager
10
12
  async def lifespan(app: FastAPI):
11
13
  # Create tables on startup (for development)
14
+ # In production, use Alembic migrations
12
15
  async with engine.begin() as conn:
13
16
  await conn.run_sync(Base.metadata.create_all)
14
17
  yield
15
18
  await engine.dispose()
16
19
 
20
+
17
21
  app = FastAPI(
18
22
  title="{{projectName}} - Hexagonal Architecture",
19
- lifespan=lifespan
23
+ lifespan=lifespan,
20
24
  )
21
25
 
22
26
  app.include_router(auth_router, prefix=settings.API_V1_STR + "/auth", tags=["Auth"])
27
+ app.include_router(payment_router, prefix=settings.API_V1_STR + "/payments", tags=["Payments"])
28
+
23
29
 
24
30
  @app.get("/health")
25
31
  def health():
26
32
  return {
27
- "status": "ok",
33
+ "status": "ok",
28
34
  "architecture": "hexagonal",
29
- "project": settings.PROJECT_NAME
35
+ "project": settings.PROJECT_NAME,
30
36
  }
@@ -5,9 +5,13 @@ terraform {
5
5
 
6
6
  required_providers {
7
7
  aws = {
8
- source = "hashicorp/aws"
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 = string
31
- default = "us-east-1"
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 = string
37
- default = "dev"
40
+ type = string
41
+ default = "dev"
38
42
  }
39
43
 
40
44
  variable "app_name" {
41
45
  description = "Application name"
42
- type = string
43
- default = "{{projectNameKebabCase}}"
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 = var.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 = var.app_name
59
- environment = var.environment
60
- vpc_id = module.vpc.vpc_id
61
- subnet_ids = module.vpc.private_subnet_ids
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 = var.app_name
69
- environment = var.environment
70
- vpc_id = module.vpc.vpc_id
71
- subnet_ids = module.vpc.private_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 = module.rds.endpoint
101
+ value = module.rds.endpoint
82
102
  sensitive = true
83
- }
103
+ }
104
+
105
+ output "db_name" {
106
+ value = module.rds.db_name
107
+ }