#74 — revenue-forecast
PLANIFIÉ
Priorité: 🟠 HAUTE · Type: C (Celery) · Conteneur: rgz-beat · Code: app/tasks/forecast.pyDépendances: #4 rgz-db, #19 moteur-facturation
Description
Prévision de revenus trimestriales basée sur l'ARPU moyen (Average Revenue Per User) = 45,000 FCFA/revendeur/mois. Génère 3 scénarios pour la direction commerciale :
- Pessimiste (-20%) : Perte 20% revendeurs dans 6 mois
- Réaliste : Tendance actuelle linéaire
- Optimiste (+30%) : Gain 30% revendeurs dans 6 mois
Utilisé pour :
- Budgeting : Projections trimestrielles/annuelles
- Planning : Embauches, infrastructure, couts marketing
- Negociations : Avec partenaires, investisseurs
- Reporting : Actionnaires, ARCEP (capacité expansion)
Généré une fois par mois, intégré dans rapports de gestion.
Architecture Interne
Flux de Génération
Mensuellement le 1er du mois à 07:00 UTC:
↓
Celery Beat déclenche rgz.forecast.revenue
↓
Récupérer données du mois précédent:
1. Compter revendeurs actifs (status=active)
2. Calculer ARPU total:
SUM(invoice_total_fcfa) / COUNT(DISTINCT reseller_id) / 1
(mensuel, pas besoin diviser par 12)
3. Récupérer historique 12 mois:
SELECT month, count_resellers, total_revenue
FROM revenue_monthly WHERE month >= NOW() - 12 months
↓
Analyser tendance:
- Linear regression: resellers = f(month)
- Slope indique: gain/perte par mois
↓
Générer 3 scénarios à 6 mois:
1. Pessimiste: slope * 0.8 (réduit de 20%)
2. Réaliste: slope * 1.0 (tendance actuelle)
3. Optimiste: slope * 1.3 (augmenté de 30%)
↓
Calculer revenue pour chaque scénario:
Projected_revendeurs * ARPU * 6 mois
↓
Enregistrer forecast + notifier managementSchéma de Données
sql
-- Table source (créée par #19)
TABLE invoices:
id UUID PK
reseller_id UUID FK
month DATE
total_fcfa DECIMAL(12,2)
status CHECK(draft|sent|paid|overdue|cancelled)
-- Table mensuelle revenue (créée par #19 ou #74)
TABLE revenue_monthly:
id UUID PK
month DATE UNIQUE
active_reseller_count INT
total_revenue_fcfa DECIMAL(12,2)
arpu_fcfa DECIMAL(10,2) (total / reseller_count)
average_subscribers_per_reseller DECIMAL(10,1)
churn_percent DECIMAL(5,2)
new_resellers INT
churned_resellers INT
created_at TIMESTAMP
-- Table forecast (créée par #74)
TABLE revenue_forecast:
id UUID PK
forecast_month DATE (mois du forecast, ex: 2026-02)
forecast_horizon_months INT (6)
base_month DATE (mois actuel -1, ex: 2026-01)
base_reseller_count INT (487)
base_arpu_fcfa DECIMAL(10,2) (45000)
-- Scénario pessimiste
pessimistic_reseller_count_6m INT
pessimistic_revenue_6m_fcfa DECIMAL(12,2)
pessimistic_churn_rate_percent DECIMAL(5,2)
-- Scénario réaliste
realistic_reseller_count_6m INT
realistic_revenue_6m_fcfa DECIMAL(12,2)
realistic_growth_rate_percent DECIMAL(5,2)
realistic_churn_rate_percent DECIMAL(5,2)
-- Scénario optimiste
optimistic_reseller_count_6m INT
optimistic_revenue_6m_fcfa DECIMAL(12,2)
optimistic_growth_rate_percent DECIMAL(5,2)
trend_slope DECIMAL(10,4) (revendeurs par mois, linear)
trend_r_squared DECIMAL(5,4) (qualité regression)
generated_at TIMESTAMP
reviewed_by UUID FK (CFO)
reviewed_at TIMESTAMPExemple Forecast
FORECAST REVENUE — Février 2026
Base: Janvier 2026
Horizon: 6 mois (Février — Juillet 2026)
═══════════════════════════════════════════════════════════════
BASE METRICS (Janvier 2026):
Revendeurs actifs: 487
ARPU moyen: 45,000 FCFA/mois
Total revenue Janvier: 21.9M FCFA
Churn rate (dernier mois): 2.1%
New resellers (dernier mois): 8
ANALYSE TENDANCE (12 mois):
Pente (linear regression): 3.2 revendeurs/mois (gain)
R² (qualité): 0.87 (bon fit)
Interprétation: +3 revendeurs/mois, tendance stable
═══════════════════════════════════════════════════════════════
SCÉNARIO 1 — PESSIMISTE (-20% churn)
───────────────────────────────────────────────────────────────
Hypothèse: Churn augmente à 4.2% (doublé), growth stagne
Revendeurs (fin juillet): 487 - (6 * 2 revendeurs/mois * 2)
= 487 - 24 = 463
Revenue 6 mois: 463 * 45,000 * (6/12) [moyenne période]
≈ 103.4M FCFA
ARPU impact: -5% (clients moins engagés)
Final estimate: 103.4M - 5.2M = 98.2M FCFA
───────────────────────────────────────────────────────────────
SCÉNARIO 2 — RÉALISTE (tendance actuelle)
───────────────────────────────────────────────────────────────
Hypothèse: Pente continue, churn stable 2.1%
Revendeurs (fin juillet): 487 + (6 * 3.2 revendeurs/mois)
= 487 + 19 = 506
Revenue 6 mois: 506 * 45,000 * (6/12)
≈ 113.9M FCFA
ARPU stable: 45,000 FCFA constant
Final estimate: 113.9M FCFA
───────────────────────────────────────────────────────────────
SCÉNARIO 3 — OPTIMISTE (+30% growth)
───────────────────────────────────────────────────────────────
Hypothèse: Marketing push réussi, gain 4.2 revendeurs/mois
Revendeurs (fin juillet): 487 + (6 * 3.2 * 1.3)
= 487 + 25 = 512
Revenue 6 mois: 512 * 45,000 * (6/12)
≈ 115.2M FCFA
ARPU impact: +3% (economies of scale)
Final estimate: 115.2M + 3.5M = 118.7M FCFA
═══════════════════════════════════════════════════════════════
SUMMARY FOR MANAGEMENT:
┌─────────────────────────────────────────────────────────┐
│ REVENUE FORECAST 6 MOIS (Fév-Jul 2026) │
├─────────────────────────────────────────────────────────┤
│ PESSIMISTE (P10): 98.2M FCFA (-13.8% vs réaliste) │
│ RÉALISTE (P50): 113.9M FCFA (base de planning) │
│ OPTIMISTE (P90): 118.7M FCFA (+4.2% vs réaliste) │
│ │
│ Range: 98.2M — 118.7M FCFA (+/-10.2%) │
│ │
│ Recommendations: │
│ • Budget réaliste + 5M FCFA contingency (pessimiste) │
│ • Plan capacité serveurs pour +506 revendeurs │
│ • Market study si churn > 3% (pessimiste trigger) │
└─────────────────────────────────────────────────────────┘
───────────────────────────────────────────────────────────
Forecast généré: 2026-02-01 à 07:30 UTC
Par: Celery task rgz.forecast.revenue
Reviewed by: [CFO signature — pending]
Prochaine révision: 2026-03-01Configuration
env
# Revenue Forecast
FORECAST_HORIZON_MONTHS=6
FORECAST_BASE_ARPU_FCFA=45000 # ARPU consensus
FORECAST_PESSIMISTIC_FACTOR=0.8 # 80% growth rate
FORECAST_OPTIMISTIC_FACTOR=1.3 # 130% growth rate
FORECAST_MIN_HISTORICAL_MONTHS=12 # Min data pour regression
# Thresholds
FORECAST_CHURN_ALERT_PERCENT=3.5 # Alert si churn > 3.5%
FORECAST_GROWTH_ALERT_PERCENT=-1.0 # Alert si growth < -1%
# Output
FORECAST_OUTPUT_DIR=/var/reports/forecasts
FORECAST_NOTIFY_CFO=true
FORECAST_SLACK_CHANNEL=#finance-forecastsEndpoints API
| Méthode | Route | Description | Réponse |
|---|---|---|---|
| GET | /api/v1/reports/forecast?months=6 | Récupérer forecast courant | |
| GET | /api/v1/reports/forecast/historical?periods=12 | Historique 12 derniers forecasts | List[forecast] |
| POST | /api/v1/reports/forecast/generate | Générer forecast manuellement (admin) | 202 Accepted |
| GET | /api/v1/reports/forecast/sensitivity | Analyse sensibilité (si ARPU change ±10%?) |
Authentification: CFO/Finance only + Admin
Celery Task
| Champ | Valeur |
|---|---|
| Task name | rgz.forecast.revenue |
| Schedule | Monthly 1st of month 07:00 UTC |
| Queue | rgz.reports |
| Timeout | 300s |
| Retry | 2x |
Logique esquisse:
python
@app.task(name='rgz.forecast.revenue', bind=True)
def forecast_revenue(self):
"""
Génère forecast revenue 6 mois (3 scénarios)
"""
try:
# 1. Récupérer base metrics (mois précédent)
last_month = datetime.utcnow().replace(day=1) - timedelta(days=1)
last_month_start = last_month.replace(day=1)
# Count active resellers
active_count = db.query(func.count(Reseller.id)).filter(
Reseller.status == ResellerStatus.active
).scalar()
# ARPU calculation
last_month_revenue = db.query(
func.sum(Invoice.total_fcfa)
).filter(
func.date_trunc('month', Invoice.invoice_date) == last_month_start,
Invoice.status != InvoiceStatus.cancelled
).scalar() or 0
arpu = last_month_revenue / active_count if active_count > 0 else 45000
# 2. Récupérer historique 12 mois
historical = db.query(
RevenueMonthly.month,
RevenueMonthly.active_reseller_count
).filter(
RevenueMonthly.month >= datetime.utcnow() - timedelta(days=365)
).order_by(RevenueMonthly.month).all()
if len(historical) < 12:
logger.warn("Insufficient historical data, skipping forecast")
return {'status': 'skipped', 'reason': 'insufficient_data'}
# 3. Linear regression
months_num = np.arange(len(historical))
reseller_counts = np.array([h[1] for h in historical])
coeffs = np.polyfit(months_num, reseller_counts, 1)
slope = coeffs[0] # revendeurs per month
intercept = coeffs[1]
# Calculate R²
predicted = np.polyval(coeffs, months_num)
ss_res = np.sum((reseller_counts - predicted) ** 2)
ss_tot = np.sum((reseller_counts - np.mean(reseller_counts)) ** 2)
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
# 4. Générer 3 scénarios
scenarios = {}
for scenario_name, growth_factor in [
('pessimistic', 0.8),
('realistic', 1.0),
('optimistic', 1.3)
]:
adjusted_slope = slope * growth_factor
revendeurs_6m = active_count + (adjusted_slope * 6)
revenue_6m = revendeurs_6m * arpu * 0.5 # 6 mois = 0.5 années
scenarios[scenario_name] = {
'reseller_count': int(revendeurs_6m),
'revenue_fcfa': int(revenue_6m),
'growth_rate': float(adjusted_slope * 12) # Annualized
}
# 5. Enregistrer forecast
forecast = RevenueForecast(
forecast_month=datetime.utcnow().replace(day=1),
forecast_horizon_months=6,
base_month=last_month_start,
base_reseller_count=active_count,
base_arpu_fcfa=int(arpu),
pessimistic_reseller_count_6m=scenarios['pessimistic']['reseller_count'],
pessimistic_revenue_6m_fcfa=scenarios['pessimistic']['revenue_fcfa'],
realistic_reseller_count_6m=scenarios['realistic']['reseller_count'],
realistic_revenue_6m_fcfa=scenarios['realistic']['revenue_fcfa'],
optimistic_reseller_count_6m=scenarios['optimistic']['reseller_count'],
optimistic_revenue_6m_fcfa=scenarios['optimistic']['revenue_fcfa'],
trend_slope=float(slope),
trend_r_squared=float(r_squared),
generated_at=datetime.utcnow()
)
db.add(forecast)
db.commit()
# 6. Notifier CFO
send_email.delay(
to='cfo@rgz.bj',
subject='Monthly Revenue Forecast Generated',
template='forecast_notification',
context={'forecast': forecast, 'scenarios': scenarios}
)
return {
'status': 'success',
'scenarios': scenarios,
'r_squared': r_squared
}
except Exception as e:
logger.error(f"Revenue forecast failed: {e}")
self.retry(exc=e, countdown=600)Commandes Utiles
bash
# Déclencher forecast manuellement
docker-compose exec rgz-api celery -A app.celery_app call rgz.forecast.revenue
# Récupérer forecast courant
curl -H "Authorization: Bearer {cfo_token}" \
"http://api-rgz.duckdns.org/api/v1/reports/forecast?months=6" | jq
# Analyse sensibilité (ARPU +/- 10%)
curl -H "Authorization: Bearer {cfo_token}" \
"http://api-rgz.duckdns.org/api/v1/reports/forecast/sensitivity" | jq
# Historique 12 derniers forecasts
curl -H "Authorization: Bearer {cfo_token}" \
"http://api-rgz.duckdns.org/api/v1/reports/forecast/historical?periods=12" | jq
# Logs forecast generation
docker-compose logs rgz-beat | grep "rgz.forecast.revenue"
# Vérifier revenue_monthly table (debug)
docker-compose exec rgz-db psql -U $POSTGRES_USER -d $POSTGRES_DB -c \
"SELECT month, active_reseller_count, total_revenue_fcfa, arpu_fcfa FROM revenue_monthly ORDER BY month DESC LIMIT 12;"Implémentation TODO
- [ ] Schéma DB
revenue_monthly+revenue_forecast - [ ] Tâche Celery
rgz.forecast.revenuedansapp/tasks/forecast.py - [ ] Fonction
_linear_regression()(numpy polyfit) - [ ] Endpoints API GET/POST /api/v1/reports/forecast*
- [ ] Intégration #19 moteur-facturation (revenue_monthly calculation)
- [ ] Analyse sensibilité (si ARPU change ±10%)
- [ ] Email notification CFO avec 3 scénarios
- [ ] Tests: données synthétiques 12 mois avec tendance
- [ ] Dashboard finance: visualisation 3 scénarios
- [ ] Documentation: ARPU definition, regression method, scenario assumptions
Dernière mise à jour: 2026-02-21