Register all active platform services and base features in core DB so admin can manage them, add delivery/hospitality/sports-center backend phases, update apps catalog and production deploy tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
9.6 KiB
Python
236 lines
9.6 KiB
Python
"""Connector application services — Phase 12.7.
|
|
|
|
Dispatches are simulated locally via mock provider implementations. No
|
|
httpx calls and no imports from other services — the mocks only prove out
|
|
the request/response contract shape until real connector SDKs land.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.events.publisher import InMemoryEventPublisher, get_event_publisher
|
|
from app.events.types import HospitalityEventType
|
|
from app.models.connectors import ConnectorDispatch, ConnectorRegistration
|
|
from app.models.types import AuditAction, ConnectorStatus, DispatchStatus
|
|
from app.providers.mocks import (
|
|
MockAccountingProvider,
|
|
MockCommunicationProvider,
|
|
MockCRMProvider,
|
|
MockDeliveryProvider,
|
|
MockLoyaltyProvider,
|
|
MockWebsiteBuilderProvider,
|
|
)
|
|
from app.repositories.connectors import (
|
|
ConnectorDispatchRepository,
|
|
ConnectorRegistrationRepository,
|
|
)
|
|
from app.repositories.foundation import VenueRepository
|
|
from app.schemas.connectors import ConnectorDispatchCreate, ConnectorRegistrationCreate
|
|
from app.services.audit_service import AuditService
|
|
from app.validators import validate_code, validate_connector_status, validate_non_empty
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
# One dispatcher per connector kind — all are local no-op mocks; hospitality
|
|
# never calls real provider SDKs directly.
|
|
_MOCK_DISPATCHERS = {
|
|
"delivery": MockDeliveryProvider(),
|
|
"accounting": MockAccountingProvider(),
|
|
"crm": MockCRMProvider(),
|
|
"loyalty": MockLoyaltyProvider(),
|
|
"communication": MockCommunicationProvider(),
|
|
"website": MockWebsiteBuilderProvider(),
|
|
}
|
|
|
|
|
|
class _ConnectorsBase:
|
|
def __init__(
|
|
self,
|
|
session: AsyncSession,
|
|
publisher: InMemoryEventPublisher | None = None,
|
|
) -> None:
|
|
self.session = session
|
|
self.audit = AuditService(session)
|
|
self.publisher = publisher or get_event_publisher()
|
|
self.venues = VenueRepository(session)
|
|
|
|
async def _require_venue(self, tenant_id: UUID, venue_id: UUID):
|
|
venue = await self.venues.get(tenant_id, venue_id)
|
|
if venue is None:
|
|
raise NotFoundError("واحد پذیرایی یافت نشد", error_code="venue_not_found")
|
|
return venue
|
|
|
|
|
|
class ConnectorRegistrationService(_ConnectorsBase):
|
|
def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
|
|
super().__init__(session, publisher)
|
|
self.repo = ConnectorRegistrationRepository(session)
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: ConnectorRegistrationCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> ConnectorRegistration:
|
|
if body.venue_id is not None:
|
|
await self._require_venue(tenant_id, body.venue_id)
|
|
code = validate_code(body.code)
|
|
if await self.repo.get_by_code(tenant_id, code) is not None:
|
|
raise AppError(
|
|
"کد رابط اتصال تکراری است",
|
|
status_code=409,
|
|
error_code="connector_registration_code_exists",
|
|
)
|
|
actor_id = actor.user_id if actor else None
|
|
entity = ConnectorRegistration(
|
|
tenant_id=tenant_id,
|
|
venue_id=body.venue_id,
|
|
kind=body.kind,
|
|
code=code,
|
|
name=validate_non_empty(body.name, field="name"),
|
|
status=validate_connector_status(body.status),
|
|
external_ref=body.external_ref,
|
|
config=body.config,
|
|
created_by=actor_id,
|
|
updated_by=actor_id,
|
|
)
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="connector_registration",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=actor_id,
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
self.publisher.publish(
|
|
event_type=HospitalityEventType.CONNECTOR_REGISTRATION_CREATED,
|
|
aggregate_type="connector_registration",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"code": entity.code, "kind": entity.kind.value},
|
|
)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> ConnectorRegistration:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError(
|
|
"رابط اتصال یافت نشد", error_code="connector_registration_not_found"
|
|
)
|
|
return entity
|
|
|
|
async def list(self, tenant_id: UUID, *, offset: int, limit: int):
|
|
return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
|
|
|
|
|
|
class ConnectorDispatchService(_ConnectorsBase):
|
|
def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
|
|
super().__init__(session, publisher)
|
|
self.repo = ConnectorDispatchRepository(session)
|
|
self.registrations = ConnectorRegistrationRepository(session)
|
|
|
|
async def create(
|
|
self,
|
|
tenant_id: UUID,
|
|
body: ConnectorDispatchCreate,
|
|
*,
|
|
actor: CurrentUser | None = None,
|
|
) -> ConnectorDispatch:
|
|
if body.venue_id is not None:
|
|
await self._require_venue(tenant_id, body.venue_id)
|
|
registration = await self.registrations.get(tenant_id, body.registration_id)
|
|
if registration is None:
|
|
raise NotFoundError(
|
|
"رابط اتصال یافت نشد", error_code="connector_registration_not_found"
|
|
)
|
|
event_type = validate_non_empty(body.event_type, field="event_type")
|
|
actor_id = actor.user_id if actor else None
|
|
|
|
if registration.status != ConnectorStatus.ACTIVE:
|
|
status = DispatchStatus.FAILED
|
|
response = {"ok": False, "reason": "registration_not_active"}
|
|
else:
|
|
dispatcher = _MOCK_DISPATCHERS.get(registration.kind.value)
|
|
response = {"ok": True, "provider": "mock", "kind": registration.kind.value}
|
|
if dispatcher is not None:
|
|
if hasattr(dispatcher, "send"):
|
|
await dispatcher.send(
|
|
tenant_id=tenant_id,
|
|
channel="default",
|
|
template_key=event_type,
|
|
to=body.payload_ref or "",
|
|
payload={"event_type": event_type},
|
|
)
|
|
elif hasattr(dispatcher, "create_receivable_ref"):
|
|
response = await dispatcher.create_receivable_ref(
|
|
tenant_id=tenant_id,
|
|
reference=body.payload_ref or event_type,
|
|
payload={"event_type": event_type},
|
|
)
|
|
elif hasattr(dispatcher, "create_delivery_ref"):
|
|
response = await dispatcher.create_delivery_ref(
|
|
tenant_id=tenant_id,
|
|
reference=body.payload_ref or event_type,
|
|
payload={"event_type": event_type},
|
|
)
|
|
elif hasattr(dispatcher, "resolve_contact"):
|
|
response = await dispatcher.resolve_contact(
|
|
tenant_id=tenant_id, contact_ref=body.payload_ref or event_type
|
|
)
|
|
elif hasattr(dispatcher, "resolve_member"):
|
|
response = await dispatcher.resolve_member(
|
|
tenant_id=tenant_id, member_ref=body.payload_ref or event_type
|
|
)
|
|
elif hasattr(dispatcher, "resolve_site"):
|
|
response = await dispatcher.resolve_site(
|
|
tenant_id=tenant_id, site_ref=body.payload_ref or event_type
|
|
)
|
|
status = DispatchStatus.SENT
|
|
|
|
entity = ConnectorDispatch(
|
|
tenant_id=tenant_id,
|
|
venue_id=body.venue_id,
|
|
registration_id=body.registration_id,
|
|
direction=body.direction,
|
|
event_type=event_type,
|
|
payload_ref=body.payload_ref,
|
|
status=status,
|
|
response_json=response,
|
|
created_by=actor_id,
|
|
updated_by=actor_id,
|
|
)
|
|
await self.repo.add(entity)
|
|
await self.audit.record(
|
|
tenant_id=tenant_id,
|
|
entity_type="connector_dispatch",
|
|
entity_id=entity.id,
|
|
action=AuditAction.CREATE,
|
|
actor_user_id=actor_id,
|
|
)
|
|
await self.session.commit()
|
|
await self.session.refresh(entity)
|
|
self.publisher.publish(
|
|
event_type=HospitalityEventType.CONNECTOR_DISPATCH_CREATED,
|
|
aggregate_type="connector_dispatch",
|
|
aggregate_id=entity.id,
|
|
tenant_id=tenant_id,
|
|
payload={"status": entity.status.value, "event_type": entity.event_type},
|
|
)
|
|
return entity
|
|
|
|
async def get(self, tenant_id: UUID, entity_id: UUID) -> ConnectorDispatch:
|
|
entity = await self.repo.get(tenant_id, entity_id)
|
|
if entity is None:
|
|
raise NotFoundError(
|
|
"ارسال رابط اتصال یافت نشد", error_code="connector_dispatch_not_found"
|
|
)
|
|
return entity
|
|
|
|
async def list(self, tenant_id: UUID, *, offset: int, limit: int):
|
|
return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
|