Files
orion/app/modules/marketplace/docs/order-integration.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:37 +01:00

26 KiB

Letzshop Order Integration Guide

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

Table of Contents


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

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

Response:

{
  "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:

# 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:

{
  "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

# 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:

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

Response:

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

For more details, see Order Item Exception System.


Fulfillment Operations

Confirm Order

Confirm that you can fulfill the order:

# 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:

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:

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):

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:

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

Response:

{
  "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

{
  "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

{
  "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.

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.

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.

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.

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:

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:

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)


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