79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
"""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,
|
|
)
|