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
This commit is contained in:
mjalmoudy94
2026-10-14 23:19:05 +03:30
parent fef5a9deab
commit d532929189
117 changed files with 13119 additions and 640 deletions
+32 -2
View File
@@ -24,7 +24,11 @@ import pytest_asyncio
from httpx import AsyncClient
from app.domain.identity.models import PlatformRole
from app.domain.workforce.pipeline import Stage
from app.infrastructure.repositories.engine import PipelineStateRepository
from tests.billing_helpers import fund_org
from tests.conftest import auth_header, signup
from tests.legacy_tasks import legacy_task
@pytest_asyncio.fixture(autouse=True)
@@ -33,7 +37,9 @@ async def _platform_seed(seeded):
return seeded
async def test_end_to_end_smoke_path(client: AsyncClient):
async def test_end_to_end_smoke_path(client: AsyncClient, session_factory):
"""Decision D-D: the task made over HTTP enters the engine and refuses the legacy run, so
the run, its metering and its audit are walked on a pre-v2 task (see step 10)."""
# -- 1. signup -------------------------------------------------------- #
# The platform owner, not a plain manager: step 6 drives the sandbox
# directly, and v2 makes container/command access owner-only
@@ -184,6 +190,9 @@ async def test_end_to_end_smoke_path(client: AsyncClient):
assert thread.status_code == 201, thread.text
thread_id = thread.json()["id"]
# prepaid (D-B): the HTTP org starts at zero credit, and the chat costs model calls.
await fund_org(session_factory, org_id)
message = await client.post(
f"/api/organizations/{org_id}/threads/{thread_id}/messages",
json={"content": "برای ماژول پرداخت تست بنویس.", "employee_id": employee_id},
@@ -212,10 +221,31 @@ async def test_end_to_end_smoke_path(client: AsyncClient):
)
assert task.status_code == 201, task.text
task_id = task.json()["id"]
# Decision D-D: a task made over HTTP is the engine's from manager review, so it has a
# pipeline state and the legacy run button refuses it.
async with session_factory() as session:
state = await PipelineStateRepository(session).get(task_id)
assert state is not None and state.stage is Stage.PENDING_MANAGER_REVIEW
refused = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
json={"employee_id": employee_id},
headers=headers,
)
assert refused.status_code == 409, refused.text
# -- 10. run task ------------------------------------------------------ #
# The legacy run path serves pre-v2 tasks, which have no pipeline state, so the run, its
# metering and its audit are checked on one of those (decision D-D).
run_task_id = await legacy_task(
session_factory,
org_id,
signup_body["user"]["id"],
title="افزودن تست برای ماژول پرداخت",
project_id=project_id,
assigned_employee_id=employee_id,
)
run = await client.post(
f"/api/organizations/{org_id}/tasks/{task_id}/runs",
f"/api/organizations/{org_id}/tasks/{run_task_id}/runs",
json={"employee_id": employee_id},
headers=headers,
)