Files
smart-org/apps/api/tests/test_billing_settle_ledger.py
T
mjalmoudy94 d532929189 feat(integration): prepaid billing, engine UI, credits console, direct tasks, v2 final
- Prepaid credit system: org wallet, debit on usage, chat gate, admin credit console
- Engine UI: /manager, /tasks, /tasks/[id], /workforce, /dashboard, /traces pages
- Direct task creation (HTTP POST /tasks, MCP tasks_create) at pending_manager_review
- Pause/resume/cancel/answer actions in engine API and UI
- Employee cap (7 per org default), review pipeline guard (active reviewer required)
- Scheduler: wide window prevents org starvation; archived employees excluded from cap
- Tenant isolation: project-id cross-org 404, engine never uses another tenant's sandbox
- Order route fix: manager built with pipeline states + employee profiles
- Contract extension: ModelGrantPort.get_org_policy with DefaultOrgPolicy
- Engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed)
- api-engine.ts typed client; api-credit.ts client
- Alembic migration c4d5e6f7a8b9: prepaid credit + engine UI tables
- 859 tests passed, 1 skipped; ruff/mypy/tsc/eslint/vitest all green

Refs: PLAN-v2.md §9 (T1–T43), todo-v2-final.md
2026-10-14 23:19:05 +03:30

113 lines
4.5 KiB
Python

"""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 == []