Skip to content

Commit afbeb05

Browse files
committed
Sync payment integration code from payment-integration branch
- Add payment models, CRUD functions, and Razorpay service - Add payment API routes and frontend components - Add database migration for payment tables - Add .gitignore entry for .env - Add .env.example with placeholder Razorpay keys - Keep upstream-contribution README style
1 parent 1e2e7a1 commit afbeb05

File tree

8 files changed

+394
-4
lines changed

8 files changed

+394
-4
lines changed

.env.example

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Domain
2+
# This would be set to the production domain with an env var on deployment
3+
# used by Traefik to transmit traffic and acquire TLS certificates
4+
DOMAIN=localhost
5+
# To test the local Traefik config
6+
# DOMAIN=localhost.tiangolo.com
7+
8+
# Used by the backend to generate links in emails to the frontend
9+
FRONTEND_HOST=http://localhost:5173
10+
# In staging and production, set this env var to the frontend host, e.g.
11+
# FRONTEND_HOST=https://dashboard.example.com
12+
13+
# Environment: local, staging, production
14+
ENVIRONMENT=local
15+
16+
PROJECT_NAME="Full Stack FastAPI Project"
17+
STACK_NAME=full-stack-fastapi-project
18+
19+
# Backend
20+
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
21+
SECRET_KEY=changethis
22+
FIRST_SUPERUSER=[email protected]
23+
FIRST_SUPERUSER_PASSWORD=changethis
24+
25+
# Emails
26+
SMTP_HOST=
27+
SMTP_USER=
28+
SMTP_PASSWORD=
29+
EMAILS_FROM_EMAIL=[email protected]
30+
SMTP_TLS=True
31+
SMTP_SSL=False
32+
SMTP_PORT=587
33+
34+
# Postgres
35+
POSTGRES_SERVER=localhost
36+
POSTGRES_PORT=5432
37+
POSTGRES_DB=app
38+
POSTGRES_USER=postgres
39+
POSTGRES_PASSWORD=changethis
40+
41+
SENTRY_DSN=
42+
43+
# Configure these with your own Docker registry images
44+
DOCKER_IMAGE_BACKEND=backend
45+
DOCKER_IMAGE_FRONTEND=frontend
46+
47+
# Razorpay Payment Gateway Configuration
48+
# Get your API keys from: https://dashboard.razorpay.com/app/keys
49+
# Test keys are available immediately (no KYC required)
50+
# Live keys require KYC verification (1-2 working days)
51+
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
52+
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxx
53+
# Optional: For production webhook verification
54+
# Get webhook secret from: Settings → Webhooks in Razorpay Dashboard
55+
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_modules/
44
/playwright-report/
55
/blob-report/
66
/playwright/.cache/
7+
.env

backend/app/api/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, private, users, utils
3+
from app.api.routes import items, login, payments, private, users, utils
44
from app.core.config import settings
55

66
api_router = APIRouter()
77
api_router.include_router(login.router)
88
api_router.include_router(users.router)
99
api_router.include_router(utils.router)
1010
api_router.include_router(items.router)
11+
api_router.include_router(payments.router)
1112

1213

1314
if settings.ENVIRONMENT == "local":

backend/app/api/routes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import payments
2+

backend/app/core/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ def emails_enabled(self) -> bool:
9494
FIRST_SUPERUSER: EmailStr
9595
FIRST_SUPERUSER_PASSWORD: str
9696

