- Add marketplace-integration.md: Full architecture for bidirectional marketplace integration (products, orders, fulfillment, inventory sync) - Add multi-marketplace-product-architecture.md: Detailed product data model with translations and vendor override pattern - Add product-migration-database-changes.md: Database migration plan with SQL scripts and model updates - Update mkdocs.yml with new documentation entries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <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 Wizamart 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[Vendor Storefront<br/>Wizamart 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 "Wizamart Core"
|
|
MP[Marketplace Products]
|
|
P[Vendor 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 |
|
|
| **Vendor 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 "Vendor 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: Vendor 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, vendor 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` | Vendor-specific overrides and settings |
|
|
| `product_translations` | Vendor-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 + vendor 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[Vendor 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" # Vendor's own Wizamart 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)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.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 ===
|
|
vendor = relationship("Vendor", 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_vendor_status", "vendor_id", "status"),
|
|
Index("idx_order_channel", "channel", "channel_order_id"),
|
|
Index("idx_order_vendor_date", "vendor_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', 'vendor: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, vendor_id: int, api_url: str, api_token: str):
|
|
self.vendor_id = vendor_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(
|
|
vendor_id=self.vendor_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 "Vendor Actions"
|
|
VA[Vendor marks order shipped]
|
|
VD[Vendor 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 Vendor as Vendor 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
|
|
|
|
Vendor->>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 vendors 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 | Vendor action | All |
|
|
|
|
### 4.3 Inventory Data Model Extensions
|
|
|
|
```python
|
|
class InventorySyncConfig(Base, TimestampMixin):
|
|
"""Per-vendor, per-marketplace sync configuration."""
|
|
__tablename__ = "inventory_sync_configs"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.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("vendor_id", "marketplace", name="uq_inventory_sync_vendor_marketplace"),
|
|
)
|
|
|
|
|
|
class InventorySyncLog(Base, TimestampMixin):
|
|
"""Log of inventory sync operations."""
|
|
__tablename__ = "inventory_sync_logs"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.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)
|
|
vendor_id = product.vendor_id
|
|
|
|
# Get enabled real-time sync configs
|
|
configs = self.db.query(InventorySyncConfig).filter(
|
|
InventorySyncConfig.vendor_id == vendor_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, vendor_id: int, marketplace: str):
|
|
"""Run scheduled batch sync for a marketplace."""
|
|
config = self._get_sync_config(vendor_id, marketplace)
|
|
adapter = self.adapters.get(marketplace)
|
|
|
|
log = InventorySyncLog(
|
|
vendor_id=vendor_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 vendor linked to this marketplace
|
|
products = self._get_products_for_sync(vendor_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.vendor_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 vendor | Poll orders from marketplace |
|
|
| `inventory_sync_{marketplace}` | Every 15 min | Per vendor | Sync inventory to marketplace |
|
|
| `codeswholesale_catalog_sync` | Every 6 hours | Global | Update digital product catalog |
|
|
| `product_price_sync` | Daily | Per vendor | 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)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.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("vendor_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 vendor 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 vendor 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)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.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(vendor_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
|
|
|
|
| Wizamart 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
|
|
- [Vendor Contact Inheritance](../development/migration/vendor-contact-inheritance.md) - Override pattern reference
|
|
- [Database Migrations](../development/migration/database-migrations.md) - Migration guidelines
|