From 3be425284c57b20a50a49762876213e4140c3e66 Mon Sep 17 00:00:00 2001 From: ImanThiyanga Date: Wed, 14 Oct 2026 09:44:32 +0530 Subject: [PATCH] Repair botched rebase resolutions from ui-bug-fixers The rebase onto Dev resolved five conflicts; four were wrong. In the service files Dev's constructor parameter lists won while the branch's field declarations and constructor bodies stayed, so the bodies assigned parameters that no longer existed. - AdjustmentService: restore fifo, approvals, db and serialTracking parameters (Dev's glAccounts kept, configuration dropped). - FifoCostingService: restore the LowStockTracker field, parameter and assignment. - CountService: restore _serialTracking and _adjustments, and delete Dev's PostAdjustmentJournalAsync. Dev's call site did not survive the rebase, so the method was orphaned dead code referencing fields this class no longer has. The branch routes count variances through AdjustmentService.SubmitAsync, which posts via Dev's role-keyed glCodes, so nothing is lost by removing it. - PurchaseInvoicePaymentService: this one compiled. paymentDay was computed but the GL period and EntryDate still used now, so a back-dated payment posted its journal to today's period. Use paymentDay, and resolve "today" through BusinessClock, matching SalesInvoicePaymentService. - ErpDbContextModelSnapshot: the resolution dropped the whole Expense module from the definition and relationship sections while leaving its navigation entries behind, so the snapshot no longer loaded. Rebuilt as the branch's snapshot plus Dev's GlAccountConfig blocks; EF now reports no pending model changes. Tests: BankDepositTestSupport's FakeGeneralLedger gains the ListAccountsAsync member Dev added to IGeneralLedgerService, and DualApprovalTests passes a StubGlAccountConfigService in place of the removed IConfiguration. Co-Authored-By: Claude Opus 5 --- Backend/ERPCore.Tests/DualApprovalTests.cs | 6 +- .../TestSupport/BankDepositTestSupport.cs | 4 + .../Migrations/ErpDbContextModelSnapshot.cs | 632 +++++++++++++++++- Backend/ERPCore/Services/AdjustmentService.cs | 7 +- Backend/ERPCore/Services/CountService.cs | 44 +- .../Services/PurchaseInvoicePaymentService.cs | 9 +- .../Services/Stock/FifoCostingService.cs | 4 +- 7 files changed, 657 insertions(+), 49 deletions(-) diff --git a/Backend/ERPCore.Tests/DualApprovalTests.cs b/Backend/ERPCore.Tests/DualApprovalTests.cs index 222b69b..638f4f0 100644 --- a/Backend/ERPCore.Tests/DualApprovalTests.cs +++ b/Backend/ERPCore.Tests/DualApprovalTests.cs @@ -9,6 +9,7 @@ using ERPCore.Infra.Auth; using ERPCore.Infra.Persistence; using ERPCore.Repositories; using ERPCore.Services; +using ERPCore.Services.Gl; using ERPCore.Services.Interfaces; using ERPCore.Services.Stock; using ERPCore.System.Errors; @@ -300,7 +301,10 @@ public class DualApprovalTests new Repository(db), new Repository(db), new Repository(db), new Repository(db), new Repository(db), mutator, new FixedValuation(), numbers, user, new FakeBranchConfig(), notifications, approvals, uow, db, gl, - new ConfigurationBuilder().Build(), + new StubGlAccountConfigService( + (GlAccountRole.Inventory, "1200"), + (GlAccountRole.InventoryGain, "4900"), + (GlAccountRole.InventoryLoss, "5900")), new SerialTrackingService(new Repository(db), new Repository(db), new Repository(db), new Repository(db), user)); var prices = new PriceAdjustmentService( diff --git a/Backend/ERPCore.Tests/TestSupport/BankDepositTestSupport.cs b/Backend/ERPCore.Tests/TestSupport/BankDepositTestSupport.cs index 6cc9501..e56526c 100644 --- a/Backend/ERPCore.Tests/TestSupport/BankDepositTestSupport.cs +++ b/Backend/ERPCore.Tests/TestSupport/BankDepositTestSupport.cs @@ -45,6 +45,10 @@ internal sealed class FakeGeneralLedger : IGeneralLedgerService public Task GetPeriodByDateAsync(DateOnly date, CancellationToken ct) => Task.FromResult(new GlPeriod(1, 1)); public Task> ListBankAccountsAsync(CancellationToken ct) => Task.FromResult(Accounts); + + /// Not implemented on purpose — nothing here picks a GL account by code; see + /// for the fake that does. + public Task> ListAccountsAsync(CancellationToken ct) => throw new NotSupportedException(); } /// The in-memory provider has no transactions: run the work, then save — what the real one commits. diff --git a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs index e09569e..5e03eaf 100644 --- a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs +++ b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using ERPCore.Infra.Persistence; using Microsoft.EntityFrameworkCore; @@ -1651,6 +1651,564 @@ namespace ERPCore.Migrations b.ToTable("hr_employment_types", (string)null); }); + modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b => + { + b.Property("ExpenseId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BillReference") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CancelReason") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("DueDate") + .HasColumnType("date"); + + b.Property("ExpenseCategoryId") + .HasColumnType("int"); + + b.Property("ExpenseDate") + .HasColumnType("date"); + + b.Property("ExpenseTitleId") + .HasColumnType("int"); + + b.Property("GlExpenseAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GlReversalJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Payee") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PaymentStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Unpaid"); + + b.Property("PeriodMonth") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("ExpenseId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ExpenseCategoryId"); + + b.HasIndex("ExpenseDate"); + + b.HasIndex("ExpenseTitleId"); + + b.HasIndex("PaymentStatus"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.ToTable("expenses", null, t => + { + t.HasCheckConstraint("ck_expenses_amount", "`Amount` > 0"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseCategory", b => + { + b.Property("ExpenseCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseCategoryId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("ExpenseCategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("expense_categories", (string)null); + + b.HasData( + new + { + ExpenseCategoryId = 1, + Code = "UTILITY", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Utility", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseCategoryId = 2, + Code = "RENTAL", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Rental", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseCategoryId = 3, + Code = "SALARIES", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Salaries", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseCategoryId = 4, + Code = "OTHER", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Other", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b => + { + b.Property("ExpensePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpensePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ChequeNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("ExpenseId") + .HasColumnType("int"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaymentDate") + .HasColumnType("datetime(6)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("ExpensePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ExpenseId"); + + b.ToTable("expense_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b => + { + b.Property("ExpenseTitleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseTitleId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpenseCategoryId") + .HasColumnType("int"); + + b.Property("GlExpenseAccountCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("ExpenseTitleId"); + + b.HasIndex("ExpenseCategoryId", "Name") + .IsUnique(); + + b.ToTable("expense_titles", (string)null); + + b.HasData( + new + { + ExpenseTitleId = 1, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Electricity", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 2, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Water", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 3, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Telephone", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 4, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Internet", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 5, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Gas", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }, + new + { + ExpenseTitleId = 6, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Building Rent", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 7, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Vehicle Rent", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 8, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Equipment Rent", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 9, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Staff Salaries", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 10, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Wages", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 11, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Overtime", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 12, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Bonus", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 13, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "EPF / ETF Contribution", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }, + new + { + ExpenseTitleId = 14, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Stationery", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 15, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Transport", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 16, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Repairs & Maintenance", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 17, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Cleaning", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 18, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Miscellaneous", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b => { b.Property("ConfigId") @@ -8139,6 +8697,78 @@ namespace ERPCore.Migrations b.Navigation("SalaryComponent"); }); + modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category") + .WithMany() + .HasForeignKey("ExpenseCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ExpenseTitle", "Title") + .WithMany() + .HasForeignKey("ExpenseTitleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + + b.Navigation("Category"); + + b.Navigation("Creator"); + + b.Navigation("Title"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Expense", "Expense") + .WithMany("Payments") + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Expense"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b => + { + b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category") + .WithMany("Titles") + .HasForeignKey("ExpenseCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b => { b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") diff --git a/Backend/ERPCore/Services/AdjustmentService.cs b/Backend/ERPCore/Services/AdjustmentService.cs index 3642694..a7d5cd7 100644 --- a/Backend/ERPCore/Services/AdjustmentService.cs +++ b/Backend/ERPCore/Services/AdjustmentService.cs @@ -53,9 +53,10 @@ public sealed class AdjustmentService : IAdjustmentService public AdjustmentService( IRepository adjustments, IRepository warehouses, IRepository items, - IRepository reasonCodes, IRepository ledger, IStockMutator mutator, - INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, INotificationService notifications, IUnitOfWork uow, - IGeneralLedgerService gl, IGlAccountConfigService glAccounts) + IRepository reasonCodes, IRepository ledger, IStockMutator mutator, IFifoCostingService fifo, + INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, INotificationService notifications, + IApprovalService approvals, IUnitOfWork uow, ErpDbContext db, + IGeneralLedgerService gl, IGlAccountConfigService glAccounts, ISerialTrackingService serialTracking) { _serialTracking = serialTracking; _adjustments = adjustments; diff --git a/Backend/ERPCore/Services/CountService.cs b/Backend/ERPCore/Services/CountService.cs index cd02c5b..7ff851d 100644 --- a/Backend/ERPCore/Services/CountService.cs +++ b/Backend/ERPCore/Services/CountService.cs @@ -38,14 +38,15 @@ public sealed class CountService : ICountService private readonly IRepository _stockLayers; private readonly IRepository _batches; private readonly IRepository _serials; - private readonly IGlAccountConfigService _glAccounts; + private readonly ISerialTrackingService _serialTracking; + private readonly AdjustmentService _adjustments; public CountService( IRepository counts, IRepository warehouses, IRepository items, IRepository reasonCodes, IFifoCostingService fifo, INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, IUnitOfWork uow, - IGeneralLedgerService gl, IRepository stockLayers, IRepository batches, - IRepository serials, IGlAccountConfigService glAccounts) + IRepository stockLayers, IRepository batches, + IRepository serials, ISerialTrackingService serialTracking, AdjustmentService adjustments) { _serialTracking = serialTracking; _counts = counts; @@ -60,42 +61,7 @@ public sealed class CountService : ICountService _stockLayers = stockLayers; _batches = batches; _serials = serials; - _glAccounts = glAccounts; - } - - /// Same logic as AdjustmentService's private method of the same name — see there for why - /// gains/losses post as separate lines instead of a net. - private async Task PostAdjustmentJournalAsync( - string docNo, DateTime now, IReadOnlyList refs, CancellationToken ct) - { - var gain = refs.Where(r => r.Direction == Direction.In).Sum(r => r.Value); - var loss = refs.Where(r => r.Direction == Direction.Out).Sum(r => r.Value); - if (gain <= 0 && loss <= 0) return null; - - var glCodes = await _glAccounts.GetCodesAsync(ct); - var period = await _gl.GetPeriodByDateAsync(BusinessClock.ToBusinessDate(now), ct); - var lines = new List(); - if (gain > 0) - { - lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.Inventory], gain, 0m, $"Adjustment {docNo} — stock increase")); - lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.InventoryGain], 0m, gain, $"Adjustment {docNo} — inventory gain")); - } - if (loss > 0) - { - lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.InventoryLoss], loss, 0m, $"Adjustment {docNo} — inventory loss")); - lines.Add(new GlJournalEntryLineRequest(glCodes[GlAccountRole.Inventory], 0m, loss, $"Adjustment {docNo} — stock decrease")); - } - - var result = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest - { - PeriodId = period.PeriodId, - EntryDate = BusinessClock.ToBusinessDate(now), - SourceModule = "ADJUSTMENT", - Reference = docNo, - Narration = $"Stock adjustment {docNo}", - Lines = lines - }, ct); - return result.JournalNo; + _adjustments = adjustments; } public async Task> ListAsync( diff --git a/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs b/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs index 52ff97f..b104526 100644 --- a/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs +++ b/Backend/ERPCore/Services/PurchaseInvoicePaymentService.cs @@ -64,11 +64,11 @@ public sealed class PurchaseInvoicePaymentService : IPurchaseInvoicePaymentServi var payment = await _uow.ExecuteInTransactionAsync(async token => { var glCodes = await _glAccounts.GetCodesAsync(token); - var period = await _gl.GetPeriodByDateAsync(BusinessClock.ToBusinessDate(now), token); + var period = await _gl.GetPeriodByDateAsync(paymentDay, token); var posted = await _gl.PostJournalEntryAsync(new GlJournalEntryRequest { PeriodId = period.PeriodId, - EntryDate = BusinessClock.ToBusinessDate(now), + EntryDate = paymentDay, SourceModule = "PURCHASE_INVOICE_PAYMENT", Reference = invoice.DocNo, Narration = $"Payment against invoice {invoice.DocNo}", @@ -104,13 +104,14 @@ public sealed class PurchaseInvoicePaymentService : IPurchaseInvoicePaymentServi /// /// The payment's calendar day (also the GL entry date) and the timestamp stored on it — same - /// rule as . Omitted ⇒ today. One day of slack on + /// rule as . Omitted ⇒ today — the business day + /// (), not the UTC one. One day of slack on /// "future": the client sends its local date, which runs ahead of UTC for users east of it. /// Today keeps the actual time of entry; a back-dated payment is stamped at that day's start. /// private static (DateOnly Day, DateTime Stamp) ResolvePaymentDate(DateOnly? requested, DateTime now) { - var today = DateOnly.FromDateTime(now); + var today = BusinessClock.ToBusinessDate(now); var day = requested ?? today; if (day > today.AddDays(1)) throw new DomainException(ErrorCodes.Validation, "Payment date cannot be in the future.", 400); diff --git a/Backend/ERPCore/Services/Stock/FifoCostingService.cs b/Backend/ERPCore/Services/Stock/FifoCostingService.cs index 18205e9..418a2e4 100644 --- a/Backend/ERPCore/Services/Stock/FifoCostingService.cs +++ b/Backend/ERPCore/Services/Stock/FifoCostingService.cs @@ -27,12 +27,13 @@ public sealed class FifoCostingService : IFifoCostingService private readonly ErpDbContext _db; private readonly IGeneralLedgerService _gl; private readonly IGlAccountConfigService _glAccounts; + private readonly LowStockTracker? _lowStock; /// Records each issue for the after-request low-stock check; optional so callers outside a request (tests) can omit it. public FifoCostingService( IRepository layers, IRepository ledger, IRepository journal, ErpDbContext db, - IGeneralLedgerService gl, IGlAccountConfigService glAccounts) + IGeneralLedgerService gl, IGlAccountConfigService glAccounts, LowStockTracker? lowStock = null) { _layers = layers; _ledger = ledger; @@ -40,6 +41,7 @@ public sealed class FifoCostingService : IFifoCostingService _db = db; _gl = gl; _glAccounts = glAccounts; + _lowStock = lowStock; } public async Task CreateInboundLayerAsync(