feat: integer cents money handling, order page fixes, and vendor filter persistence

Money Handling Architecture:
- Store all monetary values as integer cents (€105.91 = 10591)
- Add app/utils/money.py with Money class and conversion helpers
- Add static/shared/js/money.js for frontend formatting
- Update all database models to use _cents columns (Product, Order, etc.)
- Update CSV processor to convert prices to cents on import
- Add Alembic migration for Float to Integer conversion
- Create .architecture-rules/money.yaml with 7 validation rules
- Add docs/architecture/money-handling.md documentation

Order Details Page Fixes:
- Fix customer name showing 'undefined undefined' - use flat field names
- Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse
- Fix shipping address using wrong nested object structure
- Enrich order detail API response with vendor info

Vendor Filter Persistence Fixes:
- Fix orders.js: restoreSavedVendor now sets selectedVendor and filters
- Fix orders.js: init() only loads orders if no saved vendor to restore
- Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor()
- Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown
- Align vendor selector placeholder text between pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 20:33:48 +01:00
parent 7f0d32c18d
commit a19c84ea4e
56 changed files with 6155 additions and 447 deletions

View File

@@ -8,7 +8,9 @@ Complete guide for bidirectional order management with Letzshop marketplace via
- [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)
@@ -196,6 +198,129 @@ Local orders track their sync status:
---
## Product Exceptions
When importing orders from Letzshop, products are matched by GTIN. If a product is not found in the vendor'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 vendor'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?vendor_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 vendor 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_vendor_catalog`)
- Bulk marketplace sync
### Exception Statistics
Get counts via API:
```bash
curl -X GET /api/v1/admin/order-exceptions/stats?vendor_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](../implementation/order-item-exceptions.md).
---
## Fulfillment Operations
### Confirm Order
@@ -243,6 +368,106 @@ 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
### Vendor Endpoints
@@ -283,6 +508,33 @@ Base path: `/api/v1/admin/letzshop`
| GET | `/vendors/{id}/orders` | List vendor's Letzshop orders |
| POST | `/vendors/{id}/sync` | Trigger sync for vendor |
### Order Endpoints
Base path: `/api/v1/admin/orders`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `` | List orders (cross-vendor) |
| GET | `/stats` | Get order statistics |
| GET | `/vendors` | Get vendors 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
@@ -502,6 +754,34 @@ curl -X GET /api/v1/vendor/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
@@ -525,6 +805,7 @@ curl -X GET /api/v1/vendor/letzshop/logs \
## Related Documentation
- [Order Item Exception System](../implementation/order-item-exceptions.md)
- [Marketplace Integration (CSV Import)](marketplace-integration.md)
- [Vendor RBAC](../backend/vendor-rbac.md)
- [Admin Integration Guide](../backend/admin-integration-guide.md)
@@ -534,6 +815,22 @@ curl -X GET /api/v1/vendor/letzshop/logs \
## 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 vendor 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