Compare commits

...

1 Commits

Author SHA1 Message Date
ImanThiyanga 3be425284c 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 <noreply@anthropic.com>
2026-10-14 09:49:09 +05:30
7 changed files with 657 additions and 49 deletions
+5 -1
View File
@@ -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<StockAdjustment>(db), new Repository<Warehouse>(db), new Repository<Item>(db),
new Repository<ReasonCode>(db), new Repository<StockLedger>(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<Serial>(db), new Repository<SerialMovement>(db), new Repository<StockLayer>(db),
new Repository<ProductConfig>(db), user));
var prices = new PriceAdjustmentService(
@@ -45,6 +45,10 @@ internal sealed class FakeGeneralLedger : IGeneralLedgerService
public Task<GlPeriod> GetPeriodByDateAsync(DateOnly date, CancellationToken ct) => Task.FromResult(new GlPeriod(1, 1));
public Task<IReadOnlyList<GlBankAccount>> ListBankAccountsAsync(CancellationToken ct) => Task.FromResult(Accounts);
/// <summary>Not implemented on purpose — nothing here picks a GL account by code; see
/// <see cref="StubGeneralLedgerService"/> for the fake that does.</summary>
public Task<IReadOnlyList<GlAccount>> ListAccountsAsync(CancellationToken ct) => throw new NotSupportedException();
}
/// <summary>The in-memory provider has no transactions: run the work, then save — what the real one commits.</summary>
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
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<int>("ExpenseId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseId"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("BalanceAmount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("BillReference")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("BranchId")
.HasColumnType("int");
b.Property<string>("CancelReason")
.HasMaxLength(250)
.HasColumnType("varchar(250)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("CreatedBy")
.HasColumnType("int");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<string>("DocNo")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateOnly?>("DueDate")
.HasColumnType("date");
b.Property<int>("ExpenseCategoryId")
.HasColumnType("int");
b.Property<DateOnly>("ExpenseDate")
.HasColumnType("date");
b.Property<int>("ExpenseTitleId")
.HasColumnType("int");
b.Property<string>("GlExpenseAccountCode")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("GlJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime?>("GlPostedAt")
.HasColumnType("datetime(6)");
b.Property<string>("GlReversalJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<decimal>("PaidAmount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("Payee")
.HasMaxLength(150)
.HasColumnType("varchar(150)");
b.Property<string>("PaymentStatus")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Unpaid");
b.Property<string>("PeriodMonth")
.HasMaxLength(7)
.HasColumnType("varchar(7)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Draft");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("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<int>("ExpenseCategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseCategoryId"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Active");
b.Property<DateTime?>("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<int>("ExpensePaymentId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpensePaymentId"));
b.Property<decimal>("Amount")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("BankAccountName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("ChequeNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("CreatedBy")
.HasColumnType("int");
b.Property<int>("ExpenseId")
.HasColumnType("int");
b.Property<long>("GlBankAccountId")
.HasColumnType("bigint");
b.Property<string>("GlJournalNo")
.HasMaxLength(30)
.HasColumnType("varchar(30)");
b.Property<DateTime>("PaymentDate")
.HasColumnType("datetime(6)");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<string>("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<int>("ExpenseTitleId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ExpenseTitleId"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("ExpenseCategoryId")
.HasColumnType("int");
b.Property<string>("GlExpenseAccountCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("int unsigned");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasDefaultValue("Active");
b.Property<DateTime?>("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<int>("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")
@@ -53,9 +53,10 @@ public sealed class AdjustmentService : IAdjustmentService
public AdjustmentService(
IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator,
INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, INotificationService notifications, IUnitOfWork uow,
IGeneralLedgerService gl, IGlAccountConfigService glAccounts)
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> 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;
+5 -39
View File
@@ -38,14 +38,15 @@ public sealed class CountService : ICountService
private readonly IRepository<StockLayer> _stockLayers;
private readonly IRepository<Batch> _batches;
private readonly IRepository<Serial> _serials;
private readonly IGlAccountConfigService _glAccounts;
private readonly ISerialTrackingService _serialTracking;
private readonly AdjustmentService _adjustments;
public CountService(
IRepository<StockCount> counts, IRepository<Warehouse> warehouses,
IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IFifoCostingService fifo,
INumberSequenceService numbers, ICurrentUser currentUser, IBranchConfigService branchConfig, IUnitOfWork uow,
IGeneralLedgerService gl, IRepository<StockLayer> stockLayers, IRepository<Batch> batches,
IRepository<Serial> serials, IGlAccountConfigService glAccounts)
IRepository<StockLayer> stockLayers, IRepository<Batch> batches,
IRepository<Serial> 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;
}
/// <summary>Same logic as <c>AdjustmentService</c>'s private method of the same name — see there for why
/// gains/losses post as separate lines instead of a net.</summary>
private async Task<string?> PostAdjustmentJournalAsync(
string docNo, DateTime now, IReadOnlyList<StockLedger> 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<GlJournalEntryLineRequest>();
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<PagedResponse<CountSummaryDto>> ListAsync(
@@ -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
/// <summary>
/// The payment's calendar day (also the GL entry date) and the timestamp stored on it — same
/// rule as <see cref="SalesInvoicePaymentService"/>. Omitted ⇒ today. One day of slack on
/// rule as <see cref="SalesInvoicePaymentService"/>. Omitted ⇒ today — the <em>business</em> day
/// (<see cref="BusinessClock"/>), 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.
/// </summary>
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);
@@ -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;
/// <param name="lowStock">Records each issue for the after-request low-stock check; optional so callers outside a request (tests) can omit it.</param>
public FifoCostingService(
IRepository<StockLayer> layers, IRepository<StockLedger> ledger,
IRepository<JournalEntryStub> 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<StockLayer> CreateInboundLayerAsync(