first commit

This commit is contained in:
Anurag Jangid 2026-10-08 02:52:58 +05:30
commit 34ea15a54d
246 changed files with 18660 additions and 0 deletions

View file

@ -0,0 +1,203 @@
import json
import secrets
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session, joinedload
from app.core.config import Settings, get_settings
from app.core.errors import AppError
from app.models.enums import OrderStatus
from app.models.orders import Order
from app.models.payments import PaymentWebhookEvent
from app.services.orders import fulfill_order_payment, get_order_by_provider_order_id
from app.schemas.orders import PaymentCheckoutPublic
from app.services.payments.gateway import PaymentGateway, PaymentVerification, get_payment_gateway
class PaymentError(AppError):
pass
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def _record_webhook_event(
db: Session,
*,
provider: str,
provider_event_id: str,
event_type: str,
payload: dict,
) -> bool:
"""Return False if event was already processed (idempotent)."""
existing = db.scalar(
select(PaymentWebhookEvent).where(PaymentWebhookEvent.provider_event_id == provider_event_id)
)
if existing:
return False
db.add(
PaymentWebhookEvent(
provider=provider,
provider_event_id=provider_event_id,
event_type=event_type,
payload=json.dumps(payload),
)
)
db.flush()
return True
def confirm_payment_for_order(
db: Session,
order: Order,
verification: PaymentVerification,
*,
provider_payment_id: str,
) -> Order:
if order.status == OrderStatus.paid:
return order
if order.status not in (OrderStatus.pending,):
raise PaymentError("Order is not awaiting payment", status_code=409)
if order.provider_order_id and verification.provider_order_id != order.provider_order_id:
raise PaymentError("Payment order mismatch", status_code=400)
if verification.amount_paise != order.total_paise:
raise PaymentError("Payment amount mismatch", status_code=400)
if verification.currency.upper() != order.currency.upper():
raise PaymentError("Payment currency mismatch", status_code=400)
return fulfill_order_payment(db, order, provider_payment_id=provider_payment_id)
def verify_checkout_and_confirm(
db: Session,
*,
order: Order,
provider_order_id: str,
provider_payment_id: str,
signature: str,
gateway: PaymentGateway | None = None,
) -> Order:
gateway = gateway or get_payment_gateway()
if order.provider_order_id != provider_order_id:
raise PaymentError("Invalid payment order", status_code=400)
try:
gateway.verify_checkout_signature(
provider_order_id=provider_order_id,
provider_payment_id=provider_payment_id,
signature=signature,
)
except Exception as exc:
raise PaymentError("Payment verification failed", status_code=400) from exc
verification = PaymentVerification(
provider_order_id=provider_order_id,
provider_payment_id=provider_payment_id,
amount_paise=order.total_paise,
currency=order.currency,
)
return confirm_payment_for_order(db, order, verification, provider_payment_id=provider_payment_id)
def process_webhook(
db: Session,
*,
body: bytes,
signature: str | None,
gateway: PaymentGateway | None = None,
) -> dict:
gateway = gateway or get_payment_gateway()
try:
gateway.verify_webhook_signature(body, signature)
except Exception as exc:
raise PaymentError("Invalid webhook signature", status_code=400) from exc
payload = json.loads(body.decode())
event_type = payload.get("event", "unknown")
provider_event_id = payload.get("id") or payload.get("event_id") or secrets.token_hex(8)
if not _record_webhook_event(
db,
provider=gateway.provider_name,
provider_event_id=str(provider_event_id),
event_type=event_type,
payload=payload,
):
db.commit()
return {"status": "duplicate_ignored"}
if event_type == "payment.failed":
order = _order_from_webhook_payload(db, payload)
if order and order.status == OrderStatus.pending:
from app.services.orders import cancel_pending_order
cancel_pending_order(db, order, status=OrderStatus.failed)
db.commit()
return {"status": "payment_failed_handled"}
verification = gateway.parse_webhook_payment(payload)
if verification is None:
db.commit()
return {"status": "ignored"}
order = get_order_by_provider_order_id(db, verification.provider_order_id)
if not order:
db.commit()
return {"status": "order_not_found"}
confirm_payment_for_order(
db,
order,
verification,
provider_payment_id=verification.provider_payment_id,
)
db.commit()
return {"status": "paid"}
def _order_from_webhook_payload(db: Session, payload: dict) -> Order | None:
entity = payload.get("payload", {}).get("payment", {}).get("entity", {})
provider_order_id = entity.get("order_id")
if not provider_order_id:
return None
return get_order_by_provider_order_id(db, provider_order_id)
def build_checkout_payload(order: Order, gateway: PaymentGateway | None = None) -> PaymentCheckoutPublic:
gateway = gateway or get_payment_gateway()
settings = get_settings()
return PaymentCheckoutPublic(
provider=gateway.provider_name,
key_id=gateway.public_key_id,
provider_order_id=order.provider_order_id or "",
amount_paise=order.total_paise,
currency=order.currency,
order_id=order.id,
expires_at=order.expires_at,
timeout_minutes=settings.order_payment_timeout_minutes,
)
def mock_complete_pending_order(db: Session, order: Order) -> Order:
settings = get_settings()
if settings.payment_provider.lower() != "mock":
raise PaymentError("Mock completion is not enabled", status_code=404)
gateway = get_payment_gateway(settings)
from app.services.payments.gateway import MockPaymentGateway
if not isinstance(gateway, MockPaymentGateway):
raise PaymentError("Mock gateway required", status_code=404)
provider_order_id = order.provider_order_id or ""
provider_payment_id = f"mock_pay_{order.id}"
signature = gateway.build_mock_checkout_signature(provider_order_id, provider_payment_id)
return verify_checkout_and_confirm(
db,
order=order,
provider_order_id=provider_order_id,
provider_payment_id=provider_payment_id,
signature=signature,
gateway=gateway,
)