# Letzshop Order Integration Guide

Complete guide for bidirectional order management with Letzshop marketplace via GraphQL API.

## Table of Contents

- [Overview](#overview)
- [Architecture](#architecture)
- [Setup and Configuration](#setup-and-configuration)
- [Order Import](#order-import)
- [Product Exceptions](#product-exceptions)
- [Fulfillment Operations](#fulfillment-operations)
- [Shipping and Tracking](#shipping-and-tracking)
- [API Reference](#api-reference)
- [Database Models](#database-models)
- [Troubleshooting](#troubleshooting)

---

## Overview

The Letzshop Order Integration provides bidirectional synchronization with Letzshop marketplace:

- **Order Import**: Fetch unconfirmed orders from Letzshop via GraphQL
- **Order Confirmation**: Confirm or reject inventory units
- **Tracking Updates**: Set shipment tracking information
- **Audit Trail**: Complete logging of all sync operations

### Key Features

- **Encrypted Credentials**: API keys stored with Fernet encryption
- **Per-Store Configuration**: Each store manages their own Letzshop connection
- **Admin Oversight**: Platform admins can manage any store's integration
- **Queue-Based Fulfillment**: Retry logic for failed operations
- **Multi-Channel Support**: Orders tracked with channel attribution

---

## Architecture

### System Components

```
                    ┌─────────────────────────────────────────┐
                    │           Frontend Interfaces            │
                    ├─────────────────────────────────────────┤
                    │  Store Portal          Admin Portal     │
                    │  /store/letzshop       /admin/letzshop  │
                    └─────────────────────────────────────────┘
                                        │
                    ┌─────────────────────────────────────────┐
                    │              API Layer                   │
                    ├─────────────────────────────────────────┤
                    │  /api/v1/store/letzshop/*               │
                    │  /api/v1/admin/letzshop/*                │
                    └─────────────────────────────────────────┘
                                        │
                    ┌─────────────────────────────────────────┐
                    │           Service Layer                  │
                    ├─────────────────────────────────────────┤
                    │  LetzshopClient        CredentialsService│
                    │  (GraphQL)             (Encryption)      │
                    └─────────────────────────────────────────┘
                                        │
                    ┌─────────────────────────────────────────┐
                    │           Data Layer                     │
                    ├─────────────────────────────────────────┤
                    │  StoreLetzshopCredentials               │
                    │  LetzshopOrder                           │
                    │  LetzshopFulfillmentQueue                │
                    │  LetzshopSyncLog                         │
                    └─────────────────────────────────────────┘
                                        │
                    ┌─────────────────────────────────────────┐
                    │         Letzshop GraphQL API             │
                    │         https://letzshop.lu/graphql      │
                    └─────────────────────────────────────────┘
```

### Data Flow

1. **Credentials Setup**: Store/Admin stores encrypted API key
2. **Order Import**: System fetches unconfirmed shipments from Letzshop
3. **Order Processing**: Orders stored locally with Letzshop IDs
4. **Fulfillment**: Store confirms/rejects orders, sets tracking
5. **Sync Back**: Operations sent to Letzshop via GraphQL mutations

---

## Setup and Configuration

### Prerequisites

- Letzshop API key (obtained from Letzshop merchant portal)
- Active store account on the platform

### Step 1: Configure API Credentials

#### Via Store Portal

1. Navigate to **Settings > Letzshop Integration**
2. Enter your Letzshop API key
3. Click **Test Connection** to verify
4. Enable **Auto-Sync** if desired (optional)
5. Click **Save**

#### Via Admin Portal

1. Navigate to **Marketplace > Letzshop**
2. Select the store from the list
3. Click **Configure Credentials**
4. Enter the API key
5. Click **Save & Test**

### Step 2: Test Connection

```bash
# Test connection via API
curl -X POST /api/v1/store/letzshop/test \
  -H "Authorization: Bearer $TOKEN"
```

Response:
```json
{
  "success": true,
  "message": "Connection successful",
  "response_time_ms": 245.5
}
```

### Configuration Options

| Setting | Default | Description |
|---------|---------|-------------|
| `api_endpoint` | `https://letzshop.lu/graphql` | GraphQL endpoint URL |
| `auto_sync_enabled` | `false` | Enable automatic order sync |
| `sync_interval_minutes` | `15` | Auto-sync interval (5-1440 minutes) |

---

## Order Import

### Manual Import

Import orders on-demand via the store portal or API:

```bash
# Trigger order import
curl -X POST /api/v1/store/letzshop/orders/import \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"operation": "order_import"}'
```

Response:
```json
{
  "success": true,
  "message": "Import completed: 5 imported, 2 updated",
  "orders_imported": 5,
  "orders_updated": 2,
  "errors": []
}
```

### What Gets Imported

The import fetches **unconfirmed shipments** from Letzshop containing:

- Order ID and number
- Customer email and name
- Order total and currency
- Inventory units (products to fulfill)
- Shipping/billing addresses
- Current order state

### Order States

| Letzshop State | Description |
|----------------|-------------|
| `unconfirmed` | Awaiting store confirmation |
| `confirmed` | Store confirmed, ready to ship |
| `shipped` | Tracking number set |
| `delivered` | Delivery confirmed |
| `returned` | Items returned |

### Sync Status

Local orders track their sync status:

| Status | Description |
|--------|-------------|
| `pending` | Imported, awaiting action |
| `confirmed` | Confirmed with Letzshop |
| `rejected` | Rejected with Letzshop |
| `shipped` | Tracking set with Letzshop |

---

## Product Exceptions

When importing orders from Letzshop, products are matched by GTIN. If a product is not found in the store's catalog, the system **gracefully imports the order** with a placeholder product and creates an exception record for resolution.

### Exception Workflow

```
Import Order → Product not found by GTIN
                        │
                        ▼
            Create order with placeholder
            + Flag item: needs_product_match=True
            + Create OrderItemException record
                        │
                        ▼
            Exception appears in QC dashboard
                        │
            ┌───────────┴───────────┐
            │                       │
        Resolve                  Ignore
    (assign product)         (with reason)
            │                       │
            ▼                       ▼
    Order can be confirmed    Still blocks confirmation
```

### Exception Types

| Type | Description |
|------|-------------|
| `product_not_found` | GTIN not in store's product catalog |
| `gtin_mismatch` | GTIN format issue |
| `duplicate_gtin` | Multiple products with same GTIN |

### Exception Statuses

| Status | Description | Blocks Confirmation |
|--------|-------------|---------------------|
| `pending` | Awaiting resolution | **Yes** |
| `resolved` | Product assigned | No |
| `ignored` | Marked as ignored | **Yes** |

**Important:** Both `pending` and `ignored` exceptions block order confirmation to Letzshop.

### Viewing Exceptions

Navigate to **Marketplace > Letzshop > Exceptions** tab to see all unmatched products.

The dashboard shows:
- **Pending**: Exceptions awaiting resolution
- **Resolved**: Exceptions that have been matched
- **Ignored**: Exceptions marked as ignored
- **Orders Affected**: Orders with at least one exception

### Resolving Exceptions

#### Via Admin UI

1. Navigate to **Marketplace > Letzshop > Exceptions**
2. Click **Resolve** on the pending exception
3. Search for the correct product by name, SKU, or GTIN
4. Select the product and click **Confirm**
5. Optionally check "Apply to all exceptions with this GTIN" for bulk resolution

#### Via API

```bash
# Resolve a single exception
curl -X POST /api/v1/admin/order-exceptions/{exception_id}/resolve \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": 123,
    "notes": "Matched to correct product manually"
  }'

# Bulk resolve all exceptions with same GTIN
curl -X POST /api/v1/admin/order-exceptions/bulk-resolve?store_id=1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "gtin": "4006381333931",
    "product_id": 123,
    "notes": "Product imported to catalog"
  }'
```

### Auto-Matching

When products are imported to the store catalog (via product sync or manual import), the system automatically:

1. Collects GTINs of newly imported products
2. Finds pending exceptions with matching GTINs
3. Resolves them by assigning the new product

This happens during:
- Single product import (`copy_to_store_catalog`)
- Bulk marketplace sync

### Exception Statistics

Get counts via API:

```bash
curl -X GET /api/v1/admin/order-exceptions/stats?store_id=1 \
  -H "Authorization: Bearer $TOKEN"
```

Response:
```json
{
  "pending": 15,
  "resolved": 42,
  "ignored": 3,
  "total": 60,
  "orders_with_exceptions": 8
}
```

For more details, see [Order Item Exception System](../orders/exceptions.md).

---

## Fulfillment Operations

### Confirm Order

Confirm that you can fulfill the order:

```bash
# Confirm all inventory units in an order
curl -X POST /api/v1/store/letzshop/orders/{order_id}/confirm \
  -H "Authorization: Bearer $TOKEN"

# Or confirm specific units
curl -X POST /api/v1/store/letzshop/orders/{order_id}/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"inventory_unit_ids": ["unit_abc123", "unit_def456"]}'
```

### Reject Order

Reject order if you cannot fulfill:

```bash
curl -X POST /api/v1/store/letzshop/orders/{order_id}/reject \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Out of stock"}'
```

### Set Tracking

Add tracking information for shipment:

```bash
curl -X POST /api/v1/store/letzshop/orders/{order_id}/tracking \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tracking_number": "1Z999AA10123456784",
    "tracking_carrier": "ups"
  }'
```

Supported carriers: `dhl`, `ups`, `fedex`, `post_lu`, etc.

---

## Shipping and Tracking

The system captures shipping information from Letzshop and provides local shipping management features.

### Letzshop Nomenclature

Letzshop uses specific terminology for order references:

| Term | Example | Description |
|------|---------|-------------|
| **Order Number** | `R532332163` | Customer-facing order reference |
| **Shipment Number** | `H74683403433` | Carrier shipment ID for tracking |
| **Hash ID** | `nvDv5RQEmCwbjo` | Internal Letzshop reference |

### Order Fields

Orders imported from Letzshop include:

| Field | Description |
|-------|-------------|
| `external_order_number` | Letzshop order number (e.g., R532332163) |
| `shipment_number` | Carrier shipment number (e.g., H74683403433) |
| `shipping_carrier` | Carrier code (greco, colissimo, xpresslogistics) |
| `tracking_number` | Tracking number (if available) |
| `tracking_url` | Full tracking URL |

### Carrier Detection

The system automatically detects the carrier from Letzshop shipment data:

| Carrier | Code | Label URL Prefix |
|---------|------|------------------|
| Greco | `greco` | `https://dispatchweb.fr/Tracky/Home/` |
| Colissimo | `colissimo` | Configurable in settings |
| XpressLogistics | `xpresslogistics` | Configurable in settings |

### Mark as Shipped

Mark orders as shipped locally (does **not** sync to Letzshop):

```bash
curl -X POST /api/v1/admin/orders/{order_id}/ship \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tracking_number": "1Z999AA10123456784",
    "tracking_url": "https://tracking.example.com/1Z999AA10123456784",
    "shipping_carrier": "ups"
  }'
```

**Note:** This updates the local order status to `shipped` and sets the `shipped_at` timestamp. It does not send anything to Letzshop API.

### Download Shipping Label

Get the shipping label URL for an order:

```bash
curl -X GET /api/v1/admin/orders/{order_id}/shipping-label \
  -H "Authorization: Bearer $TOKEN"
```

Response:
```json
{
  "shipment_number": "H74683403433",
  "shipping_carrier": "greco",
  "label_url": "https://dispatchweb.fr/Tracky/Home/H74683403433",
  "tracking_number": null,
  "tracking_url": null
}
```

The label URL is constructed from:
- **Carrier label URL prefix** (configured in Admin Settings)
- **Shipment number** from the order

### Carrier Label Settings

Configure carrier label URL prefixes in **Admin > Settings > Shipping**:

| Setting | Default | Description |
|---------|---------|-------------|
| Greco Label URL | `https://dispatchweb.fr/Tracky/Home/` | Greco tracking/label prefix |
| Colissimo Label URL | *(empty)* | Colissimo tracking prefix |
| XpressLogistics Label URL | *(empty)* | XpressLogistics prefix |

The full label URL is: `{prefix}{shipment_number}`

### Tracking Information

Letzshop does not expose Greco tracking information via API. The tracking URL visible in the Letzshop web UI is auto-generated by Letzshop using the dispatchweb.fr prefix.

For orders using Greco carrier:
1. The shipment number (e.g., `H74683403433`) is captured during import
2. The tracking URL can be constructed: `https://dispatchweb.fr/Tracky/Home/{shipment_number}`
3. Use the Download Label feature to get this URL

---

## API Reference

### Store Endpoints

Base path: `/api/v1/store/letzshop`

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/status` | Get integration status |
| GET | `/credentials` | Get credentials (API key masked) |
| POST | `/credentials` | Create/update credentials |
| PATCH | `/credentials` | Partial update credentials |
| DELETE | `/credentials` | Remove credentials |
| POST | `/test` | Test stored credentials |
| POST | `/test-key` | Test API key without saving |
| GET | `/orders` | List Letzshop orders |
| GET | `/orders/{id}` | Get order details |
| POST | `/orders/import` | Import orders from Letzshop |
| POST | `/orders/{id}/confirm` | Confirm order |
| POST | `/orders/{id}/reject` | Reject order |
| POST | `/orders/{id}/tracking` | Set tracking info |
| GET | `/logs` | List sync logs |
| GET | `/queue` | List fulfillment queue |

### Admin Endpoints

Base path: `/api/v1/admin/letzshop`

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/stores` | List stores with Letzshop status |
| GET | `/stores/{id}/credentials` | Get store credentials |
| POST | `/stores/{id}/credentials` | Set store credentials |
| PATCH | `/stores/{id}/credentials` | Update store credentials |
| DELETE | `/stores/{id}/credentials` | Delete store credentials |
| POST | `/stores/{id}/test` | Test store connection |
| POST | `/test` | Test any API key |
| GET | `/stores/{id}/orders` | List store's Letzshop orders |
| POST | `/stores/{id}/sync` | Trigger sync for store |

### Order Endpoints

Base path: `/api/v1/admin/orders`

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `` | List orders (cross-store) |
| GET | `/stats` | Get order statistics |
| GET | `/stores` | Get stores with orders |
| GET | `/{id}` | Get order details |
| PATCH | `/{id}/status` | Update order status |
| POST | `/{id}/ship` | Mark as shipped |
| GET | `/{id}/shipping-label` | Get shipping label URL |

### Exception Endpoints

Base path: `/api/v1/admin/order-exceptions`

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `` | List exceptions |
| GET | `/stats` | Get exception statistics |
| GET | `/{id}` | Get exception details |
| POST | `/{id}/resolve` | Resolve with product |
| POST | `/{id}/ignore` | Mark as ignored |
| POST | `/bulk-resolve` | Bulk resolve by GTIN |

### Response Schemas

#### Credentials Response

```json
{
  "id": 1,
  "store_id": 5,
  "api_key_masked": "letz****",
  "api_endpoint": "https://letzshop.lu/graphql",
  "auto_sync_enabled": false,
  "sync_interval_minutes": 15,
  "last_sync_at": "2025-01-15T10:30:00Z",
  "last_sync_status": "success",
  "last_sync_error": null,
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}
```

#### Order Response

```json
{
  "id": 123,
  "store_id": 5,
  "letzshop_order_id": "gid://letzshop/Order/12345",
  "letzshop_shipment_id": "gid://letzshop/Shipment/67890",
  "letzshop_order_number": "LS-2025-001234",
  "letzshop_state": "unconfirmed",
  "customer_email": "customer@example.com",
  "customer_name": "John Doe",
  "total_amount": "99.99",
  "currency": "EUR",
  "sync_status": "pending",
  "inventory_units": [
    {"id": "gid://letzshop/InventoryUnit/111", "state": "unconfirmed"}
  ],
  "created_at": "2025-01-15T10:00:00Z",
  "updated_at": "2025-01-15T10:00:00Z"
}
```

---

## Database Models

### StoreLetzshopCredentials

Stores encrypted API credentials per store.

```python
class StoreLetzshopCredentials(Base):
    __tablename__ = "store_letzshop_credentials"

    id: int                      # Primary key
    store_id: int               # FK to stores (unique)
    api_key_encrypted: str       # Fernet encrypted API key
    api_endpoint: str            # GraphQL endpoint URL
    auto_sync_enabled: bool      # Enable auto-sync
    sync_interval_minutes: int   # Sync interval
    last_sync_at: datetime       # Last sync timestamp
    last_sync_status: str        # success, failed, partial
    last_sync_error: str         # Error message if failed
```

### LetzshopOrder

Tracks imported orders from Letzshop.

```python
class LetzshopOrder(Base):
    __tablename__ = "letzshop_orders"

    id: int                      # Primary key
    store_id: int               # FK to stores
    letzshop_order_id: str       # Letzshop order GID
    letzshop_shipment_id: str    # Letzshop shipment GID
    letzshop_order_number: str   # Human-readable order number
    local_order_id: int          # FK to orders (if imported locally)
    letzshop_state: str          # Current Letzshop state
    customer_email: str          # Customer email
    customer_name: str           # Customer name
    total_amount: str            # Order total
    currency: str                # Currency code
    raw_order_data: JSON         # Full order data from Letzshop
    inventory_units: JSON        # List of inventory units
    sync_status: str             # pending, confirmed, rejected, shipped
    tracking_number: str         # Tracking number (if set)
    tracking_carrier: str        # Carrier code
```

### LetzshopFulfillmentQueue

Queue for outbound operations with retry logic.

```python
class LetzshopFulfillmentQueue(Base):
    __tablename__ = "letzshop_fulfillment_queue"

    id: int                      # Primary key
    store_id: int               # FK to stores
    letzshop_order_id: int       # FK to letzshop_orders
    operation: str               # confirm, reject, set_tracking
    payload: JSON                # Operation data
    status: str                  # pending, processing, completed, failed
    attempts: int                # Retry count
    max_attempts: int            # Max retries (default 3)
    error_message: str           # Last error if failed
    response_data: JSON          # Response from Letzshop
```

### LetzshopSyncLog

Audit trail for all sync operations.

```python
class LetzshopSyncLog(Base):
    __tablename__ = "letzshop_sync_logs"

    id: int                      # Primary key
    store_id: int               # FK to stores
    operation_type: str          # order_import, confirm, etc.
    direction: str               # inbound, outbound
    status: str                  # success, failed, partial
    records_processed: int       # Total records
    records_succeeded: int       # Successful records
    records_failed: int          # Failed records
    error_details: JSON          # Detailed error info
    started_at: datetime         # Operation start time
    completed_at: datetime       # Operation end time
    duration_seconds: int        # Total duration
    triggered_by: str            # user_id, scheduler, webhook
```

---

## Security

### API Key Encryption

API keys are encrypted using Fernet symmetric encryption:

```python
from app.utils.encryption import encrypt_value, decrypt_value

# Encrypt before storing
encrypted_key = encrypt_value(api_key)

# Decrypt when needed
api_key = decrypt_value(encrypted_key)
```

The encryption key is derived from the application's `jwt_secret_key` using PBKDF2.

### Access Control

- **Stores**: Can only manage their own Letzshop integration
- **Admins**: Can manage any store's integration
- **API Keys**: Never returned in plain text (always masked)

---

## Troubleshooting

### Connection Failed

**Symptoms**: "Connection failed" error when testing

**Possible Causes**:
- Invalid API key
- API key expired
- Network issues
- Letzshop service unavailable

**Solutions**:
1. Verify API key in Letzshop merchant portal
2. Regenerate API key if expired
3. Check network connectivity
4. Check Letzshop status page

### Orders Not Importing

**Symptoms**: Import runs but no orders appear

**Possible Causes**:
- No unconfirmed orders in Letzshop
- API key doesn't have required permissions
- Orders already imported

**Solutions**:
1. Check Letzshop dashboard for unconfirmed orders
2. Verify API key has order read permissions
3. Check existing orders with `sync_status: pending`

### Fulfillment Failed

**Symptoms**: Confirm/reject/tracking operations fail

**Possible Causes**:
- Order already processed
- Invalid inventory unit IDs
- API permission issues

**Solutions**:
1. Check order state in Letzshop
2. Verify inventory unit IDs are correct
3. Check fulfillment queue for retry status
4. Review error message in response

### Sync Logs

Check sync logs for detailed operation history:

```bash
curl -X GET /api/v1/store/letzshop/logs \
  -H "Authorization: Bearer $TOKEN"
```

### Order Has Unresolved Exceptions

**Symptoms**: "Order has X unresolved exception(s)" error when confirming

**Cause**: Order contains items that couldn't be matched to products during import

**Solutions**:
1. Navigate to **Marketplace > Letzshop > Exceptions** tab
2. Find the pending exceptions for this order
3. Either:
   - **Resolve**: Assign the correct product from your catalog
   - **Ignore**: Mark as ignored if product will never be matched (still blocks confirmation)
4. Retry the confirmation after resolving all exceptions

### Cannot Find Shipping Label

**Symptoms**: "Download Label" returns empty or no URL

**Possible Causes**:
- Shipment number not captured during import
- Carrier label URL prefix not configured
- Unknown carrier type

**Solutions**:
1. Re-sync the order to capture shipment data
2. Check **Admin > Settings > Shipping** for carrier URL prefixes
3. Verify the order has a valid `shipping_carrier` and `shipment_number`

---

## Best Practices

### For Stores

1. **Test connection** after setting up credentials
2. **Import orders regularly** (or enable auto-sync)
3. **Confirm orders promptly** to avoid delays
4. **Set tracking** as soon as shipment is dispatched
5. **Monitor sync logs** for any failures

### For Admins

1. **Review store status** regularly via admin dashboard
2. **Assist stores** with connection issues
3. **Monitor sync logs** for platform-wide issues
4. **Set up alerts** for failed syncs (optional)

---

## Related Documentation

- [Order Item Exception System](../orders/exceptions.md)
- [Marketplace Integration (CSV Import)](integration-guide.md)
- [Store RBAC](../tenancy/rbac.md)
- [Admin Integration Guide](../../backend/admin-integration-guide.md)
- [Exception Handling](../../development/exception-handling.md)

---

## Version History

- **v1.2** (2025-12-20): Shipping & Tracking enhancements
  - Added `shipment_number`, `shipping_carrier`, `tracking_url` fields to orders
  - Carrier detection from Letzshop shipment data (Greco, Colissimo, XpressLogistics)
  - Mark as Shipped feature (local only, does not sync to Letzshop)
  - Shipping label URL generation using configurable carrier prefixes
  - Admin settings for carrier label URL prefixes

- **v1.1** (2025-12-20): Product Exception System
  - Graceful order import when products not found by GTIN
  - Placeholder product per store for unmatched items
  - Exception tracking with pending/resolved/ignored statuses
  - Confirmation blocking until exceptions resolved
  - Auto-matching when products are imported
  - Exceptions tab in admin Letzshop management page
  - Bulk resolution by GTIN

- **v1.0** (2025-12-13): Initial Letzshop order integration
  - GraphQL client for order import
  - Encrypted credential storage
  - Fulfillment operations (confirm, reject, tracking)
  - Admin and store API endpoints
  - Sync logging and queue management
