TorbatYar/backend/services/beauty_business/app/services/packages.py
Mortezakoohjani d579d0b142 feat(loyalty): add Loyalty Platform Frontend module
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>
2026-07-27 10:50:55 +03:30

169 lines
6.7 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.packages import PackageDefinition
from app.models.types import AuditAction
from app.repositories.packages import PackageDefinitionRepository
from app.schemas.packages import PackageDefinitionCreate
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 PackageDefinitionService:
def __init__(self, session: AsyncSession) -> None:
self.session = session
self.repo = PackageDefinitionRepository(session)
self.audit = AuditService(session)
self.events = get_event_publisher()
async def create(self, tenant_id: UUID, body: PackageDefinitionCreate, *, actor: CurrentUser | None = None) -> PackageDefinition:
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 = PackageDefinition(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="package_definition",
entity_id=entity.id,
action=AuditAction.CREATE,
actor_user_id=_actor(actor),
changes=data,
)
self.events.publish(
event_type=BeautyBusinessEventType.PACKAGE_DEFINITION_CREATED,
aggregate_type="package_definition",
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) -> PackageDefinition:
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) -> PackageDefinition:
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="package_definition",
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.packages import MembershipPlan
from app.models.types import AuditAction
from app.repositories.packages import MembershipPlanRepository
from app.schemas.packages import MembershipPlanCreate
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 MembershipPlanService:
def __init__(self, session: AsyncSession) -> None:
self.session = session
self.repo = MembershipPlanRepository(session)
self.audit = AuditService(session)
self.events = get_event_publisher()
async def create(self, tenant_id: UUID, body: MembershipPlanCreate, *, actor: CurrentUser | None = None) -> MembershipPlan:
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 = MembershipPlan(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="membership_plan",
entity_id=entity.id,
action=AuditAction.CREATE,
actor_user_id=_actor(actor),
changes=data,
)
self.events.publish(
event_type=BeautyBusinessEventType.MEMBERSHIP_PLAN_CREATED,
aggregate_type="membership_plan",
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) -> MembershipPlan:
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) -> MembershipPlan:
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="membership_plan",
entity_id=entity.id,
action=AuditAction.DELETE,
actor_user_id=_actor(actor),
)
await self.session.commit()
await self.session.refresh(entity)
return entity