Add the Healthcare platform backend (phases 13.0–13.7) with Alembic migration revision fix, production deploy script, validation reports, shared UI exports, and loyalty list page client directives so Next.js build succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
"""Architecture tests — Healthcare module boundary enforcement."""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from app.models import foundation as models
|
|
|
|
FORBIDDEN_IMPORT_PREFIXES = (
|
|
"backend.services.accounting",
|
|
"backend.services.crm",
|
|
"backend.services.loyalty",
|
|
"backend.services.communication",
|
|
"backend.services.delivery",
|
|
"backend.services.sports_center",
|
|
"backend.services.automation",
|
|
"backend.services.notification",
|
|
"backend.services.file_storage",
|
|
"backend.services.identity_access",
|
|
"backend.core_service",
|
|
)
|
|
|
|
FOUNDATION_MODELS = [
|
|
models.Clinic,
|
|
models.Branch,
|
|
models.Department,
|
|
models.HealthcareRole,
|
|
models.HealthcarePermission,
|
|
models.ExternalProviderConfig,
|
|
models.HealthcareConfiguration,
|
|
models.HealthcareSetting,
|
|
models.HealthcareAuditLog,
|
|
]
|
|
|
|
|
|
def test_all_models_have_tenant_id():
|
|
from app.core.database import Base
|
|
import app.models # noqa: F401
|
|
|
|
skip = {"alembic_version"}
|
|
for table in Base.metadata.tables.values():
|
|
if table.name in skip:
|
|
continue
|
|
assert "tenant_id" in table.columns, f"{table.name} missing tenant_id"
|
|
|
|
|
|
def test_foundation_aggregates_are_independent():
|
|
assert len(FOUNDATION_MODELS) == 9
|
|
for model in FOUNDATION_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_no_clinical_engine_tables():
|
|
from app.core.database import Base
|
|
import app.models # noqa: F401
|
|
|
|
names = set(Base.metadata.tables.keys())
|
|
assert "doctors" in names
|
|
assert "patients" in names
|
|
assert "departments" in names
|
|
assert "outbox_events" in names
|
|
assert "appointments" in names
|
|
assert "schedules" in names
|
|
assert "visit_sessions" in names
|
|
assert "medical_records" in names
|
|
assert "prescription_orders" in names
|
|
assert "delivery_job_intents" in names
|
|
forbidden = {
|
|
"routing_engine_registrations",
|
|
"drivers",
|
|
}
|
|
assert forbidden.isdisjoint(names)
|
|
|
|
|
|
def test_permissions_defined():
|
|
from app.permissions.definitions import ALL_PERMISSIONS, PERMISSION_PREFIXES
|
|
|
|
assert "healthcare.view" in ALL_PERMISSIONS
|
|
assert "healthcare.clinics.create" in ALL_PERMISSIONS
|
|
assert "healthcare.external_providers.manage" in ALL_PERMISSIONS
|
|
assert "healthcare.doctors.view" in ALL_PERMISSIONS
|
|
assert "healthcare.patients.manage" in ALL_PERMISSIONS
|
|
assert "healthcare.audit.view" in ALL_PERMISSIONS
|
|
for prefix in PERMISSION_PREFIXES:
|
|
assert any(p.startswith(prefix.rstrip(".")) or p.startswith(prefix) for p in ALL_PERMISSIONS)
|
|
|
|
|
|
def test_events_defined():
|
|
from app.events.types import HealthcareEventType
|
|
|
|
assert HealthcareEventType.CLINIC_CREATED.value == "healthcare.clinic.created"
|
|
assert HealthcareEventType.CLINIC_UPDATED.value == "healthcare.clinic.updated"
|
|
assert HealthcareEventType.DOCTOR_CREATED.value == "healthcare.doctor.created"
|
|
assert HealthcareEventType.PATIENT_CREATED.value == "healthcare.patient.created"
|
|
assert HealthcareEventType.SETTING_UPDATED.value == "healthcare.setting.updated"
|
|
|
|
|
|
def test_platform_provider_contracts_exist():
|
|
from app.providers import (
|
|
AIProvider,
|
|
AccountingProvider,
|
|
CRMProvider,
|
|
CommunicationProvider,
|
|
DeliveryProvider,
|
|
IdentityProvider,
|
|
LoyaltyProvider,
|
|
StorageProvider,
|
|
)
|
|
|
|
assert AccountingProvider is not None
|
|
assert CommunicationProvider is not None
|
|
assert DeliveryProvider is not None
|
|
assert LoyaltyProvider is not None
|
|
assert CRMProvider is not None
|
|
assert StorageProvider is not None
|
|
assert AIProvider is not None
|
|
assert IdentityProvider is not None
|
|
|
|
|
|
def test_no_forbidden_service_imports():
|
|
root = Path(__file__).resolve().parents[1]
|
|
violations: list[str] = []
|
|
for path in root.rglob("*.py"):
|
|
if "tests" in path.parts:
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
for forbidden in FORBIDDEN_IMPORT_PREFIXES:
|
|
if alias.name.startswith(forbidden) or forbidden in alias.name:
|
|
violations.append(f"{path}: import {alias.name}")
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
for forbidden in FORBIDDEN_IMPORT_PREFIXES:
|
|
if node.module.startswith(forbidden) or forbidden in node.module:
|
|
violations.append(f"{path}: from {node.module}")
|
|
assert violations == []
|
|
|
|
|
|
def test_api_ownership_is_healthcare_only():
|
|
from app.api.v1 import api_router
|
|
|
|
prefixes = {getattr(r, "path", "") for r in api_router.routes}
|
|
joined = " ".join(sorted(prefixes))
|
|
assert "/clinics" in joined or any("/clinics" in p for p in prefixes)
|
|
assert "/doctors" in joined or any("/doctors" in p for p in prefixes)
|
|
assert "/patients" in joined or any("/patients" in p for p in prefixes)
|
|
forbidden = ("accounting", "crm", "loyalty", "notification", "automation", "sports")
|
|
for name in forbidden:
|
|
assert not re.search(rf"(^|/){re.escape(name)}(/|$|\s)", joined), name
|
|
|
|
|
|
def test_folder_structure():
|
|
root = Path(__file__).resolve().parents[2]
|
|
required = [
|
|
"app/models",
|
|
"app/repositories",
|
|
"app/services",
|
|
"app/validators",
|
|
"app/schemas",
|
|
"app/events",
|
|
"app/permissions",
|
|
"app/providers",
|
|
"app/policies",
|
|
"app/specifications",
|
|
"app/commands",
|
|
"app/queries",
|
|
"app/api/v1",
|
|
"app/tests",
|
|
"alembic/versions",
|
|
"README.md",
|
|
]
|
|
for rel in required:
|
|
assert (root / rel).exists(), f"missing {rel}"
|