Files
Distribution_GL/Services/TaxService.cs
T

158 lines
6.5 KiB
C#

using General_Ledger_Service.DTOs.Common;
using General_Ledger_Service.DTOs.Tax;
using General_Ledger_Service.Enums;
using General_Ledger_Service.Exceptions;
using General_Ledger_Service.Infra.UoW;
using General_Ledger_Service.Interfaces.Repositories;
using General_Ledger_Service.Interfaces.Services;
using General_Ledger_Service.Models;
namespace General_Ledger_Service.Services
{
public class TaxService(
ITaxRepository taxRepository,
IAccountRepository accountRepository,
IAuditLogRepository auditLogRepository,
IUnitOfWork unitOfWork) : ITaxService
{
private readonly ITaxRepository _taxRepository = taxRepository;
private readonly IAccountRepository _accountRepository = accountRepository;
private readonly IAuditLogRepository _auditLogRepository = auditLogRepository;
private readonly IUnitOfWork _unitOfWork = unitOfWork;
public async Task<ApiResponse> CreateTaxCodeAsync(CreateTaxCodeRequest request)
{
if (request.EffectiveFrom == default)
{
throw new BadRequestException("effectiveFrom is required");
}
if (!Enum.TryParse<TaxCalculationBasis>(request.CalculationBasis, ignoreCase: true, out var calculationBasis))
{
throw new BadRequestException(
$"Invalid calculationBasis '{request.CalculationBasis}'. Must be one of: {string.Join(", ", Enum.GetNames<TaxCalculationBasis>())}.");
}
if (!Enum.TryParse<TaxAppliesTo>(request.AppliesTo, ignoreCase: true, out var appliesTo))
{
throw new BadRequestException(
$"Invalid appliesTo '{request.AppliesTo}'. Must be one of: {string.Join(", ", Enum.GetNames<TaxAppliesTo>())}.");
}
var taxType = await _taxRepository.GetTaxTypeByCodeAsync(request.TaxTypeCode)
?? throw new NotFoundException("Tax type not found");
long? outputTaxAccountId = null;
if (!string.IsNullOrWhiteSpace(request.OutputTaxAccountCode))
{
var outputAccount = await _accountRepository.GetByCodeAsync(request.OutputTaxAccountCode)
?? throw new NotFoundException("Output tax account not found");
outputTaxAccountId = outputAccount.AccountId;
}
long? inputTaxAccountId = null;
if (!string.IsNullOrWhiteSpace(request.InputTaxAccountCode))
{
var inputAccount = await _accountRepository.GetByCodeAsync(request.InputTaxAccountCode)
?? throw new NotFoundException("Input tax account not found");
inputTaxAccountId = inputAccount.AccountId;
}
var code = await GenerateTaxCodeAsync(taxType);
var taxCode = new TaxCode
{
TaxTypeId = taxType.TaxTypeId,
Code = code,
Name = request.Name,
RatePercent = request.RatePercent,
CalculationBasis = calculationBasis,
AppliesTo = appliesTo,
OutputTaxAccountId = outputTaxAccountId,
InputTaxAccountId = inputTaxAccountId,
EffectiveFrom = request.EffectiveFrom,
EffectiveTo = request.EffectiveTo,
IsActive = true
};
await _unitOfWork.BeginAsync();
try
{
await _taxRepository.AddTaxCodeAsync(taxCode);
await _unitOfWork.SaveChangesAsync();
await _auditLogRepository.AddAsync(new AuditLog
{
Category = AuditCategory.Ledger,
EntityName = "TaxCode",
EntityId = taxCode.TaxCodeId.ToString(),
Action = AuditAction.Create,
Description = $"Tax code {taxCode.Code} created",
PerformedBy = null
});
await _unitOfWork.SaveChangesAsync();
await _unitOfWork.CommitAsync();
}
catch
{
await _unitOfWork.RollbackAsync();
throw;
}
return ResponseHelpers.Created("Tax code created successfully", taxCode);
}
private async Task<string> GenerateTaxCodeAsync(TaxType taxType)
{
// Probes candidates directly rather than trusting a count (same fix as
// AccountService.GenerateAccountCodeAsync) so a pre-existing, manually-coded tax code
// can never collide with a generated one.
for (var seq = 1; seq <= 999; seq++)
{
var code = $"{taxType.Code}-{seq:D3}";
if (await _taxRepository.GetByCodeAsync(code) is null)
{
return code;
}
}
throw new BadRequestException("Maximum number of tax codes for this tax type has been reached.");
}
public async Task<ApiResponse> CalculateTaxAsync(CalculateTaxRequest request)
{
var taxCode = await _taxRepository.GetEffectiveTaxCodeByCodeAsync(request.TaxCode, request.TxnDate)
?? throw new NotFoundException("Tax code not found or not effective for the given date");
var taxAmount = taxCode.CalculationBasis switch
{
TaxCalculationBasis.Exclusive => request.Amount * (taxCode.RatePercent / 100),
TaxCalculationBasis.Inclusive => request.Amount - (request.Amount / (1 + (taxCode.RatePercent / 100))),
_ => throw new BadRequestException($"Unsupported calculationBasis '{taxCode.CalculationBasis}'.")
};
return ResponseHelpers.Ok("Tax calculated successfully", new CalculateTaxResponse
{
TaxCodeId = taxCode.TaxCodeId,
Code = taxCode.Code,
RatePercent = taxCode.RatePercent,
LineTotal = request.Amount,
TaxAmount = taxAmount
});
}
public async Task<ApiResponse> ListTaxTypesAsync()
{
var taxTypes = await _taxRepository.GetAllTaxTypesAsync();
return ResponseHelpers.Ok("Tax types retrieved successfully", taxTypes);
}
public async Task<ApiResponse> ListTaxCodesAsync()
{
var taxCodes = await _taxRepository.GetAllTaxCodesAsync();
return ResponseHelpers.Ok("Tax codes retrieved successfully", taxCodes);
}
}
}