refactor: move letzshop endpoints to marketplace module and add vendor service tests
Move letzshop-related functionality from tenancy to marketplace module: - Move admin letzshop routes to marketplace/routes/api/admin_letzshop.py - Move letzshop schemas to marketplace/schemas/letzshop.py - Remove letzshop code from tenancy module (admin_vendors, vendor_service) - Update model exports and imports Add comprehensive unit tests for vendor services: - test_company_service.py: Company management operations - test_platform_service.py: Platform management operations - test_vendor_domain_service.py: Vendor domain operations - test_vendor_team_service.py: Vendor team management Update module definitions: - billing, messaging, payments: Minor definition updates Add architecture proposals documentation: - Module dependency redesign session notes - Decouple modules implementation plan - Module decoupling proposal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
# app/modules/tenancy/services/admin_service.py
|
||||
"""
|
||||
Admin service for managing users, vendors, and import jobs.
|
||||
Admin service for managing users and vendors.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- User management and status control
|
||||
- Vendor creation with owner user generation
|
||||
- Vendor verification and activation
|
||||
- Marketplace import job monitoring
|
||||
- Platform statistics
|
||||
|
||||
Note: Marketplace import job monitoring has been moved to the marketplace module.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -33,11 +34,9 @@ from app.modules.tenancy.exceptions import (
|
||||
)
|
||||
from middleware.auth import AuthManager
|
||||
from app.modules.tenancy.models import Company
|
||||
from app.modules.marketplace.models import MarketplaceImportJob
|
||||
from app.modules.tenancy.models import Platform
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Role, Vendor
|
||||
from app.modules.marketplace.schemas import MarketplaceImportJobResponse
|
||||
from app.modules.tenancy.schemas.vendor import VendorCreate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -703,48 +702,8 @@ class AdminService:
|
||||
# NOTE: Vendor ownership transfer is now handled at the Company level.
|
||||
# Use company_service.transfer_ownership() instead.
|
||||
|
||||
# ============================================================================
|
||||
# MARKETPLACE IMPORT JOBS
|
||||
# ============================================================================
|
||||
|
||||
def get_marketplace_import_jobs(
|
||||
self,
|
||||
db: Session,
|
||||
marketplace: str | None = None,
|
||||
vendor_name: str | None = None,
|
||||
status: str | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> list[MarketplaceImportJobResponse]:
|
||||
"""Get filtered and paginated marketplace import jobs."""
|
||||
try:
|
||||
query = db.query(MarketplaceImportJob)
|
||||
|
||||
if marketplace:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
|
||||
)
|
||||
if vendor_name:
|
||||
query = query.filter(
|
||||
MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%")
|
||||
)
|
||||
if status:
|
||||
query = query.filter(MarketplaceImportJob.status == status)
|
||||
|
||||
jobs = (
|
||||
query.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [self._convert_job_to_response(job) for job in jobs]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve marketplace import jobs: {str(e)}")
|
||||
raise AdminOperationException(
|
||||
operation="get_marketplace_import_jobs", reason="Database query failed"
|
||||
)
|
||||
# NOTE: Marketplace import job operations have been moved to the marketplace module.
|
||||
# Use app.modules.marketplace routes for import job management.
|
||||
|
||||
# ============================================================================
|
||||
# STATISTICS
|
||||
@@ -773,30 +732,7 @@ class AdminService:
|
||||
logger.error(f"Failed to get recent vendors: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_recent_import_jobs(self, db: Session, limit: int = 10) -> list[dict]:
|
||||
"""Get recent marketplace import jobs."""
|
||||
try:
|
||||
jobs = (
|
||||
db.query(MarketplaceImportJob)
|
||||
.order_by(MarketplaceImportJob.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": j.id,
|
||||
"marketplace": j.marketplace,
|
||||
"vendor_name": j.vendor_name,
|
||||
"status": j.status,
|
||||
"total_processed": j.total_processed or 0,
|
||||
"created_at": j.created_at,
|
||||
}
|
||||
for j in jobs
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get recent import jobs: {str(e)}")
|
||||
return []
|
||||
# NOTE: get_recent_import_jobs has been moved to the marketplace module
|
||||
|
||||
# ============================================================================
|
||||
# PRIVATE HELPER METHODS
|
||||
@@ -868,28 +804,6 @@ class AdminService:
|
||||
)
|
||||
db.add(role)
|
||||
|
||||
def _convert_job_to_response(
|
||||
self, job: MarketplaceImportJob
|
||||
) -> MarketplaceImportJobResponse:
|
||||
"""Convert database model to response schema."""
|
||||
return MarketplaceImportJobResponse(
|
||||
job_id=job.id,
|
||||
status=job.status,
|
||||
marketplace=job.marketplace,
|
||||
source_url=job.source_url,
|
||||
vendor_id=job.vendor.id if job.vendor else None,
|
||||
vendor_code=job.vendor.vendor_code if job.vendor else None,
|
||||
vendor_name=job.vendor.name if job.vendor else None,
|
||||
imported=job.imported_count or 0,
|
||||
updated=job.updated_count or 0,
|
||||
total_processed=job.total_processed or 0,
|
||||
error_count=job.error_count or 0,
|
||||
error_message=job.error_message,
|
||||
created_at=job.created_at,
|
||||
started_at=job.started_at,
|
||||
completed_at=job.completed_at,
|
||||
)
|
||||
|
||||
|
||||
# Create service instance
|
||||
admin_service = AdminService()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# app/modules/tenancy/services/vendor_service.py
|
||||
"""
|
||||
Vendor service for managing vendor operations and product catalog.
|
||||
Vendor service for managing vendor operations.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Vendor creation and management
|
||||
- Vendor access control and validation
|
||||
- Vendor product catalog operations
|
||||
- Vendor filtering and search
|
||||
|
||||
Note: Product catalog operations have been moved to app.modules.catalog.services.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -15,19 +16,14 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import ValidationException
|
||||
from app.modules.catalog.exceptions import ProductAlreadyExistsException
|
||||
from app.modules.marketplace.exceptions import MarketplaceProductNotFoundException
|
||||
from app.modules.tenancy.exceptions import (
|
||||
InvalidVendorDataException,
|
||||
UnauthorizedVendorAccessException,
|
||||
VendorAlreadyExistsException,
|
||||
VendorNotFoundException,
|
||||
)
|
||||
from app.modules.marketplace.models import MarketplaceProduct
|
||||
from app.modules.catalog.models import Product
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Vendor
|
||||
from app.modules.catalog.schemas import ProductCreate
|
||||
from app.modules.tenancy.schemas.vendor import VendorCreate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -443,110 +439,10 @@ class VendorService:
|
||||
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")
|
||||
# NOTE: Product catalog operations have been moved to catalog module.
|
||||
# Use app.modules.catalog.services.product_service instead.
|
||||
# - add_product_to_catalog -> product_service.create_product
|
||||
# - get_products -> product_service.get_vendor_products
|
||||
|
||||
# Private helper methods
|
||||
def _vendor_code_exists(self, db: Session, vendor_code: str) -> bool:
|
||||
@@ -558,33 +454,6 @@ class VendorService:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user