Skip to content

Commit 9c9d1ba

Browse files
author
malekBHA
committed
2 parents 773f8d8 + f8fa312 commit 9c9d1ba

File tree

110 files changed

+17434
-240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+17434
-240
lines changed

src/TunNetCom.SilkRoadErp.Sales.Api/Features/Commandes/GetCommande/GetCommandQueryHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using TunNetCom.SilkRoadErp.Sales.Contracts.Commande;
1+
using TunNetCom.SilkRoadErp.Sales.Contracts.Commande;
22

33
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.Commandes.GetCommandes;
44

@@ -61,7 +61,9 @@ public async Task<Result<FullOrderResponse>> Handle(
6161
ItemDescription = lc.DesignationLi,
6262
ItemQuantity = lc.QteLi,
6363
UnitPriceExcludingTax = lc.PrixHt,
64+
Discount = (decimal)lc.Remise,
6465
TotalExcludingTax = lc.TotHt,
66+
VatRate = (decimal)lc.Tva,
6567
TotalIncludingTax = lc.TotTtc
6668
}).ToList()
6769
})

src/TunNetCom.SilkRoadErp.Sales.Api/Features/Customers/CreateCustomer/CreateCustomerValidator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.Customers.CreateCustomer;
1+
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.Customers.CreateCustomer;
22

33
public class CreateCustomerValidator : AbstractValidator<CreateCustomerCommand>
44
{
@@ -29,6 +29,7 @@ public CreateCustomerValidator()
2929

3030
_ = RuleFor(x => x.Mail)
3131
.MaximumLength(50).WithMessage("mail_must_be_less_than_50_characters")
32-
.EmailAddress().WithMessage("mail_must_be_a_valid_email_address");
32+
.EmailAddress().WithMessage("mail_must_be_a_valid_email_address")
33+
.When(x => !string.IsNullOrEmpty(x.Mail));
3334
}
3435
}

