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
|
@@ -10,81 +10,213 @@ variable "environment" {
|
|
|
10
10
|
|
|
11
11
|
# VPC
|
|
12
12
|
resource "aws_vpc" "main" {
|
|
13
|
-
cidr_block
|
|
13
|
+
cidr_block = "10.0.0.0/16"
|
|
14
14
|
enable_dns_hostnames = true
|
|
15
|
-
enable_dns_support
|
|
15
|
+
enable_dns_support = true
|
|
16
16
|
|
|
17
17
|
tags = {
|
|
18
|
-
Name
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
41
|
-
vpc_id
|
|
42
|
-
cidr_block
|
|
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
|
|
107
|
+
Name = "${var.app_name}-${var.environment}-private-${count.index + 1}"
|
|
47
108
|
Environment = var.environment
|
|
48
109
|
}
|
|
49
110
|
}
|
|
50
111
|
|
|
51
|
-
#
|
|
52
|
-
resource "
|
|
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
|
|
122
|
+
Name = "${var.app_name}-${var.environment}-private-rt"
|
|
57
123
|
Environment = var.environment
|
|
58
124
|
}
|
|
59
125
|
}
|
|
60
126
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
199
|
+
name = "${var.app_name}-${var.environment}-db-sg"
|
|
69
200
|
description = "Security group for database"
|
|
70
|
-
vpc_id
|
|
201
|
+
vpc_id = aws_vpc.main.id
|
|
71
202
|
|
|
72
203
|
ingress {
|
|
73
|
-
from_port
|
|
74
|
-
to_port
|
|
75
|
-
protocol
|
|
76
|
-
|
|
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
|
|
81
|
-
to_port
|
|
82
|
-
protocol
|
|
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
|
|
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
|
|
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
|
+
}
|