- 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
97 lines
3.7 KiB
Python
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
|