mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-11 11:54:59 +03:00
Single idempotent write boundary provision_organization coordinates Organization, OWNER membership or invitation, subscription/usage, system departments sales/support, audit and transactional outbox in one transaction under set_local_tenant. - New hub_platform.platform domain: PlatformOperator, hash-only PlatformToken, OrganizationProvisioning process record with state machine PENDING/IN_PROGRESS/WAITING_FOR_OWNER/COMPLETED/FAILED. - Platform API POST platform.custocrm.ru/api/v1/organizations: machine token auth + capability platform.organizations.provision, Idempotency-Key header, replay/conflict handling, no secrets in response. - OWNER invitation accept (accept_invitation + auth view) activates organization, subscription and first usage period; idempotent re-accept does not duplicate membership or period. - DB write boundary (tenancy/0005): platform INSERT/UPDATE on identity_organization and platform DML + tenant-isolation policy on identity_department/identity_employeeprofile; Organization.status ACTIVE/PENDING_OWNER (identity/0015). - System department codes centralised in identity/system_departments. Machine token without MFA is an owner-approved deviation from ADR-HUB-0031 section 9; human+MFA platform login belongs to track D. concurrent_p2p_calls derived from membership count is a separate C07 decision (PLAN-CUSTOCRM-0003 section 12). Gate: idempotent replay creates no second tenant; operator never becomes OWNER; provisioning creates no AI-agent/product/connection/demo data. Full backend regression 379 passed (one pre-existing unrelated SOCKS5 test); 19 C06 targeted tests passed. UI unchanged. Production migration/deploy, publication of production PlanVersion and bootstrap of the production platform token are not part of this commit.
66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from hub_platform.platform.provisioning_command import ProvisioningCommand
|
|
from hub_platform.platform.provisioning_models import ProvisioningSource
|
|
|
|
_REQUIRED_FIELDS = (
|
|
"name",
|
|
"slug",
|
|
"owner_email",
|
|
"plan_version_id",
|
|
"ai_agent_quantity",
|
|
"timezone",
|
|
"currency",
|
|
)
|
|
|
|
|
|
def parse_provisioning_body(
|
|
body: Any, *, idempotency_key: str, source: str
|
|
) -> tuple[ProvisioningCommand, str | None]:
|
|
"""Map the API body (SPEC-HUB-0021 §12) to a ProvisioningCommand. Returns
|
|
(command, error_message). Validation is intentionally explicit (no DRF
|
|
serializers), mirroring identity/access_payloads.py."""
|
|
if not isinstance(body, dict):
|
|
return _empty_command(idempotency_key, source), "Request body must be an object"
|
|
for field in _REQUIRED_FIELDS:
|
|
if field not in body:
|
|
return _empty_command(idempotency_key, source), f"{field} is required"
|
|
quantity_raw = body.get("ai_agent_quantity")
|
|
if not isinstance(quantity_raw, int) or isinstance(quantity_raw, bool):
|
|
return _empty_command(idempotency_key, source), "ai_agent_quantity must be an integer"
|
|
command = ProvisioningCommand(
|
|
organization_name=str(body.get("name", "")),
|
|
organization_slug=str(body.get("slug", "")),
|
|
owner_email=str(body.get("owner_email", "")),
|
|
plan_version_id=str(body.get("plan_version_id", "")),
|
|
ai_agent_quantity=quantity_raw,
|
|
source=source,
|
|
idempotency_key=idempotency_key,
|
|
timezone=str(body.get("timezone", "Europe/Moscow")),
|
|
currency=str(body.get("currency", "RUB")),
|
|
locale=str(body.get("locale", "")),
|
|
legal_name=str(body.get("legal_name", "")),
|
|
tax_profile=body.get("tax_profile") if isinstance(body.get("tax_profile"), dict) else None,
|
|
)
|
|
if not _valid_source(source):
|
|
return command, f"Unsupported source: {source}"
|
|
return command, None
|
|
|
|
|
|
def _valid_source(source: str) -> bool:
|
|
return source in ProvisioningSource.values
|
|
|
|
|
|
def _empty_command(idempotency_key: str, source: str) -> ProvisioningCommand:
|
|
return ProvisioningCommand(
|
|
organization_name="",
|
|
organization_slug="",
|
|
owner_email="",
|
|
plan_version_id="",
|
|
ai_agent_quantity=0,
|
|
source=source,
|
|
idempotency_key=idempotency_key,
|
|
)
|