"""A settled payment is credited through the credit ledger, so the owner's history shows it. What these tests defend: * settling a pending payment as succeeded writes exactly one ``adjustment`` history row with the reason ``billing/settle-payment`` and the payment's amount, the wallet goes up by that amount once, and the organization's ``last_credit_at`` is set * settling the same payment again changes nothing: the payment is already final, so neither the wallet nor the history moves * a failed payment credits nothing and writes no history row """ from __future__ import annotations from app.application.container import build_billing_service from app.domain.base import new_id from app.domain.billing.models import CreditKind, Payment, PaymentStatus from app.domain.identity.models import PlatformRole from app.infrastructure.db.models import OrganizationRow, UserRow from app.infrastructure.repositories.billing import CreditWalletRepository, PaymentRepository from app.infrastructure.repositories.credit import CreditLedgerRepository SETTLE_REASON = "billing/settle-payment" async def _org_with_pending_payment( session_factory, slug: str, amount: float ) -> tuple[str, str]: """Create an organization and one pending payment; return (organization id, payment id).""" async with session_factory() as session: user = UserRow( id=new_id(), email=f"{slug}@example.com", password_hash="x", name="t", platform_role=PlatformRole.MANAGER.value, ) session.add(user) await session.flush() org = OrganizationRow(id=new_id(), name=slug, slug=slug, owner_user_id=user.id) session.add(org) await session.flush() payment = await PaymentRepository(session).create( Payment(organization_id=org.id, amount=amount) ) await session.commit() return org.id, payment.id async def _settle( session_factory, organization_id: str, payment_id: str, *, succeeded: bool ) -> Payment: """Settle through the service the owner's endpoint calls, in its own transaction.""" async with session_factory() as session: payment = await build_billing_service(session).settle_payment( organization_id=organization_id, payment_id=payment_id, succeeded=succeeded ) await session.commit() return payment async def _balance_and_history(session_factory, organization_id: str): """The wallet balance and every credit history row of one organization.""" async with session_factory() as session: wallet = await CreditWalletRepository(session).get_for_org(organization_id) history = await CreditLedgerRepository(session).history(organization_id) return (wallet.balance if wallet else 0.0), history async def test_a_settled_payment_writes_one_adjustment_row_and_credits_once(session_factory): organization_id, payment_id = await _org_with_pending_payment( session_factory, "settle-once", 30.0 ) payment = await _settle(session_factory, organization_id, payment_id, succeeded=True) assert payment.status == PaymentStatus.SUCCEEDED balance, history = await _balance_and_history(session_factory, organization_id) assert balance == 30.0 assert [(t.kind, t.amount, t.balance_after, t.reason) for t in history] == [ (CreditKind.ADJUSTMENT, 30.0, 30.0, SETTLE_REASON) ] async with session_factory() as session: last_credit_at = await CreditLedgerRepository(session).last_credit_at_by_org() assert organization_id in last_credit_at async def test_settling_the_same_payment_twice_does_not_credit_twice(session_factory): organization_id, payment_id = await _org_with_pending_payment( session_factory, "settle-twice", 30.0 ) await _settle(session_factory, organization_id, payment_id, succeeded=True) again = await _settle(session_factory, organization_id, payment_id, succeeded=True) assert again.status == PaymentStatus.SUCCEEDED balance, history = await _balance_and_history(session_factory, organization_id) assert balance == 30.0 assert len(history) == 1 async def test_a_failed_payment_credits_nothing(session_factory): organization_id, payment_id = await _org_with_pending_payment( session_factory, "settle-failed", 30.0 ) failed = await _settle(session_factory, organization_id, payment_id, succeeded=False) assert failed.status == PaymentStatus.FAILED balance, history = await _balance_and_history(session_factory, organization_id) assert balance == 0.0 assert history == []