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>
787 lines
28 KiB
Python
787 lines
28 KiB
Python
"""Healthcare foundation application services — Phase 13.0."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import HealthcareEventType
|
|
from app.models.foundation import (
|
|
Branch,
|
|
Clinic,
|
|
Department,
|
|
ExternalProviderConfig,
|
|
HealthcareConfiguration,
|
|
HealthcareSetting,
|
|
)
|
|
from app.models.types import AuditAction
|
|
from app.repositories.foundation import (
|
|
BranchRepository,
|
|
ClinicRepository,
|
|
DepartmentRepository,
|
|
ExternalProviderConfigRepository,
|
|
HealthcareConfigurationRepository,
|
|
HealthcareSettingRepository,
|
|
)
|
|
from app.schemas.foundation import (
|
|
BranchCreate,
|
|
BranchUpdate,
|
|
ClinicCreate,
|
|
ClinicUpdate,
|
|
DepartmentCreate,
|
|
DepartmentUpdate,
|
|
ExternalProviderConfigCreate,
|
|
ExternalProviderConfigUpdate,
|
|
HealthcareConfigurationCreate,
|
|
HealthcareConfigurationUpdate,
|
|
HealthcareSettingUpsert,
|
|
)
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import (
|
|
ensure_optimistic_version,
|
|
validate_code,
|
|
validate_currency_code,
|
|
validate_non_empty,
|
|
)
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
|
|
def _apply_update(entity, data: dict) -> None:
|
|
for key, value in data.items():
|
|
setattr(entity, key, value)
|
|
|
|
|
|
def _jsonable_changes(data: dict[str, Any]) -> dict[str, Any]:
|
|
out: dict[str, Any] = {}
|
|
for key, value in data.items():
|
|
if isinstance(value, UUID):
|
|
out[key] = str(value)
|
|
elif isinstance(value, (datetime, date)):
|
|
out[key] = value.isoformat()
|
|
elif hasattr(value, "value"):
|
|
out[key] = value.value
|
|
else:
|
|
out[key] = value
|
|
return out
|
|
|
|
|
|
def _actor_id(actor: CurrentUser | None) -> str | None:
|
|
return actor.user_id if actor else None
|
|
|
|
|
|
class ClinicService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = ClinicRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: ClinicCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Clinic:
|
|
code = validate_code(body.code)
|
|
name = validate_non_empty(body.name, "name")
|
|
currency = validate_currency_code(body.currency_code)
|
|
if await self.repo.get_by_code(tenant_id, code):
|
|
raise AppError(
|
|
"کد کلینیک تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
)
|
|
entity = Clinic(
|
|
tenant_id=tenant_id,
|
|
code=code,
|
|
name=name,
|
|
description=body.description,
|
|
status=body.status,
|
|
legal_name=body.legal_name,
|
|
timezone=body.timezone,
|
|
language=body.language,
|
|
currency_code=currency,
|
|
settings=body.settings,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
try:
|
|
await self.repo.add(entity)
|
|
except IntegrityError as exc:
|
|
raise AppError(
|
|
"کد کلینیک تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
) from exc
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="clinic",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.CLINIC_CREATED,
|
|
aggregate_type="clinic",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code, "name": entity.name},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> Clinic:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError("کلینیک یافت نشد")
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[Clinic]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|
|
|
|
async def update(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
body: ClinicUpdate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Clinic:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
ensure_optimistic_version(entity, body.version)
|
|
data = body.model_dump(exclude_unset=True, exclude={"version"})
|
|
if "name" in data and data["name"] is not None:
|
|
data["name"] = validate_non_empty(data["name"], "name")
|
|
if "currency_code" in data and data["currency_code"] is not None:
|
|
data["currency_code"] = validate_currency_code(data["currency_code"])
|
|
_apply_update(entity, data)
|
|
entity.version += 1
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="clinic",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(data),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.CLINIC_UPDATED,
|
|
aggregate_type="clinic",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def soft_delete(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Clinic:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="clinic",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor_id(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
class BranchService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = BranchRepository(session)
|
|
self.clinics = ClinicRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: BranchCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Branch:
|
|
clinic = await self.clinics.get(tenant_id, body.clinic_id)
|
|
if clinic is None:
|
|
raise NotFoundError("کلینیک یافت نشد")
|
|
code = validate_code(body.code)
|
|
name = validate_non_empty(body.name, "name")
|
|
entity = Branch(
|
|
tenant_id=tenant_id,
|
|
clinic_id=body.clinic_id,
|
|
code=code,
|
|
name=name,
|
|
description=body.description,
|
|
status=body.status,
|
|
address=body.address,
|
|
timezone=body.timezone,
|
|
working_hours=body.working_hours,
|
|
metadata_json=body.metadata_json,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
try:
|
|
await self.repo.add(entity)
|
|
except IntegrityError as exc:
|
|
raise AppError(
|
|
"کد شعبه تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
) from exc
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="branch",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.BRANCH_CREATED,
|
|
aggregate_type="branch",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code, "clinic_id": str(entity.clinic_id)},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> Branch:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError("شعبه یافت نشد")
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[Branch]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|
|
|
|
async def update(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
body: BranchUpdate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Branch:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
data = body.model_dump(exclude_unset=True)
|
|
if "name" in data and data["name"] is not None:
|
|
data["name"] = validate_non_empty(data["name"], "name")
|
|
_apply_update(entity, data)
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="branch",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(data),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.BRANCH_UPDATED,
|
|
aggregate_type="branch",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def soft_delete(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Branch:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="branch",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor_id(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
class DepartmentService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = DepartmentRepository(session)
|
|
self.clinics = ClinicRepository(session)
|
|
self.branches = BranchRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: DepartmentCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Department:
|
|
clinic = await self.clinics.get(tenant_id, body.clinic_id)
|
|
if clinic is None:
|
|
raise NotFoundError("کلینیک یافت نشد")
|
|
if body.branch_id is not None:
|
|
branch = await self.branches.get(tenant_id, body.branch_id)
|
|
if branch is None:
|
|
raise NotFoundError("شعبه یافت نشد")
|
|
code = validate_code(body.code)
|
|
name = validate_non_empty(body.name, "name")
|
|
entity = Department(
|
|
tenant_id=tenant_id,
|
|
clinic_id=body.clinic_id,
|
|
branch_id=body.branch_id,
|
|
code=code,
|
|
name=name,
|
|
description=body.description,
|
|
status=body.status,
|
|
specialty=body.specialty,
|
|
metadata_json=body.metadata_json,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
try:
|
|
await self.repo.add(entity)
|
|
except IntegrityError as exc:
|
|
raise AppError(
|
|
"کد بخش تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
) from exc
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="department",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.DEPARTMENT_CREATED,
|
|
aggregate_type="department",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code, "clinic_id": str(entity.clinic_id)},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> Department:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError("بخش یافت نشد")
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[Department]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|
|
|
|
async def update(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
body: DepartmentUpdate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Department:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
data = body.model_dump(exclude_unset=True)
|
|
if "name" in data and data["name"] is not None:
|
|
data["name"] = validate_non_empty(data["name"], "name")
|
|
if "branch_id" in data and data["branch_id"] is not None:
|
|
branch = await self.branches.get(tenant_id, data["branch_id"])
|
|
if branch is None:
|
|
raise NotFoundError("شعبه یافت نشد")
|
|
_apply_update(entity, data)
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="department",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(data),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.DEPARTMENT_UPDATED,
|
|
aggregate_type="department",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def soft_delete(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> Department:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="department",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor_id(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
class ExternalProviderConfigService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = ExternalProviderConfigRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: ExternalProviderConfigCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> ExternalProviderConfig:
|
|
code = validate_code(body.code)
|
|
name = validate_non_empty(body.name, "name")
|
|
adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
|
|
if await self.repo.get_by_code(tenant_id, code):
|
|
raise AppError(
|
|
"کد ارائهدهنده تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
)
|
|
entity = ExternalProviderConfig(
|
|
tenant_id=tenant_id,
|
|
clinic_id=body.clinic_id,
|
|
code=code,
|
|
name=name,
|
|
provider_kind=body.provider_kind,
|
|
adapter_key=adapter_key,
|
|
status=body.status,
|
|
priority=body.priority,
|
|
credentials_ref=body.credentials_ref,
|
|
config=body.config,
|
|
capabilities=body.capabilities,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="external_provider_config",
|
|
entity_id=entity.id,
|
|
action=AuditAction.REGISTER,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.EXTERNAL_PROVIDER_REGISTERED,
|
|
aggregate_type="external_provider_config",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code, "adapter_key": entity.adapter_key},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> ExternalProviderConfig:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError("ارائهدهنده یافت نشد")
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[ExternalProviderConfig]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|
|
|
|
async def update(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
body: ExternalProviderConfigUpdate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> ExternalProviderConfig:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
ensure_optimistic_version(entity, body.version)
|
|
data = body.model_dump(exclude_unset=True, exclude={"version"})
|
|
if "name" in data and data["name"] is not None:
|
|
data["name"] = validate_non_empty(data["name"], "name")
|
|
if "adapter_key" in data and data["adapter_key"] is not None:
|
|
data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
|
|
_apply_update(entity, data)
|
|
entity.version += 1
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="external_provider_config",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(data),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.EXTERNAL_PROVIDER_UPDATED,
|
|
aggregate_type="external_provider_config",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def soft_delete(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> ExternalProviderConfig:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="external_provider_config",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor_id(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
class HealthcareConfigurationService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = HealthcareConfigurationRepository(session)
|
|
self.clinics = ClinicRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: HealthcareConfigurationCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> HealthcareConfiguration:
|
|
clinic = await self.clinics.get(tenant_id, body.clinic_id)
|
|
if clinic is None:
|
|
raise NotFoundError("کلینیک یافت نشد")
|
|
code = validate_code(body.code)
|
|
currency = validate_currency_code(body.currency_code)
|
|
entity = HealthcareConfiguration(
|
|
tenant_id=tenant_id,
|
|
clinic_id=body.clinic_id,
|
|
branch_id=body.branch_id,
|
|
code=code,
|
|
working_hours=body.working_hours,
|
|
clinical_policies=body.clinical_policies,
|
|
privacy_policies=body.privacy_policies,
|
|
custom_fields=body.custom_fields,
|
|
timezone=body.timezone,
|
|
language=body.language,
|
|
currency_code=currency,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
try:
|
|
await self.repo.add(entity)
|
|
except IntegrityError as exc:
|
|
raise AppError(
|
|
"کد پیکربندی تکراری است",
|
|
error_code="duplicate_code",
|
|
status_code=409,
|
|
) from exc
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="healthcare_configuration",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.CONFIGURATION_CREATED,
|
|
aggregate_type="healthcare_configuration",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> HealthcareConfiguration:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError("پیکربندی یافت نشد")
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[HealthcareConfiguration]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|
|
|
|
async def update(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
body: HealthcareConfigurationUpdate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> HealthcareConfiguration:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
ensure_optimistic_version(entity, body.version)
|
|
data = body.model_dump(exclude_unset=True, exclude={"version"})
|
|
if "currency_code" in data and data["currency_code"] is not None:
|
|
data["currency_code"] = validate_currency_code(data["currency_code"])
|
|
_apply_update(entity, data)
|
|
entity.version += 1
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="healthcare_configuration",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(data),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.CONFIGURATION_UPDATED,
|
|
aggregate_type="healthcare_configuration",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def soft_delete(
|
|
self,
|
|
tenant_id: UUID,
|
|
entity_id: UUID,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> HealthcareConfiguration:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="healthcare_configuration",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor_id(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
class HealthcareSettingService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = HealthcareSettingRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def upsert(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: HealthcareSettingUpsert,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> HealthcareSetting:
|
|
key = validate_non_empty(body.key, "key")
|
|
existing = await self.repo.get_by_key(
|
|
tenant_id,
|
|
key,
|
|
clinic_id=body.clinic_id,
|
|
branch_id=body.branch_id,
|
|
)
|
|
if existing is None:
|
|
entity = HealthcareSetting(
|
|
tenant_id=tenant_id,
|
|
clinic_id=body.clinic_id,
|
|
branch_id=body.branch_id,
|
|
key=key,
|
|
value=body.value,
|
|
description=body.description,
|
|
created_by=_actor_id(actor),
|
|
updated_by=_actor_id(actor),
|
|
)
|
|
await self.repo.add(entity)
|
|
else:
|
|
entity = existing
|
|
entity.value = body.value
|
|
entity.description = body.description
|
|
entity.updated_by = _actor_id(actor)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="healthcare_setting",
|
|
entity_id=entity.id,
|
|
action=AuditAction.UPDATE if existing else AuditAction.CREATE,
|
|
actor_user_id=_actor_id(actor),
|
|
changes=_jsonable_changes(body.model_dump()),
|
|
)
|
|
self.events.publish(
|
|
event_type=HealthcareEventType.SETTING_UPDATED,
|
|
aggregate_type="healthcare_setting",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"key": entity.key},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def list(
|
|
self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
|
|
) -> list[HealthcareSetting]:
|
|
return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
|