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>
297 lines
11 KiB
Python
297 lines
11 KiB
Python
"""Healthcare foundation aggregates — Phase 13.0.
|
|
|
|
Independent aggregates use UUID references within healthcare_db only.
|
|
No SQLAlchemy relationship graphs between aggregates.
|
|
No appointment/clinical workflow engines — shells and contracts only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from sqlalchemy import Index, String, Text, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from sqlalchemy.types import JSON
|
|
|
|
from app.core.database import Base
|
|
from app.models.base import (
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
SoftDeleteMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
UUIDPrimaryKeyMixin,
|
|
)
|
|
from app.models.types import AuditAction, GUID, LifecycleStatus, ProviderKind, ProviderStatus
|
|
|
|
|
|
class Clinic(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Root healthcare clinic shell for a tenant."""
|
|
|
|
__tablename__ = "clinics"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
status: Mapped[LifecycleStatus] = mapped_column(
|
|
default=LifecycleStatus.DRAFT, nullable=False
|
|
)
|
|
legal_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
timezone: Mapped[str] = mapped_column(String(64), default="Asia/Tehran", nullable=False)
|
|
language: Mapped[str] = mapped_column(String(16), default="fa", nullable=False)
|
|
currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False)
|
|
settings: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_clinics_tenant_code"),
|
|
Index("ix_clinics_tenant_status", "tenant_id", "status"),
|
|
Index("ix_clinics_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class Branch(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
"""Operational branch under a clinic."""
|
|
|
|
__tablename__ = "branches"
|
|
|
|
clinic_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
status: Mapped[LifecycleStatus] = mapped_column(
|
|
default=LifecycleStatus.ACTIVE, nullable=False
|
|
)
|
|
address: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
timezone: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
working_hours: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "clinic_id", "code", name="uq_branches_tenant_code"
|
|
),
|
|
Index("ix_branches_clinic", "tenant_id", "clinic_id"),
|
|
Index("ix_branches_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class Department(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
"""Clinical department shell under a branch/clinic."""
|
|
|
|
__tablename__ = "departments"
|
|
|
|
clinic_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
branch_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
status: Mapped[LifecycleStatus] = mapped_column(
|
|
default=LifecycleStatus.ACTIVE, nullable=False
|
|
)
|
|
specialty: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "clinic_id", "code", name="uq_departments_tenant_code"
|
|
),
|
|
Index("ix_departments_clinic", "tenant_id", "clinic_id"),
|
|
Index("ix_departments_branch", "tenant_id", "branch_id"),
|
|
Index("ix_departments_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class HealthcareRole(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
"""Local healthcare RBAC role catalog shell."""
|
|
|
|
__tablename__ = "healthcare_roles"
|
|
|
|
clinic_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
status: Mapped[LifecycleStatus] = mapped_column(
|
|
default=LifecycleStatus.ACTIVE, nullable=False
|
|
)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_healthcare_roles_tenant_code"),
|
|
Index("ix_healthcare_roles_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class HealthcarePermission(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
"""Local healthcare permission catalog shell (complements platform healthcare.* tree)."""
|
|
|
|
__tablename__ = "healthcare_permissions"
|
|
|
|
role_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
code: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_healthcare_permissions_tenant_code"),
|
|
Index("ix_healthcare_permissions_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class ExternalProviderConfig(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""External provider registration shell — credentials stay here."""
|
|
|
|
__tablename__ = "external_provider_configs"
|
|
|
|
clinic_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
provider_kind: Mapped[ProviderKind] = mapped_column(
|
|
default=ProviderKind.CUSTOM, nullable=False
|
|
)
|
|
adapter_key: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
status: Mapped[ProviderStatus] = mapped_column(
|
|
default=ProviderStatus.REGISTERED, nullable=False
|
|
)
|
|
priority: Mapped[int] = mapped_column(default=100, nullable=False)
|
|
credentials_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
capabilities: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "code", name="uq_external_provider_configs_tenant_code"
|
|
),
|
|
Index("ix_external_provider_configs_kind", "tenant_id", "provider_kind"),
|
|
Index("ix_external_provider_configs_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class HealthcareConfiguration(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Tenant healthcare configuration — hours, policies, locale shells."""
|
|
|
|
__tablename__ = "healthcare_configurations"
|
|
|
|
clinic_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
branch_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
working_hours: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
clinical_policies: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
privacy_policies: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
custom_fields: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
timezone: Mapped[str] = mapped_column(String(64), default="Asia/Tehran", nullable=False)
|
|
language: Mapped[str] = mapped_column(String(16), default="fa", nullable=False)
|
|
currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"clinic_id",
|
|
"code",
|
|
name="uq_healthcare_configurations_tenant_code",
|
|
),
|
|
Index("ix_healthcare_configurations_clinic", "tenant_id", "clinic_id"),
|
|
Index("ix_healthcare_configurations_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class HealthcareSetting(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
"""Key/value healthcare settings shell."""
|
|
|
|
__tablename__ = "healthcare_settings"
|
|
|
|
clinic_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
branch_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
key: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
value: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"clinic_id",
|
|
"branch_id",
|
|
"key",
|
|
name="uq_healthcare_settings_tenant_key",
|
|
),
|
|
Index("ix_healthcare_settings_tenant_deleted", "tenant_id", "is_deleted"),
|
|
)
|
|
|
|
|
|
class HealthcareAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
"""Append-only healthcare audit trail."""
|
|
|
|
__tablename__ = "healthcare_audit_logs"
|
|
|
|
entity_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
action: Mapped[AuditAction] = mapped_column(nullable=False)
|
|
actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
changes: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index(
|
|
"ix_healthcare_audit_entity",
|
|
"tenant_id",
|
|
"entity_type",
|
|
"entity_id",
|
|
),
|
|
)
|