- 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>
696 lines
23 KiB
Python
696 lines
23 KiB
Python
# app/services/vendor_service.py
|
|
"""
|
|
Vendor service for managing vendor operations and product catalog.
|
|
|
|
This module provides classes and functions for:
|
|
- Vendor creation and management
|
|
- Vendor access control and validation
|
|
- Vendor product catalog operations
|
|
- Vendor filtering and search
|
|
"""
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.exceptions import (
|
|
InvalidVendorDataException,
|
|
MarketplaceProductNotFoundException,
|
|
ProductAlreadyExistsException,
|
|
UnauthorizedVendorAccessException,
|
|
ValidationException,
|
|
VendorAlreadyExistsException,
|
|
VendorNotFoundException,
|
|
)
|
|
from models.database.marketplace_product import MarketplaceProduct
|
|
from models.database.product import Product
|
|
from models.database.user import User
|
|
from models.database.vendor import Vendor
|
|
from models.schema.product import ProductCreate
|
|
from models.schema.vendor import VendorCreate
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class VendorService:
|
|
"""Service class for vendor operations following the application's service pattern."""
|
|
|
|
def create_vendor(
|
|
self, db: Session, vendor_data: VendorCreate, current_user: User
|
|
) -> Vendor:
|
|
"""
|
|
Create a new vendor under a company.
|
|
|
|
DEPRECATED: This method is for self-service vendor creation by company owners.
|
|
For admin operations, use admin_service.create_vendor() instead.
|
|
|
|
The new architecture:
|
|
- Companies are the business entities with owners and contact info
|
|
- Vendors are storefronts/brands under companies
|
|
- The company_id is required in vendor_data
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_data: Vendor creation data (must include company_id)
|
|
current_user: User creating the vendor (must be company owner or admin)
|
|
|
|
Returns:
|
|
Created vendor object
|
|
|
|
Raises:
|
|
VendorAlreadyExistsException: If vendor code already exists
|
|
UnauthorizedVendorAccessException: If user is not company owner
|
|
InvalidVendorDataException: If vendor data is invalid
|
|
"""
|
|
from models.database.company import Company
|
|
|
|
try:
|
|
# Validate company_id is provided
|
|
if not hasattr(vendor_data, "company_id") or not vendor_data.company_id:
|
|
raise InvalidVendorDataException(
|
|
"company_id is required to create a vendor", field="company_id"
|
|
)
|
|
|
|
# Get company and verify ownership
|
|
company = (
|
|
db.query(Company).filter(Company.id == vendor_data.company_id).first()
|
|
)
|
|
if not company:
|
|
raise InvalidVendorDataException(
|
|
f"Company with ID {vendor_data.company_id} not found",
|
|
field="company_id",
|
|
)
|
|
|
|
# Check if user is company owner or admin
|
|
if (
|
|
current_user.role != "admin"
|
|
and company.owner_user_id != current_user.id
|
|
):
|
|
raise UnauthorizedVendorAccessException(
|
|
f"company-{vendor_data.company_id}", current_user.id
|
|
)
|
|
|
|
# Normalize vendor code to uppercase
|
|
normalized_vendor_code = vendor_data.vendor_code.upper()
|
|
|
|
# Check if vendor code already exists (case-insensitive check)
|
|
if self._vendor_code_exists(db, normalized_vendor_code):
|
|
raise VendorAlreadyExistsException(normalized_vendor_code)
|
|
|
|
# Create vendor linked to company
|
|
new_vendor = Vendor(
|
|
company_id=company.id,
|
|
vendor_code=normalized_vendor_code,
|
|
subdomain=vendor_data.subdomain.lower(),
|
|
name=vendor_data.name,
|
|
description=vendor_data.description,
|
|
letzshop_csv_url_fr=vendor_data.letzshop_csv_url_fr,
|
|
letzshop_csv_url_en=vendor_data.letzshop_csv_url_en,
|
|
letzshop_csv_url_de=vendor_data.letzshop_csv_url_de,
|
|
is_active=True,
|
|
is_verified=(current_user.role == "admin"),
|
|
)
|
|
|
|
db.add(new_vendor)
|
|
db.flush() # Get ID without committing - endpoint handles commit
|
|
|
|
logger.info(
|
|
f"New vendor created: {new_vendor.vendor_code} under company {company.name} by {current_user.username}"
|
|
)
|
|
return new_vendor
|
|
|
|
except (
|
|
VendorAlreadyExistsException,
|
|
UnauthorizedVendorAccessException,
|
|
InvalidVendorDataException,
|
|
):
|
|
raise # Re-raise custom exceptions - endpoint handles rollback
|
|
except Exception as e:
|
|
logger.error(f"Error creating vendor: {str(e)}")
|
|
raise ValidationException("Failed to create vendor")
|
|
|
|
def get_vendors(
|
|
self,
|
|
db: Session,
|
|
current_user: User,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
active_only: bool = True,
|
|
verified_only: bool = False,
|
|
) -> tuple[list[Vendor], int]:
|
|
"""
|
|
Get vendors with filtering.
|
|
|
|
Args:
|
|
db: Database session
|
|
current_user: Current user requesting vendors
|
|
skip: Number of records to skip
|
|
limit: Maximum number of records to return
|
|
active_only: Filter for active vendors only
|
|
verified_only: Filter for verified vendors only
|
|
|
|
Returns:
|
|
Tuple of (vendors_list, total_count)
|
|
"""
|
|
try:
|
|
query = db.query(Vendor)
|
|
|
|
# Non-admin users can only see active and verified vendors, plus their own
|
|
if current_user.role != "admin":
|
|
# Get vendor IDs the user owns through companies
|
|
from models.database.company import Company
|
|
|
|
owned_vendor_ids = (
|
|
db.query(Vendor.id)
|
|
.join(Company)
|
|
.filter(Company.owner_user_id == current_user.id)
|
|
.subquery()
|
|
)
|
|
query = query.filter(
|
|
(Vendor.is_active == True)
|
|
& ((Vendor.is_verified == True) | (Vendor.id.in_(owned_vendor_ids)))
|
|
)
|
|
else:
|
|
# Admin can apply filters
|
|
if active_only:
|
|
query = query.filter(Vendor.is_active == True)
|
|
if verified_only:
|
|
query = query.filter(Vendor.is_verified == True)
|
|
|
|
total = query.count()
|
|
vendors = query.offset(skip).limit(limit).all()
|
|
|
|
return vendors, total
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting vendors: {str(e)}")
|
|
raise ValidationException("Failed to retrieve vendors")
|
|
|
|
def get_vendor_by_code(
|
|
self, db: Session, vendor_code: str, current_user: User
|
|
) -> Vendor:
|
|
"""
|
|
Get vendor by vendor code with access control.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_code: Vendor code to find
|
|
current_user: Current user requesting the vendor
|
|
|
|
Returns:
|
|
Vendor object
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
UnauthorizedVendorAccessException: If access denied
|
|
"""
|
|
try:
|
|
vendor = (
|
|
db.query(Vendor)
|
|
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
|
|
.first()
|
|
)
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(vendor_code)
|
|
|
|
# Check access permissions
|
|
if not self._can_access_vendor(vendor, current_user):
|
|
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
|
|
|
|
return vendor
|
|
|
|
except (VendorNotFoundException, UnauthorizedVendorAccessException):
|
|
raise # Re-raise custom exceptions
|
|
except Exception as e:
|
|
logger.error(f"Error getting vendor {vendor_code}: {str(e)}")
|
|
raise ValidationException("Failed to retrieve vendor")
|
|
|
|
def get_vendor_by_id(self, db: Session, vendor_id: int) -> Vendor:
|
|
"""
|
|
Get vendor by ID (admin use - no access control).
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID to find
|
|
|
|
Returns:
|
|
Vendor object with company and owner loaded
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from models.database.company import Company
|
|
|
|
vendor = (
|
|
db.query(Vendor)
|
|
.options(joinedload(Vendor.company).joinedload(Company.owner))
|
|
.filter(Vendor.id == vendor_id)
|
|
.first()
|
|
)
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
|
|
|
return vendor
|
|
|
|
def get_active_vendor_by_code(self, db: Session, vendor_code: str) -> Vendor:
|
|
"""
|
|
Get active vendor by vendor_code for public access (no auth required).
|
|
|
|
This method is specifically designed for public endpoints where:
|
|
- No authentication is required
|
|
- Only active vendors should be returned
|
|
- Inactive/disabled vendors are hidden
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_code: Vendor code (case-insensitive)
|
|
|
|
Returns:
|
|
Vendor object with company and owner loaded
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found or inactive
|
|
"""
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from models.database.company import Company
|
|
|
|
vendor = (
|
|
db.query(Vendor)
|
|
.options(joinedload(Vendor.company).joinedload(Company.owner))
|
|
.filter(
|
|
func.upper(Vendor.vendor_code) == vendor_code.upper(),
|
|
Vendor.is_active == True,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not vendor:
|
|
logger.warning(f"Vendor not found or inactive: {vendor_code}")
|
|
raise VendorNotFoundException(vendor_code, identifier_type="code")
|
|
|
|
return vendor
|
|
|
|
def get_vendor_by_identifier(self, db: Session, identifier: str) -> Vendor:
|
|
"""
|
|
Get vendor by ID or vendor_code (admin use - no access control).
|
|
|
|
Args:
|
|
db: Database session
|
|
identifier: Either vendor ID (int as string) or vendor_code (string)
|
|
|
|
Returns:
|
|
Vendor object with company and owner loaded
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from models.database.company import Company
|
|
|
|
# Try as integer ID first
|
|
try:
|
|
vendor_id = int(identifier)
|
|
return self.get_vendor_by_id(db, vendor_id)
|
|
except (ValueError, TypeError):
|
|
pass # Not an integer, treat as vendor_code
|
|
except VendorNotFoundException:
|
|
pass # ID not found, try as vendor_code
|
|
|
|
# Try as vendor_code (case-insensitive)
|
|
vendor = (
|
|
db.query(Vendor)
|
|
.options(joinedload(Vendor.company).joinedload(Company.owner))
|
|
.filter(func.upper(Vendor.vendor_code) == identifier.upper())
|
|
.first()
|
|
)
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(identifier, identifier_type="code")
|
|
|
|
return vendor
|
|
|
|
def toggle_verification(self, db: Session, vendor_id: int) -> tuple[Vendor, str]:
|
|
"""
|
|
Toggle vendor verification status.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
|
|
Returns:
|
|
Tuple of (updated vendor, status message)
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
vendor.is_verified = not vendor.is_verified
|
|
# No commit here - endpoint handles transaction
|
|
|
|
status = "verified" if vendor.is_verified else "unverified"
|
|
logger.info(f"Vendor {vendor.vendor_code} {status}")
|
|
return vendor, f"Vendor {vendor.vendor_code} is now {status}"
|
|
|
|
def set_verification(
|
|
self, db: Session, vendor_id: int, is_verified: bool
|
|
) -> tuple[Vendor, str]:
|
|
"""
|
|
Set vendor verification status to specific value.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
is_verified: Target verification status
|
|
|
|
Returns:
|
|
Tuple of (updated vendor, status message)
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
vendor.is_verified = is_verified
|
|
# No commit here - endpoint handles transaction
|
|
|
|
status = "verified" if is_verified else "unverified"
|
|
logger.info(f"Vendor {vendor.vendor_code} set to {status}")
|
|
return vendor, f"Vendor {vendor.vendor_code} is now {status}"
|
|
|
|
def toggle_status(self, db: Session, vendor_id: int) -> tuple[Vendor, str]:
|
|
"""
|
|
Toggle vendor active status.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
|
|
Returns:
|
|
Tuple of (updated vendor, status message)
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
vendor.is_active = not vendor.is_active
|
|
# No commit here - endpoint handles transaction
|
|
|
|
status = "active" if vendor.is_active else "inactive"
|
|
logger.info(f"Vendor {vendor.vendor_code} {status}")
|
|
return vendor, f"Vendor {vendor.vendor_code} is now {status}"
|
|
|
|
def set_status(
|
|
self, db: Session, vendor_id: int, is_active: bool
|
|
) -> tuple[Vendor, str]:
|
|
"""
|
|
Set vendor active status to specific value.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Vendor ID
|
|
is_active: Target active status
|
|
|
|
Returns:
|
|
Tuple of (updated vendor, status message)
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
"""
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
vendor.is_active = is_active
|
|
# No commit here - endpoint handles transaction
|
|
|
|
status = "active" if is_active else "inactive"
|
|
logger.info(f"Vendor {vendor.vendor_code} set to {status}")
|
|
return vendor, f"Vendor {vendor.vendor_code} is now {status}"
|
|
|
|
def add_product_to_catalog(
|
|
self, db: Session, vendor: Vendor, product: ProductCreate
|
|
) -> Product:
|
|
"""
|
|
Add existing product to vendor catalog with vendor -specific settings.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor : Vendor to add product to
|
|
product: Vendor product data
|
|
|
|
Returns:
|
|
Created Product object
|
|
|
|
Raises:
|
|
MarketplaceProductNotFoundException: If product not found
|
|
ProductAlreadyExistsException: If product already in vendor
|
|
"""
|
|
try:
|
|
# Check if product exists
|
|
marketplace_product = self._get_product_by_id_or_raise(
|
|
db, product.marketplace_product_id
|
|
)
|
|
|
|
# Check if product already in vendor
|
|
if self._product_in_catalog(db, vendor.id, marketplace_product.id):
|
|
raise ProductAlreadyExistsException(
|
|
vendor.vendor_code, product.marketplace_product_id
|
|
)
|
|
|
|
# Create vendor-product association
|
|
new_product = Product(
|
|
vendor_id=vendor.id,
|
|
marketplace_product_id=marketplace_product.id,
|
|
**product.model_dump(exclude={"marketplace_product_id"}),
|
|
)
|
|
|
|
db.add(new_product)
|
|
db.flush() # Get ID without committing - endpoint handles commit
|
|
|
|
logger.info(
|
|
f"MarketplaceProduct {product.marketplace_product_id} added to vendor {vendor.vendor_code}"
|
|
)
|
|
return new_product
|
|
|
|
except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
|
|
raise # Re-raise custom exceptions - endpoint handles rollback
|
|
except Exception as e:
|
|
logger.error(f"Error adding product to vendor : {str(e)}")
|
|
raise ValidationException("Failed to add product to vendor ")
|
|
|
|
def get_products(
|
|
self,
|
|
db: Session,
|
|
vendor: Vendor,
|
|
current_user: User,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
active_only: bool = True,
|
|
featured_only: bool = False,
|
|
) -> tuple[list[Product], int]:
|
|
"""
|
|
Get products in vendor catalog with filtering.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor : Vendor to get products from
|
|
current_user: Current user requesting products
|
|
skip: Number of records to skip
|
|
limit: Maximum number of records to return
|
|
active_only: Filter for active products only
|
|
featured_only: Filter for featured products only
|
|
|
|
Returns:
|
|
Tuple of (products_list, total_count)
|
|
|
|
Raises:
|
|
UnauthorizedVendorAccessException: If vendor access denied
|
|
"""
|
|
try:
|
|
# Check access permissions
|
|
if not self._can_access_vendor(vendor, current_user):
|
|
raise UnauthorizedVendorAccessException(
|
|
vendor.vendor_code, current_user.id
|
|
)
|
|
|
|
# Query vendor products
|
|
query = db.query(Product).filter(Product.vendor_id == vendor.id)
|
|
|
|
if active_only:
|
|
query = query.filter(Product.is_active == True)
|
|
if featured_only:
|
|
query = query.filter(Product.is_featured == True)
|
|
|
|
total = query.count()
|
|
products = query.offset(skip).limit(limit).all()
|
|
|
|
return products, total
|
|
|
|
except UnauthorizedVendorAccessException:
|
|
raise # Re-raise custom exceptions
|
|
except Exception as e:
|
|
logger.error(f"Error getting vendor products: {str(e)}")
|
|
raise ValidationException("Failed to retrieve vendor products")
|
|
|
|
# Private helper methods
|
|
def _vendor_code_exists(self, db: Session, vendor_code: str) -> bool:
|
|
"""Check if vendor code already exists (case-insensitive)."""
|
|
return (
|
|
db.query(Vendor)
|
|
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
|
|
.first()
|
|
is not None
|
|
)
|
|
|
|
def _get_product_by_id_or_raise(
|
|
self, db: Session, marketplace_product_id: int
|
|
) -> MarketplaceProduct:
|
|
"""Get marketplace product by database ID or raise exception."""
|
|
product = (
|
|
db.query(MarketplaceProduct)
|
|
.filter(MarketplaceProduct.id == marketplace_product_id)
|
|
.first()
|
|
)
|
|
if not product:
|
|
raise MarketplaceProductNotFoundException(str(marketplace_product_id))
|
|
return product
|
|
|
|
def _product_in_catalog(
|
|
self, db: Session, vendor_id: int, marketplace_product_id: int
|
|
) -> bool:
|
|
"""Check if product is already in vendor."""
|
|
return (
|
|
db.query(Product)
|
|
.filter(
|
|
Product.vendor_id == vendor_id,
|
|
Product.marketplace_product_id == marketplace_product_id,
|
|
)
|
|
.first()
|
|
is not None
|
|
)
|
|
|
|
def _can_access_vendor(self, vendor: Vendor, user: User) -> bool:
|
|
"""Check if user can access vendor."""
|
|
# Admins can always access
|
|
if user.role == "admin":
|
|
return True
|
|
|
|
# Company owners can access their vendors
|
|
if vendor.company and vendor.company.owner_user_id == user.id:
|
|
return True
|
|
|
|
# Others can only access active and verified vendors
|
|
return vendor.is_active and vendor.is_verified
|
|
|
|
def _is_vendor_owner(self, vendor: Vendor, user: User) -> bool:
|
|
"""Check if user is vendor owner (via company ownership)."""
|
|
return vendor.company and vendor.company.owner_user_id == user.id
|
|
|
|
def can_update_vendor(self, vendor: Vendor, user: User) -> bool:
|
|
"""
|
|
Check if user has permission to update vendor settings.
|
|
|
|
Permission granted to:
|
|
- Admins (always)
|
|
- Vendor owners (company owner)
|
|
- Team members with appropriate role (owner role in VendorUser)
|
|
"""
|
|
# Admins can always update
|
|
if user.role == "admin":
|
|
return True
|
|
|
|
# Check if user is vendor owner via company
|
|
if self._is_vendor_owner(vendor, user):
|
|
return True
|
|
|
|
# Check if user is owner via VendorUser relationship
|
|
if user.is_owner_of(vendor.id):
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_vendor(
|
|
self,
|
|
db: Session,
|
|
vendor_id: int,
|
|
vendor_update,
|
|
current_user: User,
|
|
) -> "Vendor":
|
|
"""
|
|
Update vendor profile with permission checking.
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
InsufficientPermissionsException: If user lacks permission
|
|
"""
|
|
from app.exceptions import InsufficientPermissionsException
|
|
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
|
|
# Check permissions in service layer
|
|
if not self.can_update_vendor(vendor, current_user):
|
|
raise InsufficientPermissionsException(
|
|
required_permission="vendor:profile:update"
|
|
)
|
|
|
|
# Apply updates
|
|
update_data = vendor_update.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
if hasattr(vendor, field):
|
|
setattr(vendor, field, value)
|
|
|
|
db.add(vendor)
|
|
db.flush()
|
|
db.refresh(vendor)
|
|
return vendor
|
|
|
|
def update_marketplace_settings(
|
|
self,
|
|
db: Session,
|
|
vendor_id: int,
|
|
marketplace_config: dict,
|
|
current_user: User,
|
|
) -> dict:
|
|
"""
|
|
Update marketplace integration settings with permission checking.
|
|
|
|
Raises:
|
|
VendorNotFoundException: If vendor not found
|
|
InsufficientPermissionsException: If user lacks permission
|
|
"""
|
|
from app.exceptions import InsufficientPermissionsException
|
|
|
|
vendor = self.get_vendor_by_id(db, vendor_id)
|
|
|
|
# Check permissions in service layer
|
|
if not self.can_update_vendor(vendor, current_user):
|
|
raise InsufficientPermissionsException(
|
|
required_permission="vendor:settings:update"
|
|
)
|
|
|
|
# Update Letzshop URLs
|
|
if "letzshop_csv_url_fr" in marketplace_config:
|
|
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
|
if "letzshop_csv_url_en" in marketplace_config:
|
|
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
|
if "letzshop_csv_url_de" in marketplace_config:
|
|
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
|
|
|
db.add(vendor)
|
|
db.flush()
|
|
db.refresh(vendor)
|
|
|
|
return {
|
|
"message": "Marketplace settings updated successfully",
|
|
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
|
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
|
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
|
}
|
|
|
|
|
|
# Create service instance following the same pattern as other services
|
|
vendor_service = VendorService()
|