"""POS Lite aggregates — Phase 12.4. Lightweight point-of-sale shell: registers, shifts, tickets, and ticket lines for small venue formats. No payment posting, no accounting entries. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, Index, Integer, 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, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import GUID, LifecycleStatus, PosShiftStatus, PosTicketStatus class PosRegister( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """A physical or virtual POS register within a venue.""" __tablename__ = "pos_registers" venue_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) status: Mapped[LifecycleStatus] = mapped_column( default=LifecycleStatus.ACTIVE, nullable=False ) description: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "code", name="uq_pos_registers_tenant_venue_code" ), Index("ix_pos_registers_venue", "tenant_id", "venue_id"), Index("ix_pos_registers_status", "tenant_id", "status"), Index("ix_pos_registers_tenant_deleted", "tenant_id", "is_deleted"), ) class PosShift( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """A cashier shift opened against a register.""" __tablename__ = "pos_shifts" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) register_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) opened_by: Mapped[str | None] = mapped_column(String(100), nullable=True) status: Mapped[PosShiftStatus] = mapped_column( default=PosShiftStatus.OPEN, nullable=False ) opened_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) __table_args__ = ( Index("ix_pos_shifts_venue", "tenant_id", "venue_id"), Index("ix_pos_shifts_register", "tenant_id", "register_id"), Index("ix_pos_shifts_status", "tenant_id", "status"), Index("ix_pos_shifts_tenant_deleted", "tenant_id", "is_deleted"), ) class PosTicket( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """A sale ticket opened against a register / shift, optionally linked to a table or QR ordering session.""" __tablename__ = "pos_tickets" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) register_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) shift_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) table_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) ordering_session_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) status: Mapped[PosTicketStatus] = mapped_column( default=PosTicketStatus.DRAFT, nullable=False ) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) notes: Mapped[str | None] = mapped_column(Text, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "venue_id", "code", name="uq_pos_tickets_tenant_venue_code" ), Index("ix_pos_tickets_venue", "tenant_id", "venue_id"), Index("ix_pos_tickets_register", "tenant_id", "register_id"), Index("ix_pos_tickets_shift", "tenant_id", "shift_id"), Index("ix_pos_tickets_status", "tenant_id", "status"), Index("ix_pos_tickets_tenant_deleted", "tenant_id", "is_deleted"), ) class PosTicketLine( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """A single menu-item line on a POS ticket.""" __tablename__ = "pos_ticket_lines" venue_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) ticket_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) menu_item_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) quantity: Mapped[int] = mapped_column(Integer, default=1, nullable=False) unit_price: Mapped[int] = mapped_column(Integer, default=0, nullable=False) currency_code: Mapped[str] = mapped_column(String(3), default="IRR", nullable=False) modifier_option_ids: Mapped[list | None] = mapped_column(JSON, nullable=True) line_notes: Mapped[str | None] = mapped_column(Text, nullable=True) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) __table_args__ = ( Index("ix_pos_ticket_lines_venue", "tenant_id", "venue_id"), Index("ix_pos_ticket_lines_ticket", "tenant_id", "ticket_id"), Index("ix_pos_ticket_lines_tenant_deleted", "tenant_id", "is_deleted"), )