using ERPCore.Domain.Enums; using ERPCore.Dtos.Accounts; using ERPCore.System.Errors; using ERPCore.Tests.TestSupport; using Microsoft.EntityFrameworkCore; using static ERPCore.Tests.TestSupport.BankDepositTestSupport; namespace ERPCore.Tests; /// Recording and cancelling bank deposits (docs/BANK_DEPOSIT_MODULE_PLAN.md §7.2, §11). public class BankDepositServiceTests { private static readonly DateOnly Today = Clock.Today; private static readonly DateOnly Start = Today.AddDays(-10); private static readonly DateOnly Day = Today.AddDays(-2); [Fact] public async Task Create_PostsBankDebitCashCredit_AndLowersCashInHand() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); await db.SaveChangesAsync(); var gl = new FakeGeneralLedger(); var dto = await DepositService(db, Start, gl).CreateAsync(Request(600, depositDate: Day.AddDays(1))); Assert.Equal(BankDepositStatus.Posted, dto.Status); Assert.Equal(("BOC Current", "Bank of Ceylon", "0001234567"), (dto.ToBankAccountName, dto.ToBankName, dto.ToAccountNumber)); Assert.Equal("JE-1", dto.GlJournalNo); var entry = Assert.Single(gl.Posted); Assert.Equal(Day.AddDays(1), entry.EntryDate); Assert.Equal("BANK_DEPOSIT", entry.SourceModule); Assert.Collection(entry.Lines, l => Assert.Equal(("1010-01", 600m, 0m), (l.AccountCode, l.DebitAmount, l.CreditAmount)), l => Assert.Equal(("1000-01", 0m, 600m), (l.AccountCode, l.DebitAmount, l.CreditAmount))); var position = await CashService(db, Start).GetPositionAsync(Day, Branch1); Assert.Equal((600m, 400m), (position.Position.Deposited, position.Position.Closing)); } [Fact] public async Task Create_MoreThanCashInHand_IsRejected() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000, card: 5000)); await db.SaveChangesAsync(); var gl = new FakeGeneralLedger(); var ex = await Assert.ThrowsAsync(() => DepositService(db, Start, gl).CreateAsync(Request(1000.01m))); Assert.Equal(ErrorCodes.BankDepositExceedsCashInHand, ex.Code); Assert.Empty(gl.Posted); } [Fact] public async Task Create_BackDatedDepositThatOverdrawsALaterDay_IsRejected() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); db.BankDeposits.Add(Deposit(Day.AddDays(1), 900)); // the next day already banked most of it await db.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => DepositService(db, Start, new FakeGeneralLedger()).CreateAsync(Request(500))); Assert.Equal(ErrorCodes.BankDepositExceedsCashInHand, ex.Code); Assert.Contains("100.00", ex.Message); } [Fact] public async Task Cancel_ReversesGl_AndPutsTheCashBack() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); await db.SaveChangesAsync(); var gl = new FakeGeneralLedger(); var service = DepositService(db, Start, gl); var created = await service.CreateAsync(Request(1000)); var cancelled = await service.CancelAsync(created.BankDepositId, new CancelBankDepositRequest { Reason = " wrong bank " }); Assert.Equal(BankDepositStatus.Cancelled, cancelled.Status); Assert.Equal("wrong bank", cancelled.CancelReason); Assert.Equal("JE-2", cancelled.GlReversalJournalNo); Assert.Collection(gl.Posted[1].Lines, l => Assert.Equal(("1000-01", 1000m, 0m), (l.AccountCode, l.DebitAmount, l.CreditAmount)), l => Assert.Equal(("1010-01", 0m, 1000m), (l.AccountCode, l.DebitAmount, l.CreditAmount))); Assert.Equal(1000m, (await CashService(db, Start).GetPositionAsync(Day, Branch1)).Position.Closing); var again = await Assert.ThrowsAsync(() => service.CancelAsync(created.BankDepositId, new CancelBankDepositRequest { Reason = "again" })); Assert.Equal(ErrorCodes.BankDepositNotCancellable, again.Code); } [Theory] [InlineData(FakeGeneralLedger.BankAccountId, FakeGeneralLedger.BankAccountId, ErrorCodes.BankDepositCashAccountInvalid)] [InlineData(FakeGeneralLedger.CashAccountId, FakeGeneralLedger.CashAccountId, ErrorCodes.BankDepositBankAccountInvalid)] [InlineData(99L, FakeGeneralLedger.BankAccountId, ErrorCodes.BankDepositCashAccountInvalid)] public async Task Create_WrongAccountTypes_AreRejected(long from, long to, string code) { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); await db.SaveChangesAsync(); var request = Request(100); request.FromGlCashAccountId = from; request.ToGlBankAccountId = to; var ex = await Assert.ThrowsAsync(() => DepositService(db, Start, new FakeGeneralLedger()).CreateAsync(request)); Assert.Equal(code, ex.Code); } [Fact] public async Task Create_CashAndBankAccountsSharingAnId_AreToldApartByType() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); await db.SaveChangesAsync(); // GL numbers cash and bank accounts in separate tables, so both can be account 1. var gl = new FakeGeneralLedger { Accounts = [ new("Cash", 1, "main", null, "Cash in Transit", "CASH-000001", 22, "1400-01-01", "LKR"), new("Bank", 1, "Main - 01", "Commercial Bank", null, "2032434543", 2, "1000-01", "LKR") ] }; var request = Request(400); request.FromGlCashAccountId = 1; request.ToGlBankAccountId = 1; var dto = await DepositService(db, Start, gl).CreateAsync(request); Assert.Equal(("main", "Main - 01"), (dto.FromCashAccountName, dto.ToBankAccountName)); Assert.Collection(Assert.Single(gl.Posted).Lines, l => Assert.Equal(("1000-01", 400m), (l.AccountCode, l.DebitAmount)), l => Assert.Equal(("1400-01-01", 400m), (l.AccountCode, l.CreditAmount))); } [Fact] public async Task Create_CannotUseAnotherBranchsCash() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000, warehouseId: Warehouse2)); await db.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => DepositService(db, Start, new FakeGeneralLedger()).CreateAsync(Request(100, branchId: Branch1))); Assert.Equal(ErrorCodes.BankDepositExceedsCashInHand, ex.Code); } [Theory] [InlineData(-11, null)] // before tracking starts [InlineData(1, null)] // business date in the future [InlineData(-2, -3)] // deposited before the business day [InlineData(-2, 2)] // deposit date beyond tomorrow public async Task Create_InvalidDates_AreRejected(int businessOffset, int? depositOffset) { await using var db = CreateContext(); var request = Request(100); request.BusinessDate = Today.AddDays(businessOffset); request.DepositDate = depositOffset is { } d ? Today.AddDays(d) : null; var ex = await Assert.ThrowsAsync(() => DepositService(db, Start, new FakeGeneralLedger()).CreateAsync(request)); Assert.Equal(ErrorCodes.BankDepositDateInvalid, ex.Code); } [Fact] public async Task Create_GlRejection_LeavesNoDeposit() { await using var db = CreateContext(); db.SalesSlips.Add(CashSlip(Day, 1000)); await db.SaveChangesAsync(); await Assert.ThrowsAsync(() => DepositService(db, Start, new FakeGeneralLedger { Reject = true }).CreateAsync(Request(500))); Assert.Equal(0, await db.BankDeposits.CountAsync()); } [Fact] public async Task List_FiltersAndTotalsEveryMatchingRow() { await using var db = CreateContext(); db.BankDeposits.AddRange( Deposit(Day, 100), Deposit(Day, 200), Deposit(Day.AddDays(-1), 400), Deposit(Day, 800, branchId: Branch2), Deposit(Day, 1600, status: BankDepositStatus.Cancelled)); await db.SaveChangesAsync(); var result = await DepositService(db, Start, new FakeGeneralLedger()).ListAsync( new() { PageSize = 1 }, new BankDepositListFilter(Day, Day, Branch1, null, BankDepositStatus.Posted)); Assert.Single(result.Items); Assert.Equal(2, result.Pagination.TotalItems); Assert.Equal(300m, result.TotalAmount); } private static CreateBankDepositRequest Request(decimal amount, int branchId = Branch1, DateOnly? depositDate = null) => new() { BranchId = branchId, BusinessDate = Day, DepositDate = depositDate, Amount = amount, FromGlCashAccountId = FakeGeneralLedger.CashAccountId, ToGlBankAccountId = FakeGeneralLedger.BankAccountId, DepositSlipNo = "SLIP-1" }; }