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:
@@ -21,7 +21,7 @@ from .customer import Customer, CustomerAddress
|
|||||||
from .inventory import Inventory
|
from .inventory import Inventory
|
||||||
from .letzshop import (
|
from .letzshop import (
|
||||||
LetzshopFulfillmentQueue,
|
LetzshopFulfillmentQueue,
|
||||||
LetzshopOrder,
|
LetzshopHistoricalImportJob,
|
||||||
LetzshopSyncLog,
|
LetzshopSyncLog,
|
||||||
VendorLetzshopCredentials,
|
VendorLetzshopCredentials,
|
||||||
)
|
)
|
||||||
@@ -91,7 +91,7 @@ __all__ = [
|
|||||||
"OrderItem",
|
"OrderItem",
|
||||||
# Letzshop Integration
|
# Letzshop Integration
|
||||||
"VendorLetzshopCredentials",
|
"VendorLetzshopCredentials",
|
||||||
"LetzshopOrder",
|
|
||||||
"LetzshopFulfillmentQueue",
|
"LetzshopFulfillmentQueue",
|
||||||
"LetzshopSyncLog",
|
"LetzshopSyncLog",
|
||||||
|
"LetzshopHistoricalImportJob",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ Database models for Letzshop marketplace integration.
|
|||||||
|
|
||||||
Provides models for:
|
Provides models for:
|
||||||
- VendorLetzshopCredentials: Per-vendor API key storage (encrypted)
|
- VendorLetzshopCredentials: Per-vendor API key storage (encrypted)
|
||||||
- LetzshopOrder: External order tracking and mapping
|
|
||||||
- LetzshopFulfillmentQueue: Outbound operation queue with retry
|
- LetzshopFulfillmentQueue: Outbound operation queue with retry
|
||||||
- LetzshopSyncLog: Audit trail for sync operations
|
- 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 (
|
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})>"
|
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):
|
class LetzshopFulfillmentQueue(Base, TimestampMixin):
|
||||||
"""
|
"""
|
||||||
Queue for outbound fulfillment operations to Letzshop.
|
Queue for outbound fulfillment operations to Letzshop.
|
||||||
|
|
||||||
Supports retry logic for failed operations.
|
Supports retry logic for failed operations.
|
||||||
|
References the unified orders table.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "letzshop_fulfillment_queue"
|
__tablename__ = "letzshop_fulfillment_queue"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||||
letzshop_order_id = Column(
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
|
||||||
Integer, ForeignKey("letzshop_orders.id"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Operation type
|
# Operation type
|
||||||
operation = Column(
|
operation = Column(
|
||||||
String(50), nullable=False
|
String(50), nullable=False
|
||||||
) # confirm, reject, set_tracking, return
|
) # confirm_item, decline_item, set_tracking
|
||||||
|
|
||||||
# Operation payload
|
# Operation payload
|
||||||
payload = Column(JSON, nullable=False)
|
payload = Column(JSON, nullable=False)
|
||||||
@@ -174,15 +102,16 @@ class LetzshopFulfillmentQueue(Base, TimestampMixin):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
vendor = relationship("Vendor")
|
vendor = relationship("Vendor")
|
||||||
letzshop_order = relationship("LetzshopOrder")
|
order = relationship("Order")
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_fulfillment_queue_status", "status", "vendor_id"),
|
Index("idx_fulfillment_queue_status", "status", "vendor_id"),
|
||||||
Index("idx_fulfillment_queue_retry", "status", "next_retry_at"),
|
Index("idx_fulfillment_queue_retry", "status", "next_retry_at"),
|
||||||
|
Index("idx_fulfillment_queue_order", "order_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
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):
|
class LetzshopSyncLog(Base, TimestampMixin):
|
||||||
@@ -228,3 +157,60 @@ class LetzshopSyncLog(Base, TimestampMixin):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<LetzshopSyncLog(id={self.id}, type='{self.operation_type}', status='{self.status}')>"
|
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}')>"
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
# models/database/order.py
|
# models/database/order.py
|
||||||
|
"""
|
||||||
|
Unified Order model for all sales channels.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Direct orders (from vendor's own storefront)
|
||||||
|
- Marketplace orders (Letzshop, etc.)
|
||||||
|
|
||||||
|
Design principles:
|
||||||
|
- Customer/address data is snapshotted at order time (preserves history)
|
||||||
|
- customer_id FK links to Customer record (may be inactive for marketplace imports)
|
||||||
|
- channel field distinguishes order source
|
||||||
|
- external_* fields store marketplace-specific references
|
||||||
|
"""
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Boolean,
|
Boolean,
|
||||||
@@ -6,6 +19,7 @@ from sqlalchemy import (
|
|||||||
DateTime,
|
DateTime,
|
||||||
Float,
|
Float,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
Index,
|
||||||
Integer,
|
Integer,
|
||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
@@ -18,7 +32,12 @@ from models.database.base import TimestampMixin
|
|||||||
|
|
||||||
|
|
||||||
class Order(Base, TimestampMixin):
|
class Order(Base, TimestampMixin):
|
||||||
"""Customer orders."""
|
"""
|
||||||
|
Unified order model for all sales channels.
|
||||||
|
|
||||||
|
Stores orders from direct sales and marketplaces (Letzshop, etc.)
|
||||||
|
with snapshotted customer and address data.
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "orders"
|
__tablename__ = "orders"
|
||||||
|
|
||||||
@@ -27,69 +46,130 @@ class Order(Base, TimestampMixin):
|
|||||||
customer_id = Column(
|
customer_id = Column(
|
||||||
Integer, ForeignKey("customers.id"), nullable=False, index=True
|
Integer, ForeignKey("customers.id"), nullable=False, index=True
|
||||||
)
|
)
|
||||||
|
order_number = Column(String(100), nullable=False, unique=True, index=True)
|
||||||
|
|
||||||
order_number = Column(String, nullable=False, unique=True, index=True)
|
# === Channel/Source ===
|
||||||
|
|
||||||
# Order channel/source
|
|
||||||
channel = Column(
|
channel = Column(
|
||||||
String(50), default="direct", index=True
|
String(50), default="direct", nullable=False, index=True
|
||||||
) # direct, letzshop, amazon, etc.
|
) # direct, letzshop
|
||||||
|
|
||||||
|
# External references (for marketplace orders)
|
||||||
external_order_id = Column(
|
external_order_id = Column(
|
||||||
String(100), nullable=True, index=True
|
String(100), nullable=True, index=True
|
||||||
) # External order reference
|
) # Marketplace order ID
|
||||||
external_channel_data = Column(JSON, nullable=True) # Channel-specific metadata
|
external_shipment_id = Column(
|
||||||
|
String(100), nullable=True, index=True
|
||||||
|
) # Marketplace shipment ID
|
||||||
|
external_order_number = Column(String(100), nullable=True) # Marketplace order #
|
||||||
|
external_data = Column(JSON, nullable=True) # Raw marketplace data for debugging
|
||||||
|
|
||||||
# Order status
|
# === Status ===
|
||||||
status = Column(String, nullable=False, default="pending", index=True)
|
# pending: awaiting confirmation
|
||||||
# pending, processing, shipped, delivered, cancelled, refunded
|
# processing: confirmed, being prepared
|
||||||
|
# shipped: shipped with tracking
|
||||||
|
# delivered: delivered to customer
|
||||||
|
# cancelled: order cancelled/declined
|
||||||
|
# refunded: order refunded
|
||||||
|
status = Column(String(50), nullable=False, default="pending", index=True)
|
||||||
|
|
||||||
# Financial
|
# === Financials ===
|
||||||
subtotal = Column(Float, nullable=False)
|
subtotal = Column(Float, nullable=True) # May not be available from marketplace
|
||||||
tax_amount = Column(Float, default=0.0)
|
tax_amount = Column(Float, nullable=True)
|
||||||
shipping_amount = Column(Float, default=0.0)
|
shipping_amount = Column(Float, nullable=True)
|
||||||
discount_amount = Column(Float, default=0.0)
|
discount_amount = Column(Float, nullable=True)
|
||||||
total_amount = Column(Float, nullable=False)
|
total_amount = Column(Float, nullable=False)
|
||||||
currency = Column(String, default="EUR")
|
currency = Column(String(10), default="EUR")
|
||||||
|
|
||||||
# Addresses (stored as IDs)
|
# === Customer Snapshot (preserved at order time) ===
|
||||||
shipping_address_id = Column(
|
customer_first_name = Column(String(100), nullable=False)
|
||||||
Integer, ForeignKey("customer_addresses.id"), nullable=False
|
customer_last_name = Column(String(100), nullable=False)
|
||||||
)
|
customer_email = Column(String(255), nullable=False)
|
||||||
billing_address_id = Column(
|
customer_phone = Column(String(50), nullable=True)
|
||||||
Integer, ForeignKey("customer_addresses.id"), nullable=False
|
customer_locale = Column(String(10), nullable=True) # en, fr, de, lb
|
||||||
)
|
|
||||||
|
|
||||||
# Shipping
|
# === Shipping Address Snapshot ===
|
||||||
shipping_method = Column(String, nullable=True)
|
ship_first_name = Column(String(100), nullable=False)
|
||||||
tracking_number = Column(String, nullable=True)
|
ship_last_name = Column(String(100), nullable=False)
|
||||||
|
ship_company = Column(String(200), nullable=True)
|
||||||
|
ship_address_line_1 = Column(String(255), nullable=False)
|
||||||
|
ship_address_line_2 = Column(String(255), nullable=True)
|
||||||
|
ship_city = Column(String(100), nullable=False)
|
||||||
|
ship_postal_code = Column(String(20), nullable=False)
|
||||||
|
ship_country_iso = Column(String(5), nullable=False)
|
||||||
|
|
||||||
# Notes
|
# === Billing Address Snapshot ===
|
||||||
|
bill_first_name = Column(String(100), nullable=False)
|
||||||
|
bill_last_name = Column(String(100), nullable=False)
|
||||||
|
bill_company = Column(String(200), nullable=True)
|
||||||
|
bill_address_line_1 = Column(String(255), nullable=False)
|
||||||
|
bill_address_line_2 = Column(String(255), nullable=True)
|
||||||
|
bill_city = Column(String(100), nullable=False)
|
||||||
|
bill_postal_code = Column(String(20), nullable=False)
|
||||||
|
bill_country_iso = Column(String(5), nullable=False)
|
||||||
|
|
||||||
|
# === Tracking ===
|
||||||
|
shipping_method = Column(String(100), nullable=True)
|
||||||
|
tracking_number = Column(String(100), nullable=True)
|
||||||
|
tracking_provider = Column(String(100), nullable=True)
|
||||||
|
|
||||||
|
# === Notes ===
|
||||||
customer_notes = Column(Text, nullable=True)
|
customer_notes = Column(Text, nullable=True)
|
||||||
internal_notes = Column(Text, nullable=True)
|
internal_notes = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Timestamps
|
# === Timestamps ===
|
||||||
paid_at = Column(DateTime, nullable=True)
|
order_date = Column(
|
||||||
shipped_at = Column(DateTime, nullable=True)
|
DateTime(timezone=True), nullable=False
|
||||||
delivered_at = Column(DateTime, nullable=True)
|
) # When customer placed order
|
||||||
cancelled_at = Column(DateTime, nullable=True)
|
confirmed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
shipped_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
delivered_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
cancelled_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# === Relationships ===
|
||||||
vendor = relationship("Vendor")
|
vendor = relationship("Vendor")
|
||||||
customer = relationship("Customer", back_populates="orders")
|
customer = relationship("Customer", back_populates="orders")
|
||||||
items = relationship(
|
items = relationship(
|
||||||
"OrderItem", back_populates="order", cascade="all, delete-orphan"
|
"OrderItem", back_populates="order", cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
shipping_address = relationship(
|
|
||||||
"CustomerAddress", foreign_keys=[shipping_address_id]
|
# Composite indexes for common queries
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_order_vendor_status", "vendor_id", "status"),
|
||||||
|
Index("idx_order_vendor_channel", "vendor_id", "channel"),
|
||||||
|
Index("idx_order_vendor_date", "vendor_id", "order_date"),
|
||||||
)
|
)
|
||||||
billing_address = relationship("CustomerAddress", foreign_keys=[billing_address_id])
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Order(id={self.id}, order_number='{self.order_number}', status='{self.status}')>"
|
return f"<Order(id={self.id}, order_number='{self.order_number}', channel='{self.channel}', status='{self.status}')>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customer_full_name(self) -> str:
|
||||||
|
"""Customer full name from snapshot."""
|
||||||
|
return f"{self.customer_first_name} {self.customer_last_name}".strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ship_full_name(self) -> str:
|
||||||
|
"""Shipping address full name."""
|
||||||
|
return f"{self.ship_first_name} {self.ship_last_name}".strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bill_full_name(self) -> str:
|
||||||
|
"""Billing address full name."""
|
||||||
|
return f"{self.bill_first_name} {self.bill_last_name}".strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_marketplace_order(self) -> bool:
|
||||||
|
"""Check if this is a marketplace order."""
|
||||||
|
return self.channel != "direct"
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(Base, TimestampMixin):
|
class OrderItem(Base, TimestampMixin):
|
||||||
"""Individual items in an order."""
|
"""
|
||||||
|
Individual items in an order.
|
||||||
|
|
||||||
|
Stores product snapshot at time of order plus external references
|
||||||
|
for marketplace items.
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "order_items"
|
__tablename__ = "order_items"
|
||||||
|
|
||||||
@@ -97,21 +177,48 @@ class OrderItem(Base, TimestampMixin):
|
|||||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
|
||||||
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||||
|
|
||||||
# Product details at time of order (snapshot)
|
# === Product Snapshot (preserved at order time) ===
|
||||||
product_name = Column(String, nullable=False)
|
product_name = Column(String(255), nullable=False)
|
||||||
product_sku = Column(String, nullable=True)
|
product_sku = Column(String(100), nullable=True)
|
||||||
|
gtin = Column(String(50), nullable=True) # EAN/UPC/ISBN etc.
|
||||||
|
gtin_type = Column(String(20), nullable=True) # ean13, upc, isbn, etc.
|
||||||
|
|
||||||
|
# === Pricing ===
|
||||||
quantity = Column(Integer, nullable=False)
|
quantity = Column(Integer, nullable=False)
|
||||||
unit_price = Column(Float, nullable=False)
|
unit_price = Column(Float, nullable=False)
|
||||||
total_price = Column(Float, nullable=False)
|
total_price = Column(Float, nullable=False)
|
||||||
|
|
||||||
# Inventory tracking
|
# === External References (for marketplace items) ===
|
||||||
|
external_item_id = Column(String(100), nullable=True) # e.g., Letzshop inventory unit ID
|
||||||
|
external_variant_id = Column(String(100), nullable=True) # e.g., Letzshop variant ID
|
||||||
|
|
||||||
|
# === Item State (for marketplace confirmation flow) ===
|
||||||
|
# confirmed_available: item confirmed and available
|
||||||
|
# confirmed_unavailable: item confirmed but not available (declined)
|
||||||
|
item_state = Column(String(50), nullable=True)
|
||||||
|
|
||||||
|
# === Inventory Tracking ===
|
||||||
inventory_reserved = Column(Boolean, default=False)
|
inventory_reserved = Column(Boolean, default=False)
|
||||||
inventory_fulfilled = Column(Boolean, default=False)
|
inventory_fulfilled = Column(Boolean, default=False)
|
||||||
|
|
||||||
# Relationships
|
# === Relationships ===
|
||||||
order = relationship("Order", back_populates="items")
|
order = relationship("Order", back_populates="items")
|
||||||
product = relationship("Product")
|
product = relationship("Product")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<OrderItem(id={self.id}, order_id={self.order_id}, product_id={self.product_id})>"
|
return f"<OrderItem(id={self.id}, order_id={self.order_id}, product_id={self.product_id}, gtin='{self.gtin}')>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_confirmed(self) -> bool:
|
||||||
|
"""Check if item has been confirmed (available or unavailable)."""
|
||||||
|
return self.item_state in ("confirmed_available", "confirmed_unavailable")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""Check if item is confirmed as available."""
|
||||||
|
return self.item_state == "confirmed_available"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_declined(self) -> bool:
|
||||||
|
"""Check if item was declined (unavailable)."""
|
||||||
|
return self.item_state == "confirmed_unavailable"
|
||||||
|
|||||||
Reference in New Issue
Block a user