feat: Implement procurement services and number sequence management

- Added INumberSequenceService interface for generating document numbers.
- Created NumberSequenceService to handle atomic document number issuance.
- Introduced IPurchaseOrderService interface and implemented PurchaseOrderService for managing purchase orders.
- Added IRequisitionService interface and implemented RequisitionService for handling requisitions.
- Created IRfqService interface and implemented RfqService for managing RFQs and vendor quotations.
- Defined necessary DTOs and domain entities for procurement processes.
- Ensured proper validation and error handling across services.
This commit is contained in:
2026-07-13 10:28:58 +05:30
parent 783696fa97
commit 4e84a15db7
41 changed files with 3495 additions and 6 deletions
+17
View File
@@ -0,0 +1,17 @@
namespace ERPCore.Domain;
/// <summary>
/// Document-type prefixes for <see cref="Entities.NumberSequence"/> and the
/// generated document numbers (docs/10 §B.8.2). One prefix per numbered document.
/// </summary>
public static class DocumentTypes
{
public const string Requisition = "PR";
public const string Rfq = "RFQ";
public const string PurchaseOrder = "PO";
public const string Grn = "GRN";
public const string Transfer = "TRF";
public const string Adjustment = "ADJ";
public const string Count = "CNT";
public const string PurchaseReturn = "PRET";
}
@@ -0,0 +1,15 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// Per-document-type, per-year running counter behind human document numbers
/// (FR-X-03): <c>PR-2026-00001</c>, <c>PO-2026-00042</c>, … Numbers are issued
/// inside the document's transaction so they are unique and gap-controlled.
/// Model: docs/10 Part C.7.
/// </summary>
public class NumberSequence
{
public long SequenceId { get; set; }
public string DocType { get; set; } = string.Empty;
public int Year { get; set; }
public long LastNumber { get; set; }
}
+28
View File
@@ -0,0 +1,28 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// Purchase-order line (FR-PROC-03). <see cref="Tax"/> is the line tax rate
/// (e.g. 0.18); <see cref="QtyReceived"/> accrues as GRNs confirm (FR-PROC-07).
/// Model: docs/10 Part C.2.
/// </summary>
public class PoLine
{
public long PoLineId { get; set; }
public long PoId { get; set; }
public PurchaseOrder? PurchaseOrder { get; set; }
public long ItemId { get; set; }
public Item? Item { get; set; }
public long UomId { get; set; }
public Uom? Uom { get; set; }
public long WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
public decimal Qty { get; set; }
public decimal UnitPrice { get; set; }
public decimal Tax { get; set; }
public decimal QtyReceived { get; set; }
}
@@ -0,0 +1,36 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Purchase order header (FR-PROC-03..06). Mutable aggregate with a
/// <see cref="RowVersion"/> ETag token; editable while open (FR-PROC-05).
/// Phase 1 auto-approves on creation; <see cref="ApprovalRequired"/> is retained
/// for the future approval workflow. Totals are computed server-side from lines
/// (not stored). Model: docs/10 Part C.2.
/// </summary>
public class PurchaseOrder
{
public long PoId { get; set; }
public string DocNo { get; set; } = string.Empty;
public long VendorId { get; set; }
public Vendor? Vendor { get; set; }
public long? RequisitionId { get; set; }
public Requisition? Requisition { get; set; }
public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft;
public bool ApprovalRequired { get; set; }
public long CreatedBy { get; set; }
public User? Creator { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
public uint RowVersion { get; set; }
public ICollection<PoLine> Lines { get; set; } = new List<PoLine>();
}
@@ -0,0 +1,21 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Purchase requisition header (FR-PROC-01). <see cref="RequestedBy"/> is the audit
/// actor from the token (never the body). Model: docs/10 Part C.2.
/// </summary>
public class Requisition
{
public long RequisitionId { get; set; }
public string DocNo { get; set; } = string.Empty;
public long RequestedBy { get; set; }
public User? Requester { get; set; }
public RequisitionStatus Status { get; set; } = RequisitionStatus.Draft;
public DateTime CreatedAt { get; set; }
public ICollection<RequisitionLine> Lines { get; set; } = new List<RequisitionLine>();
}
@@ -0,0 +1,16 @@
namespace ERPCore.Domain.Entities;
/// <summary>Requisition line (FR-PROC-01). Model: docs/10 Part C.2.</summary>
public class RequisitionLine
{
public long ReqLineId { get; set; }
public long RequisitionId { get; set; }
public Requisition? Requisition { get; set; }
public long ItemId { get; set; }
public Item? Item { get; set; }
public decimal Qty { get; set; }
public DateOnly? RequiredBy { get; set; }
}
+22
View File
@@ -0,0 +1,22 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Request for Quotation header (FR-PROC-02) raised from a requisition. Vendor
/// quotations attach for comparison. Model: docs/10 Part C.2.
/// </summary>
public class Rfq
{
public long RfqId { get; set; }
public string DocNo { get; set; } = string.Empty;
public long RequisitionId { get; set; }
public Requisition? Requisition { get; set; }
public RfqStatus Status { get; set; } = RfqStatus.Open;
public DateTime CreatedAt { get; set; }
public ICollection<RfqLine> Lines { get; set; } = new List<RfqLine>();
public ICollection<VendorQuotation> Quotations { get; set; } = new List<VendorQuotation>();
}
@@ -0,0 +1,15 @@
namespace ERPCore.Domain.Entities;
/// <summary>RFQ line — an item + quantity being quoted (FR-PROC-02). Model: docs/10 Part C.2.</summary>
public class RfqLine
{
public long RfqLineId { get; set; }
public long RfqId { get; set; }
public Rfq? Rfq { get; set; }
public long ItemId { get; set; }
public Item? Item { get; set; }
public decimal Qty { get; set; }
}
+20
View File
@@ -0,0 +1,20 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Application user (FR-X-01). In Phase 1 authentication/RBAC are deferred; this
/// table exists so mutations can be stamped with an audit actor and documents can
/// carry a `createdBy`/`requestedBy` FK. A seeded <c>system</c> user (id 1) is the
/// fallback actor until `/auth/login` lands (§6). Model: docs/10 Part C.7.
/// </summary>
public class User
{
/// <summary>Seeded fallback actor used while auth is deferred.</summary>
public const long SystemUserId = 1;
public long UserId { get; set; }
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public EntityStatus Status { get; set; } = EntityStatus.Active;
}
@@ -0,0 +1,26 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// A vendor's quotation against an RFQ (FR-PROC-02). Per-item pricing lives in
/// <see cref="Lines"/>.
/// <para>
/// Deviation note: docs/10 Part C.2 models <c>VENDOR_QUOTATION</c> with scalar
/// <c>unit_price</c>/<c>lead_days</c> and no item reference, which cannot represent
/// the per-line pricing the API contract requires (docs/11 §3.2). This header +
/// <see cref="VendorQuotationLine"/> split follows the authoritative API shape.
/// </para>
/// </summary>
public class VendorQuotation
{
public long QuotationId { get; set; }
public long RfqId { get; set; }
public Rfq? Rfq { get; set; }
public long VendorId { get; set; }
public Vendor? Vendor { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<VendorQuotationLine> Lines { get; set; } = new List<VendorQuotationLine>();
}
@@ -0,0 +1,16 @@
namespace ERPCore.Domain.Entities;
/// <summary>Per-item quoted price and lead time within a <see cref="VendorQuotation"/> (docs/11 §3.2).</summary>
public class VendorQuotationLine
{
public long QuotationLineId { get; set; }
public long QuotationId { get; set; }
public VendorQuotation? Quotation { get; set; }
public long ItemId { get; set; }
public Item? Item { get; set; }
public decimal UnitPrice { get; set; }
public int LeadDays { get; set; }
}
@@ -0,0 +1,17 @@
namespace ERPCore.Domain.Enums;
/// <summary>
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on
/// creation, so <see cref="PendingApproval"/> is reserved (not entered) until the
/// approval workflow is enabled (FR-PROC-04). Stored as a string.
/// </summary>
public enum PurchaseOrderStatus
{
Draft,
PendingApproval,
Approved,
PartiallyReceived,
FullyReceived,
Closed,
Cancelled
}
@@ -0,0 +1,8 @@
namespace ERPCore.Domain.Enums;
/// <summary>Purchase-requisition lifecycle (docs/11 §3.1; docs/10 §B.8.1). Stored as a string.</summary>
public enum RequisitionStatus
{
Draft,
Submitted
}
@@ -0,0 +1,8 @@
namespace ERPCore.Domain.Enums;
/// <summary>RFQ lifecycle (docs/11 §3.2). Stored as a string.</summary>
public enum RfqStatus
{
Open,
Closed
}