using Microsoft.EntityFrameworkCore; using SchotzIadKmTracker.Api.Data; using SchotzIadKmTracker.Api.Models.DTOs; using SchotzIadKmTracker.Api.Models.Entities; using SchotzIadKmTracker.Api.Services.Exceptions; namespace SchotzIadKmTracker.Api.Services; /// public class MontantRegleService : IMontantRegleService { private const decimal TauxPrevisionnel = 0.80m; private readonly AppDbContext _context; private readonly IBaremeService _baremeService; public MontantRegleService(AppDbContext context, IBaremeService baremeService) { _context = context; _baremeService = baremeService; } /// public async Task UpsertMontantRegleAsync( MontantRegleDto dto, CancellationToken cancellationToken = default) { // Défense en profondeur : le validateur FluentValidation impose déjà Montant >= 0, // mais le service refuse aussi les montants négatifs (Exigence 14.4). if (dto.Montant < 0m) { throw new MontantRegleNegatifException("Le montant réglé ne peut pas être négatif."); } // Upsert par la clé métier (Annee, Mois, VehiculeId) — index unique // UX_MontantsRegles_Periode_Vehicule garantit l'unicité (Property 26, Exigence 14.3). var existant = await _context.MontantsRegles .FirstOrDefaultAsync( m => m.Annee == dto.Annee && m.Mois == dto.Mois && m.VehiculeId == dto.VehiculeId, cancellationToken); if (existant is null) { var montantRegle = new MontantRegle { Annee = dto.Annee, Mois = dto.Mois, VehiculeId = dto.VehiculeId, Montant = dto.Montant, UpdatedAt = DateTime.UtcNow, }; _context.MontantsRegles.Add(montantRegle); await _context.SaveChangesAsync(cancellationToken); return ToDto(montantRegle); } existant.Montant = dto.Montant; existant.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(cancellationToken); return ToDto(existant); } /// public async Task> GetMontantsReglesAsync( int annee, int vehiculeId, CancellationToken cancellationToken = default) { var montants = await _context.MontantsRegles .AsNoTracking() .Where(m => m.Annee == annee && m.VehiculeId == vehiculeId) .OrderBy(m => m.Mois) .ToListAsync(cancellationToken); return montants.Select(ToDto).ToList(); } /// public async Task GetMontantPrevisionnelAsync( int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default) { // Puissance fiscale du véhicule concerné : détermine la tranche du barème. var vehicule = await _context.Vehicules .AsNoTracking() .FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken) ?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable."); // Total_Annuel_Partiel : somme des distances de l'année pour ce véhicule, // jusqu'au mois sélectionné inclus (les mois postérieurs sont exclus). // SQLite ne sait pas appliquer SUM sur une colonne decimal : on ramène les distances // concernées et on somme côté client (null traité comme 0). var distancesPartielles = await _context.Trajets .AsNoTracking() .Where(t => t.VehiculeId == vehiculeId && t.Date.Year == annee && t.Date.Month <= mois) .Select(t => t.Distance) .ToListAsync(cancellationToken); var totalAnnuelPartiel = distancesPartielles.Sum(d => d ?? 0m); // Barème plein appliqué au total partiel (sans minoration). var bareme = await _baremeService.CalculerMontantAsync( annee, vehicule.PuissanceFiscale, totalAnnuelPartiel, cancellationToken); var montantBaremePlein = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero); // Montant_Previsionnel = 80% du barème plein, arrondi à 2 décimales (indicatif — Exigences 13.5/13.6). var montantPrevisionnel = Math.Round(bareme.Montant * TauxPrevisionnel, 2, MidpointRounding.AwayFromZero); return new MontantPrevisionnelDto( Annee: annee, Mois: mois, VehiculeId: vehiculeId, TotalAnnuelPartielKm: Math.Round(totalAnnuelPartiel, 2, MidpointRounding.AwayFromZero), MontantBaremePlein: montantBaremePlein, MontantPrevisionnel: montantPrevisionnel); } private static MontantRegleDto ToDto(MontantRegle m) => new(m.Annee, m.Mois, m.VehiculeId, m.Montant); }