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:
261
app/modules/marketplace/docs/admin-guide.md
Normal file
261
app/modules/marketplace/docs/admin-guide.md
Normal 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
|
||||
322
app/modules/marketplace/docs/api.md
Normal file
322
app/modules/marketplace/docs/api.md
Normal 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.
|
||||
1345
app/modules/marketplace/docs/architecture.md
Normal file
1345
app/modules/marketplace/docs/architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
297
app/modules/marketplace/docs/data-model.md
Normal file
297
app/modules/marketplace/docs/data-model.md
Normal 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 |
|
||||
601
app/modules/marketplace/docs/import-improvements.md
Normal file
601
app/modules/marketplace/docs/import-improvements.md
Normal 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 |
|
||||
73
app/modules/marketplace/docs/index.md
Normal file
73
app/modules/marketplace/docs/index.md
Normal 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
|
||||
BIN
app/modules/marketplace/docs/integration-guide.md
Normal file
BIN
app/modules/marketplace/docs/integration-guide.md
Normal file
Binary file not shown.
716
app/modules/marketplace/docs/job-queue.md
Normal file
716
app/modules/marketplace/docs/job-queue.md
Normal 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*
|
||||
839
app/modules/marketplace/docs/order-integration.md
Normal file
839
app/modules/marketplace/docs/order-integration.md
Normal 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
|
||||
Reference in New Issue
Block a user