""" App — API Backend District 87-16, Région 87 100% logiciel libre · Hébergé au Québec """ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from app.core.config import settings from app.core.audit import AuditMiddleware from app.core.database import engine, Base from app.services.tenant_schemas import ensure_all_tenant_schemas from app.routers import admin, auth, membres, postes, accueil, invitations, collectes, reunions, depenses, calendrier, rapports, rotations, parametres, presences, pv, rapports_rsg, jetons, anniversaires, commandes_jetons, litterature, ventes_litterature, contributions, config_repartition, propositions, operations_bancaires, evenements, notifications, journal # Import models pour que create_all crée toutes les tables from app.models.sync_log import SyncLog # noqa: F401 from app.models.proposition import Proposition # noqa: F401 from app.models.operation_bancaire import OperationBancaire # noqa: F401 from app.models.evenement import Evenement # noqa: F401 from app.models.candidature import Candidature # noqa: F401 from app.models.poste_module import PosteModule # noqa: F401 from app.models.reserve import Reserve # noqa: F401 from app.models.transaction_comptable import TransactionComptable # noqa: F401 from app.models.mouvement_reserve import MouvementReserve # noqa: F401 from app.models.journal_action import JournalAction # noqa: F401 from app.models.push_subscription import PushSubscription # noqa: F401 from app.models.destinataire_contribution import DestinataireContribution # noqa: F401 from app.models.rapport_adopte import RapportAdopte # noqa: F401 from app.models.vente_jeton import VenteJeton # noqa: F401 from app.models.historique_modification import HistoriqueModification # noqa: F401 from app.models.instance_admin import InstanceAdmin # noqa: F401 @asynccontextmanager async def lifespan(app: FastAPI): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await conn.execute(text("ALTER TABLE groupes ADD COLUMN IF NOT EXISTS tenant_schema VARCHAR(80) UNIQUE")) await conn.execute(text( "UPDATE groupes SET tenant_schema = 'grp_' || replace(id::text, '-', '') " "WHERE tenant_schema IS NULL" )) await conn.execute(text( "ALTER TABLE groupes " "ADD COLUMN IF NOT EXISTS assemblee_affaires_ordre VARCHAR(20) DEFAULT 'premiere'" )) await conn.execute(text("ALTER TABLE postes ADD COLUMN IF NOT EXISTS groupe_id UUID REFERENCES groupes(id)")) await conn.execute(text( "UPDATE postes SET groupe_id = (SELECT id FROM groupes ORDER BY cree_le LIMIT 1) " "WHERE groupe_id IS NULL AND EXISTS (SELECT 1 FROM groupes)" )) await conn.execute(text("ALTER TABLE postes ALTER COLUMN groupe_id SET NOT NULL")) await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS date_adoption DATE")) await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS etant_donne_que TEXT")) await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS groupe_a_decide_de TEXT")) await conn.execute(text("ALTER TABLE propositions DROP COLUMN IF EXISTS type_proposition")) await conn.execute(text("ALTER TABLE membres ADD COLUMN IF NOT EXISTS notification_preferences JSON")) await conn.execute(text("ALTER TABLE jetons ADD COLUMN IF NOT EXISTS reunion_id UUID REFERENCES reunions(id)")) await conn.execute(text("ALTER TABLE ventes_litterature ADD COLUMN IF NOT EXISTS reunion_id UUID REFERENCES reunions(id)")) await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_type VARCHAR(40)")) await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_id UUID")) for table in ( "propositions", "depenses", "collectes", "reunions", "pv_reunions", "reserves", "litterature", "destinataires_contributions", "envois_contributions", ): await conn.execute(text( f"ALTER TABLE {table} " "ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMPTZ DEFAULT now() NOT NULL" )) await conn.execute(text( "ALTER TABLE envois_contributions " "ALTER COLUMN destination TYPE VARCHAR(120) USING destination::text" )) await conn.execute(text("DROP TABLE IF EXISTS votes")) await conn.execute(text("DROP TABLE IF EXISTS options_sondage")) await conn.execute(text("DROP TABLE IF EXISTS sondages")) await conn.execute(text("DROP TABLE IF EXISTS decisions")) await conn.execute(text( "INSERT INTO destinataires_contributions (id, groupe_id, nom, protege, actif, cree_le) " "SELECT CAST(md5(random()::text || clock_timestamp()::text) AS uuid), g.id, 'District 87-16', true, true, now() " "FROM groupes g " "WHERE NOT EXISTS (" " SELECT 1 FROM destinataires_contributions d " " WHERE d.groupe_id = g.id AND d.nom = 'District 87-16'" ")" )) for table in ( "affectations", "candidatures", "collectes", "config_repartition", "depenses", "destinataires_contributions", "envois_contributions", "evenements", "historique_modifications", "invitations", "jetons", "journal_actions", "litterature", "membres", "mouvements_reserves", "operations_bancaires", "postes", "propositions", "push_subscriptions", "rapports_adoptes", "rapports_rsg", "reserves", "reunions", "rotations", "sync_log", "transactions_comptables", "ventes_jetons", "ventes_litterature", ): await conn.execute(text(f"CREATE INDEX IF NOT EXISTS idx_{table}_groupe_id ON {table} (groupe_id)")) await ensure_all_tenant_schemas(conn) yield await engine.dispose() app = FastAPI( title="App API", description="API du App — District 87-16, Région 87", version="0.1.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(AuditMiddleware) # Montage des routes app.include_router(auth.router, prefix="/api") app.include_router(admin.router, prefix="/api") app.include_router(membres.router, prefix="/api") app.include_router(postes.router, prefix="/api") app.include_router(accueil.router, prefix="/api") app.include_router(invitations.router, prefix="/api") app.include_router(collectes.router, prefix="/api") app.include_router(reunions.router, prefix="/api") app.include_router(depenses.router, prefix="/api") app.include_router(calendrier.router, prefix="/api") app.include_router(rapports.router, prefix="/api") app.include_router(rotations.router, prefix="/api") app.include_router(parametres.router, prefix="/api") app.include_router(presences.router, prefix="/api") app.include_router(pv.router, prefix="/api") app.include_router(rapports_rsg.router, prefix="/api") app.include_router(jetons.router, prefix="/api") app.include_router(anniversaires.router, prefix="/api") app.include_router(commandes_jetons.router, prefix="/api") app.include_router(litterature.router, prefix="/api") app.include_router(ventes_litterature.router, prefix="/api") app.include_router(contributions.router, prefix="/api") app.include_router(config_repartition.router, prefix="/api") app.include_router(propositions.router, prefix="/api") app.include_router(operations_bancaires.router, prefix="/api") app.include_router(evenements.router, prefix="/api") app.include_router(notifications.router, prefix="/api") app.include_router(journal.router, prefix="/api") @app.get("/api/health") async def root_health(): return {"status": "ok", "app": "App", "version": "0.1.0"}