Files
smart-org/apps/api/tests/test_billing_starting_credit.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

97 lines
3.7 KiB
Python

"""The optional starting credit for new organizations (``BILLING_STARTING_CREDIT``, D-B).
What these tests defend:
* with the setting at 0, an organization created over HTTP or by the wizard starts with an
empty wallet and no ``starting_credit`` history row, so the platform owner credits it by hand
* with a positive setting, both paths give the new organization that credit and exactly one
``starting_credit`` history row, and no other organization is credited
"""
from __future__ import annotations
from httpx import AsyncClient
from app.core.config import settings
from app.domain.billing.models import CreditKind
from app.infrastructure.repositories.billing import CreditWalletRepository
from app.infrastructure.repositories.credit import CreditLedgerRepository
from tests.conftest import auth_header, signup
STARTING_REASON = "اعتبار آغازین"
def _payload(name: str, slug: str) -> dict:
return {
"org_name": name,
"slug": slug,
"app_name": f"app-{slug}",
"app_type": "web_app",
"language": "node",
"framework": "next",
"git_source": "platform",
}
async def _http_org(client: AsyncClient, token: str, name: str) -> str:
response = await client.post(
"/api/organizations", json={"name": name}, headers=auth_header(token)
)
assert response.status_code == 201, response.text
return response.json()["id"]
async def _wizard_org(client: AsyncClient, token: str, name: str, slug: str) -> str:
response = await client.post(
"/api/setup/complete", json=_payload(name, slug), headers=auth_header(token)
)
assert response.status_code == 201, response.text
return response.json()["organization"]["id"]
async def _credit_state(session_factory, organization_id: str):
"""The wallet balance and the starting-credit history of one organization."""
async with session_factory() as session:
wallet = await CreditWalletRepository(session).get_for_org(organization_id)
starting = await CreditLedgerRepository(session).history(
organization_id, kinds=(CreditKind.STARTING_CREDIT,)
)
return (wallet.balance if wallet else 0.0), starting
async def test_with_the_default_setting_a_new_organization_starts_empty(
client, session_factory, seeded, monkeypatch
):
monkeypatch.setattr(settings, "billing_starting_credit", 0.0)
http_manager = await signup(client, "start-http@example.com")
wizard_manager = await signup(client, "start-wizard@example.com")
http_org = await _http_org(client, http_manager["access_token"], "سازمان HTTP")
wizard_org = await _wizard_org(
client, wizard_manager["access_token"], "سازمان ویزارد", "start-wizard-org"
)
for organization_id in (http_org, wizard_org):
balance, starting = await _credit_state(session_factory, organization_id)
assert balance == 0.0
assert starting == []
async def test_a_positive_setting_credits_both_paths_once(
client, session_factory, seeded, monkeypatch
):
monkeypatch.setattr(settings, "billing_starting_credit", 3.0)
http_manager = await signup(client, "credit-http@example.com")
wizard_manager = await signup(client, "credit-wizard@example.com")
http_org = await _http_org(client, http_manager["access_token"], "سازمان الف")
wizard_org = await _wizard_org(
client, wizard_manager["access_token"], "سازمان ب", "credit-wizard-org"
)
for organization_id in (http_org, wizard_org):
balance, starting = await _credit_state(session_factory, organization_id)
assert balance == 3.0
assert len(starting) == 1
assert starting[0].amount == 3.0
assert starting[0].balance_after == 3.0
assert starting[0].reason == STARTING_REASON