158 lines
6.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|