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:
@@ -116,6 +116,9 @@ query {
|
||||
code
|
||||
provider
|
||||
}
|
||||
data {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +197,9 @@ query {
|
||||
code
|
||||
provider
|
||||
}
|
||||
data {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,6 +278,9 @@ query GetShipment($id: ID!) {
|
||||
code
|
||||
provider
|
||||
}
|
||||
data {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,6 +358,9 @@ query GetShipmentsPaginated($first: Int!, $after: String) {{
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
data {{
|
||||
__typename
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -182,7 +182,7 @@ class LetzshopOrderService:
|
||||
|
||||
def list_orders(
|
||||
self,
|
||||
vendor_id: int,
|
||||
vendor_id: int | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
status: str | None = None,
|
||||
@@ -190,10 +190,10 @@ class LetzshopOrderService:
|
||||
search: str | None = None,
|
||||
) -> tuple[list[Order], int]:
|
||||
"""
|
||||
List Letzshop orders for a vendor.
|
||||
List Letzshop orders for a vendor (or all vendors).
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID to filter by.
|
||||
vendor_id: Vendor ID to filter by. If None, returns all vendors.
|
||||
skip: Number of records to skip.
|
||||
limit: Maximum number of records to return.
|
||||
status: Filter by order status (pending, processing, shipped, etc.)
|
||||
@@ -203,10 +203,13 @@ class LetzshopOrderService:
|
||||
Returns a tuple of (orders, total_count).
|
||||
"""
|
||||
query = self.db.query(Order).filter(
|
||||
Order.vendor_id == vendor_id,
|
||||
Order.channel == "letzshop",
|
||||
)
|
||||
|
||||
# Filter by vendor if specified
|
||||
if vendor_id is not None:
|
||||
query = query.filter(Order.vendor_id == vendor_id)
|
||||
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
@@ -242,25 +245,25 @@ class LetzshopOrderService:
|
||||
|
||||
return orders, total
|
||||
|
||||
def get_order_stats(self, vendor_id: int) -> dict[str, int]:
|
||||
def get_order_stats(self, vendor_id: int | None = None) -> dict[str, int]:
|
||||
"""
|
||||
Get order counts by status for a vendor's Letzshop orders.
|
||||
Get order counts by status for Letzshop orders.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID to filter by. If None, returns stats for all vendors.
|
||||
|
||||
Returns:
|
||||
Dict with counts for each status.
|
||||
"""
|
||||
status_counts = (
|
||||
self.db.query(
|
||||
Order.status,
|
||||
func.count(Order.id).label("count"),
|
||||
)
|
||||
.filter(
|
||||
Order.vendor_id == vendor_id,
|
||||
Order.channel == "letzshop",
|
||||
)
|
||||
.group_by(Order.status)
|
||||
.all()
|
||||
)
|
||||
query = self.db.query(
|
||||
Order.status,
|
||||
func.count(Order.id).label("count"),
|
||||
).filter(Order.channel == "letzshop")
|
||||
|
||||
if vendor_id is not None:
|
||||
query = query.filter(Order.vendor_id == vendor_id)
|
||||
|
||||
status_counts = query.group_by(Order.status).all()
|
||||
|
||||
stats = {
|
||||
"pending": 0,
|
||||
@@ -277,18 +280,18 @@ class LetzshopOrderService:
|
||||
stats["total"] += count
|
||||
|
||||
# Count orders with declined items
|
||||
declined_items_count = (
|
||||
declined_query = (
|
||||
self.db.query(func.count(func.distinct(OrderItem.order_id)))
|
||||
.join(Order, OrderItem.order_id == Order.id)
|
||||
.filter(
|
||||
Order.vendor_id == vendor_id,
|
||||
Order.channel == "letzshop",
|
||||
OrderItem.item_state == "confirmed_unavailable",
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
stats["has_declined_items"] = declined_items_count
|
||||
if vendor_id is not None:
|
||||
declined_query = declined_query.filter(Order.vendor_id == vendor_id)
|
||||
|
||||
stats["has_declined_items"] = declined_query.scalar() or 0
|
||||
|
||||
return stats
|
||||
|
||||
@@ -464,6 +467,62 @@ class LetzshopOrderService:
|
||||
order.updated_at = datetime.now(UTC)
|
||||
return order
|
||||
|
||||
def get_orders_without_tracking(
|
||||
self,
|
||||
vendor_id: int,
|
||||
limit: int = 100,
|
||||
) -> list[Order]:
|
||||
"""Get orders that have been confirmed but don't have tracking info."""
|
||||
return (
|
||||
self.db.query(Order)
|
||||
.filter(
|
||||
Order.vendor_id == vendor_id,
|
||||
Order.channel == "letzshop",
|
||||
Order.status == "processing", # Confirmed orders
|
||||
Order.tracking_number.is_(None),
|
||||
Order.external_shipment_id.isnot(None), # Has shipment ID
|
||||
)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def update_tracking_from_shipment_data(
|
||||
self,
|
||||
order: Order,
|
||||
shipment_data: dict[str, Any],
|
||||
) -> bool:
|
||||
"""
|
||||
Update order tracking from Letzshop shipment data.
|
||||
|
||||
Args:
|
||||
order: The order to update.
|
||||
shipment_data: Raw shipment data from Letzshop API.
|
||||
|
||||
Returns:
|
||||
True if tracking was updated, False otherwise.
|
||||
"""
|
||||
tracking_data = shipment_data.get("tracking") or {}
|
||||
tracking_number = tracking_data.get("code") or tracking_data.get("number")
|
||||
|
||||
if not tracking_number:
|
||||
return False
|
||||
|
||||
tracking_provider = tracking_data.get("provider")
|
||||
# Handle carrier object format: tracking { carrier { name code } }
|
||||
if not tracking_provider and tracking_data.get("carrier"):
|
||||
carrier = tracking_data.get("carrier", {})
|
||||
tracking_provider = carrier.get("code") or carrier.get("name")
|
||||
|
||||
order.tracking_number = tracking_number
|
||||
order.tracking_provider = tracking_provider
|
||||
order.updated_at = datetime.now(UTC)
|
||||
|
||||
logger.info(
|
||||
f"Updated tracking for order {order.order_number}: "
|
||||
f"{tracking_provider} {tracking_number}"
|
||||
)
|
||||
return True
|
||||
|
||||
def get_order_items(self, order: Order) -> list[OrderItem]:
|
||||
"""Get all items for an order."""
|
||||
return (
|
||||
@@ -733,7 +792,7 @@ class LetzshopOrderService:
|
||||
pass
|
||||
|
||||
if needs_update:
|
||||
self.db.commit() # Commit update immediately
|
||||
self.db.commit() # noqa: SVC-006 - background task needs incremental commits
|
||||
stats["updated"] += 1
|
||||
else:
|
||||
stats["skipped"] += 1
|
||||
@@ -741,7 +800,7 @@ class LetzshopOrderService:
|
||||
# Create new order using unified service
|
||||
try:
|
||||
self.create_order(vendor_id, shipment)
|
||||
self.db.commit() # Commit each order immediately
|
||||
self.db.commit() # noqa: SVC-006 - background task needs incremental commits
|
||||
stats["imported"] += 1
|
||||
except Exception as e:
|
||||
self.db.rollback() # Rollback failed order
|
||||
@@ -948,7 +1007,7 @@ class LetzshopOrderService:
|
||||
status="pending",
|
||||
)
|
||||
self.db.add(job)
|
||||
self.db.commit()
|
||||
self.db.commit() # noqa: SVC-006 - job must be visible immediately before background task starts
|
||||
self.db.refresh(job)
|
||||
return job
|
||||
|
||||
|
||||
Reference in New Issue
Block a user