Skip to content

#47 — ARCEP Rapports Trimestriels

PLANIFIÉ

Priorité: 🔴 CRITIQUE · Type: TYPE B · Conteneur: rgz-api · Code: app/api/v1/endpoints/reports.pyDépendances: #1 rgz-api, #4 rgz-db


Description

Rapports trimestriels conformité ARCEP (Autorité de Régulation des Communications Électroniques du Bénin). Le système génère automatiquement quatre types de rapports :

  1. Rapport global : abonnés uniques, volume data, incidents >30min downtime, opérateurs (MTN MoMo, Moov, Wave, Celtiis).
  2. Coverage géographique : GPS par site revendeur (#68 site survey), heatmap RF RSSI (#41 monitoring).
  3. Traçabilité : sample 100 sessions aléatoires avec MSISDN, MAC, timestamp, NAS-ID.
  4. Qualité de service : SLA uptime %, P95 latence, disponibilité services.

Les rapports sont générés au jour 1 du trimestre suivant (ex: Q1 terminé 2026-03-31 → rapport généré 2026-04-01). Format: JSON pour API, PDF pour archivage/impression. Une Celery task s'exécute automatiquement, et un endpoint admin permet de régénérer manuellement.

Obligation légale : rétention 12 mois (logs + rapports), traçabilité MSISDN+MAC+IP+timestamp, incident reporting <24h, modification de données interdite (immuable via #45).

Architecture Interne

1. Trigger génération (automatique):
   └─> Celery task: rgz.arcep.quarterly
       ├─> Cron: "0 1 1 * *" (1er jour du mois, 01:00)
       │   Exécute: if quarter_ended: generate_report()
       └─> Ou endpoint: POST /api/v1/admin/reports/arcep/generate?quarter=Q1&year=2026

2. Collecte données (chaque trimestre):
   ├─> Période: Q1 = 01/01-03/31, Q2 = 04/01-06/30, etc.
   ├─> Queries:
   │   ├─ COUNT(DISTINCT subscriber_id) → abonnés
   │   ├─ SUM(bytes_in + bytes_out) → volume data
   │   ├─ COUNT incidents >30min → incidents
   │   ├─ SELECT nas_id, COUNT(*) → sessions par operator
   │   ├─ SELECT lat, lon, signal_strength → couverture RF
   │   └─ SAMPLE 100 radius_sessions → traçabilité
   └─> Agrégation par reseller + global

3. Rapport global (JSON):
   └─> {
       "quarter": "Q1",
       "year": 2026,
       "generated_at": "2026-04-01T01:15:00Z",
       "period_from": "2026-01-01T00:00:00Z",
       "period_to": "2026-03-31T23:59:59Z",
       "summary": {
         "total_subscribers": 12500,
         "active_subscribers": 9300,
         "new_subscribers": 2100,
         "total_data_gb": 15640,
         "total_sessions": 450000,
         "incidents_count": 3,
         "incidents_mttr_avg_min": 45
       },
       "by_provider": {
         "MTN_MOMO": {
           "transactions": 8500,
           "revenue_fcfa": 425000000
         },
         "MOOV_FLOOZ": {
           "transactions": 2100,
           "revenue_fcfa": 105000000
         },
         "WAVE_BENIN": {
           "transactions": 950,
           "revenue_fcfa": 47500000
         },
         "CELTIIS_CASH": {
           "transactions": 100,
           "revenue_fcfa": 5000000
         }
       },
       "by_reseller": {
         "access_tech_connect": {
           "subscribers": 4200,
           "data_gb": 6120,
           "incidents": 1
         },
         "access_network_plus": {
           "subscribers": 3800,
           "data_gb": 5210,
           "incidents": 1
         }
       },
       "quality": {
         "uptime_percent": 99.7,
         "p95_latency_ms": 85,
         "p99_latency_ms": 150,
         "packet_loss_percent": 0.1
       }
     }

4. Rapport coverage (JSON + GeoJSON):
   └─> {
       "sites": [
         {
           "reseller": "access_tech_connect",
           "site_number": 1,
           "location": {
             "latitude": 6.4969,
             "longitude": 2.6289,
             "city": "Cotonou",
             "quarter": "Haie-Vive"
           },
           "coverage": {
             "rssi_avg_dbm": -55,
             "rssi_min_dbm": -70,
             "clients_count": 523,
             "uptime_percent": 99.8
           }
         }
       ],
       "heatmap_data": "geojson_polygon_collection"
     }

5. Rapport traçabilité (sample):
   └─> {
       "sample_size": 100,
       "sample_timestamp": "2026-04-01T01:15:00Z",
       "sessions": [
         {
           "session_id": "uuid",
           "subscriber_ref": "RGZ-0197979964",
           "mac_address": "AA:BB:CC:DD:EE:FF",
           "nas_id": "access_tech_connect_s1",
           "session_start": "2026-03-15T10:30:00Z",
           "session_stop": "2026-03-15T12:45:00Z",
           "bytes_in": 512000000,
           "bytes_out": 256000000
         },
         ...
       ]
     }

6. Format PDF (A4, signed):
   └─> WeasyPrint génère depuis template HTML
   └─> Signé numeriquement (future APDP requirement)
   └─> Footer: "Rapport confidentiel ARCEP — Généré {date} — Sig: {hash}"

7. Endpoints API:
   └─> GET /api/v1/reports/arcep?quarter=Q1&year=2026
       └─> 200 JSON report
   └─> GET /api/v1/reports/arcep?quarter=Q1&year=2026&format=pdf
       └─> 200 PDF download
   └─> GET /api/v1/reports/coverage?reseller_id=
       └─> 200 heatmap GeoJSON
   └─> GET /api/v1/reports/traceability?quarter=Q1&year=2026
       └─> 200 sample sessions
   └─> POST /api/v1/admin/reports/arcep/generate
       └─> 202 {job_id, status}

8. Storage:
   └─> Stockage DB: arcep_reports table
   └─> Archivage: S3 ou disque local/nfs
   └─> Rétention: 12 mois (obligation légale)

Configuration

Variables d'environnement

env
ARCEP_REPORTING_ENABLED=true          # Activation
ARCEP_GENERATION_SCHEDULE=0 1 1 * *   # Cron: 1er jour mois 01:00
ARCEP_REPORT_RETENTION_DAYS=365       # 12 mois
ARCEP_ARCHIVE_BACKEND=s3              # s3 ou local
ARCEP_ARCHIVE_BUCKET=rgz-arcep        # S3 bucket
ARCEP_ARCHIVE_PATH=/reports/arcep/    # Prefix
ARCEP_PDF_INCLUDE_SIGNATURE=false     # Signature numérique (future)

Model SQLAlchemy

python
# app/models/compliance.py

class ArchepReport(Base):
    __tablename__ = "arcep_reports"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
    quarter = Column(String(2), nullable=False)  # Q1, Q2, Q3, Q4
    year = Column(Integer, nullable=False)
    period_from = Column(DateTime, nullable=False)
    period_to = Column(DateTime, nullable=False)

    # Summary metrics
    total_subscribers = Column(Integer, nullable=False)
    active_subscribers = Column(Integer, nullable=False)
    new_subscribers = Column(Integer, nullable=False)
    total_data_gb = Column(BigInteger, nullable=False)
    total_sessions = Column(Integer, nullable=False)
    incidents_count = Column(Integer, nullable=False)
    incidents_mttr_avg_min = Column(Float, nullable=False)

    # Quality metrics
    uptime_percent = Column(Float, nullable=False)
    p95_latency_ms = Column(Float, nullable=False)
    p99_latency_ms = Column(Float, nullable=False)
    packet_loss_percent = Column(Float, nullable=False)

    # Content
    report_json = Column(JSON, nullable=False)
    coverage_geojson = Column(JSON, nullable=False)
    traceability_sample = Column(JSON, nullable=False)

    # Storage
    pdf_url = Column(String(500), nullable=True)
    archive_path = Column(String(500), nullable=True)

    # Audit
    generated_at = Column(DateTime, default=utcnow, nullable=False)
    generated_by = Column(String(50))  # system ou admin_id
    signed_at = Column(DateTime, nullable=True)
    signature_hash = Column(String(64), nullable=True)  # SHA-256 signé

    created_at = Column(DateTime, default=utcnow, nullable=False)
    updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)

    __table_args__ = (
        UniqueConstraint("quarter", "year", name="uq_arcep_quarter_year"),
    )

Endpoints API

MéthodeRouteRéponseAuth
GET/api/v1/reports/arcep?quarter=Q1&year=2026200 JSONAdmin
GET/api/v1/reports/arcep?quarter=Q1&year=2026&format=pdf200 PDFAdmin
GET/api/v1/reports/coverage?reseller_id=&quarter=200 GeoJSONAdmin
GET/api/v1/reports/traceability?quarter=Q1&year=2026200 JSON sampleAdmin
POST/api/v1/admin/reports/arcep/generate202 {job_id}Admin
GET/api/v1/admin/reports/arcep/list200 {items: [...]}Admin

Commandes Utiles

bash
# Générer rapport Q1 2026
curl -X POST http://localhost:8000/api/v1/admin/reports/arcep/generate \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"quarter": "Q1", "year": 2026}'

# Récupérer rapport JSON
curl -X GET 'http://localhost:8000/api/v1/reports/arcep?quarter=Q1&year=2026' \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

# Télécharger PDF
curl -X GET 'http://localhost:8000/api/v1/reports/arcep?quarter=Q1&year=2026&format=pdf' \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  --output arcep_q1_2026.pdf

# Voir heatmap coverage
curl -X GET 'http://localhost:8000/api/v1/reports/coverage?quarter=Q1' \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

# Voir sample traçabilité
curl -X GET 'http://localhost:8000/api/v1/reports/traceability?quarter=Q1&year=2026' \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

# Lister tous les rapports
curl -X GET 'http://localhost:8000/api/v1/admin/reports/arcep/list' \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq

# Requête DB: métriques Q1
psql -h localhost -U rgz_admin -d rgz_db -c \
  "SELECT quarter, year, total_subscribers, total_data_gb, uptime_percent
   FROM arcep_reports WHERE year = 2026 ORDER BY quarter;"

Implémentation TODO

  • [ ] Classe ArchepReportService avec generate_report(quarter, year)
  • [ ] Fonction collect_summary_metrics() (COUNT, SUM sur radius_sessions)
  • [ ] Fonction generate_coverage_geojson() (FROM reseller_sites, integ #68 #41)
  • [ ] Fonction sample_traceability_sessions() (RANDOM 100 sessions)
  • [ ] Fonction generate_pdf() (WeasyPrint template HTML)
  • [ ] Endpoint GET /api/v1/reports/arcep JSON/PDF
  • [ ] Endpoint GET /api/v1/reports/coverage GeoJSON
  • [ ] Endpoint GET /api/v1/reports/traceability sample
  • [ ] Celery task: rgz.arcep.quarterly (1er jour mois 01:00)
  • [ ] Model ArchepReport pour DB + JSON storage
  • [ ] Signature numérique (future: APDP requirement)
  • [ ] Archivage S3 ou local disk (retention 12 mois)
  • [ ] Tests: générer rapport, vérifier métriques, check PDF valide
  • [ ] Intégration #45 logs immuables : logguer génération rapport
  • [ ] Intégration #71 rapport-arcep : utiliser ArchepReport comme source

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone