#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 :
- Rapport global : abonnés uniques, volume data, incidents >30min downtime, opérateurs (MTN MoMo, Moov, Wave, Celtiis).
- Coverage géographique : GPS par site revendeur (#68 site survey), heatmap RF RSSI (#41 monitoring).
- Traçabilité : sample 100 sessions aléatoires avec MSISDN, MAC, timestamp, NAS-ID.
- 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
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
# 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éthode | Route | Réponse | Auth |
|---|---|---|---|
| GET | /api/v1/reports/arcep?quarter=Q1&year=2026 | 200 JSON | Admin |
| GET | /api/v1/reports/arcep?quarter=Q1&year=2026&format=pdf | 200 PDF | Admin |
| GET | /api/v1/reports/coverage?reseller_id=&quarter= | 200 GeoJSON | Admin |
| GET | /api/v1/reports/traceability?quarter=Q1&year=2026 | 200 JSON sample | Admin |
| POST | /api/v1/admin/reports/arcep/generate | 202 {job_id} | Admin |
| GET | /api/v1/admin/reports/arcep/list | 200 {items: [...]} | Admin |
Commandes Utiles
# 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
ArchepReportServiceavecgenerate_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