Add frontend/modules/loyalty with types, API client, design system, feature pages and thin App Router routes under app/loyalty/. BFF proxy at app/api/loyalty/. Include loyalty frontend docs. Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
9.5 KiB
Python
246 lines
9.5 KiB
Python
"""Services."""
|
|
from __future__ import annotations
|
|
from uuid import UUID
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import BeautyBusinessEventType
|
|
from app.models.types import AuditAction
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import validate_code
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
def _actor(actor):
|
|
return actor.user_id if actor else None
|
|
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import BeautyBusinessEventType
|
|
from app.models.salon import TreatmentRoom
|
|
from app.models.types import AuditAction
|
|
from app.repositories.salon import TreatmentRoomRepository
|
|
from app.schemas.salon import TreatmentRoomCreate
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import validate_code
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
|
|
def _actor(actor: CurrentUser | None) -> str | None:
|
|
return actor.user_id if actor else None
|
|
|
|
|
|
class TreatmentRoomService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = TreatmentRoomRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(self, tenant_id: UUID, body: TreatmentRoomCreate, *, actor: CurrentUser | None = None) -> TreatmentRoom:
|
|
code = validate_code(body.code)
|
|
if await self.repo.get_by_code(tenant_id, code):
|
|
raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
|
|
data = body.model_dump()
|
|
entity = TreatmentRoom(tenant_id=tenant_id, **data, created_by=_actor(actor), updated_by=_actor(actor))
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="treatment_room",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor(actor),
|
|
changes=data,
|
|
)
|
|
self.events.publish(
|
|
event_type=BeautyBusinessEventType.TREATMENT_ROOM_CREATED,
|
|
aggregate_type="treatment_room",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"id": str(entity.id)},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> TreatmentRoom:
|
|
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):
|
|
return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
|
|
|
|
async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> TreatmentRoom:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="treatment_room",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import BeautyBusinessEventType
|
|
from app.models.salon import Station
|
|
from app.models.types import AuditAction
|
|
from app.repositories.salon import StationRepository
|
|
from app.schemas.salon import StationCreate
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import validate_code
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
|
|
def _actor(actor: CurrentUser | None) -> str | None:
|
|
return actor.user_id if actor else None
|
|
|
|
|
|
class StationService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = StationRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(self, tenant_id: UUID, body: StationCreate, *, actor: CurrentUser | None = None) -> Station:
|
|
code = validate_code(body.code)
|
|
if await self.repo.get_by_code(tenant_id, code):
|
|
raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
|
|
data = body.model_dump()
|
|
entity = Station(tenant_id=tenant_id, **data, created_by=_actor(actor), updated_by=_actor(actor))
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="station",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor(actor),
|
|
changes=data,
|
|
)
|
|
self.events.publish(
|
|
event_type=BeautyBusinessEventType.STATION_CREATED,
|
|
aggregate_type="station",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"id": str(entity.id)},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> Station:
|
|
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):
|
|
return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
|
|
|
|
async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Station:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="station",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.events.publisher import get_event_publisher
|
|
from app.events.types import BeautyBusinessEventType
|
|
from app.models.salon import BranchPolicy
|
|
from app.models.types import AuditAction
|
|
from app.repositories.salon import BranchPolicyRepository
|
|
from app.schemas.salon import BranchPolicyCreate
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import validate_code
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
|
|
def _actor(actor: CurrentUser | None) -> str | None:
|
|
return actor.user_id if actor else None
|
|
|
|
|
|
class BranchPolicyService:
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
self.repo = BranchPolicyRepository(session)
|
|
self.audit = AuditService(session)
|
|
self.events = get_event_publisher()
|
|
|
|
async def create(self, tenant_id: UUID, body: BranchPolicyCreate, *, actor: CurrentUser | None = None) -> BranchPolicy:
|
|
code = validate_code(body.code)
|
|
if await self.repo.get_by_code(tenant_id, code):
|
|
raise AppError("کد تکراری است", error_code="duplicate_code", status_code=409)
|
|
data = body.model_dump()
|
|
entity = BranchPolicy(tenant_id=tenant_id, **data, created_by=_actor(actor), updated_by=_actor(actor))
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="branch_policy",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=_actor(actor),
|
|
changes=data,
|
|
)
|
|
self.events.publish(
|
|
event_type=BeautyBusinessEventType.BRANCH_POLICY_CREATED,
|
|
aggregate_type="branch_policy",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"id": str(entity.id)},
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> BranchPolicy:
|
|
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):
|
|
return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
|
|
|
|
async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> BranchPolicy:
|
|
entity = await self.get(tenant_id, entity_id)
|
|
await self.repo.soft_delete(entity, deleted_by=_actor(actor))
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="branch_policy",
|
|
entity_id=entity.id,
|
|
action=AuditAction.DELETE,
|
|
actor_user_id=_actor(actor),
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
return entity
|