97+
# Razorpay Payment Gateway Configuration
98+
RAZORPAY_KEY_ID: str = ""
99+
RAZORPAY_KEY_SECRET: str = ""
100+
RAZORPAY_WEBHOOK_SECRET: str | None = None
101+
102+
@computed_field # type: ignore[prop-decorator]
103+
@property
104+
def razorpay_enabled(self) -> bool:
105+
return bool(self.RAZORPAY_KEY_ID and self.RAZORPAY_KEY_SECRET)
106+
97107
def _check_default_secret(self, var_name: str, value: str | None) -> None:
98108
if value == "changethis":
99109
message = (
@@ -112,6 +122,17 @@ def _enforce_non_default_secrets(self) -> Self:
112122
self._check_default_secret(
113123
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
114124
)
125+
# Warn if Razorpay keys are not set in production
126+
if (
127+
self.ENVIRONMENT != "local"
128+
and not self.razorpay_enabled
129+
and self.RAZORPAY_KEY_ID == ""
130+
):
131+
warnings.warn(
132+
"RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET are not set. "
133+
"Payment functionality will be disabled.",
134+
stacklevel=1,
135+
)
115136

116137
return self
117138

backend/app/crud.py

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import uuid
22
from typing import Any
33

4-
from sqlmodel import Session, select
4+
from sqlmodel import Session, func, select
55

66
from app.core.security import get_password_hash, verify_password
7-
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
7+
from app.models import (
8+
Item,
9+
ItemCreate,
10+
Order,
11+
OrderCreate,
12+
Payment,
13+
PaymentCreate,
14+
User,
15+
UserCreate,
16+
UserUpdate,
17+
)
818

919

1020
def create_user(*, session: Session, user_create: UserCreate) -> User:
@@ -52,3 +62,149 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
5262
session.commit()
5363
session.refresh(db_item)
5464
return db_item
65+
66+
67+
# Payment CRUD functions
68+
def create_order(
69+
*, session: Session, order_in: OrderCreate, user_id: uuid.UUID, razorpay_order_id: str
70+
) -> Order:
71+
"""
72+
Create an order in the database.
73+
74+
Args:
75+
session: Database session
76+
order_in: Order creation data
77+
user_id: User ID who created the order
78+
razorpay_order_id: Razorpay order ID
79+
80+
Returns:
81+
Created Order object
82+
"""
83+
db_order = Order.model_validate(
84+
order_in, update={"user_id": user_id, "razorpay_order_id": razorpay_order_id, "status": "created"}
85+
)
86+
session.add(db_order)
87+
session.commit()
88+
session.refresh(db_order)
89+
return db_order
90+
91+
92+
def get_order_by_id(*, session: Session, order_id: uuid.UUID) -> Order | None:
93+
"""
94+
Get order by UUID.
95+
96+
Args:
97+
session: Database session
98+
order_id: Order UUID
99+
100+
Returns:
101+
Order object or None if not found
102+
"""
103+
return session.get(Order, order_id)
104+
105+
106+
def get_order_by_razorpay_id(
107+
*, session: Session, razorpay_order_id: str
108+
) -> Order | None:
109+
"""
110+
Get order by Razorpay order ID.
111+
112+
Args:
113+
session: Database session
114+
razorpay_order_id: Razorpay order ID
115+
116+
Returns:
117+
Order object or None if not found
118+
"""
119+
statement = select(Order).where(Order.razorpay_order_id == razorpay_order_id)
120+
return session.exec(statement).first()
121+
122+
123+
def update_order_status(
124+
*, session: Session, order: Order, status: str
125+
) -> Order:
126+
"""
127+
Update order status.
128+
129+
Args:
130+
session: Database session
131+
order: Order object to update
132+
status: New status
133+
134+
Returns:
135+
Updated Order object
136+
"""
137+
from datetime import datetime
138+
order.status = status
139+
order.updated_at = datetime.utcnow()
140+
session.add(order)
141+
session.commit()
142+
session.refresh(order)
143+
return order
144+
145+
146+
def create_payment(*, session: Session, payment_in: PaymentCreate) -> Payment:
147+
"""
148+
Create a payment record in the database.
149+
150+
Args:
151+
session: Database session
152+
payment_in: Payment creation data
153+
154+
Returns:
155+
Created Payment object
156+
"""
157+
db_payment = Payment.model_validate(payment_in)
158+
session.add(db_payment)
159+
session.commit()
160+
session.refresh(db_payment)
161+
return db_payment
162+
163+
164+
def get_payments_by_order(*, session: Session, order_id: uuid.UUID) -> list[Payment]:
165+
"""
166+
Get all payments for an order.
167+
168+
Args:
169+
session: Database session
170+
order_id: Order UUID
171+
172+
Returns:
173+
List of Payment objects
174+
"""
175+
statement = select(Payment).where(Payment.order_id == order_id)
176+
return list(session.exec(statement).all())
177+
178+
179+
def get_user_orders(
180+
*, session: Session, user_id: uuid.UUID, skip: int = 0, limit: int = 100
181+
) -> tuple[list[Order], int]:
182+
"""
183+
Get paginated list of orders for a user.
184+
185+
Args:
186+
session: Database session
187+
user_id: User UUID
188+
skip: Number of records to skip
189+
limit: Maximum number of records to return
190+
191+
Returns:
192+
Tuple of (list of Order objects, total count)
193+
"""
194+
count_statement = (
195+
select(func.count())
196+
.select_from(Order)
197+
.where(Order.user_id == user_id)
198+
)
199+
count = session.exec(count_statement).one()
200+
201+
statement = (
202+
select(Order)
203+
.where(Order.user_id == user_id)
204+
.order_by(Order.created_at.desc())
205+
.offset(skip)
206+
.limit(limit)
207+
)
208+
orders = list(session.exec(statement).all())
209+
210+
return orders, count

0 commit comments

Comments
 (0)