Some checks failed
- Extract login/dashboard from billing module into core (matching admin pattern) - Add merchant auth API with path-isolated cookies (path=/merchants) - Add merchant base layout with sidebar/header partials and Alpine.js init - Add frontend detection and login redirect for MERCHANT type - Wire merchant token in shared api-client.js (get/clear) - Migrate billing templates to merchant base with dark mode support - Fix Tailwind: rename shop→storefront in sources and config - DRY Makefile tailwind targets with TAILWIND_FRONTENDS loop - Rebuild all Tailwind outputs (production minified) - Add Gitea Actions CI workflow (ruff, pytest, architecture, docs) - Add Gitea deployment documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
5.4 KiB
Python
188 lines
5.4 KiB
Python
# 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 typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, EmailStr
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_current_merchant_from_cookie_or_header
|
|
from app.core.database import get_db
|
|
from app.modules.tenancy.models import Merchant
|
|
from models.schema.auth import UserContext
|
|
|
|
from .merchant_auth import merchant_auth_router
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
# Include auth routes (/auth/login, /auth/logout, /auth/me)
|
|
router.include_router(merchant_auth_router, tags=["merchant-auth"])
|
|
|
|
# Account routes are defined below with /account prefix
|
|
_account_router = APIRouter(prefix="/account")
|
|
|
|
|
|
# ============================================================================
|
|
# SCHEMAS
|
|
# ============================================================================
|
|
|
|
|
|
class MerchantProfileUpdate(BaseModel):
|
|
"""Schema for updating merchant profile information."""
|
|
|
|
name: str | None = None
|
|
contact_email: EmailStr | None = None
|
|
contact_phone: str | None = None
|
|
website: str | None = None
|
|
business_address: str | None = None
|
|
tax_number: str | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# HELPERS
|
|
# ============================================================================
|
|
|
|
|
|
def _get_user_merchant(db: Session, user_context: UserContext) -> Merchant:
|
|
"""
|
|
Get the first active merchant owned by the authenticated user.
|
|
|
|
Args:
|
|
db: Database session
|
|
user_context: Authenticated user context
|
|
|
|
Returns:
|
|
Merchant: The user's active merchant
|
|
|
|
Raises:
|
|
HTTPException: 404 if user does not own any active merchant
|
|
"""
|
|
merchant = (
|
|
db.query(Merchant)
|
|
.filter(
|
|
Merchant.owner_user_id == user_context.id,
|
|
Merchant.is_active == True, # noqa: E712
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not merchant:
|
|
raise HTTPException(status_code=404, detail="Merchant not found")
|
|
|
|
return merchant
|
|
|
|
|
|
# ============================================================================
|
|
# ACCOUNT ENDPOINTS
|
|
# ============================================================================
|
|
|
|
|
|
@_account_router.get("/stores")
|
|
async def merchant_stores(
|
|
request: Request,
|
|
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
List all stores belonging to the merchant.
|
|
|
|
Returns a list of store summary dicts with basic info for each store
|
|
owned by the authenticated merchant.
|
|
"""
|
|
merchant = _get_user_merchant(db, current_user)
|
|
|
|
stores = []
|
|
for store in merchant.stores:
|
|
stores.append(
|
|
{
|
|
"id": store.id,
|
|
"name": store.name,
|
|
"store_code": store.store_code,
|
|
"is_active": store.is_active,
|
|
"created_at": store.created_at.isoformat() if store.created_at else None,
|
|
}
|
|
)
|
|
|
|
return {"stores": stores}
|
|
|
|
|
|
@_account_router.get("/profile")
|
|
async def merchant_profile(
|
|
request: Request,
|
|
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Get the authenticated merchant's profile information.
|
|
|
|
Returns merchant details including contact info, business details,
|
|
and verification status.
|
|
"""
|
|
merchant = _get_user_merchant(db, current_user)
|
|
|
|
return {
|
|
"id": merchant.id,
|
|
"name": merchant.name,
|
|
"contact_email": merchant.contact_email,
|
|
"contact_phone": merchant.contact_phone,
|
|
"website": merchant.website,
|
|
"business_address": merchant.business_address,
|
|
"tax_number": merchant.tax_number,
|
|
"is_verified": merchant.is_verified,
|
|
}
|
|
|
|
|
|
@_account_router.put("/profile")
|
|
async def update_merchant_profile(
|
|
request: Request,
|
|
profile_data: MerchantProfileUpdate,
|
|
current_user: UserContext = Depends(get_current_merchant_from_cookie_or_header),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Update the authenticated merchant's profile information.
|
|
|
|
Accepts partial updates - only provided fields are changed.
|
|
"""
|
|
merchant = _get_user_merchant(db, current_user)
|
|
|
|
# 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 {
|
|
"id": merchant.id,
|
|
"name": merchant.name,
|
|
"contact_email": merchant.contact_email,
|
|
"contact_phone": merchant.contact_phone,
|
|
"website": merchant.website,
|
|
"business_address": merchant.business_address,
|
|
"tax_number": merchant.tax_number,
|
|
"is_verified": merchant.is_verified,
|
|
}
|
|
|
|
|
|
# Include account routes in main router
|
|
router.include_router(_account_router, tags=["merchant-account"])
|