Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1346 lines
41 KiB
Markdown
1346 lines
41 KiB
Markdown
# Multi-Marketplace Integration Architecture
|
|
|
|
## Executive Summary
|
|
|
|
This document defines the complete architecture for integrating Orion with multiple external marketplaces (Letzshop, Amazon, eBay) and digital product suppliers (CodesWholesale). The integration is **bidirectional**, supporting both inbound flows (products, orders) and outbound flows (inventory sync, fulfillment status).
|
|
|
|
**Key Capabilities:**
|
|
|
|
| Capability | Description |
|
|
|------------|-------------|
|
|
| **Multi-Marketplace Products** | Import and normalize products from multiple sources |
|
|
| **Multi-Language Support** | Translations with language fallback |
|
|
| **Unified Order Management** | Orders from all channels in one place |
|
|
| **Digital Product Fulfillment** | On-demand license key retrieval from suppliers |
|
|
| **Inventory Sync** | Real-time and scheduled stock updates to marketplaces |
|
|
| **Fulfillment Sync** | Order status and tracking back to marketplaces |
|
|
|
|
---
|
|
|
|
## System Overview
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "External Systems"
|
|
LS[Letzshop<br/>CSV + GraphQL]
|
|
AZ[Amazon<br/>API]
|
|
EB[eBay<br/>API]
|
|
CW[CodesWholesale<br/>Digital Supplier API]
|
|
WS[Store Storefront<br/>Orion Shop]
|
|
end
|
|
|
|
subgraph "Integration Layer"
|
|
subgraph "Inbound Adapters"
|
|
PI[Product Importers]
|
|
OI[Order Importers]
|
|
DPI[Digital Product Importer]
|
|
end
|
|
subgraph "Outbound Adapters"
|
|
IS[Inventory Sync]
|
|
FS[Fulfillment Sync]
|
|
PE[Product Export]
|
|
end
|
|
end
|
|
|
|
subgraph "Orion Core"
|
|
MP[Marketplace Products]
|
|
P[Store Products]
|
|
O[Unified Orders]
|
|
I[Inventory]
|
|
F[Fulfillment]
|
|
DL[Digital License Pool]
|
|
end
|
|
|
|
LS -->|CSV Import| PI
|
|
AZ -->|API Pull| PI
|
|
EB -->|API Pull| PI
|
|
CW -->|Catalog Sync| DPI
|
|
|
|
LS -->|GraphQL Poll| OI
|
|
AZ -->|API Poll| OI
|
|
EB -->|API Poll| OI
|
|
WS -->|Direct| O
|
|
|
|
PI --> MP
|
|
DPI --> MP
|
|
MP --> P
|
|
OI --> O
|
|
|
|
I -->|Real-time/Scheduled| IS
|
|
F -->|Status Update| FS
|
|
P -->|CSV Export| PE
|
|
|
|
IS --> LS
|
|
IS --> AZ
|
|
IS --> EB
|
|
FS --> LS
|
|
FS --> AZ
|
|
FS --> EB
|
|
PE --> LS
|
|
|
|
CW -->|On-demand Keys| DL
|
|
DL --> F
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Phases
|
|
|
|
| Phase | Scope | Priority | Dependencies |
|
|
|-------|-------|----------|--------------|
|
|
| **Phase 1** | Product Import | High | None |
|
|
| **Phase 2** | Order Import | High | Phase 1 |
|
|
| **Phase 3** | Order Fulfillment Sync | High | Phase 2 |
|
|
| **Phase 4** | Inventory Sync | Medium | Phase 1 |
|
|
|
|
---
|
|
|
|
## Marketplace Capabilities Matrix
|
|
|
|
| Marketplace | Products In | Products Out | Orders In | Fulfillment Out | Inventory Out | Method |
|
|
|-------------|-------------|--------------|-----------|-----------------|---------------|--------|
|
|
| **Letzshop** | CSV Import | CSV Export | GraphQL Poll | GraphQL | CSV/GraphQL | File + API |
|
|
| **Amazon** | API | N/A | API Poll | API | API (Real-time) | API |
|
|
| **eBay** | API | N/A | API Poll | API | API (Real-time) | API |
|
|
| **CodesWholesale** | API Catalog | N/A | N/A | On-demand Keys | N/A | API |
|
|
| **Store Storefront** | N/A | N/A | Direct DB | Internal | Internal | Direct |
|
|
|
|
---
|
|
|
|
## Part 1: Product Integration
|
|
|
|
### 1.1 Architecture Overview
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Source Layer"
|
|
LS_CSV[Letzshop CSV<br/>Multi-language feeds]
|
|
AZ_API[Amazon API<br/>Product catalog]
|
|
EB_API[eBay API<br/>Product catalog]
|
|
CW_API[CodesWholesale API<br/>Digital catalog]
|
|
end
|
|
|
|
subgraph "Import Layer"
|
|
LSI[LetzshopImporter]
|
|
AZI[AmazonImporter]
|
|
EBI[EbayImporter]
|
|
CWI[CodesWholesaleImporter]
|
|
end
|
|
|
|
subgraph "Canonical Layer"
|
|
MP[(marketplace_products)]
|
|
MPT[(marketplace_product_translations)]
|
|
end
|
|
|
|
subgraph "Store Layer"
|
|
P[(products)]
|
|
PT[(product_translations)]
|
|
end
|
|
|
|
LS_CSV --> LSI
|
|
AZ_API --> AZI
|
|
EB_API --> EBI
|
|
CW_API --> CWI
|
|
|
|
LSI --> MP
|
|
AZI --> MP
|
|
EBI --> MP
|
|
CWI --> MP
|
|
|
|
MP --> MPT
|
|
MP --> P
|
|
P --> PT
|
|
```
|
|
|
|
### 1.2 Digital Product Supplier Integration (CodesWholesale)
|
|
|
|
CodesWholesale provides digital products (game keys, gift cards, software licenses) that need special handling:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant CW as CodesWholesale API
|
|
participant Sync as Catalog Sync Job
|
|
participant MP as marketplace_products
|
|
participant P as products
|
|
participant LP as License Pool
|
|
|
|
Note over Sync: Scheduled catalog sync (e.g., every 6 hours)
|
|
Sync->>CW: GET /products (catalog)
|
|
CW-->>Sync: Product catalog with prices, availability
|
|
Sync->>MP: Upsert products (marketplace='codeswholesale')
|
|
Sync->>MP: Update prices, availability flags
|
|
|
|
Note over P: Store adds product to their catalog
|
|
P->>MP: Link to marketplace_product
|
|
|
|
Note over LP: Order placed - need license key
|
|
LP->>CW: POST /orders (purchase key on-demand)
|
|
CW-->>LP: License key / download link
|
|
LP->>LP: Store for fulfillment
|
|
```
|
|
|
|
**Key Characteristics:**
|
|
|
|
| Aspect | Behavior |
|
|
|--------|----------|
|
|
| **Catalog Sync** | Scheduled job fetches full catalog, updates prices/availability |
|
|
| **License Keys** | Purchased on-demand at fulfillment time (not pre-stocked) |
|
|
| **Inventory** | Virtual - always "available" but subject to supplier stock |
|
|
| **Pricing** | Dynamic - supplier prices may change, store sets markup |
|
|
| **Regions** | Products may have region restrictions (EU, US, Global) |
|
|
|
|
### 1.3 Product Data Model
|
|
|
|
See [Multi-Marketplace Product Architecture](../development/migration/multi-marketplace-product-architecture.md) for detailed schema.
|
|
|
|
**Summary of tables:**
|
|
|
|
| Table | Purpose |
|
|
|-------|---------|
|
|
| `marketplace_products` | Canonical product data from all sources |
|
|
| `marketplace_product_translations` | Localized titles, descriptions per language |
|
|
| `products` | Store-specific overrides and settings |
|
|
| `product_translations` | Store-specific localized overrides |
|
|
|
|
### 1.4 Import Job Flow
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Pending: Job Created
|
|
Pending --> Processing: Worker picks up
|
|
Processing --> Downloading: Fetch source data
|
|
Downloading --> Parsing: Parse rows
|
|
Parsing --> Upserting: Update database
|
|
Upserting --> Completed: All rows processed
|
|
Upserting --> PartiallyCompleted: Some rows failed
|
|
Processing --> Failed: Fatal error
|
|
Completed --> [*]
|
|
PartiallyCompleted --> [*]
|
|
Failed --> [*]
|
|
```
|
|
|
|
---
|
|
|
|
## Part 2: Order Integration
|
|
|
|
### 2.1 Unified Order Model
|
|
|
|
Orders from all channels (marketplaces + store storefront) flow into a unified order management system.
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Order Sources"
|
|
LS_O[Letzshop Orders<br/>GraphQL Poll]
|
|
AZ_O[Amazon Orders<br/>API Poll]
|
|
EB_O[eBay Orders<br/>API Poll]
|
|
VS_O[Store Storefront<br/>Direct]
|
|
end
|
|
|
|
subgraph "Order Import"
|
|
OI[Order Importer Service]
|
|
OQ[Order Import Queue]
|
|
end
|
|
|
|
subgraph "Unified Orders"
|
|
O[(orders)]
|
|
OI_T[(order_items)]
|
|
OS[(order_status_history)]
|
|
end
|
|
|
|
LS_O -->|Poll every N min| OI
|
|
AZ_O -->|Poll every N min| OI
|
|
EB_O -->|Poll every N min| OI
|
|
VS_O -->|Direct insert| O
|
|
|
|
OI --> OQ
|
|
OQ --> O
|
|
O --> OI_T
|
|
O --> OS
|
|
```
|
|
|
|
### 2.2 Order Data Model
|
|
|
|
```python
|
|
class OrderChannel(str, Enum):
|
|
"""Order source channel."""
|
|
STOREFRONT = "storefront" # Store's own Orion shop
|
|
LETZSHOP = "letzshop"
|
|
AMAZON = "amazon"
|
|
EBAY = "ebay"
|
|
|
|
class OrderStatus(str, Enum):
|
|
"""Unified order status."""
|
|
PENDING = "pending" # Awaiting payment/confirmation
|
|
CONFIRMED = "confirmed" # Payment confirmed
|
|
PROCESSING = "processing" # Being prepared
|
|
READY_FOR_SHIPMENT = "ready_for_shipment" # Physical: packed
|
|
SHIPPED = "shipped" # Physical: in transit
|
|
DELIVERED = "delivered" # Physical: delivered
|
|
FULFILLED = "fulfilled" # Digital: key/download sent
|
|
CANCELLED = "cancelled"
|
|
REFUNDED = "refunded"
|
|
PARTIALLY_REFUNDED = "partially_refunded"
|
|
|
|
class Order(Base, TimestampMixin):
|
|
"""Unified order from all channels."""
|
|
__tablename__ = "orders"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
|
|
|
# === CHANNEL TRACKING ===
|
|
channel = Column(SQLEnum(OrderChannel), nullable=False, index=True)
|
|
channel_order_id = Column(String, index=True) # External order ID
|
|
channel_order_url = Column(String) # Link to order in marketplace
|
|
|
|
# === CUSTOMER INFO ===
|
|
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True)
|
|
customer_email = Column(String, nullable=False)
|
|
customer_name = Column(String)
|
|
customer_phone = Column(String)
|
|
|
|
# === ADDRESSES ===
|
|
shipping_address = Column(JSON) # For physical products
|
|
billing_address = Column(JSON)
|
|
|
|
# === ORDER TOTALS ===
|
|
subtotal = Column(Float, nullable=False)
|
|
shipping_cost = Column(Float, default=0)
|
|
tax_amount = Column(Float, default=0)
|
|
discount_amount = Column(Float, default=0)
|
|
total = Column(Float, nullable=False)
|
|
currency = Column(String(3), default="EUR")
|
|
|
|
# === STATUS ===
|
|
status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING, index=True)
|
|
|
|
# === FULFILLMENT TYPE ===
|
|
requires_shipping = Column(Boolean, default=True) # False for digital-only
|
|
is_fully_digital = Column(Boolean, default=False)
|
|
|
|
# === TIMESTAMPS ===
|
|
ordered_at = Column(DateTime, nullable=False) # When customer placed order
|
|
confirmed_at = Column(DateTime)
|
|
shipped_at = Column(DateTime)
|
|
delivered_at = Column(DateTime)
|
|
fulfilled_at = Column(DateTime) # For digital products
|
|
|
|
# === SYNC STATUS ===
|
|
last_synced_at = Column(DateTime) # Last sync with marketplace
|
|
sync_status = Column(String) # 'synced', 'pending', 'error'
|
|
sync_error = Column(Text)
|
|
|
|
# === RELATIONSHIPS ===
|
|
store = relationship("Store", back_populates="orders")
|
|
customer = relationship("Customer", back_populates="orders")
|
|
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
|
|
status_history = relationship("OrderStatusHistory", back_populates="order")
|
|
|
|
__table_args__ = (
|
|
Index("idx_order_store_status", "store_id", "status"),
|
|
Index("idx_order_channel", "channel", "channel_order_id"),
|
|
Index("idx_order_store_date", "store_id", "ordered_at"),
|
|
)
|
|
|
|
|
|
class OrderItem(Base, TimestampMixin):
|
|
"""Individual item in an order."""
|
|
__tablename__ = "order_items"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id", ondelete="CASCADE"), nullable=False)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
|
|
|
|
# === PRODUCT SNAPSHOT (at time of order) ===
|
|
product_name = Column(String, nullable=False)
|
|
product_sku = Column(String)
|
|
product_gtin = Column(String)
|
|
product_image_url = Column(String)
|
|
|
|
# === PRICING ===
|
|
unit_price = Column(Float, nullable=False)
|
|
quantity = Column(Integer, nullable=False, default=1)
|
|
subtotal = Column(Float, nullable=False)
|
|
tax_amount = Column(Float, default=0)
|
|
discount_amount = Column(Float, default=0)
|
|
total = Column(Float, nullable=False)
|
|
|
|
# === PRODUCT TYPE ===
|
|
is_digital = Column(Boolean, default=False)
|
|
|
|
# === DIGITAL FULFILLMENT ===
|
|
license_key = Column(String) # For digital products
|
|
download_url = Column(String)
|
|
download_expiry = Column(DateTime)
|
|
digital_fulfilled_at = Column(DateTime)
|
|
|
|
# === PHYSICAL FULFILLMENT ===
|
|
shipped_quantity = Column(Integer, default=0)
|
|
|
|
# === SUPPLIER TRACKING (for CodesWholesale etc) ===
|
|
supplier = Column(String) # 'codeswholesale', 'internal', etc.
|
|
supplier_order_id = Column(String) # Supplier's order reference
|
|
supplier_cost = Column(Float) # What we paid supplier
|
|
|
|
# === RELATIONSHIPS ===
|
|
order = relationship("Order", back_populates="items")
|
|
product = relationship("Product")
|
|
|
|
|
|
class OrderStatusHistory(Base, TimestampMixin):
|
|
"""Audit trail of order status changes."""
|
|
__tablename__ = "order_status_history"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
order_id = Column(Integer, ForeignKey("orders.id", ondelete="CASCADE"), nullable=False)
|
|
|
|
from_status = Column(SQLEnum(OrderStatus))
|
|
to_status = Column(SQLEnum(OrderStatus), nullable=False)
|
|
changed_by = Column(String) # 'system', 'store:123', 'marketplace:letzshop'
|
|
reason = Column(String)
|
|
metadata = Column(JSON) # Additional context (tracking number, etc.)
|
|
|
|
order = relationship("Order", back_populates="status_history")
|
|
```
|
|
|
|
### 2.3 Order Import Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Scheduler as Scheduler
|
|
participant Poller as Order Poller
|
|
participant MP as Marketplace API
|
|
participant Queue as Import Queue
|
|
participant Worker as Import Worker
|
|
participant DB as Database
|
|
participant Notify as Notification Service
|
|
|
|
Scheduler->>Poller: Trigger poll (every N minutes)
|
|
Poller->>MP: Fetch orders since last_sync
|
|
MP-->>Poller: New/updated orders
|
|
|
|
loop For each order
|
|
Poller->>Queue: Enqueue order import job
|
|
end
|
|
|
|
Worker->>Queue: Pick up job
|
|
Worker->>DB: Check if order exists (by channel_order_id)
|
|
|
|
alt New Order
|
|
Worker->>DB: Create order + items
|
|
Worker->>Notify: New order notification
|
|
else Existing Order
|
|
Worker->>DB: Update order status/details
|
|
end
|
|
|
|
Worker->>DB: Update last_synced_at
|
|
```
|
|
|
|
### 2.4 Letzshop Order Integration (GraphQL)
|
|
|
|
```python
|
|
# Example GraphQL queries for Letzshop order integration
|
|
|
|
LETZSHOP_ORDERS_QUERY = """
|
|
query GetOrders($since: DateTime, $status: [OrderStatus!]) {
|
|
orders(
|
|
filter: {
|
|
updatedAt: { gte: $since }
|
|
status: { in: $status }
|
|
}
|
|
first: 100
|
|
) {
|
|
edges {
|
|
node {
|
|
id
|
|
orderNumber
|
|
status
|
|
createdAt
|
|
updatedAt
|
|
customer {
|
|
email
|
|
firstName
|
|
lastName
|
|
phone
|
|
}
|
|
shippingAddress {
|
|
street
|
|
city
|
|
postalCode
|
|
country
|
|
}
|
|
items {
|
|
product {
|
|
id
|
|
sku
|
|
name
|
|
}
|
|
quantity
|
|
unitPrice
|
|
totalPrice
|
|
}
|
|
totals {
|
|
subtotal
|
|
shipping
|
|
tax
|
|
total
|
|
currency
|
|
}
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
class LetzshopOrderImporter:
|
|
"""Import orders from Letzshop via GraphQL."""
|
|
|
|
def __init__(self, store_id: int, api_url: str, api_token: str):
|
|
self.store_id = store_id
|
|
self.api_url = api_url
|
|
self.api_token = api_token
|
|
|
|
async def fetch_orders_since(self, since: datetime) -> list[dict]:
|
|
"""Fetch orders updated since given timestamp."""
|
|
# Implementation: Execute GraphQL query
|
|
pass
|
|
|
|
def map_to_order(self, letzshop_order: dict) -> OrderCreate:
|
|
"""Map Letzshop order to unified Order schema."""
|
|
return OrderCreate(
|
|
store_id=self.store_id,
|
|
channel=OrderChannel.LETZSHOP,
|
|
channel_order_id=letzshop_order["id"],
|
|
customer_email=letzshop_order["customer"]["email"],
|
|
customer_name=f"{letzshop_order['customer']['firstName']} {letzshop_order['customer']['lastName']}",
|
|
status=self._map_status(letzshop_order["status"]),
|
|
# ... map remaining fields
|
|
)
|
|
|
|
def _map_status(self, letzshop_status: str) -> OrderStatus:
|
|
"""Map Letzshop status to unified status."""
|
|
mapping = {
|
|
"PENDING": OrderStatus.PENDING,
|
|
"PAID": OrderStatus.CONFIRMED,
|
|
"PROCESSING": OrderStatus.PROCESSING,
|
|
"SHIPPED": OrderStatus.SHIPPED,
|
|
"DELIVERED": OrderStatus.DELIVERED,
|
|
"CANCELLED": OrderStatus.CANCELLED,
|
|
}
|
|
return mapping.get(letzshop_status, OrderStatus.PENDING)
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: Order Fulfillment Sync
|
|
|
|
### 3.1 Fulfillment Architecture
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Store Actions"
|
|
VA[Store marks order shipped]
|
|
VD[Store marks delivered]
|
|
VF[Digital fulfillment triggered]
|
|
end
|
|
|
|
subgraph "Fulfillment Service"
|
|
FS[Fulfillment Service]
|
|
DFS[Digital Fulfillment Service]
|
|
end
|
|
|
|
subgraph "Outbound Sync"
|
|
SQ[Sync Queue]
|
|
SW[Sync Workers]
|
|
end
|
|
|
|
subgraph "External Systems"
|
|
LS_API[Letzshop GraphQL]
|
|
AZ_API[Amazon API]
|
|
EB_API[eBay API]
|
|
CW_API[CodesWholesale API]
|
|
EMAIL[Email Service]
|
|
end
|
|
|
|
VA --> FS
|
|
VD --> FS
|
|
VF --> DFS
|
|
|
|
FS --> SQ
|
|
DFS --> CW_API
|
|
DFS --> EMAIL
|
|
|
|
SQ --> SW
|
|
SW --> LS_API
|
|
SW --> AZ_API
|
|
SW --> EB_API
|
|
```
|
|
|
|
### 3.2 Physical Product Fulfillment
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Store as Store UI
|
|
participant API as Fulfillment API
|
|
participant DB as Database
|
|
participant Queue as Sync Queue
|
|
participant Worker as Sync Worker
|
|
participant MP as Marketplace API
|
|
|
|
Store->>API: Mark order as shipped (tracking #)
|
|
API->>DB: Update order status
|
|
API->>DB: Add status history entry
|
|
API->>Queue: Enqueue fulfillment sync job
|
|
|
|
Worker->>Queue: Pick up job
|
|
Worker->>DB: Get order details
|
|
Worker->>MP: Update fulfillment status
|
|
|
|
alt Sync Success
|
|
MP-->>Worker: 200 OK
|
|
Worker->>DB: Mark sync_status='synced'
|
|
else Sync Failed
|
|
MP-->>Worker: Error
|
|
Worker->>DB: Mark sync_status='error', store error
|
|
Worker->>Queue: Retry with backoff
|
|
end
|
|
```
|
|
|
|
### 3.3 Digital Product Fulfillment (CodesWholesale)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Order as Order Service
|
|
participant DFS as Digital Fulfillment Service
|
|
participant CW as CodesWholesale API
|
|
participant DB as Database
|
|
participant Email as Email Service
|
|
participant Customer as Customer
|
|
|
|
Note over Order: Order confirmed, contains digital item
|
|
Order->>DFS: Fulfill digital items
|
|
|
|
loop For each digital item
|
|
DFS->>DB: Check product supplier
|
|
|
|
alt Supplier = CodesWholesale
|
|
DFS->>CW: POST /orders (purchase key)
|
|
CW-->>DFS: License key + download info
|
|
DFS->>DB: Store license key on order_item
|
|
else Internal / Pre-loaded
|
|
DFS->>DB: Get key from license pool
|
|
DFS->>DB: Mark key as used
|
|
end
|
|
end
|
|
|
|
DFS->>DB: Update order status to FULFILLED
|
|
DFS->>Email: Send fulfillment email
|
|
Email->>Customer: License keys + download links
|
|
```
|
|
|
|
### 3.4 Fulfillment Service Implementation
|
|
|
|
```python
|
|
class FulfillmentService:
|
|
"""Service for managing order fulfillment across channels."""
|
|
|
|
def __init__(
|
|
self,
|
|
db: Session,
|
|
digital_fulfillment: "DigitalFulfillmentService",
|
|
sync_queue: "SyncQueue",
|
|
):
|
|
self.db = db
|
|
self.digital_fulfillment = digital_fulfillment
|
|
self.sync_queue = sync_queue
|
|
|
|
async def mark_shipped(
|
|
self,
|
|
order_id: int,
|
|
tracking_number: str | None = None,
|
|
carrier: str | None = None,
|
|
shipped_items: list[int] | None = None, # Partial shipment
|
|
) -> Order:
|
|
"""Mark order (or items) as shipped."""
|
|
order = self._get_order(order_id)
|
|
|
|
# Update order
|
|
order.status = OrderStatus.SHIPPED
|
|
order.shipped_at = datetime.utcnow()
|
|
|
|
# Add tracking info
|
|
if tracking_number:
|
|
self._add_status_history(
|
|
order,
|
|
OrderStatus.SHIPPED,
|
|
metadata={"tracking_number": tracking_number, "carrier": carrier}
|
|
)
|
|
|
|
# Queue sync to marketplace
|
|
if order.channel != OrderChannel.STOREFRONT:
|
|
self.sync_queue.enqueue(
|
|
SyncJob(
|
|
type="fulfillment",
|
|
order_id=order_id,
|
|
channel=order.channel,
|
|
data={"tracking_number": tracking_number, "carrier": carrier}
|
|
)
|
|
)
|
|
|
|
self.db.commit()
|
|
return order
|
|
|
|
async def fulfill_digital_items(self, order_id: int) -> Order:
|
|
"""Fulfill digital items in order."""
|
|
order = self._get_order(order_id)
|
|
|
|
digital_items = [item for item in order.items if item.is_digital]
|
|
|
|
for item in digital_items:
|
|
await self.digital_fulfillment.fulfill_item(item)
|
|
|
|
# Check if fully fulfilled
|
|
if all(item.digital_fulfilled_at for item in digital_items):
|
|
if order.is_fully_digital:
|
|
order.status = OrderStatus.FULFILLED
|
|
order.fulfilled_at = datetime.utcnow()
|
|
|
|
self.db.commit()
|
|
return order
|
|
|
|
|
|
class DigitalFulfillmentService:
|
|
"""Service for fulfilling digital products."""
|
|
|
|
def __init__(
|
|
self,
|
|
db: Session,
|
|
codeswholesale_client: "CodesWholesaleClient",
|
|
email_service: "EmailService",
|
|
):
|
|
self.db = db
|
|
self.codeswholesale = codeswholesale_client
|
|
self.email_service = email_service
|
|
|
|
async def fulfill_item(self, item: OrderItem) -> OrderItem:
|
|
"""Fulfill a single digital order item."""
|
|
if item.digital_fulfilled_at:
|
|
return item # Already fulfilled
|
|
|
|
# Get license key based on supplier
|
|
if item.supplier == "codeswholesale":
|
|
key_data = await self._fulfill_from_codeswholesale(item)
|
|
else:
|
|
key_data = await self._fulfill_from_internal_pool(item)
|
|
|
|
# Update item
|
|
item.license_key = key_data.get("license_key")
|
|
item.download_url = key_data.get("download_url")
|
|
item.download_expiry = key_data.get("expiry")
|
|
item.digital_fulfilled_at = datetime.utcnow()
|
|
item.supplier_order_id = key_data.get("supplier_order_id")
|
|
item.supplier_cost = key_data.get("cost")
|
|
|
|
return item
|
|
|
|
async def _fulfill_from_codeswholesale(self, item: OrderItem) -> dict:
|
|
"""Purchase key from CodesWholesale on-demand."""
|
|
# Get the marketplace product to find CodesWholesale product ID
|
|
product = item.product
|
|
mp = product.marketplace_product
|
|
|
|
if mp.marketplace != "codeswholesale":
|
|
raise ValueError(f"Product {product.id} is not from CodesWholesale")
|
|
|
|
# Purchase from CodesWholesale
|
|
result = await self.codeswholesale.purchase_code(
|
|
product_id=mp.marketplace_product_id,
|
|
quantity=item.quantity,
|
|
)
|
|
|
|
return {
|
|
"license_key": result["codes"][0]["code"], # First code for qty=1
|
|
"download_url": result.get("download_url"),
|
|
"supplier_order_id": result["order_id"],
|
|
"cost": result["total_price"],
|
|
}
|
|
|
|
async def _fulfill_from_internal_pool(self, item: OrderItem) -> dict:
|
|
"""Get key from internal pre-loaded pool."""
|
|
# Implementation for stores who pre-load their own keys
|
|
pass
|
|
```
|
|
|
|
### 3.5 Letzshop Fulfillment Sync (GraphQL)
|
|
|
|
```python
|
|
LETZSHOP_UPDATE_FULFILLMENT = """
|
|
mutation UpdateOrderFulfillment($orderId: ID!, $input: FulfillmentInput!) {
|
|
updateOrderFulfillment(orderId: $orderId, input: $input) {
|
|
order {
|
|
id
|
|
status
|
|
fulfillment {
|
|
status
|
|
trackingNumber
|
|
carrier
|
|
shippedAt
|
|
}
|
|
}
|
|
errors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
class LetzshopFulfillmentSync:
|
|
"""Sync fulfillment status to Letzshop."""
|
|
|
|
async def sync_shipment(
|
|
self,
|
|
order: Order,
|
|
tracking_number: str | None,
|
|
carrier: str | None,
|
|
) -> SyncResult:
|
|
"""Update Letzshop with shipment info."""
|
|
variables = {
|
|
"orderId": order.channel_order_id,
|
|
"input": {
|
|
"status": "SHIPPED",
|
|
"trackingNumber": tracking_number,
|
|
"carrier": carrier,
|
|
"shippedAt": order.shipped_at.isoformat(),
|
|
}
|
|
}
|
|
|
|
result = await self._execute_mutation(
|
|
LETZSHOP_UPDATE_FULFILLMENT,
|
|
variables
|
|
)
|
|
|
|
if result.get("errors"):
|
|
return SyncResult(success=False, errors=result["errors"])
|
|
|
|
return SyncResult(success=True)
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: Inventory Sync
|
|
|
|
### 4.1 Inventory Sync Architecture
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph "Inventory Changes"
|
|
OC[Order Created<br/>Reserve stock]
|
|
OF[Order Fulfilled<br/>Deduct stock]
|
|
OX[Order Cancelled<br/>Release stock]
|
|
MA[Manual Adjustment]
|
|
SI[Stock Import]
|
|
end
|
|
|
|
subgraph "Inventory Service"
|
|
IS[Inventory Service]
|
|
EQ[Event Queue]
|
|
end
|
|
|
|
subgraph "Sync Strategy"
|
|
RT[Real-time Sync<br/>For API marketplaces]
|
|
SC[Scheduled Batch<br/>For file-based]
|
|
end
|
|
|
|
subgraph "Outbound"
|
|
LS_S[Letzshop<br/>CSV/GraphQL]
|
|
AZ_S[Amazon API]
|
|
EB_S[eBay API]
|
|
end
|
|
|
|
OC --> IS
|
|
OF --> IS
|
|
OX --> IS
|
|
MA --> IS
|
|
SI --> IS
|
|
|
|
IS --> EQ
|
|
EQ --> RT
|
|
EQ --> SC
|
|
|
|
RT --> AZ_S
|
|
RT --> EB_S
|
|
SC --> LS_S
|
|
```
|
|
|
|
### 4.2 Sync Strategies
|
|
|
|
| Strategy | Use Case | Trigger | Marketplaces |
|
|
|----------|----------|---------|--------------|
|
|
| **Real-time** | API-based marketplaces | Inventory change event | Amazon, eBay |
|
|
| **Scheduled Batch** | File-based or rate-limited | Cron job (configurable) | Letzshop |
|
|
| **On-demand** | Manual trigger | Store action | All |
|
|
|
|
### 4.3 Inventory Data Model Extensions
|
|
|
|
```python
|
|
class InventorySyncConfig(Base, TimestampMixin):
|
|
"""Per-store, per-marketplace sync configuration."""
|
|
__tablename__ = "inventory_sync_configs"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
|
marketplace = Column(String, nullable=False) # 'letzshop', 'amazon', 'ebay'
|
|
|
|
# === SYNC SETTINGS ===
|
|
is_enabled = Column(Boolean, default=True)
|
|
sync_strategy = Column(String, default="scheduled") # 'realtime', 'scheduled', 'manual'
|
|
sync_interval_minutes = Column(Integer, default=15) # For scheduled
|
|
|
|
# === CREDENTIALS ===
|
|
api_credentials = Column(JSON) # Encrypted credentials
|
|
|
|
# === STOCK RULES ===
|
|
safety_stock = Column(Integer, default=0) # Reserve buffer
|
|
out_of_stock_threshold = Column(Integer, default=0)
|
|
sync_zero_stock = Column(Boolean, default=True) # Sync when stock=0
|
|
|
|
# === STATUS ===
|
|
last_sync_at = Column(DateTime)
|
|
last_sync_status = Column(String)
|
|
last_sync_error = Column(Text)
|
|
items_synced_count = Column(Integer, default=0)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("store_id", "marketplace", name="uq_inventory_sync_store_marketplace"),
|
|
)
|
|
|
|
|
|
class InventorySyncLog(Base, TimestampMixin):
|
|
"""Log of inventory sync operations."""
|
|
__tablename__ = "inventory_sync_logs"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
|
marketplace = Column(String, nullable=False)
|
|
|
|
sync_type = Column(String) # 'full', 'incremental', 'single_product'
|
|
started_at = Column(DateTime, nullable=False)
|
|
completed_at = Column(DateTime)
|
|
|
|
status = Column(String) # 'success', 'partial', 'failed'
|
|
items_processed = Column(Integer, default=0)
|
|
items_succeeded = Column(Integer, default=0)
|
|
items_failed = Column(Integer, default=0)
|
|
|
|
errors = Column(JSON) # List of errors
|
|
```
|
|
|
|
### 4.4 Inventory Sync Service
|
|
|
|
```python
|
|
class InventorySyncService:
|
|
"""Service for syncing inventory to marketplaces."""
|
|
|
|
def __init__(
|
|
self,
|
|
db: Session,
|
|
sync_adapters: dict[str, "MarketplaceSyncAdapter"],
|
|
):
|
|
self.db = db
|
|
self.adapters = sync_adapters
|
|
|
|
async def sync_inventory_change(
|
|
self,
|
|
product_id: int,
|
|
new_quantity: int,
|
|
change_reason: str,
|
|
):
|
|
"""Handle inventory change event - trigger real-time syncs."""
|
|
product = self.db.query(Product).get(product_id)
|
|
store_id = product.store_id
|
|
|
|
# Get enabled real-time sync configs
|
|
configs = self.db.query(InventorySyncConfig).filter(
|
|
InventorySyncConfig.store_id == store_id,
|
|
InventorySyncConfig.is_enabled == True,
|
|
InventorySyncConfig.sync_strategy == "realtime",
|
|
).all()
|
|
|
|
for config in configs:
|
|
adapter = self.adapters.get(config.marketplace)
|
|
if adapter:
|
|
await self._sync_single_product(adapter, config, product, new_quantity)
|
|
|
|
async def run_scheduled_sync(self, store_id: int, marketplace: str):
|
|
"""Run scheduled batch sync for a marketplace."""
|
|
config = self._get_sync_config(store_id, marketplace)
|
|
adapter = self.adapters.get(marketplace)
|
|
|
|
log = InventorySyncLog(
|
|
store_id=store_id,
|
|
marketplace=marketplace,
|
|
sync_type="full",
|
|
started_at=datetime.utcnow(),
|
|
status="running",
|
|
)
|
|
self.db.add(log)
|
|
self.db.commit()
|
|
|
|
try:
|
|
# Get all products for this store linked to this marketplace
|
|
products = self._get_products_for_sync(store_id, marketplace)
|
|
|
|
# Build inventory update payload
|
|
inventory_data = []
|
|
for product in products:
|
|
available_qty = self._calculate_available_quantity(product, config)
|
|
inventory_data.append({
|
|
"sku": product.store_sku or product.marketplace_product.marketplace_product_id,
|
|
"quantity": available_qty,
|
|
})
|
|
|
|
# Sync via adapter
|
|
result = await adapter.sync_inventory_batch(config, inventory_data)
|
|
|
|
log.completed_at = datetime.utcnow()
|
|
log.status = "success" if result.all_succeeded else "partial"
|
|
log.items_processed = len(inventory_data)
|
|
log.items_succeeded = result.succeeded_count
|
|
log.items_failed = result.failed_count
|
|
log.errors = result.errors
|
|
|
|
config.last_sync_at = datetime.utcnow()
|
|
config.last_sync_status = log.status
|
|
config.items_synced_count = log.items_succeeded
|
|
|
|
except Exception as e:
|
|
log.completed_at = datetime.utcnow()
|
|
log.status = "failed"
|
|
log.errors = [{"error": str(e)}]
|
|
config.last_sync_error = str(e)
|
|
|
|
self.db.commit()
|
|
return log
|
|
|
|
def _calculate_available_quantity(
|
|
self,
|
|
product: Product,
|
|
config: InventorySyncConfig,
|
|
) -> int:
|
|
"""Calculate quantity to report, applying safety stock."""
|
|
inventory = product.inventory_entries[0] if product.inventory_entries else None
|
|
if not inventory:
|
|
return 0
|
|
|
|
available = inventory.quantity - inventory.reserved_quantity
|
|
available -= config.safety_stock # Apply safety buffer
|
|
|
|
if available <= config.out_of_stock_threshold:
|
|
return 0 if config.sync_zero_stock else config.out_of_stock_threshold
|
|
|
|
return max(0, available)
|
|
```
|
|
|
|
### 4.5 Letzshop Inventory Sync
|
|
|
|
```python
|
|
class LetzshopInventorySyncAdapter:
|
|
"""Sync inventory to Letzshop via CSV or GraphQL."""
|
|
|
|
async def sync_inventory_batch(
|
|
self,
|
|
config: InventorySyncConfig,
|
|
inventory_data: list[dict],
|
|
) -> SyncResult:
|
|
"""Sync inventory batch to Letzshop."""
|
|
# Option 1: CSV Export (upload to SFTP/web location)
|
|
if config.api_credentials.get("method") == "csv":
|
|
return await self._sync_via_csv(config, inventory_data)
|
|
|
|
# Option 2: GraphQL mutations
|
|
return await self._sync_via_graphql(config, inventory_data)
|
|
|
|
async def _sync_via_csv(
|
|
self,
|
|
config: InventorySyncConfig,
|
|
inventory_data: list[dict],
|
|
) -> SyncResult:
|
|
"""Generate and upload CSV inventory file."""
|
|
# Generate CSV
|
|
csv_content = self._generate_inventory_csv(inventory_data)
|
|
|
|
# Upload to configured location (SFTP, S3, etc.)
|
|
upload_location = config.api_credentials.get("upload_url")
|
|
# ... upload logic
|
|
|
|
return SyncResult(success=True, succeeded_count=len(inventory_data))
|
|
|
|
async def _sync_via_graphql(
|
|
self,
|
|
config: InventorySyncConfig,
|
|
inventory_data: list[dict],
|
|
) -> SyncResult:
|
|
"""Update inventory via GraphQL mutations."""
|
|
mutation = """
|
|
mutation UpdateInventory($input: InventoryUpdateInput!) {
|
|
updateInventory(input: $input) {
|
|
success
|
|
errors { sku, message }
|
|
}
|
|
}
|
|
"""
|
|
# ... execute mutation
|
|
```
|
|
|
|
---
|
|
|
|
## Part 5: Scheduler & Job Management
|
|
|
|
### 5.1 Scheduled Jobs Overview
|
|
|
|
| Job | Default Schedule | Configurable | Description |
|
|
|-----|------------------|--------------|-------------|
|
|
| `order_import_{marketplace}` | Every 5 min | Per store | Poll orders from marketplace |
|
|
| `inventory_sync_{marketplace}` | Every 15 min | Per store | Sync inventory to marketplace |
|
|
| `codeswholesale_catalog_sync` | Every 6 hours | Global | Update digital product catalog |
|
|
| `product_price_sync` | Daily | Per store | Sync price changes to marketplace |
|
|
| `sync_retry_failed` | Every 10 min | Global | Retry failed sync jobs |
|
|
|
|
### 5.2 Job Configuration Model
|
|
|
|
```python
|
|
class ScheduledJob(Base, TimestampMixin):
|
|
"""Configurable scheduled job."""
|
|
__tablename__ = "scheduled_jobs"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
store_id = Column(Integer, ForeignKey("stores.id"), nullable=True) # Null = global
|
|
|
|
job_type = Column(String, nullable=False) # 'order_import', 'inventory_sync', etc.
|
|
marketplace = Column(String) # Relevant marketplace if applicable
|
|
|
|
# === SCHEDULE ===
|
|
is_enabled = Column(Boolean, default=True)
|
|
cron_expression = Column(String) # Cron format
|
|
interval_minutes = Column(Integer) # Simple interval alternative
|
|
|
|
# === EXECUTION ===
|
|
last_run_at = Column(DateTime)
|
|
last_run_status = Column(String)
|
|
last_run_duration_ms = Column(Integer)
|
|
next_run_at = Column(DateTime)
|
|
|
|
# === RETRY CONFIG ===
|
|
max_retries = Column(Integer, default=3)
|
|
retry_delay_seconds = Column(Integer, default=60)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("store_id", "job_type", "marketplace", name="uq_scheduled_job"),
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Roadmap
|
|
|
|
### Phase 1: Product Import (Weeks 1-2)
|
|
|
|
**Goal:** Multi-marketplace product import with translations and digital product support.
|
|
|
|
| Task | Priority | Status |
|
|
|------|----------|--------|
|
|
| Add product_type, is_digital fields to marketplace_products | High | [ ] |
|
|
| Create marketplace_product_translations table | High | [ ] |
|
|
| Create product_translations table | High | [ ] |
|
|
| Implement BaseMarketplaceImporter pattern | High | [ ] |
|
|
| Refactor LetzshopImporter from CSV processor | High | [ ] |
|
|
| Add CodesWholesale catalog importer | High | [ ] |
|
|
| Implement store override pattern on products | Medium | [ ] |
|
|
| Add translation override support | Medium | [ ] |
|
|
| Update API endpoints for translations | Medium | [ ] |
|
|
|
|
**Detailed tasks:** See [Multi-Marketplace Product Architecture](../development/migration/multi-marketplace-product-architecture.md)
|
|
|
|
### Phase 2: Order Import (Weeks 3-4)
|
|
|
|
**Goal:** Unified order management with multi-channel support.
|
|
|
|
| Task | Priority | Status |
|
|
|------|----------|--------|
|
|
| Design unified orders schema | High | [ ] |
|
|
| Create orders, order_items, order_status_history tables | High | [ ] |
|
|
| Implement BaseOrderImporter pattern | High | [ ] |
|
|
| Implement LetzshopOrderImporter (GraphQL) | High | [ ] |
|
|
| Create order polling scheduler | High | [ ] |
|
|
| Build order list/detail store UI | Medium | [ ] |
|
|
| Add order notifications | Medium | [ ] |
|
|
| Implement order search and filtering | Medium | [ ] |
|
|
|
|
### Phase 3: Order Fulfillment Sync (Weeks 5-6)
|
|
|
|
**Goal:** Sync fulfillment status back to marketplaces, handle digital delivery.
|
|
|
|
| Task | Priority | Status |
|
|
|------|----------|--------|
|
|
| Implement FulfillmentService | High | [ ] |
|
|
| Implement DigitalFulfillmentService | High | [ ] |
|
|
| Integrate CodesWholesale key purchase API | High | [ ] |
|
|
| Create fulfillment sync queue | High | [ ] |
|
|
| Implement LetzshopFulfillmentSync | High | [ ] |
|
|
| Build fulfillment UI (mark shipped, add tracking) | Medium | [ ] |
|
|
| Digital fulfillment email templates | Medium | [ ] |
|
|
| Fulfillment retry logic | Medium | [ ] |
|
|
|
|
### Phase 4: Inventory Sync (Weeks 7-8)
|
|
|
|
**Goal:** Real-time and scheduled inventory sync to marketplaces.
|
|
|
|
| Task | Priority | Status |
|
|
|------|----------|--------|
|
|
| Create inventory_sync_configs table | High | [ ] |
|
|
| Create inventory_sync_logs table | High | [ ] |
|
|
| Implement InventorySyncService | High | [ ] |
|
|
| Implement LetzshopInventorySyncAdapter | High | [ ] |
|
|
| Create inventory change event system | High | [ ] |
|
|
| Build sync configuration UI | Medium | [ ] |
|
|
| Add sync status dashboard | Medium | [ ] |
|
|
| Implement real-time sync for future marketplaces | Low | [ ] |
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Credential Storage
|
|
|
|
```python
|
|
# All marketplace credentials should be encrypted at rest
|
|
class MarketplaceCredential(Base, TimestampMixin):
|
|
"""Encrypted marketplace credentials."""
|
|
__tablename__ = "marketplace_credentials"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
|
marketplace = Column(String, nullable=False)
|
|
|
|
# Encrypted using application-level encryption
|
|
credentials_encrypted = Column(LargeBinary, nullable=False)
|
|
|
|
# Metadata (not sensitive)
|
|
credential_type = Column(String) # 'api_key', 'oauth', 'basic'
|
|
expires_at = Column(DateTime)
|
|
is_valid = Column(Boolean, default=True)
|
|
last_validated_at = Column(DateTime)
|
|
```
|
|
|
|
### API Rate Limiting
|
|
|
|
- Respect marketplace API rate limits
|
|
- Implement exponential backoff for failures
|
|
- Queue operations to smooth out request bursts
|
|
|
|
### Data Privacy
|
|
|
|
- Customer data from marketplaces should follow GDPR guidelines
|
|
- Implement data retention policies
|
|
- Provide data export/deletion capabilities
|
|
|
|
---
|
|
|
|
## Monitoring & Observability
|
|
|
|
### Key Metrics
|
|
|
|
| Metric | Description | Alert Threshold |
|
|
|--------|-------------|-----------------|
|
|
| `order_import_lag_seconds` | Time since last successful order poll | > 15 min |
|
|
| `inventory_sync_lag_seconds` | Time since last inventory sync | > 30 min |
|
|
| `fulfillment_sync_queue_depth` | Pending fulfillment syncs | > 100 |
|
|
| `sync_error_rate` | Failed syncs / total syncs | > 5% |
|
|
| `digital_fulfillment_success_rate` | Successful key retrievals | < 95% |
|
|
|
|
### Health Checks
|
|
|
|
```python
|
|
@router.get("/health/marketplace-integrations")
|
|
async def check_marketplace_health(store_id: int):
|
|
"""Check health of marketplace integrations."""
|
|
return {
|
|
"letzshop": {
|
|
"order_import": check_last_sync("letzshop", "order_import"),
|
|
"inventory_sync": check_last_sync("letzshop", "inventory_sync"),
|
|
},
|
|
"codeswholesale": {
|
|
"catalog_sync": check_last_sync("codeswholesale", "catalog"),
|
|
"api_status": await check_codeswholesale_api(),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix A: CodesWholesale Integration Details
|
|
|
|
### API Endpoints Used
|
|
|
|
| Endpoint | Purpose | Frequency |
|
|
|----------|---------|-----------|
|
|
| `GET /products` | Fetch full catalog | Every 6 hours |
|
|
| `GET /products/{id}` | Get single product details | On-demand |
|
|
| `POST /orders` | Purchase license key | On order fulfillment |
|
|
| `GET /orders/{id}` | Check order status | After purchase |
|
|
| `GET /account/balance` | Check account balance | Periodically |
|
|
|
|
### Product Catalog Mapping
|
|
|
|
```python
|
|
def map_codeswholesale_product(cw_product: dict) -> dict:
|
|
"""Map CodesWholesale product to marketplace_product format."""
|
|
return {
|
|
"marketplace_product_id": f"cw_{cw_product['productId']}",
|
|
"marketplace": "codeswholesale",
|
|
"gtin": cw_product.get("ean"),
|
|
"product_type": "digital",
|
|
"is_digital": True,
|
|
"digital_delivery_method": "license_key",
|
|
"platform": cw_product.get("platform", "").lower(), # steam, origin, etc.
|
|
"region_restrictions": cw_product.get("regions"),
|
|
"price": cw_product["prices"][0]["value"], # Supplier cost
|
|
"currency": cw_product["prices"][0]["currency"],
|
|
"availability": "in_stock" if cw_product["quantity"] > 0 else "out_of_stock",
|
|
"attributes": {
|
|
"languages": cw_product.get("languages"),
|
|
"release_date": cw_product.get("releaseDate"),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix B: Status Mapping Reference
|
|
|
|
### Order Status Mapping
|
|
|
|
| Orion Status | Letzshop | Amazon | eBay |
|
|
|-----------------|----------|--------|------|
|
|
| PENDING | PENDING | Pending | - |
|
|
| CONFIRMED | PAID | Unshipped | Paid |
|
|
| PROCESSING | PROCESSING | - | - |
|
|
| SHIPPED | SHIPPED | Shipped | Shipped |
|
|
| DELIVERED | DELIVERED | - | Delivered |
|
|
| FULFILLED | - | - | - |
|
|
| CANCELLED | CANCELLED | Cancelled | Cancelled |
|
|
| REFUNDED | REFUNDED | Refunded | Refunded |
|
|
|
|
---
|
|
|
|
## Related Documents
|
|
|
|
- [Multi-Marketplace Product Architecture](../development/migration/multi-marketplace-product-architecture.md) - Detailed product data model
|
|
- [Store Contact Inheritance](../development/migration/store-contact-inheritance.md) - Override pattern reference
|
|
- [Database Migrations](../development/migration/database-migrations.md) - Migration guidelines
|