# app/modules/tenancy/routes/api/merchant.py """ Tenancy module merchant API routes. Aggregates all merchant tenancy routes: - /auth/* - Merchant authentication (login, logout, /me) - /account/* - Merchant account management (stores, profile) Auto-discovered by the route system (merchant.py in routes/api/). """ import logging from fastapi import APIRouter, Depends, Query, Request from sqlalchemy.orm import Session from app.api.deps import get_current_merchant_api, get_merchant_for_current_user from app.core.database import get_db from app.modules.tenancy.schemas import ( MerchantPortalProfileResponse, MerchantPortalProfileUpdate, MerchantPortalStoreListResponse, MerchantStoreCreate, MerchantStoreDetailResponse, MerchantStoreUpdate, ) from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.services.merchant_service import merchant_service from app.modules.tenancy.services.merchant_store_service import merchant_store_service from .email_verification import email_verification_api_router from .merchant_auth import merchant_auth_router from .user_account import merchant_account_router logger = logging.getLogger(__name__) router = APIRouter() # Include auth routes (/auth/login, /auth/logout, /auth/me, /auth/forgot-password, /auth/reset-password) router.include_router(merchant_auth_router, tags=["merchant-auth"]) # Include email verification routes (/resend-verification) router.include_router(email_verification_api_router, tags=["email-verification"]) # Account routes are defined below with /account prefix _account_router = APIRouter(prefix="/account") # ============================================================================ # ACCOUNT ENDPOINTS # ============================================================================ @_account_router.get("/stores", response_model=MerchantPortalStoreListResponse) async def merchant_stores( request: Request, skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=200, description="Max records to return"), merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ List all stores belonging to the merchant. Returns a paginated list of store summaries for the authenticated merchant. """ stores, total = merchant_service.get_merchant_stores( db, merchant.id, skip=skip, limit=limit ) can_create, _ = merchant_store_service.can_create_store(db, merchant.id) return MerchantPortalStoreListResponse( stores=stores, total=total, skip=skip, limit=limit, can_create_store=can_create, ) @_account_router.post("/stores", response_model=MerchantStoreDetailResponse) async def create_merchant_store( store_data: MerchantStoreCreate, current_user: UserContext = Depends(get_current_merchant_api), merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Create a new store under the merchant. Checks subscription tier store limits before creation. New stores are created with is_active=True, is_verified=False. """ # Service raises MaxStoresReachedException, StoreAlreadyExistsException, # or StoreValidationException — all handled by global exception handler. result = merchant_store_service.create_store( db, merchant.id, store_data.model_dump(), ) db.commit() logger.info( f"Merchant {merchant.id} ({current_user.username}) created store " f"'{store_data.store_code}'" ) return result @_account_router.get("/stores/{store_id}", response_model=MerchantStoreDetailResponse) async def get_merchant_store_detail( store_id: int, merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Get detailed store information with ownership validation. Returns store details including platform assignments. """ # StoreNotFoundException handled by global exception handler return merchant_store_service.get_store_detail(db, merchant.id, store_id) @_account_router.put("/stores/{store_id}", response_model=MerchantStoreDetailResponse) async def update_merchant_store( store_id: int, update_data: MerchantStoreUpdate, current_user: UserContext = Depends(get_current_merchant_api), merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Update store details (merchant-allowed fields only). Only name, description, and contact info fields can be updated. """ # StoreNotFoundException handled by global exception handler result = merchant_store_service.update_store( db, merchant.id, store_id, update_data.model_dump(exclude_unset=True), ) db.commit() logger.info( f"Merchant {merchant.id} ({current_user.username}) updated store {store_id}" ) return result @_account_router.get("/platforms") async def get_merchant_platforms( merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Get platforms available for the merchant (from active subscriptions). Used by the store creation/edit UI to show platform selection options. """ return merchant_store_service.get_subscribed_platform_ids(db, merchant.id) @_account_router.get("/team") async def merchant_team_overview( merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Get team members across all stores owned by the merchant. Returns a list of stores with their team members grouped by store. """ return merchant_store_service.get_merchant_team_overview(db, merchant.id) @_account_router.get("/profile", response_model=MerchantPortalProfileResponse) async def merchant_profile( request: Request, merchant=Depends(get_merchant_for_current_user), ): """ Get the authenticated merchant's profile information. Returns merchant details including contact info, business details, and verification status. """ return merchant @_account_router.put("/profile", response_model=MerchantPortalProfileResponse) async def update_merchant_profile( request: Request, profile_data: MerchantPortalProfileUpdate, current_user: UserContext = Depends(get_current_merchant_api), merchant=Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): """ Update the authenticated merchant's profile information. Accepts partial updates - only provided fields are changed. """ # Apply only the fields that were explicitly provided update_data = profile_data.model_dump(exclude_unset=True) for field_name, value in update_data.items(): setattr(merchant, field_name, value) db.commit() db.refresh(merchant) logger.info( f"Merchant profile updated: merchant_id={merchant.id}, " f"user={current_user.username}, fields={list(update_data.keys())}" ) return merchant # Include account routes in main router router.include_router(_account_router, tags=["merchant-account"]) # Include self-service user account routes router.include_router(merchant_account_router, tags=["merchant-user-account"])