# app/modules/loyalty/models/terminal_device.py """ Terminal device model. A TerminalDevice represents a paired Android (or other) POS tablet bound to a single store. Each row owns the unique `jti` that we encode in the device's long-lived JWT — that's the only handle we keep on the issued token, since the JWT itself is shown once at pairing time and then never stored. Two states render a device's token unusable: explicit `revoked_at` (merchant action, row stays for audit) or `expires_at < now` (1y default). Multiple devices per store are allowed — a store can have several cashiers each with their own tablet — so there is no unique constraint across (store_id), only an index for lookups. """ from datetime import UTC, datetime from sqlalchemy import ( CheckConstraint, Column, DateTime, ForeignKey, Index, Integer, String, ) from sqlalchemy.orm import relationship from app.core.database import Base from models.database.base import TimestampMixin class TerminalDevice(Base, TimestampMixin): """A paired POS terminal device (typically an Android tablet).""" __tablename__ = "loyalty_terminal_devices" id = Column(Integer, primary_key=True, index=True) merchant_id = Column( Integer, ForeignKey("merchants.id", ondelete="CASCADE"), nullable=False, index=True, ) store_id = Column( Integer, ForeignKey("stores.id", ondelete="CASCADE"), nullable=False, index=True, ) label = Column( String(100), nullable=False, comment="Merchant-facing device name e.g. 'Counter 1'", ) # The only handle we keep on the issued JWT. The token itself is shown # once at creation and never stored. jti = Column(String(36), nullable=False, unique=True, index=True) expires_at = Column(DateTime(timezone=True), nullable=False) last_seen_at = Column(DateTime(timezone=True), nullable=True) last_seen_ip = Column(String(45), nullable=True) revoked_at = Column(DateTime(timezone=True), nullable=True, index=True) revoked_by_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) revoked_reason = Column(String(255), nullable=True) # The user who paired the device. Used as the principal of record when # the device performs operations on the store API. created_by_id = Column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) merchant = relationship("Merchant", backref="terminal_devices") store = relationship("Store", backref="terminal_devices") revoked_by = relationship("User", foreign_keys=[revoked_by_id]) created_by = relationship("User", foreign_keys=[created_by_id]) __table_args__ = ( CheckConstraint( "expires_at > created_at", name="ck_loyalty_terminal_devices_expiry_after_creation", ), Index( "idx_loyalty_terminal_devices_store_active", "store_id", "revoked_at", ), ) def __repr__(self) -> str: return ( f"" ) @property def is_revoked(self) -> bool: return self.revoked_at is not None @property def is_expired(self) -> bool: if not self.expires_at: return False expiry = self.expires_at if expiry.tzinfo is None: expiry = expiry.replace(tzinfo=UTC) return datetime.now(UTC) >= expiry @property def status(self) -> str: """One of: active, revoked, expired.""" if self.is_revoked: return "revoked" if self.is_expired: return "expired" return "active"