# app/modules/tenancy/routes/api/admin_stores.py """ Store management endpoints for admin. Architecture Notes: - All business logic is in store_service (no direct DB operations here) - Uses domain exceptions from app/exceptions/store.py - Exception handler middleware converts domain exceptions to HTTP responses """ import logging from fastapi import APIRouter, Body, Depends, Path, Query from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.modules.tenancy.exceptions import ConfirmationRequiredException from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.schemas.store import ( StoreCreate, StoreCreateResponse, StoreDetailResponse, StoreListResponse, StoreStatsResponse, StoreUpdate, ) from app.modules.tenancy.services.admin_service import admin_service from app.modules.tenancy.services.store_service import store_service admin_stores_router = APIRouter(prefix="/stores") logger = logging.getLogger(__name__) @admin_stores_router.post("", response_model=StoreCreateResponse) def create_store( store_data: StoreCreate, db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Create a new store (storefront/brand) under an existing merchant (Admin only). This endpoint: 1. Validates that the parent merchant exists 2. Creates a new store record linked to the merchant 3. Sets up default roles (Owner, Manager, Editor, Viewer) The store inherits owner and contact information from its parent merchant. """ store = admin_service.create_store(db=db, store_data=store_data) db.commit() return StoreCreateResponse( # Store fields id=store.id, store_code=store.store_code, subdomain=store.subdomain, name=store.name, description=store.description, merchant_id=store.merchant_id, letzshop_csv_url_fr=store.letzshop_csv_url_fr, letzshop_csv_url_en=store.letzshop_csv_url_en, letzshop_csv_url_de=store.letzshop_csv_url_de, is_active=store.is_active, is_verified=store.is_verified, created_at=store.created_at, updated_at=store.updated_at, # Merchant info merchant_name=store.merchant.name, merchant_contact_email=store.merchant.contact_email, merchant_contact_phone=store.merchant.contact_phone, merchant_website=store.merchant.website, # Owner info (from merchant) owner_user_id=store.merchant.owner.id, owner_email=store.merchant.owner.email, owner_username=store.merchant.owner.username, login_url=f"http://localhost:8000/store/{store.subdomain}/login", ) @admin_stores_router.get("", response_model=StoreListResponse) def get_all_stores_admin( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), search: str | None = Query(None, description="Search by name or store code"), is_active: bool | None = Query(None), is_verified: bool | None = Query(None), merchant_id: int | None = Query(None, description="Filter by merchant ID"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """Get all stores with filtering (Admin only).""" stores, total = admin_service.get_all_stores( db=db, skip=skip, limit=limit, search=search, is_active=is_active, is_verified=is_verified, merchant_id=merchant_id, ) return StoreListResponse(stores=stores, total=total, skip=skip, limit=limit) @admin_stores_router.get("/stats", response_model=StoreStatsResponse) def get_store_statistics_endpoint( db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """Get store statistics for admin dashboard (Admin only).""" stats = admin_service.get_store_statistics(db) return StoreStatsResponse( total=stats["total"], verified=stats["verified"], pending=stats["pending"], inactive=stats["inactive"], ) def _build_store_detail_response(store) -> StoreDetailResponse: """ Helper to build StoreDetailResponse with resolved contact info. Contact fields are resolved using store override or merchant fallback. Inheritance flags indicate if value comes from merchant. """ contact_info = store.get_contact_info_with_inheritance() return StoreDetailResponse( # Store fields id=store.id, store_code=store.store_code, subdomain=store.subdomain, name=store.name, description=store.description, merchant_id=store.merchant_id, letzshop_csv_url_fr=store.letzshop_csv_url_fr, letzshop_csv_url_en=store.letzshop_csv_url_en, letzshop_csv_url_de=store.letzshop_csv_url_de, is_active=store.is_active, is_verified=store.is_verified, created_at=store.created_at, updated_at=store.updated_at, # Merchant info merchant_name=store.merchant.name, # Owner details (from merchant) owner_user_id=store.merchant.owner_user_id, owner_email=store.merchant.owner.email, owner_username=store.merchant.owner.username, # Resolved contact info with inheritance flags **contact_info, # Original merchant values for UI reference merchant_contact_email=store.merchant.contact_email, merchant_contact_phone=store.merchant.contact_phone, merchant_website=store.merchant.website, merchant_business_address=store.merchant.business_address, merchant_tax_number=store.merchant.tax_number, ) @admin_stores_router.get("/{store_identifier}", response_model=StoreDetailResponse) def get_store_details( store_identifier: str = Path(..., description="Store ID or store_code"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Get detailed store information including merchant and owner details (Admin only). Accepts either store ID (integer) or store_code (string). Returns store info with merchant contact details, owner info, and resolved contact fields (store override or merchant default). Raises: StoreNotFoundException: If store not found (404) """ store = store_service.get_store_by_identifier(db, store_identifier) return _build_store_detail_response(store) @admin_stores_router.put("/{store_identifier}", response_model=StoreDetailResponse) def update_store( store_identifier: str = Path(..., description="Store ID or store_code"), store_update: StoreUpdate = Body(...), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Update store information (Admin only). Accepts either store ID (integer) or store_code (string). **Can update:** - Basic info: name, description, subdomain - Marketplace URLs - Status: is_active, is_verified - Contact info: contact_email, contact_phone, website, business_address, tax_number (these override merchant defaults; set to empty to reset to inherit) **Cannot update:** - `store_code` (immutable) Raises: StoreNotFoundException: If store not found (404) """ store = store_service.get_store_by_identifier(db, store_identifier) store = admin_service.update_store(db, store.id, store_update) db.commit() return _build_store_detail_response(store) # NOTE: Ownership transfer is handled at the Merchant level. # Use PUT /api/v1/admin/merchants/{id}/transfer-ownership instead. @admin_stores_router.put("/{store_identifier}/verification", response_model=StoreDetailResponse) def toggle_store_verification( store_identifier: str = Path(..., description="Store ID or store_code"), verification_data: dict = Body(..., example={"is_verified": True}), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Set store verification status (Admin only). Accepts either store ID (integer) or store_code (string). Request body: { "is_verified": true/false } Raises: StoreNotFoundException: If store not found (404) """ store = store_service.get_store_by_identifier(db, store_identifier) if "is_verified" in verification_data: store, message = store_service.set_verification( db, store.id, verification_data["is_verified"] ) db.commit() # ✅ ARCH: Commit at API level for transaction control logger.info(f"Store verification updated: {message}") return _build_store_detail_response(store) @admin_stores_router.put("/{store_identifier}/status", response_model=StoreDetailResponse) def toggle_store_status( store_identifier: str = Path(..., description="Store ID or store_code"), status_data: dict = Body(..., example={"is_active": True}), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Set store active status (Admin only). Accepts either store ID (integer) or store_code (string). Request body: { "is_active": true/false } Raises: StoreNotFoundException: If store not found (404) """ store = store_service.get_store_by_identifier(db, store_identifier) if "is_active" in status_data: store, message = store_service.set_status( db, store.id, status_data["is_active"] ) db.commit() # ✅ ARCH: Commit at API level for transaction control logger.info(f"Store status updated: {message}") return _build_store_detail_response(store) @admin_stores_router.delete("/{store_identifier}") def delete_store( store_identifier: str = Path(..., description="Store ID or store_code"), confirm: bool = Query(False, description="Must be true to confirm deletion"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Delete store and all associated data (Admin only). Accepts either store ID (integer) or store_code (string). ⚠️ **WARNING: This is destructive and will delete:** - Store account - All products - All orders - All customers - All team members Requires confirmation parameter: `confirm=true` Raises: ConfirmationRequiredException: If confirm=true not provided (400) StoreNotFoundException: If store not found (404) """ if not confirm: raise ConfirmationRequiredException( operation="delete_store", message="Deletion requires confirmation parameter: confirm=true", ) store = store_service.get_store_by_identifier(db, store_identifier) message = admin_service.delete_store(db, store.id) db.commit() return {"message": message}