ifixkart-backend/app/api/v1/routers/PaymentRouter.py

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"}