""" Database models. One row per JIRA ticket. ShipStation is purely a label-generation step for these tickets (order numbers there match the JIRA ticket number 1:1, and it's not used for anything else), so it doesn't get its own rows - it enriches the matching row here with tracking_numbers instead. See app/workers.py for how that merge happens. """ from __future__ import annotations import datetime as dt from sqlalchemy import ( Column, Integer, String, Text, DateTime, Boolean, JSON, UniqueConstraint, ) from sqlalchemy.orm import declarative_base Base = declarative_base() class Order(Base): __tablename__ = "orders" __table_args__ = ( UniqueConstraint("source", "external_id", name="uq_source_external_id"), ) id = Column(Integer, primary_key=True) # "source" is kept for schema stability / possible future sources, # but in practice this is always "jira" today - see module docstring. source = Column(String(32), nullable=False, index=True) external_id = Column(String(128), nullable=False, index=True) # The AR-###### style ticket number - same as external_id for JIRA # rows, kept as its own column since it's also how ShipStation # tracking numbers get matched back to this row. ticket_number = Column(String(64), nullable=True, index=True) # Signify Health / Oak Street Health / Unknown - derived from the # SKU prefix. See app/companies.py. company = Column(String(100), nullable=False, default="Unknown", index=True) # SKUs found on this order/ticket. A ticket can have several, but # per business rule they never mix companies on one ticket. skus = Column(JSON, nullable=True) # SKU + item name pairs, e.g. [{"sku": "OK704", "item_name": "Peripheral # - Webcam"}]. Kept separately from `skus` (which stays just the bare # codes, used for company resolution/search/display) because the # ShipStation CSV template needs SKU and Item Name as distinct columns. line_items = Column(JSON, nullable=True) # Recipient info for shipping - name/phone/email/address/city/state/zip/npi. # Only needed for the emergency "Send to ShipStation" action; not shown # in the main table. shipping_info = Column(JSON, nullable=True) # Display name of whoever created the JIRA ticket. creator = Column(String(200), nullable=True) # Plain-text version of the JIRA ticket's Description field (parsed # from Atlassian Document Format - see app/adf.py). Mainly useful for # the emailed-return-label workflow, where staff write the box # requirements here rather than in a structured field. description = Column(Text, nullable=True) # Tracking numbers pulled from ShipStation and merged onto this # ticket - e.g. [{"number": "782758401696", "carrier": "ups", # "is_return": false}]. Populated by the "Pull Tracking Numbers" # action, separately from the JIRA import. tracking_numbers = Column(JSON, nullable=True) summary = Column(String(500), nullable=False, default="") status = Column(String(100), nullable=False, default="") # When the order/ticket was created in the source system source_created_at = Column(DateTime, nullable=True) # When we pulled it into this app imported_at = Column(DateTime, nullable=False, default=dt.datetime.now) # When this ticket first transitioned into a fulfilled status - used # for "fulfilled today" counts and as a rough audit trail. Set once, # on the transition; not touched again while it stays fulfilled. fulfilled_at = Column(DateTime, nullable=True) # Same idea, for cancellation - also what limits the Cancelled tab to # "cancelled today"; a ticket cancelled on a prior day falls through # to Done instead of lingering on Cancelled indefinitely. cancelled_at = Column(DateTime, nullable=True) # When this ticket was successfully sent to ShipStation via the # emergency "Send to ShipStation" action's API path specifically - # confirmed by ShipStation's own response, not just attempted. The # CSV export path intentionally does NOT set this: exporting a file # isn't the same as it actually being imported, and this app has no # way to confirm that manual step happened. shipstation_sent_at = Column(DateTime, nullable=True) # Downstream pipeline flags - useful once Odoo export is fully wired up fulfilled = Column(Boolean, nullable=False, default=False) # Full original payload from JIRA, for anything not modeled explicitly # above. raw_data = Column(JSON, nullable=True) def __repr__(self) -> str: # pragma: no cover - debugging aid return f""