using System.Globalization;
using Microsoft.EntityFrameworkCore;
using SchotzIadKmTracker.Api.Data;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Services.Exceptions;
namespace SchotzIadKmTracker.Api.Services;
///
public class RemboursementService : IRemboursementService
{
private readonly AppDbContext _context;
private readonly IBaremeService _baremeService;
private readonly IMontantRegleService _montantRegleService;
public RemboursementService(
AppDbContext context,
IBaremeService baremeService,
IMontantRegleService montantRegleService)
{
_context = context;
_baremeService = baremeService;
_montantRegleService = montantRegleService;
}
///
public async Task GetRegularisationAnnuelleAsync(
int annee, int vehiculeId, CancellationToken cancellationToken = default)
{
var vehicule = await _context.Vehicules
.AsNoTracking()
.FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken)
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable.");
// Kilomètres du véhicule par mois pour l'année (Distance nullable → 0 quand null).
// Le GROUP BY + SUM sur decimal n'est pas traduisible par SQLite : on ramène les
// trajets de l'année puis on agrège côté client.
var trajetsAnnee = await _context.Trajets
.AsNoTracking()
.Where(t => t.VehiculeId == vehiculeId && t.Date.Year == annee)
// On ramène la distance simple et l'indicateur aller/retour, puis on agrège la
// distance EFFECTIVE (doublée si A/R) côté client.
.Select(t => new { t.Date.Month, t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
var kmMoisMap = trajetsAnnee
.GroupBy(t => t.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => (t.Distance ?? 0m) * (t.AllerRetour ? 2m : 1m)));
// Total kilométrique annuel COMPLET (détermine la tranche — Exigence 14.5, Property 27).
var totalKmAnnuel = kmMoisMap.Values.Sum();
// Barème plein (100%) appliqué au total annuel : Montant_Reel = total_km × coef + base.
var bareme = await _baremeService.CalculerMontantAsync(
annee, vehicule.PuissanceFiscale, totalKmAnnuel, cancellationToken);
var coefficient = bareme.Coefficient;
var baseTranche = bareme.Base;
var montantReel = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero);
// Libellé de la tranche annuelle applicable (ex. « 5 001 à 20 000 km »).
var trancheLibelle = await BuildTrancheLibelleAsync(
annee, vehicule.PuissanceFiscale, totalKmAnnuel, bareme.BaremeConfigure, cancellationToken);
// Montant_Regle par mois pour ce véhicule/année.
var montantsRegles = await _montantRegleService.GetMontantsReglesAsync(annee, vehiculeId, cancellationToken);
var reglesMap = montantsRegles.ToDictionary(m => m.Mois, m => m.Montant);
// 12 lignes mensuelles :
// FraisReelMensuel = km_du_mois × coefficient de la tranche annuelle (Exigence 14.6),
// FraisPaye = Montant_Regle saisi du mois (0 si absent).
var lignes = new List(12);
for (var mois = 1; mois <= 12; mois++)
{
var kmMois = kmMoisMap.TryGetValue(mois, out var km) ? km : 0m;
var fraisPaye = reglesMap.TryGetValue(mois, out var paye) ? paye : 0m;
var fraisReelMensuel = Math.Round(kmMois * coefficient, 2, MidpointRounding.AwayFromZero);
lignes.Add(new LigneSyntheseMoisDto(
Mois: mois,
KmMois: Math.Round(kmMois, 2, MidpointRounding.AwayFromZero),
FraisPaye: fraisPaye,
FraisReelMensuel: fraisReelMensuel));
}
var totalPaye = Math.Round(montantsRegles.Sum(m => m.Montant), 2, MidpointRounding.AwayFromZero);
// Regularisation = Montant_Reel − total des Montant_Regle (Exigence 14.7).
var regularisation = Math.Round(montantReel - totalPaye, 2, MidpointRounding.AwayFromZero);
return new RegularisationAnnuelleDto(
Annee: annee,
VehiculeId: vehiculeId,
Plaque: vehicule.Plaque,
PuissanceFiscale: vehicule.PuissanceFiscale,
CoefficientTranche: coefficient,
BaseTranche: baseTranche,
TrancheLibelle: trancheLibelle,
Mois: lignes,
TotalKmAnnuel: Math.Round(totalKmAnnuel, 2, MidpointRounding.AwayFromZero),
TotalPaye: totalPaye,
MontantReel: montantReel,
Regularisation: regularisation);
}
///
/// Construit un libellé lisible de la tranche annuelle dont l'intervalle contient
/// . Retourne un libellé « non configuré » si aucune tranche
/// ne s'applique (barème absent ou total hors des intervalles définis).
///
private async Task BuildTrancheLibelleAsync(
int annee, int puissanceFiscale, decimal totalKmAnnuel, bool baremeConfigure, CancellationToken cancellationToken)
{
if (!baremeConfigure)
{
return "Barème non configuré";
}
var tranches = await _context.TranchesBareme
.AsNoTracking()
.Where(t => t.Annee == annee && t.PuissanceFiscale == puissanceFiscale)
.ToListAsync(cancellationToken);
var tranche = tranches
.FirstOrDefault(t => totalKmAnnuel >= t.KmMin && (t.KmMax == null || totalKmAnnuel < t.KmMax.Value));
if (tranche is null)
{
return "Barème non configuré";
}
return tranche.KmMax.HasValue
? $"{FormatKm(tranche.KmMin)} à {FormatKm(tranche.KmMax.Value)} km"
: $"{FormatKm(tranche.KmMin)} km et plus";
}
private static string FormatKm(decimal km)
{
// Espace comme séparateur de milliers, sans décimales (bornes de tranche entières).
var fr = CultureInfo.GetCultureInfo("fr-FR");
return Math.Round(km, 0, MidpointRounding.AwayFromZero).ToString("N0", fr);
}
}