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>
This commit is contained in:
2026-03-08 23:38:37 +01:00
parent 2287f4597d
commit f141cc4e6a
140 changed files with 19921 additions and 17723 deletions

View File

@@ -0,0 +1,261 @@
# Letzshop Admin Management Guide
Complete guide for managing Letzshop integration from the Admin Portal at `/admin/marketplace/letzshop`.
## Table of Contents
- [Overview](#overview)
- [Store Selection](#store-selection)
- [Products Tab](#products-tab)
- [Orders Tab](#orders-tab)
- [Exceptions Tab](#exceptions-tab)
- [Jobs Tab](#jobs-tab)
- [Settings Tab](#settings-tab)
---
## Overview
The Letzshop Management page provides a unified interface for managing Letzshop marketplace integration for all stores. Key features:
- **Multi-Store Support**: Select any store to manage their Letzshop integration
- **Product Management**: View, import, and export products
- **Order Processing**: View orders, confirm inventory, set tracking
- **Exception Handling**: Resolve product matching exceptions
- **Job Monitoring**: Track import, export, and sync operations
- **Configuration**: Manage CSV URLs, credentials, and sync settings
---
## Store Selection
At the top of the page, use the store autocomplete to select which store to manage:
1. Type to search for a store by name or code
2. Select from the dropdown
3. The page loads store-specific data for all tabs
4. Your selection is saved and restored on next visit
**Cross-Store View**: When no store is selected, the Orders and Exceptions tabs show data across all stores.
---
## Products Tab
The Products tab displays Letzshop marketplace products imported for the selected store.
### Product Listing
- **Search**: Filter by title, GTIN, SKU, or brand
- **Status Filter**: Show all, active only, or inactive only
- **Pagination**: Navigate through product pages
### Product Table Columns
| Column | Description |
|--------|-------------|
| Product | Image, title, and brand |
| Identifiers | GTIN and SKU codes |
| Price | Product price with currency |
| Status | Active/Inactive badge |
| Actions | View product details |
### Import Products
Click the **Import** button to open the import modal:
1. **Import Single Language**: Select a language and enter the CSV URL
2. **Import All Languages**: Imports from all configured CSV URLs (FR, DE, EN)
Import settings (batch size) are configured in the Settings tab.
### Export Products
Click the **Export** button to export products to the Letzshop pickup folder:
- Exports all three languages (FR, DE, EN) automatically
- Files are placed in `exports/letzshop/{store_code}/`
- Filename format: `{store_code}_products_{language}.csv`
- The export is logged and appears in the Jobs tab
Export settings (include inactive products) are configured in the Settings tab.
---
## Orders Tab
The Orders tab displays orders from Letzshop for the selected store (or all stores if none selected).
### Order Listing
- **Search**: Filter by order number, customer name, or email
- **Status Filter**: All, Pending, Confirmed, Shipped, Declined
- **Date Range**: Filter by order date
### Order Actions
| Action | Description |
|--------|-------------|
| View | Open order details modal |
| Confirm | Confirm all items in order |
| Decline | Decline all items in order |
| Set Tracking | Add tracking number and carrier |
### Order Details Modal
Shows complete order information including:
- Order number and date
- Customer name and email
- Shipping address
- Order items with confirmation status
- Tracking information (if set)
---
## Exceptions Tab
The Exceptions tab shows product matching exceptions that need resolution. See the [Order Item Exceptions documentation](../orders/exceptions.md) for details.
### Exception Types
When an order is imported and a product cannot be matched by GTIN:
1. The order is imported with a placeholder product
2. An exception is created for resolution
3. The order cannot be confirmed until exceptions are resolved
### Resolution Actions
| Action | Description |
|--------|-------------|
| Resolve | Assign the correct product to the order item |
| Bulk Resolve | Resolve all exceptions for the same GTIN |
| Ignore | Mark as ignored (still blocks confirmation) |
---
## Jobs Tab
The Jobs tab provides a unified view of all Letzshop-related operations for the selected store.
### Job Types
| Type | Icon | Color | Description |
|------|------|-------|-------------|
| Product Import | Cloud Download | Purple | Importing products from Letzshop CSV |
| Product Export | Cloud Upload | Blue | Exporting products to pickup folder |
| Historical Import | Clock | Orange | Importing historical orders |
| Order Sync | Refresh | Indigo | Syncing orders from Letzshop API |
### Job Information
Each job displays:
- **ID**: Unique job identifier
- **Type**: Import, Export, Historical Import, or Order Sync
- **Status**: Pending, Processing, Completed, Failed, or Partial
- **Records**: Success count / Total processed (failed count if any)
- **Started**: When the job began
- **Duration**: How long the job took
#### Records Column Meaning
| Job Type | Records Shows |
|----------|---------------|
| Product Import | Products imported / Total products |
| Product Export | Files exported / Total files (3 languages) |
| Historical Import | Orders imported / Total orders |
| Order Sync | Orders synced / Total orders |
### Filtering
- **Type Filter**: Show specific job types
- **Status Filter**: Show jobs with specific status
### Job Actions
| Action | Description |
|--------|-------------|
| View Errors | Show error details (for failed jobs) |
| View Details | Show complete job information |
---
## Settings Tab
The Settings tab manages Letzshop integration configuration for the selected store.
### CSV Feed URLs
Configure the URLs for Letzshop product CSV feeds:
- **French (FR)**: URL for French product data
- **German (DE)**: URL for German product data
- **English (EN)**: URL for English product data
### Import Settings
- **Batch Size**: Number of products to process per batch (100-5000)
### Export Settings
- **Include Inactive**: Whether to include inactive products in exports
### API Credentials
Configure Letzshop API access:
- **API Key**: Your Letzshop API key (encrypted at rest)
- **Test Connection**: Verify API connectivity
### Sync Settings
- **Auto-Sync Enabled**: Enable automatic order synchronization
- **Sync Interval**: How often to sync orders (in minutes)
---
## API Endpoints
### Products
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/products` | GET | List marketplace products with filters |
| `/admin/products/stats` | GET | Get product statistics |
| `/admin/letzshop/stores/{id}/export` | GET | Download CSV export |
| `/admin/letzshop/stores/{id}/export` | POST | Export to pickup folder |
### Jobs
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/letzshop/stores/{id}/jobs` | GET | List jobs for store |
| `/admin/marketplace-import-jobs` | POST | Create import job |
### Orders
See [Letzshop Order Integration](order-integration.md) for complete order API documentation.
---
## Best Practices
### Product Management
1. **Regular Imports**: Schedule regular imports to keep product data current
2. **Export Before Sync**: Export products before Letzshop's pickup schedule
3. **Monitor Jobs**: Check the Jobs tab for failed imports/exports
### Order Processing
1. **Check Exceptions First**: Resolve exceptions before confirming orders
2. **Verify Tracking**: Ensure tracking numbers are valid before submission
3. **Monitor Sync Status**: Check for failed order syncs in Jobs tab
### Troubleshooting
1. **Products Not Appearing**: Verify CSV URL is accessible and valid
2. **Export Failed**: Check write permissions on exports directory
3. **Orders Not Syncing**: Verify API credentials and test connection

View File

@@ -0,0 +1,322 @@
# Letzshop Marketplace Integration
## Introduction
This guide covers the Orion platform's integration with the Letzshop marketplace, including:
- **Product Export**: Generate Letzshop-compatible CSV files from your product catalog
- **Order Import**: Fetch and manage orders from Letzshop
- **Fulfillment Sync**: Confirm/reject orders, set tracking numbers
- **GraphQL API Reference**: Direct API access for advanced use cases
---
## Product Export
### Overview
The Orion platform can export your products to Letzshop-compatible CSV format (Google Shopping feed format). This allows you to:
- Upload your product catalog to Letzshop marketplace
- Generate feeds in multiple languages (English, French, German)
- Include all required Letzshop fields automatically
### API Endpoints
#### Store Export
```http
GET /api/v1/store/letzshop/export?language=fr
Authorization: Bearer <store_token>
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `language` | string | `en` | Language for title/description (`en`, `fr`, `de`) |
| `include_inactive` | bool | `false` | Include inactive products |
**Response:** CSV file download (`store_code_letzshop_export.csv`)
#### Admin Export
```http
GET /api/v1/admin/letzshop/export?store_id=1&language=fr
Authorization: Bearer <admin_token>
```
**Additional Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `store_id` | int | Required. Store ID to export |
### CSV Format
The export generates a tab-separated CSV file with these columns:
| Column | Description | Example |
|--------|-------------|---------|
| `id` | Product SKU | `PROD-001` |
| `title` | Product title (localized) | `Wireless Headphones` |
| `description` | Product description (localized) | `High-quality...` |
| `link` | Product URL | `https://shop.example.com/product/123` |
| `image_link` | Main product image | `https://cdn.example.com/img.jpg` |
| `additional_image_link` | Additional images (comma-separated) | `img2.jpg,img3.jpg` |
| `availability` | Stock status | `in stock` / `out of stock` |
| `price` | Regular price with currency | `49.99 EUR` |
| `sale_price` | Sale price with currency | `39.99 EUR` |
| `brand` | Brand name | `TechBrand` |
| `gtin` | Global Trade Item Number | `0012345678901` |
| `mpn` | Manufacturer Part Number | `TB-WH-001` |
| `google_product_category` | Google category ID | `Electronics > Audio` |
| `condition` | Product condition | `new` / `used` / `refurbished` |
| `atalanda:tax_rate` | Luxembourg VAT rate | `17` |
### Multi-Language Support
Products are exported with localized content based on the `language` parameter:
```bash
# French export
curl -H "Authorization: Bearer $TOKEN" \
"https://api.example.com/api/v1/store/letzshop/export?language=fr"
# German export
curl -H "Authorization: Bearer $TOKEN" \
"https://api.example.com/api/v1/store/letzshop/export?language=de"
# English export (default)
curl -H "Authorization: Bearer $TOKEN" \
"https://api.example.com/api/v1/store/letzshop/export?language=en"
```
If a translation is not available for the requested language, the system falls back to English, then to any available translation.
### Using the Export
1. **Navigate to Letzshop** in your store dashboard
2. **Click the Export tab**
3. **Select your language** (French, German, or English)
4. **Click "Download CSV"**
5. **Upload to Letzshop** via their merchant portal
---
## Order Integration
For details on order import and fulfillment, see [Letzshop Order Integration](order-integration.md).
---
## Letzshop GraphQL API Reference
The following sections document the Letzshop GraphQL API for direct integration.
---
## GraphQL API
Utilizing this API, you can retrieve and modify data on products, stores, and shipments. Letzshop uses GraphQL, allowing for precise queries.
**Endpoint**:
http://letzshop.lu/graphql
Replace YOUR_API_ACCESS_KEY with your actual key or remove the Authorization header for public data.
## Authentication
Some data is public (e.g., store description and product prices).
For protected operations (e.g., orders or vouchers), an API key is required.
Request one via your account manager or email: support@letzshop.lu
Include the key:
Authorization: Bearer YOUR_API_ACCESS_KEY
---
## Playground
- Access the interactive GraphQL Playground via the Letzshop website.
- It provides live docs, auto-complete (CTRL + Space), and run-time testing.
- **Caution**: You're working on production—mutations like confirming orders are real. [1](https://letzshop.lu/en/dev)
---
## Deprecations
The following GraphQL fields will be removed soon:
| Type | Field | Replacement |
|---------|---------------------|----------------------------------|
| Event | latitude, longitude | `#lat`, `#lng` |
| Greco | weight | `packages.weight` |
| Product | ageRestriction | `_age_restriction` (int) |
| Taxon | identifier | `slug` |
| User | billAddress, shipAddress | on `orders` instead |
| Store | facebookLink, instagramLink, twitterLink, youtubeLink | `social_media_links` |
| Store | owner | `representative` |
| Store | permalink | `slug` | [1](https://letzshop.lu/en/dev)
---
## Order Management via API
Using the API, you can:
- Fetch unconfirmed orders
- Confirm or reject them
- Set tracking numbers
- Handle returns
All of this requires at least "shop manager" API key access. Multi-store management is supported if rights allow. [1](https://letzshop.lu/en/dev)
### 1. Retrieve Unconfirmed Shipments
**Query:**
```graphql
query {
shipments(state: unconfirmed) {
nodes {
id
inventoryUnits {
id
state
}
}
}
}
``` [1](https://letzshop.lu/en/dev)
---
### 2. Confirm/Reject Inventory Units
Use inventoryUnit IDs to confirm or reject:
```graphql
mutation {
confirmInventoryUnits(input: {
inventoryUnits: [
{ inventoryUnitId: "ID1", isAvailable: true },
{ inventoryUnitId: "ID2", isAvailable: false },
{ inventoryUnitId: "ID3", isAvailable: false }
]
}) {
inventoryUnits {
id
state
}
errors {
id
code
message
}
}
}
``` [1](https://letzshop.lu/en/dev)
---
### 3. Handle Customer Returns
Use only after receiving returned items:
```graphql
mutation {
returnInventoryUnits(input: {
inventoryUnits: [
{ inventoryUnitId: "ID1" },
{ inventoryUnitId: "ID2" }
]
}) {
inventoryUnits {
id
state
}
errors {
id
code
}
}
}
``` [1](https://letzshop.lu/en/dev)
---
### 4. Set Shipment Tracking Number
Include shipping provider and tracking code:
```graphql
mutation {
setShipmentTracking(input: {
shipmentId: "SHIPMENT_ID",
code: "TRACK123",
provider: THE_SHIPPING_PROVIDER
}) {
shipment {
tracking {
code
provider
}
}
errors {
code
message
}
}
}
``` [1](https://letzshop.lu/en/dev)
---
## Event System
Subscribe by contacting support@letzshop.lu. Events are delivered via an SNS-like message structure:
```json
{
"Type": "Notification",
"MessageId": "XXX",
"TopicArn": "arn:aws:sns:eu-central-1:XXX:events",
"Message": "{\"id\":\"XXX\",\"type\":\"XXX\",\"payload\":{...}}",
"Timestamp": "2019-01-01T00:00:00.000Z",
"SignatureVersion": "1",
"Signature": "XXX",
"SigningCertURL": "...",
"UnsubscribeURL": "..."
}
``` [1](https://letzshop.lu/en/dev)
### Message Payload
Each event includes:
- `id`
- `type` (e.g., ShipmentConfirmed, UserCreated…)
- `payload` (object-specific data) [1](https://letzshop.lu/en/dev)
---
## Event Types & Payload Structure
A variety of event types are supported. Common ones include:
- `ShipmentConfirmed`, `ShipmentRefundCreated`
- `UserAnonymized`, `UserCreated`, `UserDestroyed`, `UserUpdated`
- `VariantWithPriceCrossedCreated`, `...Updated`
- `StoreCategoryCreated`, `Destroyed`, `Updated`
- `StoreCreated`, `Destroyed`, `Updated`
Exact payload structure varies per event type. [1](https://letzshop.lu/en/dev)
---
## Conclusion
This Markdown file captures all information from the Letzshop development page, formatted for use in your documentation or GitHub.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
# Marketplace Data Model
Entity relationships and database schema for the marketplace module.
## Entity Relationship Diagram
```
┌──────────────────────────┐
│ Store │ (from tenancy module)
└──────┬───────────────────┘
│ 1
┌────┴──────────────────────────────────────────────┐
│ │ │ │
▼ 1 ▼ * ▼ 0..1 ▼ *
┌──────────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Marketplace │ │ Marketplace │ │ StoreLetzshop │ │ Letzshop │
│ ImportJob │ │ Product │ │ Credentials │ │ Order │
│ │ │ │ │ │ │ │
│ source_url │ │ gtin, mpn │ │ api_key_enc │ │ letzshop_id │
│ language │ │ sku, brand │ │ auto_sync │ │ sync_status │
│ status │ │ price_cents │ │ last_sync_at │ │ customer │
│ imported_cnt │ │ is_digital │ │ default_ │ │ total_amount │
│ error_count │ │ marketplace │ │ carrier │ │ tracking │
└──────┬───────┘ └──────┬───────┘ └───────────────┘ └──────┬───────┘
│ │ │
▼ * ▼ * │
┌──────────────┐ ┌──────────────────┐ │
│ Marketplace │ │ Marketplace │ │
│ ImportError │ │ Product │ │
│ │ │ Translation │ │
│ row_number │ │ │ │
│ error_type │ │ language │ │
│ error_msg │ │ title │ │
│ row_data │ │ description │ │
└──────────────┘ │ meta_title │ │
└──────────────────┘ │
┌───────────────────────────────────────────────────────────┘
▼ * ▼ * ▼ *
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Letzshop │ │ Letzshop │ │ Letzshop │
│ FulfillmentQueue │ │ SyncLog │ │ Historical │
│ │ │ │ │ ImportJob │
│ operation │ │ operation_type │ │ │
│ payload │ │ direction │ │ current_phase │
│ status │ │ status │ │ current_page │
│ attempts │ │ records_* │ │ orders_processed │
└──────────────────┘ └──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ Letzshop │ │ Store │
│ StoreCache │ │ Onboarding │
│ │ │ │
│ letzshop_id │ │ status │
│ name, slug │ │ current_step │
│ claimed_by_ │ │ step_*_completed │
│ store_id │ │ skipped_by_admin │
└──────────────────┘ └──────────────────┘
```
## Models
### Product Import Pipeline
#### MarketplaceProduct
Canonical product data imported from marketplace sources. Serves as a staging area — stores publish selected products to their own catalog.
| Field | Type | Description |
|-------|------|-------------|
| `marketplace_product_id` | String (unique) | Unique ID from marketplace |
| `marketplace` | String | Source marketplace (e.g., "Letzshop") |
| `gtin` | String | EAN/UPC barcode |
| `mpn` | String | Manufacturer Part Number |
| `sku` | String | Merchant's internal SKU |
| `store_name` | String | Store name from marketplace |
| `source_url` | String | CSV source URL |
| `product_type_enum` | Enum | physical, digital, service, subscription |
| `is_digital` | Boolean | Whether product is digital |
| `digital_delivery_method` | Enum | download, email, in_app, streaming, license_key |
| `brand` | String | Brand name |
| `google_product_category` | String | Google Shopping category |
| `condition` | String | new, used, refurbished |
| `price_cents` | Integer | Price in cents |
| `sale_price_cents` | Integer | Sale price in cents |
| `currency` | String | Currency code (default EUR) |
| `tax_rate_percent` | Decimal | VAT rate (default 17%) |
| `image_link` | String | Main product image |
| `additional_images` | JSON | Array of additional image URLs |
| `attributes` | JSON | Flexible attributes |
| `weight_grams` | Integer | Product weight |
| `is_active` | Boolean | Whether product is active |
**Relationships:** `translations` (MarketplaceProductTranslation), `store_products` (Product)
#### MarketplaceProductTranslation
Localized content for marketplace products.
| Field | Type | Description |
|-------|------|-------------|
| `marketplace_product_id` | FK | Parent product |
| `language` | String | Language code (en, fr, de, lb) |
| `title` | String | Product title |
| `description` | Text | Full description |
| `short_description` | Text | Short description |
| `meta_title` | String | SEO title |
| `meta_description` | String | SEO description |
| `url_slug` | String | URL-friendly slug |
| `source_import_id` | Integer | Import job that created this |
| `source_file` | String | Source CSV file |
**Constraint:** Unique (`marketplace_product_id`, `language`)
#### MarketplaceImportJob
Tracks CSV product import jobs with progress and metrics.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK | Target store |
| `user_id` | FK | Who triggered the import |
| `marketplace` | String | Source marketplace (default "Letzshop") |
| `source_url` | String | CSV feed URL |
| `language` | String | Import language for translations |
| `status` | String | pending, processing, completed, failed, completed_with_errors |
| `imported_count` | Integer | New products imported |
| `updated_count` | Integer | Existing products updated |
| `error_count` | Integer | Failed rows |
| `total_processed` | Integer | Total rows processed |
| `error_message` | Text | Error message if failed |
| `celery_task_id` | String | Background task ID |
| `started_at` | DateTime | Processing start time |
| `completed_at` | DateTime | Processing end time |
**Relationships:** `store`, `user`, `errors` (MarketplaceImportError, cascade delete)
#### MarketplaceImportError
Detailed error records for individual import failures.
| Field | Type | Description |
|-------|------|-------------|
| `import_job_id` | FK | Parent import job (cascade delete) |
| `row_number` | Integer | Row in source CSV |
| `identifier` | String | Product identifier (ID, GTIN, etc.) |
| `error_type` | String | missing_title, missing_id, parse_error, validation_error |
| `error_message` | String | Human-readable error |
| `row_data` | JSON | Snapshot of key fields from failing row |
### Letzshop Order Integration
#### StoreLetzshopCredentials
Encrypted API credentials and sync settings per store.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK (unique) | One credential set per store |
| `api_key_encrypted` | String | Fernet-encrypted API key |
| `api_endpoint` | String | GraphQL endpoint URL |
| `auto_sync_enabled` | Boolean | Enable automatic order sync |
| `sync_interval_minutes` | Integer | Auto-sync interval (5-1440) |
| `test_mode_enabled` | Boolean | Test mode flag |
| `default_carrier` | String | Default shipping carrier |
| `carrier_*_label_url` | String | Per-carrier label URL prefixes |
| `last_sync_at` | DateTime | Last sync timestamp |
| `last_sync_status` | String | success, failed, partial |
| `last_sync_error` | Text | Error message if failed |
#### LetzshopOrder
Tracks orders imported from Letzshop marketplace.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK | Store that owns this order |
| `letzshop_order_id` | String | Letzshop order GID |
| `letzshop_shipment_id` | String | Letzshop shipment GID |
| `letzshop_order_number` | String | Human-readable order number |
| `external_order_number` | String | Customer-facing reference |
| `shipment_number` | String | Carrier shipment number |
| `local_order_id` | FK | Linked local order (if any) |
| `letzshop_state` | String | Current Letzshop state |
| `customer_email` | String | Customer email |
| `customer_name` | String | Customer name |
| `customer_locale` | String | Customer language for invoicing |
| `total_amount` | String | Order total |
| `currency` | String | Currency code |
| `raw_order_data` | JSON | Full order data from Letzshop |
| `inventory_units` | JSON | List of inventory units |
| `sync_status` | String | pending, confirmed, rejected, shipped |
| `shipping_carrier` | String | Carrier code |
| `tracking_number` | String | Tracking number |
| `tracking_url` | String | Full tracking URL |
| `shipping_country_iso` | String | Shipping country |
| `billing_country_iso` | String | Billing country |
| `order_date` | DateTime | Original order date from Letzshop |
#### LetzshopFulfillmentQueue
Outbound operation queue with retry logic for Letzshop fulfillment.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK | Store |
| `order_id` | FK | Linked order |
| `operation` | String | confirm_item, decline_item, set_tracking |
| `payload` | JSON | Operation data |
| `status` | String | pending, processing, completed, failed |
| `attempts` | Integer | Retry count |
| `max_attempts` | Integer | Max retries (default 3) |
| `error_message` | Text | Last error |
| `response_data` | JSON | Response from Letzshop |
#### LetzshopSyncLog
Audit trail for all Letzshop sync operations.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK | Store |
| `operation_type` | String | order_import, confirm_inventory, set_tracking, etc. |
| `direction` | String | inbound, outbound |
| `status` | String | success, failed, partial |
| `records_processed` | Integer | Total records |
| `records_succeeded` | Integer | Successful records |
| `records_failed` | Integer | Failed records |
| `error_details` | JSON | Detailed error info |
| `started_at` | DateTime | Operation start |
| `completed_at` | DateTime | Operation end |
| `duration_seconds` | Integer | Total duration |
| `triggered_by` | String | user_id, scheduler, webhook |
#### LetzshopHistoricalImportJob
Tracks progress of historical order imports for real-time progress polling.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK | Store |
| `user_id` | FK | Who triggered |
| `status` | String | pending, fetching, processing, completed, failed |
| `current_phase` | String | confirmed, declined |
| `current_page` | Integer | Current pagination page |
| `total_pages` | Integer | Total pages |
| `shipments_fetched` | Integer | Shipments fetched so far |
| `orders_processed` | Integer | Orders processed |
| `orders_imported` | Integer | New orders imported |
| `orders_updated` | Integer | Updated existing orders |
| `orders_skipped` | Integer | Duplicate orders skipped |
| `products_matched` | Integer | Products matched by EAN |
| `products_not_found` | Integer | Products not found |
| `confirmed_stats` | JSON | Stats for confirmed phase |
| `declined_stats` | JSON | Stats for declined phase |
| `celery_task_id` | String | Background task ID |
### Supporting Models
#### LetzshopStoreCache
Cache of Letzshop marketplace store directory for browsing and claiming during signup.
| Field | Type | Description |
|-------|------|-------------|
| `letzshop_id` | String (unique) | Letzshop store identifier |
| `slug` | String | URL slug |
| `name` | String | Store name |
| `merchant_name` | String | Merchant name |
| `is_active` | Boolean | Active on Letzshop |
| `description_en/fr/de` | Text | Localized descriptions |
| `email`, `phone`, `website` | String | Contact info |
| `street`, `zipcode`, `city`, `country_iso` | String | Address |
| `latitude`, `longitude` | Float | Geo coordinates |
| `categories` | JSON | Category array |
| `social_media_links` | JSON | Social links |
| `claimed_by_store_id` | FK | Store that claimed this listing |
| `claimed_at` | DateTime | When claimed |
| `last_synced_at` | DateTime | Last directory sync |
#### StoreOnboarding
Tracks completion of mandatory onboarding steps for new stores.
| Field | Type | Description |
|-------|------|-------------|
| `store_id` | FK (unique) | One record per store |
| `status` | String | not_started, in_progress, completed, skipped |
| `current_step` | Integer | Current onboarding step |
| Step 1 | — | Merchant profile completion |
| Step 2 | — | Letzshop API key setup + verification |
| Step 3 | — | Product import (CSV URL set) |
| Step 4 | — | Order sync (first sync job) |
| `skipped_by_admin` | Boolean | Admin override |
| `skipped_reason` | Text | Reason for skip |

View File

@@ -0,0 +1,601 @@
# Letzshop Order Import - Improvement Plan
## Current Status (2025-12-17)
### Schema Discovery Complete ✅
After running GraphQL introspection queries, we have identified all available fields.
### Available Fields Summary
| Data | GraphQL Path | Notes |
|------|-------------|-------|
| **EAN/GTIN** | `variant.tradeId.number` | The product barcode |
| **Trade ID Type** | `variant.tradeId.parser` | Format: gtin13, gtin14, gtin12, gtin8, isbn13, isbn10 |
| **Brand Name** | `product._brand { ... on Brand { name } }` | Union type requires fragment |
| **MPN** | `variant.mpn` | Manufacturer Part Number |
| **SKU** | `variant.sku` | Merchant's internal SKU |
| **Product Name** | `variant.product.name { en, fr, de }` | Translated names |
| **Price** | `variant.price` | Unit price |
| **Quantity** | Count of `inventoryUnits` | Each unit = 1 item |
| **Customer Language** | `order.locale` | Language for invoice (en, fr, de) |
| **Customer Country** | `order.shipAddress.country` | Country object |
### Key Findings
1. **EAN lives in `tradeId`** - Not a direct field on Variant, but nested in `tradeId.number`
2. **TradeIdParser enum values**: `gtin14`, `gtin13` (EAN-13), `gtin12` (UPC), `gtin8`, `isbn13`, `isbn10`
3. **Brand is a Union** - Must use `... on Brand { name }` fragment, also handles `BrandUnknown`
4. **No quantity field** - Each InventoryUnit represents 1 item; count units to get quantity
## Updated GraphQL Query
```graphql
query {
shipments(state: unconfirmed) {
nodes {
id
number
state
order {
id
number
email
total
completedAt
locale
shipAddress {
firstName
lastName
merchant
streetName
streetNumber
city
zipCode
phone
country {
name
iso
}
}
billAddress {
firstName
lastName
merchant
streetName
streetNumber
city
zipCode
phone
country {
name
iso
}
}
}
inventoryUnits {
id
state
variant {
id
sku
mpn
price
tradeId {
number
parser
}
product {
name { en fr de }
_brand {
... on Brand { name }
}
}
}
}
tracking {
code
provider
}
}
}
}
```
## Implementation Steps
### Step 1: Update GraphQL Queries ✅ DONE
Update in `app/services/letzshop/client_service.py`:
- `QUERY_SHIPMENTS_UNCONFIRMED`
- `QUERY_SHIPMENTS_CONFIRMED`
- `QUERY_SHIPMENT_BY_ID`
- `QUERY_SHIPMENTS_PAGINATED_TEMPLATE` ✅ (new - for historical import)
### Step 2: Update Order Service ✅ DONE
Updated `create_order()` and `update_order_from_shipment()` in `app/services/letzshop/order_service.py`:
- Extract `tradeId.number` as EAN ✅
- Store MPN if available ✅
- Store `locale` for invoice language ✅
- Store shipping/billing country ISO codes ✅
- Enrich inventory_units with EAN, MPN, SKU, product_name ✅
**Database changes:**
- Added `customer_locale` column to `LetzshopOrder`
- Added `shipping_country_iso` column to `LetzshopOrder`
- Added `billing_country_iso` column to `LetzshopOrder`
- Migration: `a9a86cef6cca_add_letzshop_order_locale_and_country_.py`
### Step 3: Match Products by EAN ✅ DONE (Basic)
When importing orders:
- Use `tradeId.number` (EAN) to find matching local product ✅
- `_match_eans_to_products()` function added ✅
- Returns match statistics (products_matched, products_not_found) ✅
**TODO for later:**
- ⬜ Decrease stock for matched product (needs careful implementation)
- ⬜ Show match status in order detail view
### Step 4: Update Frontend ✅ DONE (Historical Import)
- Added "Import History" button to Orders tab ✅
- Added historical import result display ✅
- Added `importHistoricalOrders()` JavaScript function ✅
**TODO for later:**
- ⬜ Show product details in individual order view (EAN, MPN, SKU, match status)
### Step 5: Historical Import Feature ✅ DONE
Import all confirmed orders for:
- Sales analytics (how many products sold)
- Customer records
- Historical data
**Implementation:**
- Pagination support with `get_all_shipments_paginated()`
- Deduplication by `letzshop_order_id`
- EAN matching during import ✅
- Progress callback for large imports ✅
**Endpoints Added:**
- `POST /api/v1/admin/letzshop/stores/{id}/import-history` - Import historical orders
- `GET /api/v1/admin/letzshop/stores/{id}/import-summary` - Get import statistics
**Frontend:**
- "Import History" button in Orders tab
- Result display showing imported/updated/skipped counts
**Tests:**
- Unit tests in `tests/unit/services/test_letzshop_service.py`
- Manual test script `scripts/test_historical_import.py`
## Test Results (2025-12-17)
### Query Test: PASSED ✅
```
Example shipment:
Shipment #: H43748338602
Order #: R702236251
Customer: miriana.leal@letzshop.lu
Locale: fr <<<< LANGUAGE
Total: 32.88 EUR
Ship to: Miriana Leal Ferreira
City: 1468 Luxembourg
Country: LU
Items (1):
- Pocket POP! Keychains: Marvel Avengers Infinity War - Iron Spider
SKU: 00889698273022
MPN: None
EAN: 00889698273022 (gtin14) <<<< BARCODE
Price: 5.88 EUR
```
### Known Issues / Letzshop API Bugs
#### Bug 1: `_brand` field causes server error
- **Error**: `NoMethodError: undefined method 'demodulize' for nil`
- **Trigger**: Querying `_brand { ... on Brand { name } }` on some products
- **Workaround**: Removed `_brand` from queries
- **Status**: To report to Letzshop
#### Bug 2: `tracking` field causes server error (ALL queries)
- **Error**: `NoMethodError: undefined method 'demodulize' for nil`
- **Trigger**: Including `tracking { code provider }` in ANY shipment query
- **Tested and FAILS on**:
- Paginated queries: `shipments(state: confirmed, first: 10) { nodes { tracking { code provider } } }`
- Non-paginated queries: `shipments(state: confirmed) { nodes { tracking { code provider } } }`
- Single shipment queries: Also fails (Letzshop doesn't support `node(id:)` interface)
- **Impact**: Cannot retrieve tracking numbers and carrier info at all
- **Workaround**: None - tracking info is currently unavailable via API
- **Status**: **CRITICAL - Must report to Letzshop**
- **Date discovered**: 2025-12-17
**Note**: Letzshop automatically creates tracking when orders are confirmed. The carrier picks up parcels. But we cannot retrieve this info due to the API bug.
#### Bug 3: Product table missing `gtin` field ✅ FIXED
- **Error**: `type object 'Product' has no attribute 'gtin'`
- **Cause**: `gtin` field only existed on `MarketplaceProduct` (staging table), not on `Product` (operational table)
- **Date discovered**: 2025-12-17
- **Date fixed**: 2025-12-18
- **Fix applied**:
1. Migration `cb88bc9b5f86_add_gtin_columns_to_product_table.py` adds:
- `gtin` (String(50)) - the barcode number
- `gtin_type` (String(20)) - the format type (gtin13, gtin14, etc.)
- Indexes: `idx_product_gtin`, `idx_product_store_gtin`
2. `models/database/product.py` updated with new columns
3. `_match_eans_to_products()` now queries `Product.gtin`
4. `get_products_by_eans()` now returns products by EAN lookup
- **Status**: COMPLETE
**GTIN Types Reference:**
| Type | Digits | Common Name | Region/Use |
|------|--------|-------------|------------|
| gtin13 | 13 | EAN-13 | Europe (most common) |
| gtin12 | 12 | UPC-A | North America |
| gtin14 | 14 | ITF-14 | Logistics/cases |
| gtin8 | 8 | EAN-8 | Small items |
| isbn13 | 13 | ISBN-13 | Books |
| isbn10 | 10 | ISBN-10 | Books (legacy) |
Letzshop API returns:
- `tradeId.number` → store in `gtin`
- `tradeId.parser` → store in `gtin_type`
**Letzshop Shipment States (from official docs):**
| Letzshop State | Our sync_status | Description |
|----------------|-----------------|-------------|
| `unconfirmed` | `pending` | New order, needs store confirmation |
| `confirmed` | `confirmed` | At least one product confirmed |
| `declined` | `rejected` | All products rejected |
Note: There is no "shipped" state in Letzshop. Shipping is tracked via the `tracking` field (code + provider), not as a state change.
---
## Historical Confirmed Orders Import
### Purpose
Import all historical confirmed orders from Letzshop to:
1. **Sales Analytics** - Track total products sold, revenue by product/category
2. **Customer Records** - Build customer database with order history
3. **Inventory Reconciliation** - Understand what was sold to reconcile stock
### Implementation Plan
#### 1. Add "Import Historical Orders" Feature
- New endpoint: `POST /api/v1/admin/letzshop/stores/{id}/import-history`
- Parameters:
- `state`: confirmed/shipped/delivered (default: confirmed)
- `since`: Optional date filter (import orders after this date)
- `dry_run`: Preview without saving
#### 2. Pagination Support
Letzshop likely returns paginated results. Need to handle:
```graphql
query {
shipments(state: confirmed, first: 50, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes { ... }
}
}
```
#### 3. Deduplication
- Check if order already exists by `letzshop_order_id` before inserting
- Update existing orders if data changed
#### 4. EAN Matching & Stock Adjustment
When importing historical orders:
- Match `tradeId.number` (EAN) to local products
- Calculate total quantity sold per product
- Option to adjust inventory based on historical sales
#### 5. Customer Database
Extract and store customer data:
- Email (unique identifier)
- Name (from shipping address)
- Preferred language (from `order.locale`)
- Order count, total spent
#### 6. UI: Historical Import Page
Admin interface to:
- Trigger historical import
- View import progress
- See summary: X orders imported, Y customers added, Z products matched
### Data Flow
```
Letzshop API (confirmed shipments)
┌───────────────────────┐
│ Import Service │
│ - Fetch all pages │
│ - Deduplicate │
│ - Match EAN to SKU │
└───────────────────────┘
┌───────────────────────┐
│ Database │
│ - letzshop_orders │
│ - customers │
│ - inventory updates │
└───────────────────────┘
┌───────────────────────┐
│ Analytics Dashboard │
│ - Sales by product │
│ - Revenue over time │
│ - Customer insights │
└───────────────────────┘
```
---
## Schema Reference
### Variant Fields
```
baseAmount: String
baseAmountProduct: String
baseUnit: String
countOnHand: Int
id: ID!
images: [Image]!
inPresale: Boolean!
isMaster: Boolean!
mpn: String
price: Float!
priceCrossed: Float
pricePerUnit: Float
product: Product!
properties: [Property]!
releaseAt: Iso8601Time
sku: String
tradeId: TradeId
uniqueId: String
url: String!
```
### TradeId Fields
```
isRestricted: Boolean
number: String! # <-- THE EAN/GTIN
parser: TradeIdParser! # <-- Format identifier
```
### TradeIdParser Enum
```
gtin14 - GTIN-14 (14 digits)
gtin13 - GTIN-13 / EAN-13 (13 digits, most common in Europe)
gtin12 - GTIN-12 / UPC-A (12 digits, common in North America)
gtin8 - GTIN-8 / EAN-8 (8 digits)
isbn13 - ISBN-13 (books)
isbn10 - ISBN-10 (books)
```
### Brand (via BrandUnion)
```
BrandUnion = Brand | BrandUnknown
Brand fields:
id: ID!
name: String!
identifier: String!
descriptor: String
logo: Attachment
url: String!
```
### InventoryUnit Fields
```
id: ID!
price: Float!
state: String!
taxRate: Float!
uniqueId: String
variant: Variant
```
## Reference: Letzshop Frontend Shows
From the Letzshop merchant interface:
- Order number: R532332163
- Shipment number: H74683403433
- Product: "Pop! Rocks: DJ Khaled - DJ Khaled #237"
- Brand: Funko
- Internal merchant number: MH-FU-56757
- Price: 16,95 €
- Quantity: 1
- Shipping: 2,99 €
- Total: 19,94 €
---
## Completed (2025-12-18)
### Order Stats Fix ✅
- **Issue**: Order status cards (Pending, Confirmed, etc.) were showing incorrect counts
- **Cause**: Stats were calculated client-side from only the visible page of orders
- **Fix**:
1. Added `get_order_stats()` method to `LetzshopOrderService`
2. Added `LetzshopOrderStats` schema with pending/confirmed/rejected/shipped counts
3. API now returns `stats` field with counts for ALL orders
4. JavaScript uses server-side stats instead of client-side calculation
- **Status**: COMPLETE
### Tracking Investigation ✅
- **Issue**: Letzshop API bug prevents querying tracking field
- **Added**: `--tracking` option to `letzshop_introspect.py` to investigate workarounds
- **Findings**: Bug is on Letzshop's side, no client-side workaround possible
- **Recommendation**: Store tracking info locally after setting via API
### Item-Level Confirmation ✅
- **Issue**: Orders were being confirmed/declined at order level, but Letzshop requires item-level actions
- **Letzshop Model**:
- Each `inventoryUnit` must be confirmed/declined individually via `confirmInventoryUnits` mutation
- `isAvailable: true` = confirmed, `isAvailable: false` = declined
- Inventory unit states: `unconfirmed``confirmed_available` / `confirmed_unavailable` / `returned`
- Shipment states derived from items: `unconfirmed` / `confirmed` / `declined`
- Partial confirmation allowed (some items confirmed, some declined)
- **Fix**:
1. Order detail modal now shows each item with product details (name, EAN, SKU, MPN, price)
2. Per-item confirm/decline buttons for pending items
3. Admin API endpoints for single-item and bulk operations:
- `POST /stores/{id}/orders/{id}/items/{id}/confirm`
- `POST /stores/{id}/orders/{id}/items/{id}/decline`
4. Order status automatically updates based on item states:
- All items declined → order status = "declined"
- Any item confirmed → order status = "confirmed"
### Terminology Update ✅
- Changed "Rejected" to "Declined" throughout UI to match Letzshop terminology
- Internal `sync_status` value remains "rejected" for backwards compatibility
- Filter dropdown, status badges, and action buttons now use "Declined"
- Added "Declined" stats card to orders dashboard
### Historical Import: Multiple Phases ✅
- Historical import now fetches both `confirmed` AND `unconfirmed` (pending) shipments
- Note: "declined" is NOT a valid Letzshop shipment state - declined items are tracked at inventory unit level
- Combined stats shown in import result
---
## Completed (2025-12-19)
### Historical Import Progress Bar ✅
Real-time progress feedback for historical import using background tasks with database polling.
**Implementation:**
- Background task (`app/tasks/letzshop_tasks.py`) runs historical import asynchronously
- Progress stored in `LetzshopHistoricalImportJob` database model
- Frontend polls status endpoint every 2 seconds
- Two-phase import: confirmed orders first, then unconfirmed (pending) orders
**Backend:**
- `LetzshopHistoricalImportJob` model tracks: status, current_phase, current_page, shipments_fetched, orders_processed, confirmed_stats, declined_stats
- `POST /stores/{id}/import-history` starts background job, returns job_id immediately
- `GET /stores/{id}/import-history/{job_id}/status` returns current progress
**Frontend:**
- Progress panel shows: phase (confirmed/pending), page number, shipments fetched, orders processed
- Disabled "Import History" button during import with spinner
- Final result summary shows combined stats from both phases
**Key Discovery:**
- Letzshop API has NO "declined" shipment state
- Valid states: `awaiting_order_completion`, `unconfirmed`, `completed`, `accepted`, `confirmed`
- Declined items are tracked at inventory unit level with state `confirmed_unavailable`
### Filter for Declined Items ✅
Added ability to filter orders that have at least one declined/unavailable item.
**Backend:**
- `list_orders()` accepts `has_declined_items: bool` parameter
- Uses JSON string contains check: `inventory_units.cast(String).contains("confirmed_unavailable")`
- `get_order_stats()` returns `has_declined_items` count
**Frontend:**
- "Has Declined Items" toggle button in filters section
- Shows count badge when there are orders with declined items
- Toggles between all orders and filtered view
**API:**
- `GET /stores/{id}/orders?has_declined_items=true` - filter orders
### Order Date Display ✅
Orders now display the actual order date from Letzshop instead of the import date.
**Database:**
- Added `order_date` column to `LetzshopOrder` model
- Migration: `2362c2723a93_add_order_date_to_letzshop_orders.py`
**Backend:**
- `create_order()` extracts `completedAt` from Letzshop order data and stores as `order_date`
- `update_order_from_shipment()` populates `order_date` if not already set
- Date parsing handles ISO format with timezone (including `Z` suffix)
**Frontend:**
- Order table displays `order_date` with fallback to `created_at` for legacy orders
- Format: localized date/time string
**Note:** Existing orders imported before this change will continue showing `created_at` until re-imported via historical import.
### Search Filter ✅
Added search functionality to find orders by order number, customer name, or email.
**Backend:**
- `list_orders()` accepts `search: str` parameter
- Uses ILIKE for case-insensitive partial matching across:
- `letzshop_order_number`
- `customer_name`
- `customer_email`
**Frontend:**
- Search input field with magnifying glass icon
- Debounced input (300ms) to avoid excessive API calls
- Clear button to reset search
- Resets to page 1 when search changes
**API:**
- `GET /stores/{id}/orders?search=query` - search orders
---
## Next Steps (TODO)
### Priority 1: Stock Management
When an order is confirmed/imported:
1. Match EAN from order to local product catalog
2. Decrease stock quantity for matched products
3. Handle cases where product not found (alert/log)
**Considerations:**
- Should stock decrease happen on import or only on confirmation?
- Need rollback mechanism if order is rejected
- Handle partial matches (some items found, some not)
### Priority 2: Invoice Generation
Use `customer_locale` to generate invoices in customer's language:
- Invoice template with multi-language support
- PDF generation
### Priority 3: Analytics Dashboard
Build sales analytics based on imported orders:
- Sales by product
- Sales by time period
- Customer statistics
- Revenue breakdown
---
## Files Modified (2025-12-16 to 2025-12-19)
| File | Changes |
|------|---------|
| `app/services/letzshop/client_service.py` | Added paginated query, updated all queries with EAN/locale/country |
| `app/services/letzshop/order_service.py` | Historical import, EAN matching, order stats, has_declined_items filter, search filter, order_date extraction |
| `models/database/letzshop.py` | Added locale/country/order_date columns, `LetzshopHistoricalImportJob` model |
| `models/database/product.py` | Added `gtin` and `gtin_type` columns for EAN matching |
| `models/schema/letzshop.py` | Added `LetzshopOrderStats`, `LetzshopHistoricalImportJobResponse`, `order_date` field |
| `app/api/v1/admin/letzshop.py` | Import-history endpoints, has_declined_items filter, search filter, order_date in response |
| `app/tasks/letzshop_tasks.py` | **NEW** - Background task for historical import with progress tracking |
| `app/templates/admin/partials/letzshop-orders-tab.html` | Import History button, progress panel, declined items filter, search input, order_date display |
| `static/admin/js/marketplace-letzshop.js` | Historical import polling, progress display, declined items filter, search functionality |
| `tests/unit/services/test_letzshop_service.py` | Added tests for new functionality |
| `scripts/test_historical_import.py` | Manual test script for historical import |
| `scripts/debug_historical_import.py` | **NEW** - Debug script for shipment states and declined items |
| `scripts/letzshop_introspect.py` | GraphQL schema introspection tool, tracking workaround tests |
| `alembic/versions/a9a86cef6cca_*.py` | Migration for locale/country columns |
| `alembic/versions/cb88bc9b5f86_*.py` | Migration for gtin columns on Product table |
| `alembic/versions/*_add_historical_import_jobs.py` | **NEW** - Migration for LetzshopHistoricalImportJob table |
| `alembic/versions/2362c2723a93_*.py` | **NEW** - Migration for order_date column |

View File

@@ -0,0 +1,73 @@
# Marketplace (Letzshop)
Letzshop marketplace integration for product sync, order import, and catalog synchronization.
## Overview
| Aspect | Detail |
|--------|--------|
| Code | `marketplace` |
| Classification | Optional |
| Dependencies | `inventory`, `catalog`, `orders` |
| Status | Active |
## Features
- `letzshop_sync` — Letzshop API synchronization
- `marketplace_import` — Product and order import
- `product_sync` — Bidirectional product sync
- `order_import` — Marketplace order import
- `marketplace_analytics` — Marketplace performance metrics
## Permissions
| Permission | Description |
|------------|-------------|
| `marketplace.view_integration` | View marketplace integration |
| `marketplace.manage_integration` | Manage marketplace settings |
| `marketplace.sync_products` | Trigger product sync |
## Data Model
See [Data Model](data-model.md) for full entity relationships.
- **MarketplaceProduct** — Canonical product data from marketplace sources
- **MarketplaceProductTranslation** — Localized product content (title, description)
- **MarketplaceImportJob** — CSV import job tracking with metrics
- **MarketplaceImportError** — Detailed error records per import
- **StoreLetzshopCredentials** — Encrypted API keys and sync settings
- **LetzshopOrder** — Imported orders from Letzshop
- **LetzshopFulfillmentQueue** — Outbound operation queue with retry logic
- **LetzshopSyncLog** — Audit trail for sync operations
- **LetzshopHistoricalImportJob** — Historical order import progress
- **LetzshopStoreCache** — Marketplace store directory cache
- **StoreOnboarding** — Store onboarding step tracking
## API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `*` | `/api/v1/admin/marketplace/*` | Admin marketplace management |
| `*` | `/api/v1/admin/letzshop/*` | Letzshop-specific endpoints |
| `*` | `/api/v1/admin/marketplace-products/*` | Product mapping management |
## Scheduled Tasks
| Task | Schedule | Description |
|------|----------|-------------|
| `marketplace.sync_store_directory` | Daily 02:00 | Sync store directory from Letzshop |
## Configuration
Letzshop API credentials are configured per-store via the admin UI.
## Additional Documentation
- [Data Model](data-model.md) — Entity relationships and database schema
- [Architecture](architecture.md) — Multi-marketplace integration architecture
- [Integration Guide](integration-guide.md) — CSV import guide with store/admin interfaces
- [API Reference](api.md) — Letzshop GraphQL API reference
- [Order Integration](order-integration.md) — Bidirectional order management with Letzshop
- [Admin Guide](admin-guide.md) — Admin portal management guide
- [Import Improvements](import-improvements.md) — GraphQL field mapping and EAN matching
- [Job Queue](job-queue.md) — Job queue improvements and table harmonization

Binary file not shown.

View File

@@ -0,0 +1,716 @@
# Letzshop Jobs & Tables Improvements
Implementation plan for improving the Letzshop management page jobs display and table harmonization.
## Status: Completed
### Completed
- [x] Phase 1: Job Details Modal (commit cef80af)
- [x] Phase 2: Add store column to jobs table
- [x] Phase 3: Platform settings system (rows per page)
- [x] Phase 4: Numbered pagination for jobs table
- [x] Phase 5: Admin customer management page
---
## Overview
This plan addresses 6 improvements:
1. Job details modal with proper display
2. Tab visibility fix when filters cleared
3. Add store column to jobs table
4. Harmonize all tables with table macro
5. Platform-wide rows per page setting
6. Build admin customer page
---
## 1. Job Details Modal
### Current Issue
- "View Details" shows a browser alert instead of a proper modal
- No detailed breakdown of export results
### Requirements
- Create a proper modal for job details
- For exports: show products exported per language file
- Show store name/code
- Show full timestamps and duration
- Show error details if any
### Implementation
#### 1.1 Create Job Details Modal Template
**File:** `app/templates/admin/partials/letzshop-jobs-table.html`
Add modal after the table:
```html
<!-- Job Details Modal -->
<div
x-show="showJobDetailsModal"
x-transition
class="fixed inset-0 z-30 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showJobDetailsModal = false"
x-cloak
>
<div class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6">
<header class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Job Details</h3>
<button @click="showJobDetailsModal = false">×</button>
</header>
<div class="space-y-4">
<!-- Job Info -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="font-medium">Job ID:</span> #<span x-text="selectedJobDetails?.id"></span></div>
<div><span class="font-medium">Type:</span> <span x-text="selectedJobDetails?.type"></span></div>
<div><span class="font-medium">Status:</span> <span x-text="selectedJobDetails?.status"></span></div>
<div><span class="font-medium">Store:</span> <span x-text="selectedJobDetails?.store_name || selectedStore?.name"></span></div>
</div>
<!-- Timestamps -->
<div class="text-sm">
<p><span class="font-medium">Started:</span> <span x-text="formatDate(selectedJobDetails?.started_at)"></span></p>
<p><span class="font-medium">Completed:</span> <span x-text="formatDate(selectedJobDetails?.completed_at)"></span></p>
<p><span class="font-medium">Duration:</span> <span x-text="formatDuration(selectedJobDetails?.started_at, selectedJobDetails?.completed_at)"></span></p>
</div>
<!-- Export Details (for export jobs) -->
<template x-if="selectedJobDetails?.type === 'export' && selectedJobDetails?.error_details?.products_exported">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
<h4 class="font-medium mb-2">Export Details</h4>
<p class="text-sm">Products exported: <span x-text="selectedJobDetails.error_details.products_exported"></span></p>
<template x-if="selectedJobDetails.error_details.files">
<div class="mt-2 space-y-1">
<template x-for="file in selectedJobDetails.error_details.files" :key="file.language">
<div class="text-xs flex justify-between">
<span x-text="file.language.toUpperCase()"></span>
<span x-text="file.error ? 'Failed: ' + file.error : file.filename + ' (' + (file.size_bytes / 1024).toFixed(1) + ' KB)'"></span>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Error Details -->
<template x-if="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error">
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
<h4 class="font-medium text-red-700 mb-2">Error</h4>
<p class="text-sm text-red-600" x-text="selectedJobDetails?.error_message || selectedJobDetails?.error_details?.error"></p>
</div>
</template>
</div>
</div>
</div>
```
#### 1.2 Update JavaScript State
**File:** `static/admin/js/marketplace-letzshop.js`
Add state variables:
```javascript
showJobDetailsModal: false,
selectedJobDetails: null,
```
Update `viewJobDetails` method:
```javascript
viewJobDetails(job) {
this.selectedJobDetails = job;
this.showJobDetailsModal = true;
},
```
#### 1.3 Update API to Return Full Details
**File:** `app/services/letzshop/order_service.py`
Update `list_letzshop_jobs` to include `error_details` in the response for export jobs.
---
## 2. Tab Visibility Fix
### Current Issue
- When store filter is cleared, only 2 tabs appear (Orders, Exceptions)
- Should show all tabs: Products, Orders, Exceptions, Jobs, Settings
### Root Cause
- Products, Jobs, and Settings tabs are wrapped in `<template x-if="selectedStore">`
- This is intentional for store-specific features
### Decision Required
**Option A:** Keep current behavior (store-specific tabs hidden when no store)
- Products, Jobs, Settings require a store context
- Cross-store view only shows Orders and Exceptions
**Option B:** Show all tabs but with "Select store" message
- All tabs visible
- Content shows prompt to select store
### Recommended: Option A (Current Behavior)
The current behavior is correct because:
- Products tab shows store's Letzshop products (needs store)
- Jobs tab shows store's jobs (needs store)
- Settings tab configures store's Letzshop (needs store)
- Orders and Exceptions can work cross-store
**No change needed** - document this as intentional behavior.
---
## 3. Add Store Column to Jobs Table
### Requirements
- Add store name/code column to jobs table
- Useful when viewing cross-store (future feature)
- Prepare for reusable jobs component
### Implementation
#### 3.1 Update API Response
**File:** `app/services/letzshop/order_service.py`
Add store info to job dicts:
```python
# In list_letzshop_jobs, add to each job dict:
"store_id": store_id,
"store_name": store.name if store else None,
"store_code": store.store_code if store else None,
```
Need to fetch store once at start of function.
#### 3.2 Update Table Template
**File:** `app/templates/admin/partials/letzshop-jobs-table.html`
Add column header:
```html
<th class="px-4 py-3">Store</th>
```
Add column data:
```html
<td class="px-4 py-3 text-sm">
<span x-text="job.store_code || job.store_name || '-'"></span>
</td>
```
#### 3.3 Update Schema
**File:** `models/schema/letzshop.py`
Update `LetzshopJobItem` to include store fields:
```python
store_id: int | None = None
store_name: str | None = None
store_code: str | None = None
```
---
## 4. Harmonize Tables with Table Macro
### Current State
- Different tables use different pagination styles
- Some use simple prev/next, others use numbered
- Inconsistent styling
### Requirements
- All tables use `table` macro from `shared/macros/tables.html`
- Numbered pagination with page numbers
- Consistent column styling
- Rows per page selector
### Tables to Update
| Table | File | Current Pagination |
|-------|------|-------------------|
| Jobs | letzshop-jobs-table.html | Simple prev/next |
| Products | letzshop-products-tab.html | Simple prev/next |
| Orders | letzshop-orders-tab.html | Simple prev/next |
| Exceptions | letzshop-exceptions-tab.html | Simple prev/next |
### Implementation
#### 4.1 Create/Update Table Macro
**File:** `app/templates/shared/macros/tables.html`
Ensure numbered pagination macro exists:
```html
{% macro numbered_pagination(page_var, total_var, limit_var, on_change) %}
<div class="flex items-center justify-between mt-4">
<div class="text-sm text-gray-600">
Showing <span x-text="(({{ page_var }} - 1) * {{ limit_var }}) + 1"></span>
to <span x-text="Math.min({{ page_var }} * {{ limit_var }}, {{ total_var }})"></span>
of <span x-text="{{ total_var }}"></span>
</div>
<div class="flex items-center gap-1">
<!-- First -->
<button @click="{{ page_var }} = 1; {{ on_change }}" :disabled="{{ page_var }} <= 1">«</button>
<!-- Prev -->
<button @click="{{ page_var }}--; {{ on_change }}" :disabled="{{ page_var }} <= 1"></button>
<!-- Page numbers -->
<template x-for="p in getPageNumbers({{ page_var }}, Math.ceil({{ total_var }} / {{ limit_var }}))">
<button @click="{{ page_var }} = p; {{ on_change }}" :class="p === {{ page_var }} ? 'bg-purple-600 text-white' : ''">
<span x-text="p"></span>
</button>
</template>
<!-- Next -->
<button @click="{{ page_var }}++; {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}"></button>
<!-- Last -->
<button @click="{{ page_var }} = Math.ceil({{ total_var }} / {{ limit_var }}); {{ on_change }}" :disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}">»</button>
</div>
</div>
{% endmacro %}
```
#### 4.2 Add Page Numbers Helper to JavaScript
**File:** `static/shared/js/helpers.js` or inline
```javascript
function getPageNumbers(current, total, maxVisible = 5) {
if (total <= maxVisible) {
return Array.from({length: total}, (_, i) => i + 1);
}
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + maxVisible - 1);
if (end - start < maxVisible - 1) {
start = Math.max(1, end - maxVisible + 1);
}
return Array.from({length: end - start + 1}, (_, i) => start + i);
}
```
#### 4.3 Update Each Table
Update each table to use the macro and consistent styling.
---
## 5. Platform-Wide Rows Per Page Setting
### Requirements
- Global setting for default rows per page
- Stored in platform settings (not per-user initially)
- Used by all paginated tables
- Options: 10, 20, 50, 100
### Implementation
#### 5.1 Add Platform Setting
**File:** `models/database/platform_settings.py` (create if doesn't exist)
```python
class PlatformSettings(Base):
__tablename__ = "platform_settings"
id = Column(Integer, primary_key=True)
key = Column(String(100), unique=True, nullable=False)
value = Column(String(500), nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow)
```
Or add to existing settings table if one exists.
#### 5.2 Create Settings Service
**File:** `app/services/platform_settings_service.py`
```python
class PlatformSettingsService:
def get_setting(self, db: Session, key: str, default: Any = None) -> Any:
setting = db.query(PlatformSettings).filter_by(key=key).first()
return setting.value if setting else default
def set_setting(self, db: Session, key: str, value: Any) -> None:
setting = db.query(PlatformSettings).filter_by(key=key).first()
if setting:
setting.value = str(value)
else:
setting = PlatformSettings(key=key, value=str(value))
db.add(setting)
db.flush()
def get_rows_per_page(self, db: Session) -> int:
return int(self.get_setting(db, "rows_per_page", "20"))
```
#### 5.3 Expose via API
**File:** `app/api/v1/admin/settings.py`
```python
@router.get("/platform/rows-per-page")
def get_rows_per_page(db: Session = Depends(get_db)):
return {"rows_per_page": platform_settings_service.get_rows_per_page(db)}
@router.put("/platform/rows-per-page")
def set_rows_per_page(
rows: int = Query(..., ge=10, le=100),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
platform_settings_service.set_setting(db, "rows_per_page", rows)
db.commit()
return {"rows_per_page": rows}
```
#### 5.4 Load Setting in Frontend
**File:** `static/shared/js/app.js` or similar
```javascript
// Load platform settings on app init
async function loadPlatformSettings() {
try {
const response = await apiClient.get('/admin/settings/platform/rows-per-page');
window.platformSettings = {
rowsPerPage: response.rows_per_page || 20
};
} catch {
window.platformSettings = { rowsPerPage: 20 };
}
}
```
#### 5.5 Use in Alpine Components
```javascript
// In each paginated component's init:
this.limit = window.platformSettings?.rowsPerPage || 20;
```
---
## Implementation Order
1. **Phase 1: Job Details Modal** (Quick win)
- Add modal template
- Update JS state and methods
- Test with export jobs
2. **Phase 2: Store Column** (Preparation)
- Update API response
- Update schema
- Add column to table
3. **Phase 3: Platform Settings** (Foundation)
- Create settings model/migration
- Create service
- Create API endpoint
- Frontend integration
4. **Phase 4: Table Harmonization** (Largest effort)
- Create/update table macros
- Add pagination helper function
- Update each table one by one
- Test thoroughly
5. **Phase 5: Documentation**
- Update component documentation
- Add settings documentation
---
## Files to Create/Modify
### New Files
- `models/database/platform_settings.py` (if not exists)
- `app/services/platform_settings_service.py`
- `alembic/versions/xxx_add_platform_settings.py`
### Modified Files
- `app/templates/admin/partials/letzshop-jobs-table.html`
- `app/templates/admin/partials/letzshop-products-tab.html`
- `app/templates/admin/partials/letzshop-orders-tab.html`
- `app/templates/admin/partials/letzshop-exceptions-tab.html`
- `app/templates/shared/macros/tables.html`
- `static/admin/js/marketplace-letzshop.js`
- `static/shared/js/helpers.js` or `app.js`
- `app/services/letzshop/order_service.py`
- `models/schema/letzshop.py`
- `app/api/v1/admin/settings.py` or new file
---
## 6. Admin Customer Page
### Requirements
- New page at `/admin/customers` to manage customers
- List all customers across stores
- Search and filter capabilities
- View customer details and order history
- Link to store context
### Implementation
#### 6.1 Database Model Check
**File:** `models/database/customer.py`
Verify Customer model exists with fields:
- id, store_id
- email, name, phone
- shipping address fields
- created_at, updated_at
#### 6.2 Create Customer Service
**File:** `app/services/customer_service.py`
```python
class CustomerService:
def get_customers(
self,
db: Session,
skip: int = 0,
limit: int = 20,
search: str | None = None,
store_id: int | None = None,
) -> tuple[list[dict], int]:
"""Get paginated customer list with optional filters."""
pass
def get_customer_detail(self, db: Session, customer_id: int) -> dict:
"""Get customer with order history."""
pass
def get_customer_stats(self, db: Session, store_id: int | None = None) -> dict:
"""Get customer statistics."""
pass
```
#### 6.3 Create API Endpoints
**File:** `app/api/v1/admin/customers.py`
```python
router = APIRouter(prefix="/customers")
@router.get("", response_model=CustomerListResponse)
def get_customers(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
search: str | None = Query(None),
store_id: int | None = Query(None),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""List all customers with filtering."""
pass
@router.get("/stats", response_model=CustomerStatsResponse)
def get_customer_stats(...):
"""Get customer statistics."""
pass
@router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer_detail(...):
"""Get customer with order history."""
pass
```
#### 6.4 Create Pydantic Schemas
**File:** `models/schema/customer.py`
```python
class CustomerListItem(BaseModel):
id: int
email: str
name: str | None
phone: str | None
store_id: int
store_name: str | None
order_count: int
total_spent: float
created_at: datetime
class CustomerListResponse(BaseModel):
customers: list[CustomerListItem]
total: int
skip: int
limit: int
class CustomerDetailResponse(CustomerListItem):
shipping_address: str | None
orders: list[OrderSummary]
class CustomerStatsResponse(BaseModel):
total: int
new_this_month: int
active: int # ordered in last 90 days
by_store: dict[str, int]
```
#### 6.5 Create Admin Page Route
**File:** `app/routes/admin_pages.py`
```python
@router.get("/customers", response_class=HTMLResponse)
async def admin_customers_page(request: Request, ...):
return templates.TemplateResponse(
"admin/customers.html",
{"request": request, "current_page": "customers"}
)
```
#### 6.6 Create Template
**File:** `app/templates/admin/customers.html`
Structure:
- Page header with title and stats
- Search bar and filters (store dropdown)
- Customer table with pagination
- Click row to view details modal
#### 6.7 Create Alpine Component
**File:** `static/admin/js/customers.js`
```javascript
function adminCustomers() {
return {
customers: [],
total: 0,
page: 1,
limit: 20,
search: '',
storeFilter: '',
loading: false,
stats: {},
async init() {
await Promise.all([
this.loadCustomers(),
this.loadStats()
]);
},
async loadCustomers() { ... },
async loadStats() { ... },
async viewCustomer(id) { ... },
}
}
```
#### 6.8 Add to Sidebar
**File:** `app/templates/admin/partials/sidebar.html`
Add menu item:
```html
{{ menu_item('customers', '/admin/customers', 'users', 'Customers') }}
```
### Customer Page Features
| Feature | Description |
|---------|-------------|
| List View | Paginated table of all customers |
| Search | Search by name, email, phone |
| Store Filter | Filter by store |
| Stats Cards | Total, new, active customers |
| Detail Modal | Customer info + order history |
| Quick Actions | View orders, send email |
---
## Implementation Order
1. **Phase 1: Job Details Modal** (Quick win)
- Add modal template
- Update JS state and methods
- Test with export jobs
2. **Phase 2: Store Column** (Preparation)
- Update API response
- Update schema
- Add column to table
3. **Phase 3: Platform Settings** (Foundation)
- Create settings model/migration
- Create service
- Create API endpoint
- Frontend integration
4. **Phase 4: Table Harmonization** (Largest effort)
- Create/update table macros
- Add pagination helper function
- Update each table one by one
- Test thoroughly
5. **Phase 5: Admin Customer Page**
- Create service and API
- Create schemas
- Create template and JS
- Add to sidebar
6. **Phase 6: Documentation**
- Update component documentation
- Add settings documentation
- Add customer page documentation
---
## Files to Create/Modify
### New Files
- `models/database/platform_settings.py` (if not exists)
- `app/services/platform_settings_service.py`
- `app/services/customer_service.py`
- `app/api/v1/admin/customers.py`
- `models/schema/customer.py`
- `app/templates/admin/customers.html`
- `static/admin/js/customers.js`
- `alembic/versions/xxx_add_platform_settings.py`
### Modified Files
- `app/templates/admin/partials/letzshop-jobs-table.html`
- `app/templates/admin/partials/letzshop-products-tab.html`
- `app/templates/admin/partials/letzshop-orders-tab.html`
- `app/templates/admin/partials/letzshop-exceptions-tab.html`
- `app/templates/admin/partials/sidebar.html`
- `app/templates/shared/macros/tables.html`
- `static/admin/js/marketplace-letzshop.js`
- `static/shared/js/helpers.js` or `app.js`
- `app/services/letzshop/order_service.py`
- `models/schema/letzshop.py`
- `app/api/v1/admin/__init__.py`
- `app/routes/admin_pages.py`
---
## Estimated Effort
| Task | Effort |
|------|--------|
| Job Details Modal | Small |
| Tab Visibility (no change) | None |
| Store Column | Small |
| Platform Settings | Medium |
| Table Harmonization | Large |
| Admin Customer Page | Medium |
**Total:** Large effort
---
*Plan created: 2024-12-20*

View File

@@ -0,0 +1,839 @@
# 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