# app/modules/tenancy/routes/api/admin_merchants.py """ Merchant management endpoints for admin. """ import logging from datetime import UTC, datetime from fastapi import APIRouter, Body, Depends, Path, Query, Request from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.core.environment import should_use_secure_cookies from app.modules.tenancy.exceptions import ( ConfirmationRequiredException, MerchantHasStoresException, ) from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.schemas.merchant import ( MerchantCreate, MerchantCreateResponse, MerchantDetailResponse, MerchantListResponse, MerchantResponse, MerchantTransferOwnership, MerchantTransferOwnershipResponse, MerchantUpdate, ) from app.modules.tenancy.services.merchant_service import merchant_service admin_merchants_router = APIRouter(prefix="/merchants") logger = logging.getLogger(__name__) @admin_merchants_router.post("", response_model=MerchantCreateResponse) def create_merchant_with_owner( request: Request, merchant_data: MerchantCreate, db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Create a new merchant with owner user account (Admin only). This endpoint: 1. Creates a new merchant record 2. Creates an owner user account with owner_email (if not exists) 3. Sends email verification + welcome email to owner 4. Returns credentials (temporary password shown ONCE if new user created) **Email Fields:** - `owner_email`: Used for owner's login/authentication (stored in users.email) - `contact_email`: Public business contact (stored in merchants.contact_email) Returns merchant details with owner credentials. """ merchant, owner_user, temp_password = merchant_service.create_merchant_with_owner( db, merchant_data ) db.commit() # ✅ ARCH: Commit at API level for transaction control # Send verification email to new owner (only for newly created users) if temp_password: try: from app.modules.messaging.services.email_service import EmailService from app.modules.tenancy.models import EmailVerificationToken plaintext_token = EmailVerificationToken.create_for_user(db, owner_user.id) scheme = "https" if should_use_secure_cookies() else "http" host = request.headers.get("host", "localhost:8000") verification_link = f"{scheme}://{host}/verify-email?token={plaintext_token}" email_service = EmailService(db) email_service.send_template( template_code="email_verification", to_email=owner_user.email, to_name=owner_user.username, language="en", variables={ "first_name": owner_user.username, "verification_link": verification_link, "expiry_hours": str(EmailVerificationToken.TOKEN_EXPIRY_HOURS), "platform_name": "Orion", }, ) db.commit() logger.info(f"Verification email sent to {owner_user.email}") except Exception as e: db.rollback() logger.error(f"Failed to send verification email: {e}") # noqa: SEC021 return MerchantCreateResponse( merchant=MerchantResponse( id=merchant.id, name=merchant.name, description=merchant.description, owner_user_id=merchant.owner_user_id, contact_email=merchant.contact_email, contact_phone=merchant.contact_phone, website=merchant.website, business_address=merchant.business_address, tax_number=merchant.tax_number, is_active=merchant.is_active, is_verified=merchant.is_verified, created_at=merchant.created_at.isoformat(), updated_at=merchant.updated_at.isoformat(), ), owner_user_id=owner_user.id, owner_username=owner_user.username, owner_email=owner_user.email, temporary_password=temp_password or "N/A (Existing user)", login_url="http://localhost:8000/admin/login", ) @admin_merchants_router.get("", response_model=MerchantListResponse) def get_all_merchants( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), search: str | None = Query(None, description="Search by merchant name"), is_active: bool | None = Query(None), is_verified: bool | None = Query(None), include_deleted: bool = Query(False, description="Include soft-deleted merchants"), only_deleted: bool = Query(False, description="Show only soft-deleted merchants (trash view)"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """Get all merchants with filtering (Admin only).""" merchants, total = merchant_service.get_merchants( db, skip=skip, limit=limit, search=search, is_active=is_active, is_verified=is_verified, include_deleted=include_deleted, only_deleted=only_deleted, ) return MerchantListResponse( merchants=[ MerchantResponse( id=c.id, name=c.name, description=c.description, owner_user_id=c.owner_user_id, owner_email=c.owner.email if c.owner else None, contact_email=c.contact_email, contact_phone=c.contact_phone, website=c.website, business_address=c.business_address, tax_number=c.tax_number, is_active=c.is_active, is_verified=c.is_verified, created_at=c.created_at.isoformat(), updated_at=c.updated_at.isoformat(), store_count=c.store_count, ) for c in merchants ], total=total, skip=skip, limit=limit, ) @admin_merchants_router.get("/{merchant_id}", response_model=MerchantDetailResponse) def get_merchant_details( merchant_id: int = Path(..., description="Merchant ID"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Get detailed merchant information including store counts (Admin only). """ merchant = merchant_service.get_merchant_by_id(db, merchant_id) # Count stores store_count = len(merchant.stores) active_store_count = sum(1 for v in merchant.stores if v.is_active) # Build stores list for detail view stores_list = [ { "id": v.id, "store_code": v.store_code, "name": v.name, "subdomain": v.subdomain, "is_active": v.is_active, "is_verified": v.is_verified, } for v in merchant.stores ] return MerchantDetailResponse( id=merchant.id, name=merchant.name, description=merchant.description, owner_user_id=merchant.owner_user_id, owner_email=merchant.owner.email if merchant.owner else None, owner_username=merchant.owner.username if merchant.owner else None, contact_email=merchant.contact_email, contact_phone=merchant.contact_phone, website=merchant.website, business_address=merchant.business_address, tax_number=merchant.tax_number, is_active=merchant.is_active, is_verified=merchant.is_verified, created_at=merchant.created_at.isoformat(), updated_at=merchant.updated_at.isoformat(), store_count=store_count, active_store_count=active_store_count, stores=stores_list, ) @admin_merchants_router.put("/{merchant_id}", response_model=MerchantResponse) def update_merchant( merchant_id: int = Path(..., description="Merchant ID"), merchant_update: MerchantUpdate = Body(...), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Update merchant information (Admin only). **Can update:** - Basic info: name, description - Business contact: contact_email, contact_phone, website - Business details: business_address, tax_number - Status: is_active, is_verified **Cannot update:** - `owner_user_id` (would require ownership transfer feature) """ merchant = merchant_service.update_merchant(db, merchant_id, merchant_update) db.commit() # ✅ ARCH: Commit at API level for transaction control return MerchantResponse( id=merchant.id, name=merchant.name, description=merchant.description, owner_user_id=merchant.owner_user_id, contact_email=merchant.contact_email, contact_phone=merchant.contact_phone, website=merchant.website, business_address=merchant.business_address, tax_number=merchant.tax_number, is_active=merchant.is_active, is_verified=merchant.is_verified, created_at=merchant.created_at.isoformat(), updated_at=merchant.updated_at.isoformat(), ) @admin_merchants_router.put("/{merchant_id}/verification", response_model=MerchantResponse) def toggle_merchant_verification( merchant_id: int = Path(..., description="Merchant ID"), verification_data: dict = Body(..., example={"is_verified": True}), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Toggle merchant verification status (Admin only). Request body: { "is_verified": true/false } """ is_verified = verification_data.get("is_verified", False) merchant = merchant_service.toggle_verification(db, merchant_id, is_verified) db.commit() # ✅ ARCH: Commit at API level for transaction control return MerchantResponse( id=merchant.id, name=merchant.name, description=merchant.description, owner_user_id=merchant.owner_user_id, contact_email=merchant.contact_email, contact_phone=merchant.contact_phone, website=merchant.website, business_address=merchant.business_address, tax_number=merchant.tax_number, is_active=merchant.is_active, is_verified=merchant.is_verified, created_at=merchant.created_at.isoformat(), updated_at=merchant.updated_at.isoformat(), ) @admin_merchants_router.put("/{merchant_id}/status", response_model=MerchantResponse) def toggle_merchant_status( merchant_id: int = Path(..., description="Merchant ID"), status_data: dict = Body(..., example={"is_active": True}), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Toggle merchant active status (Admin only). Request body: { "is_active": true/false } """ is_active = status_data.get("is_active", True) merchant = merchant_service.toggle_active(db, merchant_id, is_active) db.commit() # ✅ ARCH: Commit at API level for transaction control return MerchantResponse( id=merchant.id, name=merchant.name, description=merchant.description, owner_user_id=merchant.owner_user_id, contact_email=merchant.contact_email, contact_phone=merchant.contact_phone, website=merchant.website, business_address=merchant.business_address, tax_number=merchant.tax_number, is_active=merchant.is_active, is_verified=merchant.is_verified, created_at=merchant.created_at.isoformat(), updated_at=merchant.updated_at.isoformat(), ) @admin_merchants_router.post( "/{merchant_id}/transfer-ownership", response_model=MerchantTransferOwnershipResponse, ) def transfer_merchant_ownership( merchant_id: int = Path(..., description="Merchant ID"), transfer_data: MerchantTransferOwnership = Body(...), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Transfer merchant ownership to another user (Admin only). **This is a critical operation that:** - Changes the merchant's owner_user_id - Updates all associated stores' owner_user_id - Creates audit trail ⚠️ **This action is logged and should be used carefully.** **Requires:** - `new_owner_user_id`: ID of user who will become owner - `confirm_transfer`: Must be true - `transfer_reason`: Optional reason for audit trail """ merchant, old_owner, new_owner = merchant_service.transfer_ownership( db, merchant_id, transfer_data ) db.commit() # ✅ ARCH: Commit at API level for transaction control return MerchantTransferOwnershipResponse( message="Ownership transferred successfully", merchant_id=merchant.id, merchant_name=merchant.name, old_owner={ "id": old_owner.id, "username": old_owner.username, "email": old_owner.email, }, new_owner={ "id": new_owner.id, "username": new_owner.username, "email": new_owner.email, }, transferred_at=datetime.now(UTC), transfer_reason=transfer_data.transfer_reason, ) @admin_merchants_router.delete("/{merchant_id}") def delete_merchant( merchant_id: int = Path(..., description="Merchant ID"), 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 merchant and all associated stores (Admin only). ⚠️ **WARNING: This is destructive and will delete:** - Merchant account - All stores under this merchant - All products under those stores - All orders, customers, team members Requires confirmation parameter: `confirm=true` """ if not confirm: raise ConfirmationRequiredException( operation="delete_merchant", message="Deletion requires confirmation parameter: confirm=true", ) # Get merchant to check store count merchant = merchant_service.get_merchant_by_id(db, merchant_id) store_count = len(merchant.stores) if store_count > 0: raise MerchantHasStoresException(merchant_id, store_count) merchant_service.delete_merchant(db, merchant_id) db.commit() # ✅ ARCH: Commit at API level for transaction control return {"message": f"Merchant {merchant_id} deleted successfully"} @admin_merchants_router.put("/{merchant_id}/restore") def restore_merchant( merchant_id: int = Path(..., description="Merchant ID"), db: Session = Depends(get_db), current_admin: UserContext = Depends(get_current_admin_api), ): """ Restore a soft-deleted merchant (Admin only). This only restores the merchant record itself. Stores and their children must be restored separately. """ from app.core.soft_delete import restore from app.modules.tenancy.models import Merchant restored = restore(db, Merchant, merchant_id, restored_by_id=current_admin.id) db.commit() logger.info(f"Merchant {merchant_id} restored by admin {current_admin.username}") return {"message": f"Merchant '{restored.name}' restored successfully", "merchant_id": merchant_id}