feat: implement unified order schema with customer/address snapshots

- Update Order model with customer/address snapshot fields
- Add external marketplace references (external_order_id, external_shipment_id)
- Add tracking_provider field for shipping carriers
- Add order_date, confirmed_at timestamps
- Update OrderItem with gtin/gtin_type, external_item_id, item_state
- Remove LetzshopOrder model (orders now go to unified table)
- Update LetzshopFulfillmentQueue to reference orders.id

Design decision: Single orders table for all channels with snapshotted
data preserved at order time for historical accuracy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 21:17:38 +01:00
parent 575b00760b
commit 6f3a07c7d7
3 changed files with 221 additions and 128 deletions

View File

@@ -4,9 +4,12 @@ Database models for Letzshop marketplace integration.
Provides models for:
- VendorLetzshopCredentials: Per-vendor API key storage (encrypted)
- LetzshopOrder: External order tracking and mapping
- LetzshopFulfillmentQueue: Outbound operation queue with retry
- LetzshopSyncLog: Audit trail for sync operations
- LetzshopHistoricalImportJob: Progress tracking for historical imports
Note: Orders are now stored in the unified `orders` table with channel='letzshop'.
The LetzshopOrder model has been removed in favor of the unified Order model.
"""
from sqlalchemy import (
@@ -61,99 +64,24 @@ class VendorLetzshopCredentials(Base, TimestampMixin):
return f"<VendorLetzshopCredentials(vendor_id={self.vendor_id}, auto_sync={self.auto_sync_enabled})>"
class LetzshopOrder(Base, TimestampMixin):
"""
Letzshop order tracking and mapping.
Stores imported orders from Letzshop with mapping to local Order model.
"""
__tablename__ = "letzshop_orders"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
# Letzshop identifiers
letzshop_order_id = Column(String(100), nullable=False, index=True)
letzshop_shipment_id = Column(String(100), nullable=True, index=True)
letzshop_order_number = Column(String(100), nullable=True)
# Local order mapping (if imported to local system)
local_order_id = Column(Integer, ForeignKey("orders.id"), nullable=True)
# Order state from Letzshop
letzshop_state = Column(String(50), nullable=True) # unconfirmed, confirmed, etc.
# Customer info from Letzshop
customer_email = Column(String(255), nullable=True)
customer_name = Column(String(255), nullable=True)
# Order totals from Letzshop
total_amount = Column(
String(50), nullable=True
) # Store as string to preserve format
currency = Column(String(10), default="EUR")
# Customer preferences (for invoicing)
customer_locale = Column(String(10), nullable=True) # en, fr, de
# Shipping/billing country
shipping_country_iso = Column(String(5), nullable=True) # LU, DE, FR, etc.
billing_country_iso = Column(String(5), nullable=True)
# Raw data storage (for debugging/auditing)
raw_order_data = Column(JSON, nullable=True)
# Inventory units (from Letzshop)
inventory_units = Column(JSON, nullable=True) # List of inventory unit IDs
# Sync status
sync_status = Column(
String(50), default="pending"
) # pending, imported, confirmed, rejected, shipped
last_synced_at = Column(DateTime(timezone=True), nullable=True)
sync_error = Column(Text, nullable=True)
# Fulfillment status
confirmed_at = Column(DateTime(timezone=True), nullable=True)
rejected_at = Column(DateTime(timezone=True), nullable=True)
tracking_set_at = Column(DateTime(timezone=True), nullable=True)
tracking_number = Column(String(100), nullable=True)
tracking_carrier = Column(String(100), nullable=True)
# Relationships
vendor = relationship("Vendor")
local_order = relationship("Order")
__table_args__ = (
Index("idx_letzshop_order_vendor", "vendor_id", "letzshop_order_id"),
Index("idx_letzshop_order_state", "vendor_id", "letzshop_state"),
Index("idx_letzshop_order_sync", "vendor_id", "sync_status"),
)
def __repr__(self):
return f"<LetzshopOrder(id={self.id}, letzshop_id='{self.letzshop_order_id}', state='{self.letzshop_state}')>"
class LetzshopFulfillmentQueue(Base, TimestampMixin):
"""
Queue for outbound fulfillment operations to Letzshop.
Supports retry logic for failed operations.
References the unified orders table.
"""
__tablename__ = "letzshop_fulfillment_queue"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
letzshop_order_id = Column(
Integer, ForeignKey("letzshop_orders.id"), nullable=False
)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
# Operation type
operation = Column(
String(50), nullable=False
) # confirm, reject, set_tracking, return
) # confirm_item, decline_item, set_tracking
# Operation payload
payload = Column(JSON, nullable=False)
@@ -174,15 +102,16 @@ class LetzshopFulfillmentQueue(Base, TimestampMixin):
# Relationships
vendor = relationship("Vendor")
letzshop_order = relationship("LetzshopOrder")
order = relationship("Order")
__table_args__ = (
Index("idx_fulfillment_queue_status", "status", "vendor_id"),
Index("idx_fulfillment_queue_retry", "status", "next_retry_at"),
Index("idx_fulfillment_queue_order", "order_id"),
)
def __repr__(self):
return f"<LetzshopFulfillmentQueue(id={self.id}, operation='{self.operation}', status='{self.status}')>"
return f"<LetzshopFulfillmentQueue(id={self.id}, order_id={self.order_id}, operation='{self.operation}', status='{self.status}')>"
class LetzshopSyncLog(Base, TimestampMixin):
@@ -228,3 +157,60 @@ class LetzshopSyncLog(Base, TimestampMixin):
def __repr__(self):
return f"<LetzshopSyncLog(id={self.id}, type='{self.operation_type}', status='{self.status}')>"
class LetzshopHistoricalImportJob(Base, TimestampMixin):
"""
Track progress of historical order imports from Letzshop.
Enables real-time progress tracking via polling for long-running imports.
"""
__tablename__ = "letzshop_historical_import_jobs"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Status: pending | fetching | processing | completed | failed
status = Column(String(50), default="pending", nullable=False)
# Current phase: "confirmed" | "declined"
current_phase = Column(String(20), nullable=True)
# Fetch progress
current_page = Column(Integer, default=0)
total_pages = Column(Integer, nullable=True) # null = unknown yet
shipments_fetched = Column(Integer, default=0)
# Processing progress
orders_processed = Column(Integer, default=0)
orders_imported = Column(Integer, default=0)
orders_updated = Column(Integer, default=0)
orders_skipped = Column(Integer, default=0)
# EAN matching stats
products_matched = Column(Integer, default=0)
products_not_found = Column(Integer, default=0)
# Phase-specific stats (stored as JSON for combining confirmed + declined)
confirmed_stats = Column(JSON, nullable=True)
declined_stats = Column(JSON, nullable=True)
# Error handling
error_message = Column(Text, nullable=True)
# Timing
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
vendor = relationship("Vendor")
user = relationship("User")
__table_args__ = (
Index("idx_historical_import_vendor", "vendor_id", "status"),
)
def __repr__(self):
return f"<LetzshopHistoricalImportJob(id={self.id}, status='{self.status}', phase='{self.current_phase}')>"