The Phase 4 _mirror_status_to_cloud function had no way to look up the
cloud order's numeric id once it was marked synced (not in the pending
list anymore), so status updates from local staff could silently fail.
Fix:
- models/order.py: online_order_cloud_id INTEGER column added to Order
- main.py: migration for the new column
- schemas/order.py: online_order_cloud_id exposed in OrderOut
- cloud_sync.py: stores cloud_order["id"] as online_order_cloud_id
when creating the local order during the pull
- connect_orders.py: _mirror_status_to_cloud now takes the integer
cloud id directly — no more pending-list lookup; function body
reduced from ~50 lines to ~15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
7.0 KiB
Python
142 lines
7.0 KiB
Python
from sqlalchemy import Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime, timezone
|
|
from database import Base
|
|
|
|
|
|
def _utcnow():
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
class Order(Base):
|
|
__tablename__ = "orders"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
table_id = Column(Integer, ForeignKey("tables.id"), nullable=True) # nullable for online orders
|
|
opened_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
opened_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
status = Column(String, default="open", nullable=False) # open|partially_paid|paid|closed|cancelled
|
|
closed_at = Column(DateTime, nullable=True)
|
|
closed_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
business_day_id = Column(Integer, ForeignKey("business_days.id"), nullable=True)
|
|
# Xenia Connect — online order fields
|
|
source = Column(String, default="pos", nullable=False) # "pos" | "online"
|
|
online_order_ref = Column(String, nullable=True) # e.g. "ORD-0042"
|
|
online_order_cloud_id = Column(Integer, nullable=True) # cloud DB pk for status mirroring
|
|
online_status = Column(String, nullable=True) # mirrors cloud status
|
|
online_customer_name = Column(String, nullable=True)
|
|
online_customer_phone = Column(String, nullable=True)
|
|
online_customer_address = Column(Text, nullable=True)
|
|
online_customer_notes = Column(Text, nullable=True)
|
|
online_order_type = Column(String, nullable=True) # "delivery" | "dine_in"
|
|
|
|
table = relationship("Table", back_populates="orders")
|
|
opener = relationship("User", foreign_keys=[opened_by], back_populates="orders_opened")
|
|
closer = relationship("User", foreign_keys=[closed_by], back_populates="orders_closed")
|
|
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
|
|
waiters = relationship("OrderWaiter", back_populates="order", cascade="all, delete-orphan")
|
|
print_logs = relationship("PrintLog", back_populates="order", cascade="all, delete-orphan")
|
|
audit_logs = relationship("OrderAuditLog", back_populates="order", cascade="all, delete-orphan")
|
|
discounts = relationship("OrderDiscount", back_populates="order", cascade="all, delete-orphan")
|
|
|
|
|
|
class OrderWaiter(Base):
|
|
__tablename__ = "order_waiters"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
|
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
|
|
order = relationship("Order", back_populates="waiters")
|
|
waiter = relationship("User", back_populates="order_assignments")
|
|
|
|
|
|
class OrderItem(Base):
|
|
__tablename__ = "order_items"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
added_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
quantity = Column(Integer, nullable=False)
|
|
unit_price = Column(Float, nullable=False) # price snapshot at time of order
|
|
selected_options = Column(Text, nullable=True) # JSON array of option ids
|
|
removed_ingredients = Column(Text, nullable=True) # JSON array of ingredient ids
|
|
notes = Column(Text, nullable=True)
|
|
status = Column(String, default="active", nullable=False) # active|paid|cancelled
|
|
added_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
printed = Column(Boolean, default=False, nullable=False)
|
|
|
|
# Payment tracking
|
|
paid_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
paid_at = Column(DateTime, nullable=True)
|
|
payment_method = Column(String, nullable=True) # 'cash'|'card'|'other' — future use
|
|
paid_in_shift_id = Column(Integer, ForeignKey("waiter_shifts.id"), nullable=True)
|
|
|
|
order = relationship("Order", back_populates="items")
|
|
product = relationship("Product", back_populates="order_items")
|
|
added_by_user = relationship("User", foreign_keys=[added_by], back_populates="order_items")
|
|
paid_by_user = relationship("User", foreign_keys=[paid_by], back_populates="items_paid")
|
|
|
|
|
|
class PrintLog(Base):
|
|
__tablename__ = "print_log"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
|
printer_id = Column(Integer, ForeignKey("printers.id"), nullable=False)
|
|
printed_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
item_ids = Column(Text, nullable=False) # JSON array of order_item ids
|
|
success = Column(Boolean, nullable=False)
|
|
error_message = Column(Text, nullable=True)
|
|
|
|
order = relationship("Order", back_populates="print_logs")
|
|
printer = relationship("Printer", back_populates="print_logs")
|
|
|
|
|
|
class OrderAuditLog(Base):
|
|
"""Immutable append-only audit trail for every action on an order."""
|
|
__tablename__ = "order_audit_log"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
|
event_type = Column(String, nullable=False)
|
|
# ORDER_OPENED | ITEMS_ADDED | PAYMENT | PAYMENT_OFFLINE | ORDER_CLOSED | ORDER_CANCELLED | ITEM_CANCELLED
|
|
waiter_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
item_ids = Column(Text, nullable=True) # JSON list of OrderItem ids
|
|
amount = Column(Float, nullable=True) # total value for PAYMENT events
|
|
payment_method = Column(String, nullable=True)
|
|
note = Column(Text, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
# Emergency offline payment fields
|
|
offline_uuid = Column(String, nullable=True) # client-generated UUID for dedup
|
|
offline_at = Column(String, nullable=True) # ISO timestamp from client
|
|
is_duplicate = Column(Integer, nullable=False, default=0) # 1 = duplicate payment flagged
|
|
|
|
order = relationship("Order", back_populates="audit_logs")
|
|
waiter = relationship("User")
|
|
|
|
@property
|
|
def waiter_name(self):
|
|
return self.waiter.username if self.waiter else None
|
|
|
|
|
|
class OrderDiscount(Base):
|
|
"""Records a discount applied to an order or a specific item."""
|
|
__tablename__ = "order_discounts"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
|
item_id = Column(Integer, ForeignKey("order_items.id"), nullable=True) # NULL = whole-order discount
|
|
discount_type = Column(String, nullable=False) # 'percent' | 'fixed'
|
|
discount_value = Column(Float, nullable=False) # e.g. 10.0 = 10% or €10.00
|
|
applied_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
applied_at = Column(DateTime(timezone=True), default=_utcnow)
|
|
reason = Column(Text, nullable=True)
|
|
|
|
order = relationship("Order", back_populates="discounts")
|
|
item = relationship("OrderItem")
|
|
applied_by_user = relationship("User")
|