"""Credits & refund policy โ€” billing invariants. Thin public wrappers around `CreditsRepository`. The actual SQL lives in one place (`core.db.repositories.credits`) so that issue 006 can migrate callers without duplicating the atomic statements. Reserve-on-enqueue is sacred: a credit moves ONLY on `POST /documents` in the api, atomically (never below zero). Refund is sacred: idempotent via the `documents.refunded` flag (a retried/DLQ message can never double-refund). `refund_credit` honours the runtime policy `REFUND_POLICY`: - "all" โ†’ refund on any terminal failure - "infra_only" โ†’ refund everything EXCEPT user-garbage input (extraction_failed) See docs/ARCHITECTURE.md ยง8. Callers own the transaction (commit after these return). """ from __future__ import annotations import uuid from sqlalchemy.ext.asyncio import AsyncSession from src.contract_check.core.db.enums import FailureClass, RefundPolicyLike from src.contract_check.core.db.repositories import CreditsRepository from src.contract_check.core.db.repositories.credits import ( NON_REFUNDABLE_INFRA_ONLY, should_refund, ) __all__ = [ "reserve_credit", "refund_credit", "adjust_credits", "should_refund", "NON_REFUNDABLE_INFRA_ONLY", ] async def reserve_credit( session: AsyncSession, user_id: uuid.UUID, *, document_id: uuid.UUID | None = None, ) -> bool: """Atomically decrement credits_left by 1. Returns False if none available.""" return await CreditsRepository(session).reserve(user_id, document_id=document_id) async def refund_credit( session: AsyncSession, document_id: uuid.UUID, failure_class: FailureClass | str, policy: RefundPolicyLike, ) -> bool: """Refund one credit for a failed document, once. Returns False if skipped.""" return await CreditsRepository(session).refund(document_id, failure_class, policy) async def adjust_credits( session: AsyncSession, user_id: uuid.UUID, delta: int, *, kind: str = "admin_grant", document_id: uuid.UUID | None = None, invoice_id: uuid.UUID | None = None, ) -> int: """Adjust balance by ``delta`` (clamped at 0) and emit a ledger event. Default kind is ``admin_grant``; override for refunds/compensation. Returns the new balance. """ return await CreditsRepository(session).adjust( user_id, delta, kind=kind, document_id=document_id, invoice_id=invoice_id, )