154 lines
4.8 KiB
Python
154 lines
4.8 KiB
Python
"""
|
|
@router PaymentRouter (Backend/app/api/v1/routers/PaymentRouter.py)
|
|
@purpose API router handling Razorpay order initiation, cryptographic verification, status checks, and webhook ingestion.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Header, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
from app.core.database.db_session import get_db
|
|
from app.api.v1.routers.CustomerProfileRouter import get_current_customer
|
|
from app.models.EcomCustomerModel import EcomCustomer
|
|
from app.models.PaymentLedgerModel import PaymentLedger
|
|
from app.core.payment_orchestrator import payment_orchestrator
|
|
|
|
router = APIRouter(prefix="/api/v1/payment", tags=["Razorpay Payment Engine"])
|
|
|
|
# --- Request / Response Models ---
|
|
|
|
class PaymentInitiateRequest(BaseModel):
|
|
order_id: str
|
|
|
|
class PaymentInitiateResponse(BaseModel):
|
|
order_id: str
|
|
rzp_order_id: str
|
|
rzp_key_id: str
|
|
amount: int
|
|
currency: str
|
|
security_token: str
|
|
zero_amount: Optional[bool] = False
|
|
status: Optional[str] = None
|
|
invoice_id: Optional[str] = None
|
|
invoice_no: Optional[str] = None
|
|
|
|
class PaymentCancelRequest(BaseModel):
|
|
order_id: str
|
|
reason: Optional[str] = "Payment cancelled by user"
|
|
|
|
class PaymentVerifyRequest(BaseModel):
|
|
order_id: str
|
|
razorpay_order_id: str
|
|
razorpay_payment_id: str
|
|
razorpay_signature: str
|
|
|
|
@router.post("/initiate", response_model=PaymentInitiateResponse)
|
|
def initiate_payment(
|
|
payload: PaymentInitiateRequest,
|
|
request: Request,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Step 1: Initiate payment intent for an e-commerce order (creates Razorpay Order).
|
|
"""
|
|
client_ip = request.client.host if request.client else None
|
|
user_agent = request.headers.get("user-agent")
|
|
|
|
result = payment_orchestrator.initiate_payment(
|
|
db, payload.order_id, client_ip=client_ip, user_agent=user_agent
|
|
)
|
|
|
|
if "error" in result:
|
|
raise HTTPException(status_code=result["status_code"], detail=result["error"])
|
|
|
|
return result
|
|
|
|
@router.post("/verify")
|
|
def verify_payment(
|
|
payload: PaymentVerifyRequest,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Step 2: Cryptographic signature verification + secondary server-side provider fetch.
|
|
"""
|
|
result = payment_orchestrator.verify_payment(
|
|
db,
|
|
order_id=payload.order_id,
|
|
rzp_order_id=payload.razorpay_order_id,
|
|
rzp_payment_id=payload.razorpay_payment_id,
|
|
rzp_signature=payload.razorpay_signature
|
|
)
|
|
|
|
if "error" in result:
|
|
raise HTTPException(status_code=result["status_code"], detail=result["error"])
|
|
|
|
return result
|
|
|
|
@router.post("/cancel")
|
|
def cancel_payment(
|
|
payload: PaymentCancelRequest,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Cancel pending payment intent and release reserved stock.
|
|
"""
|
|
result = payment_orchestrator.cancel_payment(
|
|
db, order_id=payload.order_id, reason=payload.reason or "Payment cancelled by user"
|
|
)
|
|
|
|
if "error" in result:
|
|
raise HTTPException(status_code=result["status_code"], detail=result["error"])
|
|
|
|
return result
|
|
|
|
@router.get("/status/{order_id}")
|
|
def get_payment_status(
|
|
order_id: str,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Returns latest payment ledger status for an order.
|
|
"""
|
|
entry = (
|
|
db.query(PaymentLedger)
|
|
.filter(PaymentLedger.order_id == order_id)
|
|
.order_by(PaymentLedger.created_at.desc())
|
|
.first()
|
|
)
|
|
if not entry:
|
|
raise HTTPException(status_code=404, detail="No payment session found for this order")
|
|
|
|
return {
|
|
"order_id": order_id,
|
|
"payment_id": entry.payment_id,
|
|
"provider": entry.provider,
|
|
"status": entry.status,
|
|
"transaction_ref": entry.transaction_ref,
|
|
"amount": float(entry.amount),
|
|
"currency": entry.currency,
|
|
"created_at": entry.created_at
|
|
}
|
|
|
|
@router.post("/webhook")
|
|
async def razorpay_webhook(
|
|
request: Request,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Asynchronous Webhook receiver with HMAC verification and idempotency check.
|
|
"""
|
|
raw_body = await request.body()
|
|
signature = request.headers.get("X-Razorpay-Signature") or request.headers.get("x-razorpay-signature")
|
|
|
|
if not signature:
|
|
raise HTTPException(status_code=400, detail="Missing X-Razorpay-Signature header")
|
|
|
|
success = payment_orchestrator.process_webhook(db, raw_body, signature)
|
|
if not success:
|
|
return {"status": "ignored", "reason": "invalid_signature_or_payload"}
|
|
|
|
return {"status": "SUCCESS"}
|