src/TunNetCom.SilkRoadErp.Sales.Api/Features/Dashboard/GetRecapVentesAchats/GetRecapVentesAchatsQueryHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public async Task<RecapVentesAchatsResponse> Handle(GetRecapVentesAchatsQuery re
6464
response.AchatsNets.TotalTTC += response.AchatsFacturesDepensesTotal;
6565

6666
// 8) Paiements clients
67-
var paiementsClientResult = await mediator.Send(new GetPaiementsClientQuery(null, null, startDate, endDateInclusive, null, null, null, null, null, 1, 10000), cancellationToken);
67+
var paiementsClientResult = await mediator.Send(new GetPaiementsClientQuery(null, null, null, null, startDate, endDateInclusive, null, null, null, null, null, 1, 10000), cancellationToken);
6868
response.PaiementsClientsTotal = paiementsClientResult.Value?.Items?.Sum(p => p.Montant) ?? 0;
6969

7070
// 9) Paiements fournisseurs

src/TunNetCom.SilkRoadErp.Sales.Api/Features/DeliveryNote/GetDeliveryNotesBasedOnProductReference/GetDeliveryNotesBasedOnProductReferenceEndpoint.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
using TunNetCom.SilkRoadErp.Sales.Contracts.DeliveryNote.Responses;
1+
using TunNetCom.SilkRoadErp.Sales.Contracts.DeliveryNote.Responses;
22

33
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.DeliveryNote.GetDeliveryNotesBasedOnProductReference;
44

55
public class GetDeliveryNotesBasedOnProductReferenceEndpoint : ICarterModule
66
{
77
public void AddRoutes(IEndpointRouteBuilder app)
88
{
9-
_ = app.MapGet("/deliveryNoteHistory/{productReference}", async (
9+
_ = app.MapGet("/deliveryNoteHistory", async (
1010
IMediator mediator,
11-
string productReference,
11+
[FromQuery] string? productReference,
1212
[AsParameters] QueryStringParameters paginationQueryParams,
1313
HttpContext httpContext,
1414
CancellationToken cancellationToken) =>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
using TunNetCom.SilkRoadErp.Sales.Contracts.FactureDepense;
2+
13
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.FactureDepense.CreateFactureDepense;
24

35
public record CreateFactureDepenseCommand(
46
int IdTiersDepenseFonctionnement,
57
DateTime Date,
68
string Description,
79
decimal MontantTotal,
10+
List<FactureDepenseLigneTvaDto> LignesTVA,
811
int? AccountingYearId,
912
string? DocumentBase64) : IRequest<Result<int>>;

src/TunNetCom.SilkRoadErp.Sales.Api/Features/FactureDepense/CreateFactureDepense/CreateFactureDepenseCommandHandler.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,38 @@ public async Task<Result<int>> Handle(CreateFactureDepenseCommand command, Cance
5757

5858
var num = await _numberGeneratorService.GenerateFactureDepenseNumberAsync(accountingYearId.Value, cancellationToken);
5959

60+
var (baseHT0, montantTVA0, baseHT7, montantTVA7, baseHT13, montantTVA13, baseHT19, montantTVA19) = GetLignesFromDto(command.LignesTVA);
61+
6062
var entity = Domain.Entites.FactureDepense.Create(
6163
num,
6264
command.IdTiersDepenseFonctionnement,
6365
command.Date,
6466
command.Description ?? string.Empty,
6567
command.MontantTotal,
6668
accountingYearId.Value,
67-
documentStoragePath);
69+
documentStoragePath,
70+
baseHT0, montantTVA0, baseHT7, montantTVA7, baseHT13, montantTVA13, baseHT19, montantTVA19);
6871

6972
_context.FactureDepense.Add(entity);
7073
await _context.SaveChangesAsync(cancellationToken);
7174

7275
_logger.LogInformation("FactureDepense created with Id {Id}, Num {Num}", entity.Id, entity.Num);
7376
return Result.Ok(entity.Id);
7477
}
78+
79+
private static (decimal baseHT0, decimal montantTVA0, decimal baseHT7, decimal montantTVA7, decimal baseHT13, decimal montantTVA13, decimal baseHT19, decimal montantTVA19) GetLignesFromDto(List<Contracts.FactureDepense.FactureDepenseLigneTvaDto>? lignes)
80+
{
81+
decimal b0 = 0, t0 = 0, b7 = 0, t7 = 0, b13 = 0, t13 = 0, b19 = 0, t19 = 0;
82+
if (lignes != null)
83+
{
84+
foreach (var l in lignes)
85+
{
86+
if (l.TauxTVA == 0) { b0 = l.BaseHT; t0 = l.MontantTVA; }
87+
else if (l.TauxTVA == 7) { b7 = l.BaseHT; t7 = l.MontantTVA; }
88+
else if (l.TauxTVA == 13) { b13 = l.BaseHT; t13 = l.MontantTVA; }
89+
else if (l.TauxTVA == 19) { b19 = l.BaseHT; t19 = l.MontantTVA; }
90+
}
91+
}
92+
return (b0, t0, b7, t7, b13, t13, b19, t19);
93+
}
7594
}

src/TunNetCom.SilkRoadErp.Sales.Api/Features/FactureDepense/CreateFactureDepense/CreateFactureDepenseEndpoint.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public void AddRoutes(IEndpointRouteBuilder app)
1515
request.Date,
1616
request.Description ?? string.Empty,
1717
request.MontantTotal,
18+
request.LignesTVA ?? new(),
1819
request.AccountingYearId,
1920
request.DocumentBase64);
2021
var result = await mediator.Send(command, cancellationToken);
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
using Carter;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.EntityFrameworkCore;
4+
using TunNetCom.SilkRoadErp.Sales.Api.Features.AppParameters.GetAppParameters;
5+
using TunNetCom.SilkRoadErp.Sales.Api.Infrastructure.Constants;
6+
using TunNetCom.SilkRoadErp.Sales.Api.Infrastructure.Services;
7+
using TunNetCom.SilkRoadErp.Sales.Domain.Entites;
8+
9+
namespace TunNetCom.SilkRoadErp.Sales.Api.Features.FactureDepense.ExportToTejXml;
10+
11+
public class ExportFactureDepenseToTejXmlEndpoint : ICarterModule
12+
{
13+
public void AddRoutes(IEndpointRouteBuilder app)
14+
{
15+
_ = app.MapGet("/api/factures-depenses/{id:int}/export/tej-xml", HandleExportToTejXmlAsync)
16+
.RequireAuthorization($"Permission:{Permissions.ViewFactureDepense}")
17+
.WithTags(EndpointTags.FactureDepense)
18+
.Produces(StatusCodes.Status200OK)
19+
.Produces(StatusCodes.Status400BadRequest)
20+
.Produces(StatusCodes.Status404NotFound)
21+
.Produces(StatusCodes.Status500InternalServerError);
22+
}
23+
24+
public static async Task<Results<FileContentHttpResult, BadRequest<string>, NotFound<string>, StatusCodeHttpResult>> HandleExportToTejXmlAsync(
25+
[FromServices] SalesContext context,
26+
[FromServices] TejXmlExportService exportService,
27+
[FromServices] INumberGeneratorService numberGeneratorService,
28+
[FromServices] IMediator mediator,
29+
[FromServices] ILogger<ExportFactureDepenseToTejXmlEndpoint> logger,
30+
[FromServices] IActiveAccountingYearService activeAccountingYearService,
31+
[FromServices] IAccountingYearFinancialParametersService financialParametersService,
32+
int id,
33+
[FromQuery] string? acteDepot = "0",
34+
CancellationToken cancellationToken = default)
35+
{
36+
try
37+
{
38+
logger.LogInformation("ExportFactureDepenseToTejXmlEndpoint called for FactureDepense Id {Id}", id);
39+
40+
var appParamsResult = await mediator.Send(new GetAppParametersQuery(), cancellationToken);
41+
if (appParamsResult.IsFailed)
42+
{
43+
logger.LogError("Failed to retrieve app parameters");
44+
return TypedResults.BadRequest("Impossible de récupérer les paramètres de l'application.");
45+
}
46+
47+
var factureDepense = await context.FactureDepense
48+
.Include(f => f.IdTiersDepenseFonctionnementNavigation)
49+
.FirstOrDefaultAsync(f => f.Id == id, cancellationToken);
50+
51+
if (factureDepense == null)
52+
{
53+
logger.LogWarning("FactureDepense Id {Id} not found", id);
54+
return TypedResults.NotFound($"Facture dépense introuvable.");
55+
}
56+
57+
var tiers = factureDepense.IdTiersDepenseFonctionnementNavigation;
58+
if (tiers == null)
59+
{
60+
logger.LogWarning("Tiers dépense not found for FactureDepense Id {Id}", id);
61+
return TypedResults.BadRequest("Tiers dépense introuvable pour cette facture.");
62+
}
63+
64+
if (tiers.ExonereRetenueSource)
65+
{
66+
logger.LogWarning("TEJ export not applicable: tiers is exempt from withholding tax for FactureDepense Id {Id}", id);
67+
return TypedResults.BadRequest("Le tiers est exonéré de retenue à la source ; l'export TEJ n'est pas applicable.");
68+
}
69+
70+
var seuilRetenueSource = await financialParametersService.GetSeuilRetenueSourceAsync(1000, cancellationToken);
71+
if (factureDepense.MontantTotal < seuilRetenueSource)
72+
{
73+
logger.LogWarning(
74+
"Montant {MontantTotal} is below threshold {Seuil} for FactureDepense Id {Id}",
75+
factureDepense.MontantTotal, seuilRetenueSource, id);
76+
return TypedResults.BadRequest(
77+
$"Le montant ({factureDepense.MontantTotal:F2}) doit être supérieur ou égal au seuil ({seuilRetenueSource:F2}) pour pouvoir exporter en XML TEJ.");
78+
}
79+
80+
var existingAttribution = await context.TejCertificatFactureDepense
81+
.AsNoTracking()
82+
.FirstOrDefaultAsync(t => t.FactureDepenseId == id, cancellationToken);
83+
84+
string refCertifTej;
85+
if (existingAttribution != null)
86+
{
87+
refCertifTej = existingAttribution.RefCertif;
88+
}
89+
else
90+
{
91+
var year = factureDepense.Date.Year;
92+
var month = factureDepense.Date.Month;
93+
refCertifTej = await numberGeneratorService.GetNextTejCertificatRefAsync(year, month, cancellationToken);
94+
context.TejCertificatFactureDepense.Add(new TejCertificatFactureDepense
95+
{
96+
FactureDepenseId = id,
97+
RefCertif = refCertifTej
98+
});
99+
await context.SaveChangesAsync(cancellationToken);
100+
}
101+
102+
_ = await activeAccountingYearService.GetActiveAccountingYearIdAsync(cancellationToken);
103+
104+
var systeme = await context.Systeme
105+
.AsNoTracking()
106+
.FirstOrDefaultAsync(cancellationToken);
107+
108+
if (systeme == null)
109+
{
110+
logger.LogError("Systeme parameters not found");
111+
return TypedResults.BadRequest("Paramètres système introuvables.");
112+
}
113+
114+
var financialParams = await financialParametersService.GetAllFinancialParametersAsync(cancellationToken);
115+
if (financialParams == null)
116+
{
117+
logger.LogError("No active accounting year found");
118+
return TypedResults.BadRequest("Aucun exercice comptable actif trouvé.");
119+
}
120+
121+
if (string.IsNullOrWhiteSpace(systeme.MatriculeFiscale))
122+
{
123+
logger.LogWarning("MatriculeFiscale is missing in systeme parameters");
124+
return TypedResults.BadRequest("Le matricule fiscal de l'entreprise n'est pas configuré dans les paramètres système.");
125+
}
126+
127+
if (string.IsNullOrWhiteSpace(tiers.Matricule))
128+
{
129+
logger.LogWarning("Tiers Matricule is missing for FactureDepense Id {Id}", id);
130+
return TypedResults.BadRequest("Le matricule fiscal du tiers n'est pas configuré pour cette facture.");
131+
}
132+
133+
if (string.IsNullOrWhiteSpace(tiers.Mail))
134+
{
135+
logger.LogWarning("Tiers Mail missing for FactureDepense Id {Id}, TEJ export blocked", id);
136+
return TypedResults.BadRequest(
137+
"L'export TEJ exige l'adresse e-mail du tiers. Veuillez renseigner l'e-mail du tiers avant d'exporter.");
138+
}
139+
140+
var telDigitsOnly = new string((tiers.Tel ?? "").Where(char.IsDigit).ToArray());
141+
if (telDigitsOnly.Length != 8)
142+
{
143+
logger.LogWarning("Tiers Tel not in format XXXXXXXX (8 digits) for FactureDepense Id {Id}, TEJ export blocked", id);
144+
return TypedResults.BadRequest(
145+
"L'export TEJ exige le téléphone du tiers au format XXXXXXXX (8 chiffres). Veuillez corriger le numéro du tiers.");
146+
}
147+
148+
var tiersMatriculeNormalise = TryNormalizeMatriculeFiscal(tiers.Matricule.Trim());
149+
if (tiersMatriculeNormalise is null)
150+
{
151+
logger.LogWarning("Tiers Matricule format invalid for FactureDepense Id {Id}: expected 7 digits + 1 letter", id);
152+
return TypedResults.BadRequest(
153+
"Le matricule fiscal du tiers doit être au format 7 chiffres et une lettre clé (ex. 0001238L).");
154+
}
155+
156+
var matriculeNormaliseResult = TryNormalizeMatriculeFiscal(systeme.MatriculeFiscale.Trim());
157+
if (matriculeNormaliseResult is null)
158+
{
159+
logger.LogWarning("MatriculeFiscale format invalid: expected 7 digits + 1 letter, got {Value}", systeme.MatriculeFiscale);
160+
return TypedResults.BadRequest(
161+
"Le matricule fiscal de l'entreprise doit être au format 7 chiffres et une lettre clé (ex. 0001238L).");
162+
}
163+
164+
var acteDepotValue = acteDepot == "1" ? "1" : "0";
165+
166+
var xmlBytes = exportService.ExportFactureDepenseToTejXml(
167+
factureDepense,
168+
tiers,
169+
systeme,
170+
appParamsResult.Value,
171+
financialParams,
172+
refCertifChezDeclarant: refCertifTej,
173+
normalizedDeclarantMatricule: matriculeNormaliseResult,
174+
normalizedBeneficiaireMatricule: tiersMatriculeNormalise,
175+
beneficiaireTel8Digits: telDigitsOnly,
176+
acteDepot: acteDepotValue);
177+
178+
var validationErrors = TejXsdValidator.Validate(xmlBytes)
179+
.Where(e => !e.Contains("introuvable", StringComparison.OrdinalIgnoreCase))
180+
.ToList();
181+
if (validationErrors.Count > 0)
182+
{
183+
logger.LogWarning("TEJ XSD validation failed for FactureDepense Id {Id}: {Errors}", id, string.Join("; ", validationErrors));
184+
return TypedResults.BadRequest(
185+
"Le fichier XML n'est pas conforme au schéma TEJ: " + string.Join("; ", validationErrors));
186+
}
187+
188+
var exercice = factureDepense.Date.Year;
189+
var mois = factureDepense.Date.Month.ToString("D2");
190+
var filename = $"{matriculeNormaliseResult}-{exercice}-{mois}-{acteDepotValue}.xml";
191+
192+
logger.LogInformation("TEJ XML export completed successfully for FactureDepense Id {Id}", id);
193+
194+
return TypedResults.File(
195+
xmlBytes,
196+
contentType: "application/xml; charset=utf-8",
197+
fileDownloadName: filename);
198+
}
199+
catch (Exception ex)
200+
{
201+
logger.LogError(ex, "Error exporting FactureDepense Id {Id} to TEJ XML format", id);
202+
return TypedResults.StatusCode(500);
203+
}
204+
}
205+
206+
private static string? TryNormalizeMatriculeFiscal(string matriculeFiscale)
207+
{
208+
if (string.IsNullOrWhiteSpace(matriculeFiscale))
209+
return null;
210+
211+
var cleaned = new string(matriculeFiscale.Where(c => char.IsLetterOrDigit(c)).ToArray());
212+
if (cleaned.Length == 0)
213+
return null;
214+
215+
var digits = new string(cleaned.Where(char.IsDigit).Take(7).ToArray());
216+
var letterPart = new string(cleaned.Where(char.IsLetter).ToArray());
217+
if (letterPart.Length == 0)
218+
return null;
219+
220+
var digitsPadded = digits.PadLeft(7, '0');
221+
var letter = letterPart[0];
222+
if (!char.IsLetter(letter))
223+
return null;
224+
225+
return digitsPadded + char.ToUpperInvariant(letter);
226+
}
227+
}

src/TunNetCom.SilkRoadErp.Sales.Api/Features/FactureDepense/GetFactureDepense/GetFactureDepenseQueryHandler.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,35 @@ public async Task<Result<FactureDepenseResponse>> Handle(GetFactureDepenseQuery
1313
.AsNoTracking()
1414
.Include(f => f.IdTiersDepenseFonctionnementNavigation)
1515
.Where(f => f.Id == query.Id)
16-
.Select(f => new FactureDepenseResponse
17-
{
18-
Id = f.Id,
19-
Num = f.Num,
20-
IdTiersDepenseFonctionnement = f.IdTiersDepenseFonctionnement,
21-
TiersDepenseFonctionnementNom = f.IdTiersDepenseFonctionnementNavigation.Nom,
22-
Date = f.Date,
23-
Description = f.Description,
24-
MontantTotal = f.MontantTotal,
25-
AccountingYearId = f.AccountingYearId,
26-
Statut = f.Statut.ToString(),
27-
DocumentStoragePath = f.DocumentStoragePath,
28-
HasDocument = !string.IsNullOrEmpty(f.DocumentStoragePath)
29-
})
3016
.FirstOrDefaultAsync(cancellationToken);
3117

3218
if (entity == null)
3319
{
3420
_logger.LogWarning("FactureDepense not found: Id {Id}", query.Id);
3521
return Result.Fail(EntityNotFound.Error());
3622
}
37-
return Result.Ok(entity);
23+
24+
var response = new FactureDepenseResponse
25+
{
26+
Id = entity.Id,
27+
Num = entity.Num,
28+
IdTiersDepenseFonctionnement = entity.IdTiersDepenseFonctionnement,
29+
TiersDepenseFonctionnementNom = entity.IdTiersDepenseFonctionnementNavigation.Nom,
30+
Date = entity.Date,
31+
Description = entity.Description,
32+
MontantTotal = entity.MontantTotal,
33+
LignesTVA = new List<FactureDepenseLigneTvaDto>
34+
{
35+
new() { TauxTVA = 0, BaseHT = entity.BaseHT0, MontantTVA = entity.MontantTVA0 },
36+
new() { TauxTVA = 7, BaseHT = entity.BaseHT7, MontantTVA = entity.MontantTVA7 },
37+
new() { TauxTVA = 13, BaseHT = entity.BaseHT13, MontantTVA = entity.MontantTVA13 },
38+
new() { TauxTVA = 19, BaseHT = entity.BaseHT19, MontantTVA = entity.MontantTVA19 }
39+
},
40+
AccountingYearId = entity.AccountingYearId,
41+
Statut = entity.Statut.ToString(),
42+
DocumentStoragePath = entity.DocumentStoragePath,
43+
HasDocument = !string.IsNullOrEmpty(entity.DocumentStoragePath)
44+
};
45+
return Result.Ok(response);
3846
}
3947
}

0 commit comments

Comments
 (0)