Unify commercial runtime ownership across backend and frontend so platform, experience, and hospitality modules use the shared commercial source of truth. Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""نقطه ورود برنامه FastAPI برای Core Platform Service."""
|
||
from __future__ import annotations
|
||
|
||
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from app import __version__
|
||
from app.api.v1 import api_router, health
|
||
from app.core.cache import cache
|
||
from app.core.config import settings
|
||
from app.core.logging import configure_logging, get_logger
|
||
from app.middlewares.tenant import TenantResolutionMiddleware
|
||
from shared.exceptions import AppError
|
||
from shared.responses import ErrorDetail, ErrorResponse
|
||
|
||
configure_logging()
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
logger.info("service_starting", extra={"service": settings.service_name})
|
||
# Best-effort: ensure commercial meta-registry kinds exist (idempotent).
|
||
# Skip in unit tests — discovery_catalog / seed handle kinds there.
|
||
if settings.environment != "test":
|
||
try:
|
||
from app.commercial.service import CommercialRegistryService
|
||
from app.core.database import AsyncSessionLocal
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
await CommercialRegistryService(session).ensure_builtin_kinds()
|
||
logger.info("commercial_registry_kinds_ready")
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning("commercial_kinds_bootstrap_skipped", extra={"error": str(exc)})
|
||
yield
|
||
await cache.close()
|
||
logger.info("service_stopped")
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
app = FastAPI(
|
||
title=f"{settings.platform_name} - Core Platform Service",
|
||
version=__version__,
|
||
description="سرویس هسته پلتفرم SaaS چندمستأجری (فاز ۱).",
|
||
lifespan=lifespan,
|
||
)
|
||
|
||
# CORS - در production باید origins محدود شود.
|
||
# allow_origin_regex برای سابدامین tenantها (<slug>.torbatyar.ir)
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.cors_origin_list,
|
||
allow_origin_regex=r"https?://([a-z0-9-]+\.)*torbatyar\.ir",
|
||
allow_credentials=False,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Middleware تشخیص tenant.
|
||
app.add_middleware(
|
||
TenantResolutionMiddleware, base_domain=settings.platform_base_domain or None
|
||
)
|
||
|
||
# ثبت exception handlerها برای تبدیل خطاهای دامنه به پاسخ استاندارد.
|
||
_register_exception_handlers(app)
|
||
|
||
# routerها
|
||
app.include_router(health.router)
|
||
app.include_router(api_router, prefix=settings.api_v1_prefix)
|
||
|
||
return app
|
||
|
||
|
||
def _register_exception_handlers(app: FastAPI) -> None:
|
||
@app.exception_handler(AppError)
|
||
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
|
||
response = ErrorResponse(
|
||
error=ErrorDetail(
|
||
code=exc.error_code, message=exc.message, details=exc.details
|
||
)
|
||
)
|
||
return JSONResponse(status_code=exc.status_code, content=response.model_dump())
|
||
|
||
@app.exception_handler(Exception)
|
||
async def unhandled_handler(request: Request, exc: Exception) -> JSONResponse:
|
||
logger.error("unhandled_exception", extra={"error": str(exc)}, exc_info=exc)
|
||
response = ErrorResponse(
|
||
error=ErrorDetail(code="internal_error", message="خطای داخلی سرور")
|
||
)
|
||
return JSONResponse(status_code=500, content=response.model_dump())
|
||
|
||
|
||
app = create_app()
|