Files
orion/app/services/vendor_product_service.py
Samir Boulahtit 9920430b9e fix: correct tojson|safe usage in templates and update validator
- Remove |safe from |tojson in HTML attributes (x-data) - quotes must
  become " for browsers to parse correctly
- Update LANG-002 and LANG-003 architecture rules to document correct
  |tojson usage patterns:
  - HTML attributes: |tojson (no |safe)
  - Script blocks: |tojson|safe
- Fix validator to warn when |tojson|safe is used in x-data (breaks
  HTML attribute parsing)
- Improve code quality across services, APIs, and tests

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 22:59:51 +01:00

271 lines
9.0 KiB
Python

# app/services/vendor_product_service.py
"""
Vendor product service for managing vendor-specific product catalogs.
This module provides:
- Vendor product catalog browsing
- Product search and filtering
- Product statistics
- Product removal from catalogs
"""
import logging
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from app.exceptions import ProductNotFoundException
from models.database.product import Product
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
class VendorProductService:
"""Service for vendor product catalog operations."""
def get_products(
self,
db: Session,
skip: int = 0,
limit: int = 50,
search: str | None = None,
vendor_id: int | None = None,
is_active: bool | None = None,
is_featured: bool | None = None,
language: str = "en",
) -> tuple[list[dict], int]:
"""
Get vendor products with search and filtering.
Returns:
Tuple of (products list as dicts, total count)
"""
query = (
db.query(Product)
.join(Vendor, Product.vendor_id == Vendor.id)
.options(
joinedload(Product.vendor),
joinedload(Product.marketplace_product),
)
)
if search:
search_term = f"%{search}%"
query = query.filter(Product.vendor_sku.ilike(search_term))
if vendor_id:
query = query.filter(Product.vendor_id == vendor_id)
if is_active is not None:
query = query.filter(Product.is_active == is_active)
if is_featured is not None:
query = query.filter(Product.is_featured == is_featured)
total = query.count()
products = (
query.order_by(Product.updated_at.desc()).offset(skip).limit(limit).all()
)
result = []
for product in products:
result.append(self._build_product_list_item(product, language))
return result, total
def get_product_stats(self, db: Session) -> dict:
"""Get vendor product statistics for admin dashboard."""
total = db.query(func.count(Product.id)).scalar() or 0
active = (
db.query(func.count(Product.id))
.filter(Product.is_active == True) # noqa: E712
.scalar()
or 0
)
inactive = total - active
featured = (
db.query(func.count(Product.id))
.filter(Product.is_featured == True) # noqa: E712
.scalar()
or 0
)
# Digital/physical counts
digital = (
db.query(func.count(Product.id))
.join(Product.marketplace_product)
.filter(Product.marketplace_product.has(is_digital=True))
.scalar()
or 0
)
physical = total - digital
# Count by vendor
vendor_counts = (
db.query(
Vendor.name,
func.count(Product.id),
)
.join(Vendor, Product.vendor_id == Vendor.id)
.group_by(Vendor.name)
.all()
)
by_vendor = {name or "unknown": count for name, count in vendor_counts}
return {
"total": total,
"active": active,
"inactive": inactive,
"featured": featured,
"digital": digital,
"physical": physical,
"by_vendor": by_vendor,
}
def get_catalog_vendors(self, db: Session) -> list[dict]:
"""Get list of vendors with products in their catalogs."""
vendors = (
db.query(Vendor.id, Vendor.name, Vendor.vendor_code)
.join(Product, Vendor.id == Product.vendor_id)
.distinct()
.all()
)
return [
{"id": v.id, "name": v.name, "vendor_code": v.vendor_code} for v in vendors
]
def get_product_detail(self, db: Session, product_id: int) -> dict:
"""Get detailed vendor product information including override info."""
product = (
db.query(Product)
.options(
joinedload(Product.vendor),
joinedload(Product.marketplace_product),
joinedload(Product.translations),
)
.filter(Product.id == product_id)
.first()
)
if not product:
raise ProductNotFoundException(product_id)
mp = product.marketplace_product
override_info = product.get_override_info()
# Get marketplace product translations
mp_translations = {}
if mp:
for t in mp.translations:
mp_translations[t.language] = {
"title": t.title,
"description": t.description,
"short_description": t.short_description,
}
# Get vendor translations (overrides)
vendor_translations = {}
for t in product.translations:
vendor_translations[t.language] = {
"title": t.title,
"description": t.description,
}
return {
"id": product.id,
"vendor_id": product.vendor_id,
"vendor_name": product.vendor.name if product.vendor else None,
"vendor_code": product.vendor.vendor_code if product.vendor else None,
"marketplace_product_id": product.marketplace_product_id,
"vendor_sku": product.vendor_sku,
# Override info
**override_info,
# Vendor-specific fields
"is_featured": product.is_featured,
"is_active": product.is_active,
"display_order": product.display_order,
"min_quantity": product.min_quantity,
"max_quantity": product.max_quantity,
# Supplier tracking
"supplier": product.supplier,
"supplier_product_id": product.supplier_product_id,
"supplier_cost": product.supplier_cost,
"margin_percent": product.margin_percent,
# Digital fulfillment
"download_url": product.download_url,
"license_type": product.license_type,
"fulfillment_email_template": product.fulfillment_email_template,
# Source info from marketplace product
"source_marketplace": mp.marketplace if mp else None,
"source_vendor": mp.vendor_name if mp else None,
"source_gtin": mp.gtin if mp else None,
"source_sku": mp.sku if mp else None,
# Translations
"marketplace_translations": mp_translations,
"vendor_translations": vendor_translations,
# Timestamps
"created_at": product.created_at.isoformat()
if product.created_at
else None,
"updated_at": product.updated_at.isoformat()
if product.updated_at
else None,
}
def remove_product(self, db: Session, product_id: int) -> dict:
"""Remove a product from vendor catalog."""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise ProductNotFoundException(product_id)
vendor_name = product.vendor.name if product.vendor else "Unknown"
db.delete(product)
db.flush()
logger.info(f"Removed product {product_id} from vendor {vendor_name} catalog")
return {"message": f"Product removed from {vendor_name}'s catalog"}
def _build_product_list_item(self, product: Product, language: str) -> dict:
"""Build a product list item dict."""
mp = product.marketplace_product
# Get title from marketplace product translations
title = None
if mp:
title = mp.get_title(language)
return {
"id": product.id,
"vendor_id": product.vendor_id,
"vendor_name": product.vendor.name if product.vendor else None,
"vendor_code": product.vendor.vendor_code if product.vendor else None,
"marketplace_product_id": product.marketplace_product_id,
"vendor_sku": product.vendor_sku,
"title": title,
"brand": product.effective_brand,
"effective_price": product.effective_price,
"effective_currency": product.effective_currency,
"is_active": product.is_active,
"is_featured": product.is_featured,
"is_digital": product.is_digital,
"image_url": product.effective_primary_image_url,
"source_marketplace": mp.marketplace if mp else None,
"source_vendor": mp.vendor_name if mp else None,
"created_at": product.created_at.isoformat()
if product.created_at
else None,
"updated_at": product.updated_at.isoformat()
if product.updated_at
else None,
}
# Create service instance
vendor_product_service = VendorProductService()