Files
ERP_Distribution/Distribution/Features/RecurringExpense/RecurringExpenseService.cs
T
Kalana B. Thilakarathna c775146b64
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 13m39s
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 15m1s
10.
Implementation
A partner, not just software
HexDive Technologies' local team handles setup, data migration, training and ongoing support — in Sinhala, Tamil and English. [confirm]

1
Discovery
Map your network, products and processes.

2
Configuration
Set up depots, pricing, schemes, routes and users.

3
Migration & training
Move your data and train every team.

4
Go-live & support
Dedicated account manager and local support desk.
2026-10-22 17:47:49 +05:30

184 lines
7.6 KiB
C#

using Distribution.Data;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
using Distribution.Models;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.RecurringExpense;
public class RecurringExpenseService(
AppDBContext context,
IRecurringExpenseRepository repo,
IUnitOfWork uow,
ICurrentUserService currentUser)
{
private static RecurringExpenseResponse Map(Models.RecurringExpenseTemplate t) =>
new(t.Id, t.Name,
t.FK_CategoryId, t.Category?.Name ?? string.Empty,
t.FK_WarehouseId, t.Warehouse?.Name,
t.FK_VendorId, t.Vendor?.Name,
t.Amount, t.Payee, t.Description,
t.Frequency, t.DayOfMonth, t.DayOfWeek,
t.StartDate, t.EndDate, t.NextRunDate, t.AutoPost, t.IsPaused, t.IsActive, t.CreatedAt);
public async Task<PagedRecurringExpensesResponse> ListAsync(int page, int pageSize)
{
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 200) pageSize = 20;
var items = await repo.ListAsync(page, pageSize);
var total = await repo.CountAsync();
return new PagedRecurringExpensesResponse(items.Select(Map).ToList(), page, pageSize, total);
}
public async Task<RecurringExpenseResponse> CreateAsync(CreateRecurringExpenseRequest request, CancellationToken ct = default)
{
ValidateSchedule(request.Frequency, request.DayOfMonth, request.DayOfWeek);
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
var category = await context.ExpenseCategories.FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.IsActive, ct)
?? throw new InvalidOperationException("Category not found");
if (category.FK_ParentCategoryId is null)
throw new InvalidOperationException("Pick a specific title under a category");
var entity = new Models.RecurringExpenseTemplate
{
Name = request.Name.Trim(),
FK_CategoryId = request.CategoryId,
FK_WarehouseId = request.WarehouseId,
FK_VendorId = request.VendorId,
Amount = request.Amount,
Payee = request.Payee,
Description = request.Description,
Frequency = request.Frequency,
DayOfMonth = request.DayOfMonth,
DayOfWeek = request.DayOfWeek,
StartDate = request.StartDate.Date,
EndDate = request.EndDate?.Date,
NextRunDate = request.StartDate.Date,
AutoPost = request.AutoPost
};
await uow.BeginAsync(ct);
try { await repo.AddAsync(entity); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map((await repo.GetByIdAsync(entity.Id))!);
}
public async Task<RecurringExpenseResponse?> UpdateAsync(Guid id, UpdateRecurringExpenseRequest request, CancellationToken ct = default)
{
var entity = await repo.GetByIdAsync(id);
if (entity is null) return null;
ValidateSchedule(request.Frequency, request.DayOfMonth, request.DayOfWeek);
if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero");
entity.Name = request.Name.Trim();
entity.FK_CategoryId = request.CategoryId;
entity.FK_WarehouseId = request.WarehouseId;
entity.FK_VendorId = request.VendorId;
entity.Amount = request.Amount;
entity.Payee = request.Payee;
entity.Description = request.Description;
entity.Frequency = request.Frequency;
entity.DayOfMonth = request.DayOfMonth;
entity.DayOfWeek = request.DayOfWeek;
entity.StartDate = request.StartDate.Date;
entity.EndDate = request.EndDate?.Date;
entity.AutoPost = request.AutoPost;
await uow.BeginAsync(ct);
try { repo.Update(entity); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map((await repo.GetByIdAsync(id))!);
}
public async Task<RecurringExpenseResponse?> ToggleAsync(Guid id, bool paused, CancellationToken ct = default)
{
var entity = await repo.GetByIdAsync(id);
if (entity is null) return null;
entity.IsPaused = paused;
await uow.BeginAsync(ct);
try { repo.Update(entity); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map(entity);
}
/// <summary>
/// Materializes every active, non-paused template whose NextRunDate ≤ today
/// into an Expense row, then advances NextRunDate by one period. Idempotent
/// per call — advances only after a successful insert.
/// </summary>
public async Task<RunDueResponse> RunDueAsync(CancellationToken ct = default)
{
var today = DateTime.Today;
var due = await repo.ListDueAsync(today);
if (due.Count == 0) return new RunDueResponse(0, []);
var userId = currentUser.UserId
?? throw new InvalidOperationException("Authenticated user is required");
var createdIds = new List<Guid>();
await uow.BeginAsync(ct);
try
{
foreach (var t in due)
{
var expense = new Models.Expense
{
FK_CategoryId = t.FK_CategoryId,
FK_WarehouseId = t.FK_WarehouseId,
FK_VendorId = t.FK_VendorId,
Amount = t.Amount,
BillDate = t.NextRunDate,
PeriodCovered = new DateTime(t.NextRunDate.Year, t.NextRunDate.Month, 1),
Payee = t.Payee,
Description = t.Description,
Status = t.AutoPost ? ExpenseStatus.Posted : ExpenseStatus.Draft,
RecordedByUserId = userId,
PostedByUserId = t.AutoPost ? userId : null,
PostedAt = t.AutoPost ? DateTime.Now : null,
FK_RecurringTemplateId = t.Id
};
await context.Expenses.AddAsync(expense, ct);
createdIds.Add(expense.Id);
t.NextRunDate = AdvanceNext(t.NextRunDate, t.Frequency);
repo.Update(t);
}
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return new RunDueResponse(createdIds.Count, createdIds);
}
private static DateTime AdvanceNext(DateTime from, RecurringFrequency freq) => freq switch
{
RecurringFrequency.Weekly => from.AddDays(7),
RecurringFrequency.Monthly => from.AddMonths(1),
RecurringFrequency.Quarterly => from.AddMonths(3),
RecurringFrequency.Yearly => from.AddYears(1),
_ => throw new InvalidOperationException($"Unknown frequency {freq}")
};
private static void ValidateSchedule(RecurringFrequency freq, int? dayOfMonth, int? dayOfWeek)
{
if (freq == RecurringFrequency.Weekly)
{
if (dayOfWeek is null or < 0 or > 6)
throw new InvalidOperationException("DayOfWeek (0-6) is required for Weekly");
}
else
{
if (dayOfMonth is null or < 1 or > 31)
throw new InvalidOperationException("DayOfMonth (1-31) is required for Monthly/Quarterly/Yearly");
}
}
}