"""Payments API endpoints for purchasing Credits. docs/planning/economy.md US-2 (task 575fd14b5360). """ from typing import Annotated from fastapi import APIRouter, Depends, Header, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import CurrentUser from app.models.wallet import TransactionType from app.services.payment import ALLOWED_CREDIT_PACKS, payment_provider from app.services.wallet import wallet_service router = APIRouter() DbDep = Annotated[AsyncSession, Depends(get_db)] class PurchaseRequest(BaseModel): credits: int = Field(..., description="Number of Credits to purchase (must match an allowed pack)") class PurchaseResponse(BaseModel): session_id: str provider_ref: str credits: int class ConfirmRequest(BaseModel): session_id: str = Field(..., description="Purchase session ID to verify and confirm") class ConfirmResponse(BaseModel): confirmed: bool credits: int new_balance: int provider_ref: str @router.post("/purchase", response_model=PurchaseResponse, status_code=status.HTTP_201_CREATED) async def create_purchase_session( payload: PurchaseRequest, current_user: CurrentUser, idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None, ): """Initiate a Credit purchase session via the payment provider.""" if payload.credits not in ALLOWED_CREDIT_PACKS: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid credit pack: {payload.credits}. Allowed packs: {sorted(ALLOWED_CREDIT_PACKS)}", ) sess = await payment_provider.create_purchase( user_id=current_user.id, credits=payload.credits, idempotency_key=idempotency_key, ) return PurchaseResponse( session_id=sess.session_id, provider_ref=sess.provider_ref, credits=sess.credits, ) @router.post("/confirm", response_model=ConfirmResponse) async def confirm_purchase( payload: ConfirmRequest, current_user: CurrentUser, db: DbDep, ): """Verify purchase session with provider and credit the user's wallet. Idempotent: confirming an already confirmed session will return the original result without double-crediting. """ conf = await payment_provider.confirm(payload.session_id) if not conf.confirmed or conf.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Payment confirmation failed, invalid session, or unauthorized.", ) # Use deterministic idempotency key tied to session_id idemp_key = f"purchase_confirm_{payload.session_id}" wallet, tx = await wallet_service.credit( db, user_id=current_user.id, amount=conf.credits, tx_type=TransactionType.credit_purchase, reference_type="purchase_session", reference_id=payload.session_id, idempotency_key=idemp_key, metadata={"provider_ref": conf.provider_ref}, ) return ConfirmResponse( confirmed=True, credits=conf.credits, new_balance=wallet.balance, provider_ref=conf.provider_ref, )