From 6528fc8d9d846ff080955e1208029403ca533dc4 Mon Sep 17 00:00:00 2001
From: Dhananjaya99 <152056742+ashanruu@users.noreply.github.com>
Date: Sun, 16 Aug 2026 23:21:40 +0530
Subject: [PATCH] intigrate others sales , return and implement day end
duntinalities
---
.../Controllers/SalesDayEndController.cs | 50 +
.../Controllers/SalesInvoicesController.cs | 26 +-
Backend/ERPCore/Domain/DocumentTypes.cs | 3 +
Backend/ERPCore/Domain/Entities/BundleSale.cs | 6 +
.../ERPCore/Domain/Entities/PurchaseReturn.cs | 7 +
.../ERPCore/Domain/Entities/SalesDayEnd.cs | 46 +
.../ERPCore/Domain/Entities/SalesInvoice.cs | 9 +
.../Domain/Entities/SalesInvoicePayment.cs | 32 +
.../ERPCore/Domain/Entities/SalesReturn.cs | 6 +
Backend/ERPCore/Domain/Entities/SalesSlip.cs | 4 +
.../Domain/Entities/StockAdjustment.cs | 5 +
.../Dtos/Procurement/PurchaseReturnDtos.cs | 5 +-
Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs | 40 +
.../ERPCore/Dtos/Sales/SalesInvoiceDtos.cs | 16 +-
Backend/ERPCore/Dtos/Sales/SalesReturnDtos.cs | 5 +-
Backend/ERPCore/Dtos/Stock/AdjustmentDtos.cs | 5 +-
Backend/ERPCore/Dtos/Stock/CountDtos.cs | 2 +-
.../PurchaseReturnConfiguration.cs | 1 +
.../SalesDayEndConfiguration.cs | 44 +
.../SalesInvoiceConfiguration.cs | 22 +
.../SalesReturnConfiguration.cs | 1 +
.../StockAdjustmentConfiguration.cs | 1 +
.../ERPCore/Infra/Persistence/ErpDbContext.cs | 2 +
.../20260816130723_AddSalesDayEnd.Designer.cs | 7351 ++++++++++++++++
.../20260816130723_AddSalesDayEnd.cs | 109 +
...60816133305_AddReturnGlPosting.Designer.cs | 7365 ++++++++++++++++
.../20260816133305_AddReturnGlPosting.cs | 61 +
...6142234_AddAdjustmentGlPosting.Designer.cs | 7372 ++++++++++++++++
.../20260816142234_AddAdjustmentGlPosting.cs | 40 +
...16151344_AddBundleSaleToDayEnd.Designer.cs | 7391 ++++++++++++++++
.../20260816151344_AddBundleSaleToDayEnd.cs | 60 +
...174034_AddSalesInvoicePayments.Designer.cs | 7468 +++++++++++++++++
.../20260816174034_AddSalesInvoicePayments.cs | 87 +
.../Migrations/ErpDbContextModelSnapshot.cs | 391 +-
Backend/ERPCore/Program.cs | 2 +
Backend/ERPCore/Services/AdjustmentService.cs | 59 +-
Backend/ERPCore/Services/BundleSaleService.cs | 12 +
Backend/ERPCore/Services/CountService.cs | 61 +-
.../Interfaces/ISalesDayEndService.cs | 20 +
.../Interfaces/ISalesInvoicePaymentService.cs | 10 +
.../Production/ProductionRunService.cs | 11 +
.../ERPCore/Services/PurchaseReturnService.cs | 38 +-
.../ERPCore/Services/SalesDayEndService.cs | 319 +
.../Services/SalesInvoicePaymentService.cs | 122 +
.../ERPCore/Services/SalesInvoiceService.cs | 44 +-
.../ERPCore/Services/SalesMappingService.cs | 1 +
.../ERPCore/Services/SalesPostingService.cs | 77 +-
.../ERPCore/Services/SalesReturnService.cs | 40 +-
Backend/ERPCore/Services/SalesSlipService.cs | 49 +-
Backend/ERPCore/Services/TransferService.cs | 8 +
Backend/ERPCore/System/Errors/ErrorCodes.cs | 5 +
Backend/ERPCore/appsettings.json | 16 +
.../procurement/purchase-orders/[id]/page.tsx | 21 +-
.../procurement/purchase-orders/page.tsx | 75 +-
.../procurement/purchase-returns/page.tsx | 2 +
.../app/dashboard/receiving/grn/new/page.tsx | 79 +-
.../app/dashboard/sales/day-end/page.tsx | 281 +
.../dashboard/sales/free-issues/new/page.tsx | 68 +-
.../dashboard/sales/invoices/[id]/page.tsx | 85 +-
.../erp-system/app/dashboard/sales/page.tsx | 8 +-
.../dashboard/sales/sales-returns/page.tsx | 2 +
.../app/dashboard/stock/adjustments/page.tsx | 2 +
.../app/dashboard/stock/counts/[id]/page.tsx | 3 +
.../app/dashboard/vendors/[id]/page.tsx | 23 +-
.../sales/SalesInvoicePaymentDialog.tsx | 143 +
.../erp-system/components/ui/combobox.tsx | 95 +
Frontend/erp-system/lib/api/sales-day-end.ts | 31 +
Frontend/erp-system/lib/api/sales.ts | 12 +
Frontend/erp-system/types/procurement.ts | 3 +
Frontend/erp-system/types/sales.ts | 99 +
Frontend/erp-system/types/stock.ts | 5 +
71 files changed, 39668 insertions(+), 296 deletions(-)
create mode 100644 Backend/ERPCore/Controllers/SalesDayEndController.cs
create mode 100644 Backend/ERPCore/Domain/Entities/SalesDayEnd.cs
create mode 100644 Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs
create mode 100644 Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs
create mode 100644 Backend/ERPCore/Infra/Persistence/Configurations/SalesDayEndConfiguration.cs
create mode 100644 Backend/ERPCore/Migrations/20260816130723_AddSalesDayEnd.Designer.cs
create mode 100644 Backend/ERPCore/Migrations/20260816130723_AddSalesDayEnd.cs
create mode 100644 Backend/ERPCore/Migrations/20260816133305_AddReturnGlPosting.Designer.cs
create mode 100644 Backend/ERPCore/Migrations/20260816133305_AddReturnGlPosting.cs
create mode 100644 Backend/ERPCore/Migrations/20260816142234_AddAdjustmentGlPosting.Designer.cs
create mode 100644 Backend/ERPCore/Migrations/20260816142234_AddAdjustmentGlPosting.cs
create mode 100644 Backend/ERPCore/Migrations/20260816151344_AddBundleSaleToDayEnd.Designer.cs
create mode 100644 Backend/ERPCore/Migrations/20260816151344_AddBundleSaleToDayEnd.cs
create mode 100644 Backend/ERPCore/Migrations/20260816174034_AddSalesInvoicePayments.Designer.cs
create mode 100644 Backend/ERPCore/Migrations/20260816174034_AddSalesInvoicePayments.cs
create mode 100644 Backend/ERPCore/Services/Interfaces/ISalesDayEndService.cs
create mode 100644 Backend/ERPCore/Services/Interfaces/ISalesInvoicePaymentService.cs
create mode 100644 Backend/ERPCore/Services/SalesDayEndService.cs
create mode 100644 Backend/ERPCore/Services/SalesInvoicePaymentService.cs
create mode 100644 Frontend/erp-system/app/dashboard/sales/day-end/page.tsx
create mode 100644 Frontend/erp-system/components/sales/SalesInvoicePaymentDialog.tsx
create mode 100644 Frontend/erp-system/components/ui/combobox.tsx
create mode 100644 Frontend/erp-system/lib/api/sales-day-end.ts
diff --git a/Backend/ERPCore/Controllers/SalesDayEndController.cs b/Backend/ERPCore/Controllers/SalesDayEndController.cs
new file mode 100644
index 0000000..4695c04
--- /dev/null
+++ b/Backend/ERPCore/Controllers/SalesDayEndController.cs
@@ -0,0 +1,50 @@
+using ERPCore.Dtos.Common;
+using ERPCore.Dtos.Sales;
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+/// Cashier day-end close-out — locks a cashier's Posted sales slips for a business
+/// date and posts one consolidated GL journal entry for the day (docs/14 Sales API).
+[Route("api/v1/sales-day-end")]
+public sealed class SalesDayEndController : ApiControllerBase
+{
+ private readonly ISalesDayEndService _dayEnds;
+
+ public SalesDayEndController(ISalesDayEndService dayEnds) => _dayEnds = dayEnds;
+
+ /// List day-end closes, newest first.
+ [HttpGet]
+ [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
+ public async Task>> List(
+ [FromQuery] PageQuery query, [FromQuery] int? cashierUserId, CancellationToken ct)
+ => Ok(await _dayEnds.ListAsync(query, cashierUserId, ct));
+
+ /// What closing this cashier/date right now would include (or the existing close's totals, if already closed).
+ [HttpGet("preview")]
+ [ProducesResponseType(typeof(SalesDayEndPreviewDto), StatusCodes.Status200OK)]
+ public async Task> Preview(
+ [FromQuery] int cashierUserId, [FromQuery] DateOnly? businessDate, CancellationToken ct)
+ => Ok(await _dayEnds.PreviewAsync(cashierUserId, businessDate ?? DateOnly.FromDateTime(DateTime.UtcNow), ct));
+
+ [HttpGet("{salesDayEndId:int}")]
+ [ProducesResponseType(typeof(SalesDayEndDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int salesDayEndId, CancellationToken ct)
+ {
+ var dto = await _dayEnds.GetAsync(salesDayEndId, ct);
+ return dto is null ? NotFound() : Ok(dto);
+ }
+
+ /// Close the day: lock every Posted slip for (CashierUserId, BusinessDate) and post the
+ /// consolidated GL journal entry. Idempotent — closing an already-closed date replays it.
+ [HttpPost]
+ [ProducesResponseType(typeof(SalesDayEndDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task> Close([FromBody] CreateSalesDayEndRequest request, CancellationToken ct)
+ {
+ var dto = await _dayEnds.CloseAsync(request, ct);
+ return Created($"/api/v1/sales-day-end/{dto.SalesDayEndId}", dto);
+ }
+}
diff --git a/Backend/ERPCore/Controllers/SalesInvoicesController.cs b/Backend/ERPCore/Controllers/SalesInvoicesController.cs
index 29eab9e..9af14b7 100644
--- a/Backend/ERPCore/Controllers/SalesInvoicesController.cs
+++ b/Backend/ERPCore/Controllers/SalesInvoicesController.cs
@@ -10,8 +10,13 @@ namespace ERPCore.Controllers;
public sealed class SalesInvoicesController : ApiControllerBase
{
private readonly ISalesInvoiceService _invoices;
+ private readonly ISalesInvoicePaymentService _payments;
- public SalesInvoicesController(ISalesInvoiceService invoices) => _invoices = invoices;
+ public SalesInvoicesController(ISalesInvoiceService invoices, ISalesInvoicePaymentService payments)
+ {
+ _invoices = invoices;
+ _payments = payments;
+ }
[HttpGet]
[ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)]
@@ -70,4 +75,23 @@ public sealed class SalesInvoicesController : ApiControllerBase
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
public async Task> Cancel(int salesInvoiceId, CancellationToken ct)
=> Ok(await _invoices.CancelAsync(salesInvoiceId, ct));
+
+ /// Pay the customer's balance against this invoice, in full or in installments; posts a real GL journal entry.
+ [HttpPost("{salesInvoiceId:int}/payments")]
+ [ProducesResponseType(typeof(SalesInvoicePaymentDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task> Pay(int salesInvoiceId, [FromBody] CreateSalesInvoicePaymentRequest request, CancellationToken ct)
+ {
+ var dto = await _payments.PayAsync(salesInvoiceId, request, ct);
+ return Created($"/api/v1/sales-invoices/{salesInvoiceId}/payments/{dto.SalesInvoicePaymentId}", dto);
+ }
+
+ /// Payment history for this invoice, newest first.
+ [HttpGet("{salesInvoiceId:int}/payments")]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task>> ListPayments(int salesInvoiceId, CancellationToken ct)
+ => Ok(await _payments.ListAsync(salesInvoiceId, ct));
}
diff --git a/Backend/ERPCore/Domain/DocumentTypes.cs b/Backend/ERPCore/Domain/DocumentTypes.cs
index 69e1419..a034541 100644
--- a/Backend/ERPCore/Domain/DocumentTypes.cs
+++ b/Backend/ERPCore/Domain/DocumentTypes.cs
@@ -21,4 +21,7 @@ public static class DocumentTypes
public const string SalesSlip = "SSL";
public const string BundleSale = "BND";
public const string SalesReturn = "SRET";
+
+ /// Cashier day-end close-out (Sales) — DEND-2026-00001.
+ public const string SalesDayEnd = "DEND";
}
diff --git a/Backend/ERPCore/Domain/Entities/BundleSale.cs b/Backend/ERPCore/Domain/Entities/BundleSale.cs
index 3129935..cfdb355 100644
--- a/Backend/ERPCore/Domain/Entities/BundleSale.cs
+++ b/Backend/ERPCore/Domain/Entities/BundleSale.cs
@@ -29,5 +29,11 @@ public class BundleSale
public DateTime? UpdatedAt { get; set; }
public int ConcurrencyStamp { get; set; }
+ /// Set once this bundle's business date is closed by — a
+ /// closed bundle sale is frozen (no further edits). Same role as ;
+ /// a bundle sale is a cashier document exactly like a sales slip, just priced as a set.
+ public int? DayEndId { get; set; }
+ public SalesDayEnd? DayEnd { get; set; }
+
public ICollection Lines { get; set; } = new List();
}
diff --git a/Backend/ERPCore/Domain/Entities/PurchaseReturn.cs b/Backend/ERPCore/Domain/Entities/PurchaseReturn.cs
index a663950..26b3442 100644
--- a/Backend/ERPCore/Domain/Entities/PurchaseReturn.cs
+++ b/Backend/ERPCore/Domain/Entities/PurchaseReturn.cs
@@ -28,5 +28,12 @@ public class PurchaseReturn
public DateTime CreatedAt { get; set; }
+ /// Journal number of the real GL journal entry posted for this return (set on create — it auto-posts).
+ /// Reverses the Inventory/Clearing lines a Grn posts — carries no unit
+ /// price/VAT of its own, so it can't reverse a VAT-recoverable line the way a real credit note would.
+ public string? GlJournalNo { get; set; }
+ public DateTime? GlPostedAt { get; set; }
+
public ICollection Lines { get; set; } = new List();
}
+
diff --git a/Backend/ERPCore/Domain/Entities/SalesDayEnd.cs b/Backend/ERPCore/Domain/Entities/SalesDayEnd.cs
new file mode 100644
index 0000000..63ff8f6
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/SalesDayEnd.cs
@@ -0,0 +1,46 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// A cashier's end-of-day close-out. Aggregates every the
+/// cashier posted on , freezes them against this record
+/// () so a slip can only ever belong to one day-end,
+/// and posts one consolidated GL journal entry for the day's revenue/tax/COGS —
+/// the sales-side equivalent of how posts per receipt. Unique per
+/// (CashierUserId, BusinessDate): closing twice replays the existing record instead
+/// of creating a second one (same idempotent-replay pattern as Grn.ConfirmAsync).
+///
+public class SalesDayEnd
+{
+ public int SalesDayEndId { get; set; }
+ public string DocNo { get; set; } = string.Empty;
+
+ public int CashierUserId { get; set; }
+ public User? CashierUser { get; set; }
+
+ /// The calendar date being closed (matched against each slip's , UTC).
+ public DateOnly BusinessDate { get; set; }
+
+ public int SlipCount { get; set; }
+ public int BundleCount { get; set; }
+ public decimal Subtotal { get; set; }
+ public decimal DiscountTotal { get; set; }
+ public decimal TaxTotal { get; set; }
+ public decimal GrandTotal { get; set; }
+
+ /// Sum of the FIFO cost consumed for these slips (from ), for the COGS GL lines.
+ public decimal CostOfGoodsSold { get; set; }
+
+ /// Journal number of the real GL journal entry posted for this close (null when there was nothing to post).
+ public string? GlJournalNo { get; set; }
+ public DateTime? GlPostedAt { get; set; }
+
+ public int ClosedBy { get; set; }
+ public User? ClosedByUser { get; set; }
+ public DateTime ClosedAt { get; set; }
+
+ /// PostgreSQL xmin-backed optimistic concurrency token.
+ public uint RowVersion { get; set; }
+
+ public ICollection Slips { get; set; } = new List();
+ public ICollection BundleSales { get; set; } = new List();
+}
diff --git a/Backend/ERPCore/Domain/Entities/SalesInvoice.cs b/Backend/ERPCore/Domain/Entities/SalesInvoice.cs
index 15f40de..8e35c72 100644
--- a/Backend/ERPCore/Domain/Entities/SalesInvoice.cs
+++ b/Backend/ERPCore/Domain/Entities/SalesInvoice.cs
@@ -29,6 +29,14 @@ public class SalesInvoice
public decimal PaidAmount { get; set; }
public decimal BalanceAmount { get; set; }
+ /// Journal number of the revenue-recognition GL journal entry posted when this
+ /// invoice is Posted (Debit Accounts Receivable / Credit Sales Revenue + Tax, plus COGS —
+ /// see SalesPostingService.PostInvoiceAsync). Unlike a SalesSlip, an invoice posts its own
+ /// GL entry immediately rather than waiting for Sales Day End, since invoices aren't a
+ /// cashier document and don't go through that close-out.
+ public string? GlJournalNo { get; set; }
+ public DateTime? GlPostedAt { get; set; }
+
public int CreatedBy { get; set; }
public User? Creator { get; set; }
@@ -38,4 +46,5 @@ public class SalesInvoice
public uint RowVersion { get; set; }
public ICollection Lines { get; set; } = new List();
+ public ICollection Payments { get; set; } = new List();
}
diff --git a/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs b/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs
new file mode 100644
index 0000000..adb2386
--- /dev/null
+++ b/Backend/ERPCore/Domain/Entities/SalesInvoicePayment.cs
@@ -0,0 +1,32 @@
+namespace ERPCore.Domain.Entities;
+
+///
+/// A customer payment made against a Posted sales invoice's balance (installments
+/// allowed — see SalesInvoicePaymentService.PayAsync). Posts its own real GL journal
+/// entry (Debit the selected bank-or-cash account / Credit Accounts Receivable) before
+/// being recorded here — the mirror image of .
+///
+public class SalesInvoicePayment
+{
+ public int SalesInvoicePaymentId { get; set; }
+
+ public int SalesInvoiceId { get; set; }
+ public SalesInvoice? SalesInvoice { get; set; }
+
+ public decimal Amount { get; set; }
+ public DateTime PaymentDate { get; set; }
+
+ /// GL's numeric id for the bank/cash account the payment was received into.
+ public long GlBankAccountId { get; set; }
+ /// Snapshot of the account's display name at payment time (GL account lists have no local FK).
+ public string BankAccountName { get; set; } = string.Empty;
+
+ public string? Reference { get; set; }
+
+ /// Journal number of the real GL journal entry this payment posted.
+ public string? GlJournalNo { get; set; }
+
+ public int CreatedBy { get; set; }
+ public User? Creator { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/Backend/ERPCore/Domain/Entities/SalesReturn.cs b/Backend/ERPCore/Domain/Entities/SalesReturn.cs
index 749870c..e712d80 100644
--- a/Backend/ERPCore/Domain/Entities/SalesReturn.cs
+++ b/Backend/ERPCore/Domain/Entities/SalesReturn.cs
@@ -28,5 +28,11 @@ public class SalesReturn
public DateTime CreatedAt { get; set; }
+ /// Journal number of the real GL journal entry posted for this return (set on create — it auto-posts).
+ /// Reverses Inventory/COGS only — carries no unit price, so revenue/tax aren't
+ /// reversed here (they'd need a per-line price this document doesn't capture).
+ public string? GlJournalNo { get; set; }
+ public DateTime? GlPostedAt { get; set; }
+
public ICollection Lines { get; set; } = new List();
}
diff --git a/Backend/ERPCore/Domain/Entities/SalesSlip.cs b/Backend/ERPCore/Domain/Entities/SalesSlip.cs
index 46f697a..c3d713f 100644
--- a/Backend/ERPCore/Domain/Entities/SalesSlip.cs
+++ b/Backend/ERPCore/Domain/Entities/SalesSlip.cs
@@ -27,6 +27,10 @@ public class SalesSlip
public decimal PaidAmount { get; set; }
public decimal BalanceAmount { get; set; }
+ /// Set once this slip's business date is closed by — a closed slip is frozen (no further edits).
+ public int? DayEndId { get; set; }
+ public SalesDayEnd? DayEnd { get; set; }
+
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
diff --git a/Backend/ERPCore/Domain/Entities/StockAdjustment.cs b/Backend/ERPCore/Domain/Entities/StockAdjustment.cs
index be583d4..a59e2c8 100644
--- a/Backend/ERPCore/Domain/Entities/StockAdjustment.cs
+++ b/Backend/ERPCore/Domain/Entities/StockAdjustment.cs
@@ -27,5 +27,10 @@ public class StockAdjustment
public DateTime CreatedAt { get; set; }
public uint RowVersion { get; set; }
+ /// Journal number of the real GL journal entry posted for this adjustment (set on create — it
+ /// auto-posts). Increases and decreases post separately (never netted) so gains/losses stay visible.
+ public string? GlJournalNo { get; set; }
+ public DateTime? GlPostedAt { get; set; }
+
public ICollection Lines { get; set; } = new List();
}
diff --git a/Backend/ERPCore/Dtos/Procurement/PurchaseReturnDtos.cs b/Backend/ERPCore/Dtos/Procurement/PurchaseReturnDtos.cs
index 5854fbb..d6fc74a 100644
--- a/Backend/ERPCore/Dtos/Procurement/PurchaseReturnDtos.cs
+++ b/Backend/ERPCore/Dtos/Procurement/PurchaseReturnDtos.cs
@@ -9,12 +9,13 @@ public sealed record PurchaseReturnLineDto(int ReturnLineId, int? GrnLineId, int
public sealed record PurchaseReturnDto(
int ReturnId, string DocNo, int VendorId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
- int CreatedBy, DateTime CreatedAt, IReadOnlyList Lines, IReadOnlyList LedgerRefs);
+ int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
+ IReadOnlyList Lines, IReadOnlyList LedgerRefs);
/// Row shape for GET /purchase-returns — no lines/ledgerRefs (those need a per-row query).
public sealed record PurchaseReturnSummaryDto(
int ReturnId, string DocNo, int VendorId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
- int CreatedBy, DateTime CreatedAt, int LineCount);
+ int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
// Requests ----------------------------------------------------------------------
diff --git a/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs b/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs
new file mode 100644
index 0000000..ea97dc9
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Sales/SalesDayEndDtos.cs
@@ -0,0 +1,40 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace ERPCore.Dtos.Sales;
+
+// Responses -----------------------------------------------------------------------
+
+/// Qty/revenue sold for one item across a closed (or about-to-close) day, for the day-end report.
+/// Merges sales-slip and bundle-sale lines for the same item into one row.
+public sealed record SalesDayEndItemLineDto(int ItemId, string Sku, string Name, decimal Qty, decimal Revenue);
+
+public sealed record SalesDayEndDto(
+ int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
+ int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
+ decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
+ IReadOnlyList SlipNumbers, IReadOnlyList BundleNumbers, IReadOnlyList ItemBreakdown);
+
+public sealed record SalesDayEndSummaryDto(
+ int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
+ int SlipCount, int BundleCount, decimal GrandTotal, string? GlJournalNo, DateTime ClosedAt);
+
+/// What closing right now would include — call before POST /sales-day-end so the
+/// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.
+public sealed record SalesDayEndPreviewDto(
+ int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed,
+ int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
+ IReadOnlyList SlipNumbers, IReadOnlyList BundleNumbers,
+ IReadOnlyList DraftSlipsBlockingClose,
+ IReadOnlyList DraftBundleSalesBlockingClose);
+
+public sealed record DraftSlipBlockingCloseDto(int SalesSlipId, string SlipNo, decimal GrandTotal);
+public sealed record DraftBundleBlockingCloseDto(int BundleSaleId, string BundleNo, decimal GrandTotal);
+
+// Requests ------------------------------------------------------------------------
+
+public sealed class CreateSalesDayEndRequest
+{
+ [Required] public int CashierUserId { get; set; }
+ /// Defaults to today (UTC) when omitted.
+ public DateOnly? BusinessDate { get; set; }
+}
diff --git a/Backend/ERPCore/Dtos/Sales/SalesInvoiceDtos.cs b/Backend/ERPCore/Dtos/Sales/SalesInvoiceDtos.cs
index 6682933..3e7dbcc 100644
--- a/Backend/ERPCore/Dtos/Sales/SalesInvoiceDtos.cs
+++ b/Backend/ERPCore/Dtos/Sales/SalesInvoiceDtos.cs
@@ -17,12 +17,24 @@ public sealed record SalesInvoiceDto(
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId,
SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt,
- DateTime? UpdatedAt, SalesInvoiceTotalsDto Totals, IReadOnlyList Lines);
+ DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
+ SalesInvoiceTotalsDto Totals, IReadOnlyList Lines);
public sealed record SalesInvoiceSummaryDto(
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
string CustomerSnapshotName, int WarehouseId, SalesInvoiceType InvoiceType,
- SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt);
+ SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
+
+public sealed record SalesInvoicePaymentDto(
+ int SalesInvoicePaymentId, int SalesInvoiceId, decimal Amount, DateTime PaymentDate,
+ long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt);
+
+public sealed class CreateSalesInvoicePaymentRequest
+{
+ [Range(0.01, double.MaxValue)] public decimal Amount { get; set; }
+ [Required] public long GlBankAccountId { get; set; }
+ [StringLength(100)] public string? Reference { get; set; }
+}
public sealed record SalesInvoicePostingIssueDto(
int SalesInvoiceLineId, int ItemId, string ItemSku, string ItemName, int WarehouseId,
diff --git a/Backend/ERPCore/Dtos/Sales/SalesReturnDtos.cs b/Backend/ERPCore/Dtos/Sales/SalesReturnDtos.cs
index 4aab60b..b619d4f 100644
--- a/Backend/ERPCore/Dtos/Sales/SalesReturnDtos.cs
+++ b/Backend/ERPCore/Dtos/Sales/SalesReturnDtos.cs
@@ -9,12 +9,13 @@ public sealed record SalesReturnLineDto(int ReturnLineId, int? SalesInvoiceLineI
public sealed record SalesReturnDto(
int ReturnId, string DocNo, int CustomerId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
- int CreatedBy, DateTime CreatedAt, IReadOnlyList Lines, IReadOnlyList LedgerRefs);
+ int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
+ IReadOnlyList Lines, IReadOnlyList LedgerRefs);
/// Row shape for GET /sales-returns — no lines/ledgerRefs (those need a per-row query).
public sealed record SalesReturnSummaryDto(
int ReturnId, string DocNo, int CustomerId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
- int CreatedBy, DateTime CreatedAt, int LineCount, decimal TotalQty);
+ int CreatedBy, DateTime CreatedAt, int LineCount, decimal TotalQty, string? GlJournalNo);
///
/// Remaining returnable qty for one sales invoice line — the invoiced qty minus
diff --git a/Backend/ERPCore/Dtos/Stock/AdjustmentDtos.cs b/Backend/ERPCore/Dtos/Stock/AdjustmentDtos.cs
index d7b97ce..ab51be2 100644
--- a/Backend/ERPCore/Dtos/Stock/AdjustmentDtos.cs
+++ b/Backend/ERPCore/Dtos/Stock/AdjustmentDtos.cs
@@ -9,12 +9,13 @@ public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, in
public sealed record AdjustmentDto(
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
- int CreatedBy, DateTime CreatedAt, IReadOnlyList Lines, IReadOnlyList LedgerRefs);
+ int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
+ IReadOnlyList Lines, IReadOnlyList LedgerRefs);
/// Row shape for GET /stock-adjustments — no lines/ledgerRefs (those need a per-row query).
public sealed record AdjustmentSummaryDto(
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
- int CreatedBy, DateTime CreatedAt, int LineCount);
+ int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
// Requests ----------------------------------------------------------------------
diff --git a/Backend/ERPCore/Dtos/Stock/CountDtos.cs b/Backend/ERPCore/Dtos/Stock/CountDtos.cs
index a75a262..209187f 100644
--- a/Backend/ERPCore/Dtos/Stock/CountDtos.cs
+++ b/Backend/ERPCore/Dtos/Stock/CountDtos.cs
@@ -16,7 +16,7 @@ public sealed record CountSummaryDto(
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status,
int CreatedBy, DateTime CreatedAt, int LineCount);
-public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, IReadOnlyList LedgerRefs);
+public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList LedgerRefs);
// Requests ----------------------------------------------------------------------
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseReturnConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseReturnConfiguration.cs
index 00add0a..15d7135 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseReturnConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/PurchaseReturnConfiguration.cs
@@ -16,6 +16,7 @@ public sealed class PurchaseReturnConfiguration : IEntityTypeConfiguration r.Status).HasConversion().HasMaxLength(20).IsRequired();
builder.Property(r => r.CreatedAt).IsRequired();
+ builder.Property(r => r.GlJournalNo).HasMaxLength(30);
builder.HasOne(r => r.Vendor).WithMany().HasForeignKey(r => r.VendorId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.Warehouse).WithMany().HasForeignKey(r => r.WarehouseId).OnDelete(DeleteBehavior.Restrict);
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/SalesDayEndConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/SalesDayEndConfiguration.cs
new file mode 100644
index 0000000..89211e4
--- /dev/null
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/SalesDayEndConfiguration.cs
@@ -0,0 +1,44 @@
+using ERPCore.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ERPCore.Infra.Persistence.Configurations;
+
+public sealed class SalesDayEndConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("sales_day_ends");
+ builder.HasKey(x => x.SalesDayEndId);
+
+ builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30);
+ builder.HasIndex(x => x.DocNo).IsUnique();
+
+ builder.Property(x => x.BusinessDate).IsRequired();
+
+ foreach (var p in new[] { nameof(SalesDayEnd.Subtotal), nameof(SalesDayEnd.DiscountTotal), nameof(SalesDayEnd.TaxTotal), nameof(SalesDayEnd.GrandTotal), nameof(SalesDayEnd.CostOfGoodsSold) })
+ builder.Property(p).HasPrecision(18, 4);
+ builder.Property(x => x.BundleCount).HasDefaultValue(0);
+
+ builder.Property(x => x.GlJournalNo).HasMaxLength(30);
+ builder.Property(x => x.ClosedAt).IsRequired();
+ builder.Property(x => x.RowVersion).IsRowVersion();
+
+ builder.HasOne(x => x.CashierUser).WithMany().HasForeignKey(x => x.CashierUserId).OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne(x => x.ClosedByUser).WithMany().HasForeignKey(x => x.ClosedBy).OnDelete(DeleteBehavior.Restrict);
+
+ // A cashier can only close a given business date once (CloseAsync replays this
+ // row idempotently on a second call instead of erroring — see SalesDayEndService).
+ builder.HasIndex(x => new { x.CashierUserId, x.BusinessDate }).IsUnique();
+
+ builder.HasMany(x => x.Slips)
+ .WithOne(x => x.DayEnd)
+ .HasForeignKey(x => x.DayEndId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasMany(x => x.BundleSales)
+ .WithOne(x => x.DayEnd)
+ .HasForeignKey(x => x.DayEndId)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/SalesInvoiceConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/SalesInvoiceConfiguration.cs
index 1d723a4..9bdf744 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/SalesInvoiceConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/SalesInvoiceConfiguration.cs
@@ -40,6 +40,7 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration(p).HasPrecision(18, 4);
+ builder.Property(x => x.GlJournalNo).HasMaxLength(30);
builder.Property(x => x.CreatedAt).IsRequired();
builder.Property(x => x.RowVersion).IsRowVersion();
@@ -54,6 +55,27 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("sales_invoice_payments");
+ builder.HasKey(p => p.SalesInvoicePaymentId);
+
+ builder.Property(p => p.Amount).HasPrecision(18, 4);
+ builder.Property(p => p.PaymentDate).IsRequired();
+ builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200);
+ builder.Property(p => p.Reference).HasMaxLength(100);
+ builder.Property(p => p.GlJournalNo).HasMaxLength(30);
+ builder.Property(p => p.CreatedAt).IsRequired();
+
+ builder.HasOne(p => p.SalesInvoice).WithMany(x => x.Payments).HasForeignKey(p => p.SalesInvoiceId).OnDelete(DeleteBehavior.Restrict);
+ builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
+
+ builder.HasIndex(p => p.SalesInvoiceId);
+ }
+}
+
public sealed class SalesInvoiceLineConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/SalesReturnConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/SalesReturnConfiguration.cs
index d835cbb..f6d0c34 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/SalesReturnConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/SalesReturnConfiguration.cs
@@ -16,6 +16,7 @@ public sealed class SalesReturnConfiguration : IEntityTypeConfiguration r.Status).HasConversion().HasMaxLength(20).IsRequired();
builder.Property(r => r.CreatedAt).IsRequired();
+ builder.Property(r => r.GlJournalNo).HasMaxLength(30);
builder.HasOne(r => r.Customer).WithMany().HasForeignKey(r => r.CustomerId).OnDelete(DeleteBehavior.Restrict);
builder.HasOne(r => r.Warehouse).WithMany().HasForeignKey(r => r.WarehouseId).OnDelete(DeleteBehavior.Restrict);
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/StockAdjustmentConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/StockAdjustmentConfiguration.cs
index d50a0e6..3152fb8 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/StockAdjustmentConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/StockAdjustmentConfiguration.cs
@@ -16,6 +16,7 @@ public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration a.Status).HasConversion().HasMaxLength(20).IsRequired();
builder.Property(a => a.CreatedAt).IsRequired();
+ builder.Property(a => a.GlJournalNo).HasMaxLength(30);
builder.Property(a => a.RowVersion).IsRowVersion();
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
index 17a2ba8..c03fc5f 100644
--- a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
+++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs
@@ -88,8 +88,10 @@ public class ErpDbContext : DbContext
// --- Sales (Phase 1) ---
public DbSet SalesInvoices => Set();
public DbSet SalesInvoiceLines => Set();
+ public DbSet SalesInvoicePayments => Set();
public DbSet SalesSlips => Set();
public DbSet SalesSlipLines => Set();
+ public DbSet SalesDayEnds => Set();
public DbSet BundleSaleTemplates => Set();
public DbSet BundleSaleTemplateLines => Set();
public DbSet BundleSales => Set();
diff --git a/Backend/ERPCore/Migrations/20260816130723_AddSalesDayEnd.Designer.cs b/Backend/ERPCore/Migrations/20260816130723_AddSalesDayEnd.Designer.cs
new file mode 100644
index 0000000..c5e9068
--- /dev/null
+++ b/Backend/ERPCore/Migrations/20260816130723_AddSalesDayEnd.Designer.cs
@@ -0,0 +1,7351 @@
+//
+using System;
+using ERPCore.Infra.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ERPCore.Migrations
+{
+ [DbContext(typeof(ErpDbContext))]
+ [Migration("20260816130723_AddSalesDayEnd")]
+ partial class AddSalesDayEnd
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b =>
+ {
+ b.Property("AttendanceRecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceRecordId"));
+
+ b.Property("AttendanceDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("AttendanceStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("AttendanceUploadBatchId")
+ .HasColumnType("integer");
+
+ b.Property("CheckIn")
+ .HasColumnType("interval");
+
+ b.Property("CheckOut")
+ .HasColumnType("interval");
+
+ b.Property("DuplicateOfAttendanceRecordId")
+ .HasColumnType("integer");
+
+ b.Property("EarlyLeaveMinutes")
+ .HasColumnType("integer");
+
+ b.Property("EditedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EditedBy")
+ .HasColumnType("integer");
+
+ b.Property("EmployeeId")
+ .HasColumnType("integer");
+
+ b.Property("IsManualOverride")
+ .HasColumnType("boolean");
+
+ b.Property("LateMinutes")
+ .HasColumnType("integer");
+
+ b.Property("Notes")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("OvertimeMinutes")
+ .HasColumnType("integer");
+
+ b.Property("RowValidationStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("WorkShiftId")
+ .HasColumnType("integer");
+
+ b.Property("WorkingMinutes")
+ .HasColumnType("integer");
+
+ b.HasKey("AttendanceRecordId");
+
+ b.HasIndex("AttendanceUploadBatchId");
+
+ b.HasIndex("WorkShiftId");
+
+ b.HasIndex("EmployeeId", "AttendanceDate");
+
+ b.ToTable("hr_attendance_records", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceUploadBatch", b =>
+ {
+ b.Property("AttendanceUploadBatchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AttendanceUploadBatchId"));
+
+ b.Property("ConfirmedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ConfirmedBy")
+ .HasColumnType("integer");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("OriginalFileName")
+ .HasMaxLength(260)
+ .HasColumnType("character varying(260)");
+
+ b.Property("PeriodEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PeriodStart")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RowCountDuplicate")
+ .HasColumnType("integer");
+
+ b.Property("RowCountError")
+ .HasColumnType("integer");
+
+ b.Property("RowCountTotal")
+ .HasColumnType("integer");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("SourceType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("UploadedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UploadedBy")
+ .HasColumnType("integer");
+
+ b.HasKey("AttendanceUploadBatchId");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.HasIndex("PeriodStart", "PeriodEnd");
+
+ b.ToTable("hr_attendance_upload_batches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AuditId"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("ChangeSet")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EntityId")
+ .HasColumnType("integer");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(80)
+ .HasColumnType("character varying(80)");
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("audit_logs", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b =>
+ {
+ b.Property("BatchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BatchId"));
+
+ b.Property("BatchNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("date");
+
+ b.Property("ItemId")
+ .HasColumnType("integer");
+
+ b.HasKey("BatchId");
+
+ b.HasIndex("ItemId", "BatchNo")
+ .IsUnique();
+
+ b.ToTable("batches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
+ {
+ b.Property("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BinId"));
+
+ b.Property("BinType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("integer");
+
+ b.HasKey("BinId");
+
+ b.HasIndex("WarehouseId", "Code")
+ .IsUnique();
+
+ b.ToTable("bins", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Branch", b =>
+ {
+ b.Property("BranchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BranchId"));
+
+ b.Property("Address")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("BranchId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_branches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Brand", b =>
+ {
+ b.Property("BrandId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BrandId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("BrandId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("brands", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b =>
+ {
+ b.Property("BundleSaleId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleId"));
+
+ b.Property("BundleCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("BundleDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("BundleName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("BundleNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("BundlePrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("BundleSaleTemplateId")
+ .HasColumnType("integer");
+
+ b.Property("CashierUserId")
+ .HasColumnType("integer");
+
+ b.Property("ComponentSubtotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CustomerId")
+ .HasColumnType("integer");
+
+ b.Property("CustomerSnapshotName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("DiscountTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("GrandTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("MarginAmount")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Draft");
+
+ b.Property("TaxTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("WarehouseId")
+ .HasColumnType("integer");
+
+ b.HasKey("BundleSaleId");
+
+ b.HasIndex("BundleNo")
+ .IsUnique();
+
+ b.HasIndex("BundleSaleTemplateId");
+
+ b.HasIndex("CashierUserId");
+
+ b.HasIndex("CustomerId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sales", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b =>
+ {
+ b.Property("BundleSaleLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleLineId"));
+
+ b.Property("BundleSaleId")
+ .HasColumnType("integer");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("IncludeInBundle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("IsComponent")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("ItemId")
+ .HasColumnType("integer");
+
+ b.Property("LineTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("ParentLineId")
+ .HasColumnType("integer");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("UnitPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("integer");
+
+ b.HasKey("BundleSaleLineId");
+
+ b.HasIndex("BundleSaleId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sale_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b =>
+ {
+ b.Property("BundleSaleTemplateId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateId"));
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TemplateCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("TemplateName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("BundleSaleTemplateId");
+
+ b.HasIndex("TemplateCode")
+ .IsUnique();
+
+ b.ToTable("bundle_sale_templates", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b =>
+ {
+ b.Property("BundleSaleTemplateLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BundleSaleTemplateLineId"));
+
+ b.Property("BundleSaleTemplateId")
+ .HasColumnType("integer");
+
+ b.Property("IncludeInBundle")
+ .HasColumnType("boolean");
+
+ b.Property("ItemId")
+ .HasColumnType("integer");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("UnitPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("integer");
+
+ b.HasKey("BundleSaleTemplateLineId");
+
+ b.HasIndex("BundleSaleTemplateId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sale_template_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
+ {
+ b.Property("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CategoryId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b =>
+ {
+ b.Property("CustomerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CustomerId"));
+
+ b.Property("AddressLine1")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)");
+
+ b.Property("AddressLine2")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)");
+
+ b.Property("City")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Country")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreditDays")
+ .HasColumnType("integer");
+
+ b.Property("CreditLimit")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)");
+
+ b.Property("CustomerCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CustomerType")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("B2C");
+
+ b.Property("DefaultWarehouseId")
+ .HasColumnType("integer");
+
+ b.Property("DisplayName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Email")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Phone")
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxRegistrationNo")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("CustomerId");
+
+ b.HasIndex("CustomerCode")
+ .IsUnique();
+
+ b.HasIndex("CustomerType");
+
+ b.HasIndex("DefaultWarehouseId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Department", b =>
+ {
+ b.Property("DepartmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DepartmentId"));
+
+ b.Property("BranchId")
+ .HasColumnType("integer");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("HeadEmployeeId")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ParentDepartmentId")
+ .HasColumnType("integer");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("DepartmentId");
+
+ b.HasIndex("BranchId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("HeadEmployeeId");
+
+ b.HasIndex("ParentDepartmentId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_departments", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Designation", b =>
+ {
+ b.Property("DesignationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DesignationId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("DesignationId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_designations", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b =>
+ {
+ b.Property("EmployeeId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EmployeeId"));
+
+ b.Property("AddressLine1")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("AddressLine2")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("BranchId")
+ .HasColumnType("integer");
+
+ b.Property