# 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
