Files
orion/app/modules/tenancy/routes/api/admin_stores.py
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:20:29 +01:00

308 lines
10 KiB
Python

# 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.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
from models.schema.auth import UserContext
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_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),
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,
)
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_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}