- Fix Google Wallet class creation: add required issuerName field (merchant name), programLogo with default logo fallback, hexBackgroundColor default - Add default loyalty logo assets (200px + 512px) for programs without custom logos - Smart retry: skip retries on 400/401/403/404 client errors (not transient) - Fix enrollment success page: use sessionStorage for wallet URLs instead of authenticated API call (self-enrolled customers have no session) - Hide wallet section on success page when no wallet URLs available - Wire up T&C modal on enrollment page with program.terms_text - Add startup validation for Google/Apple Wallet configs in lifespan - Add admin wallet status dashboard endpoint and UI (moved to service layer) - Fix Apple Wallet push notifications with real APNs HTTP/2 implementation - Fix docs: correct enrollment URLs (port, path segments, /v1 prefix) - Fix test assertion: !loyalty-enroll! → !enrollment! Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
186 lines
5.6 KiB
Python
186 lines
5.6 KiB
Python
# app/core/lifespan.py
|
|
"""Application lifespan management - Clean Migration Approach.
|
|
|
|
This module provides classes and functions for:
|
|
- Application startup and shutdown events
|
|
- Logging setup
|
|
- Default user creation
|
|
- NO database table creation (handled by Alembic)
|
|
"""
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from sqlalchemy import text
|
|
|
|
from middleware.auth import AuthManager
|
|
|
|
from .config import settings
|
|
from .database import engine
|
|
from .logging import setup_logging
|
|
from .observability import init_observability, shutdown_observability
|
|
|
|
# Remove this import if not needed: from models.database.base import Base
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
auth_manager = AuthManager()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan events - Clean migration approach."""
|
|
|
|
# === STARTUP ===
|
|
app_logger = setup_logging()
|
|
app_logger.info("Starting Orion multi-tenant platform")
|
|
|
|
init_observability(
|
|
enable_metrics=settings.enable_metrics,
|
|
sentry_dsn=settings.sentry_dsn,
|
|
environment=settings.sentry_environment,
|
|
flower_url=settings.flower_url,
|
|
grafana_url=settings.grafana_url,
|
|
)
|
|
|
|
# Validate wallet configurations
|
|
_validate_wallet_config()
|
|
|
|
logger.info("[OK] Application startup completed")
|
|
|
|
yield
|
|
|
|
# === SHUTDOWN ===
|
|
app_logger.info("Shutting down Orion platform")
|
|
shutdown_observability()
|
|
|
|
|
|
def _validate_wallet_config():
|
|
"""Validate Google/Apple Wallet configuration at startup."""
|
|
try:
|
|
from app.modules.loyalty.services.google_wallet_service import (
|
|
google_wallet_service,
|
|
)
|
|
|
|
result = google_wallet_service.validate_config()
|
|
if result["configured"]:
|
|
if result["credentials_valid"]:
|
|
logger.info(
|
|
"[OK] Google Wallet configured (issuer: %s, email: %s)",
|
|
result["issuer_id"],
|
|
result.get("service_account_email", "unknown"),
|
|
)
|
|
else:
|
|
for err in result["errors"]:
|
|
logger.error("[FAIL] Google Wallet config error: %s", err)
|
|
else:
|
|
logger.info("[--] Google Wallet not configured (optional)")
|
|
|
|
# Apple Wallet config check
|
|
if settings.loyalty_apple_pass_type_id:
|
|
import os
|
|
|
|
missing = []
|
|
for field in [
|
|
"loyalty_apple_team_id",
|
|
"loyalty_apple_wwdr_cert_path",
|
|
"loyalty_apple_signer_cert_path",
|
|
"loyalty_apple_signer_key_path",
|
|
]:
|
|
val = getattr(settings, field, None)
|
|
if not val:
|
|
missing.append(field)
|
|
elif field.endswith("_path") and not os.path.isfile(val):
|
|
logger.error(
|
|
"[FAIL] Apple Wallet file not found: %s = %s",
|
|
field,
|
|
val,
|
|
)
|
|
|
|
if missing:
|
|
logger.error(
|
|
"[FAIL] Apple Wallet missing config: %s",
|
|
", ".join(missing),
|
|
)
|
|
elif not any(
|
|
not os.path.isfile(getattr(settings, f, "") or "")
|
|
for f in [
|
|
"loyalty_apple_wwdr_cert_path",
|
|
"loyalty_apple_signer_cert_path",
|
|
"loyalty_apple_signer_key_path",
|
|
]
|
|
):
|
|
logger.info(
|
|
"[OK] Apple Wallet configured (pass type: %s)",
|
|
settings.loyalty_apple_pass_type_id,
|
|
)
|
|
else:
|
|
logger.info("[--] Apple Wallet not configured (optional)")
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("Wallet config validation skipped: %s", exc)
|
|
|
|
|
|
# === NEW HELPER FUNCTION ===
|
|
def check_database_ready():
|
|
"""Check if database is ready (migrations have been run)."""
|
|
try:
|
|
with engine.connect() as conn:
|
|
# Check for tables in the public schema (PostgreSQL)
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT tablename FROM pg_catalog.pg_tables "
|
|
"WHERE schemaname = 'public' LIMIT 1"
|
|
)
|
|
)
|
|
tables = result.fetchall()
|
|
return len(tables) > 0
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def get_migration_status():
|
|
"""Get current Alembic migration status."""
|
|
try:
|
|
from alembic.config import Config
|
|
|
|
Config("alembic.ini")
|
|
|
|
# This would need more implementation to actually check status
|
|
# For now, just return a placeholder
|
|
return "Migration status check not implemented"
|
|
except Exception as e:
|
|
logger.warning(f"Could not check migration status: {e}")
|
|
return "Unknown"
|
|
|
|
|
|
# === STARTUP VERIFICATION (Optional) ===
|
|
def verify_startup_requirements():
|
|
"""Verify that all startup requirements are met."""
|
|
|
|
issues = []
|
|
|
|
# Check if database exists and has tables
|
|
if not check_database_ready():
|
|
issues.append("Database not ready - run 'make migrate-up' first")
|
|
|
|
# Add other checks as needed
|
|
# - Configuration validation
|
|
# - External service connectivity
|
|
# - Required environment variables
|
|
|
|
if issues:
|
|
logger.error("[ERROR] Startup verification failed:")
|
|
for issue in issues:
|
|
logger.error(f" - {issue}")
|
|
return False
|
|
|
|
logger.info("[OK] Startup verification passed")
|
|
return True
|
|
|
|
|
|
# You can call this in your main.py if desired:
|
|
# if not verify_startup_requirements():
|
|
# raise RuntimeError("Application startup requirements not met")
|