feat: add vendor optional fields and configuration hooks

- Implemented VendorOptionalFields component to handle optional vendor details (mobile, email, address) with validation.
- Created useCustomerConfig hook to load optional customer field settings from Sales configuration.
- Developed useProcurementConfig hook to manage Procurement configuration flags.
- Introduced accessConfigApi for managing access configuration settings.
- Added openingBalancesApi for handling opening balances operations.
- Created procurementConfigApi for managing procurement configuration settings.
- Implemented nav-routes for permission-based routing.
- Developed PDF generation for purchase orders with dynamic vendor and item details.
- Added purchase-order tax helpers for tax calculations and formatting.
- Defined types for opening balances and related operations.
This commit is contained in:
Dhananjaya99
2026-10-18 12:46:31 +05:30
parent c27db06880
commit e7b1f270a5
202 changed files with 99906 additions and 1894 deletions
@@ -10,7 +10,7 @@ public sealed record ApprovalStepDto(int Level, ApprovalDecision Decision, int D
public sealed record ApprovalConfigDto(
int StockAdjustmentLevels, decimal StockAdjustmentLevel2ThresholdValue,
int PriceAdjustmentLevels, decimal PriceAdjustmentLevel2ThresholdPercent,
IReadOnlyList<string> Level2RoleCodes, DateTime? UpdatedAt);
IReadOnlyList<int> Level2RoleIds, DateTime? UpdatedAt);
public sealed class UpdateApprovalConfigRequest
{
@@ -18,7 +18,8 @@ public sealed class UpdateApprovalConfigRequest
[Range(0, double.MaxValue)] public decimal StockAdjustmentLevel2ThresholdValue { get; set; }
[Range(0, 2)] public int PriceAdjustmentLevels { get; set; }
[Range(0, 1000)] public decimal PriceAdjustmentLevel2ThresholdPercent { get; set; }
public List<string> Level2RoleCodes { get; set; } = new();
/// <summary>Roles allowed to give the second approval; empty = anyone with the Approve action.</summary>
public List<int> Level2RoleIds { get; set; } = new();
}
public sealed class ApproveRequest
-28
View File
@@ -1,28 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace ERPCore.Dtos.Auth;
/// <summary>AuthHex's Role projection (ERP_Auth_Service/API_DOCUMENTATION.md, RoleManager section).</summary>
public sealed class AuthHexRoleDto
{
public Guid RoleId { get; set; }
public string? Code { get; set; }
public string? Name { get; set; }
public bool? IsSystemRole { get; set; }
public DateTime CreatedAt { get; set; }
}
public sealed class CreateAuthHexRoleRequest
{
[Required] public string Code { get; set; } = string.Empty;
public string? Name { get; set; }
public bool? IsSystemRole { get; set; }
}
public sealed class UpdateAuthHexRoleRequest
{
[Required] public Guid RoleId { get; set; }
public string? Code { get; set; }
public string? Name { get; set; }
public bool? IsSystemRole { get; set; }
}
+3 -1
View File
@@ -57,7 +57,9 @@ public sealed class RegisterRequest
{
/// <summary>Optional — AuthHex requires a client-supplied id; ERPCore generates one when omitted.</summary>
public Guid? UserId { get; set; }
[Required] public Guid RoleId { get; set; }
/// <summary>Ignored for self-registration and never taken from a caller: the ERP assigns roles itself. Sent to AuthHex only
/// while the old AuthHex (which still demands a role id) is deployed — see <c>AuthHex:LegacyRoleId</c>.</summary>
public Guid? RoleId { get; set; }
[Required] public Guid UserTypeId { get; set; }
public string? Fullname { get; set; }
public string? UserName { get; set; }
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace ERPCore.Dtos.Config;
/// <summary>Access configuration resource: which role a person gets when they have none.</summary>
public sealed record AccessConfigDto(int? DefaultRoleId, string? DefaultRoleName, DateTime? UpdatedAt, int? UpdatedBy);
/// <summary>Full replacement. <c>null</c> clears the default (people without a role then have no permissions at all).</summary>
public sealed class UpdateAccessConfigRequest
{
[Range(1, int.MaxValue)] public int? DefaultRoleId { get; set; }
}
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Config;
/// <summary>Procurement configuration resource.</summary>
public sealed record ProcurementConfigDto(
VendorContactMode VendorContactMode, bool VendorAddressEnabled, bool PoTaxEnabled,
DateTime? UpdatedAt, int? UpdatedBy,
bool VendorPaymentTermsEnabled = true, bool VendorTaxRegEnabled = true);
/// <summary>
/// Full replacement of the flags. <c>UpdatedBy</c> is derived from the token, never posted.
/// Nullable so an omitted flag is a 400 rather than silently binding to a default
/// (same reasoning as <see cref="UpdateGrnConfigRequest"/>).
/// </summary>
public sealed class UpdateProcurementConfigRequest
{
[Required] public VendorContactMode? VendorContactMode { get; set; }
[Required] public bool? VendorAddressEnabled { get; set; }
[Required] public bool? PoTaxEnabled { get; set; }
[Required] public bool? VendorPaymentTermsEnabled { get; set; }
[Required] public bool? VendorTaxRegEnabled { get; set; }
}
+11 -1
View File
@@ -6,7 +6,10 @@ namespace ERPCore.Dtos.Config;
/// the generic cash customer (null until the option has been switched on at least once).</summary>
public sealed record SalesConfigDto(
bool AllowCashCustomer, int? CashCustomerId, string? CashCustomerName, bool AllowFreeItems, bool AllowDiscounts, bool AllowOverStockSale,
bool AllowWarehouseSelection, int? DefaultWarehouseId, string? DefaultWarehouseName, DateTime? UpdatedAt, int? UpdatedBy);
bool AllowWarehouseSelection, int? DefaultWarehouseId, string? DefaultWarehouseName, DateTime? UpdatedAt, int? UpdatedBy,
bool CustomerPhoneEnabled = true, bool CustomerEmailEnabled = true, bool CustomerAddressEnabled = true,
bool CustomerCityEnabled = true, bool CustomerCountryEnabled = true,
bool CustomerCreditLimitEnabled = true, bool CustomerCreditDaysEnabled = true);
/// <summary>
/// Full replacement of the flags. <c>UpdatedBy</c> is derived from the token, never posted.
@@ -22,4 +25,11 @@ public sealed class UpdateSalesConfigRequest
[Required] public bool? AllowWarehouseSelection { get; set; }
/// <summary>Required when <see cref="AllowWarehouseSelection"/> is false.</summary>
public int? DefaultWarehouseId { get; set; }
[Required] public bool? CustomerPhoneEnabled { get; set; }
[Required] public bool? CustomerEmailEnabled { get; set; }
[Required] public bool? CustomerAddressEnabled { get; set; }
[Required] public bool? CustomerCityEnabled { get; set; }
[Required] public bool? CustomerCountryEnabled { get; set; }
[Required] public bool? CustomerCreditLimitEnabled { get; set; }
[Required] public bool? CustomerCreditDaysEnabled { get; set; }
}
+4 -2
View File
@@ -27,7 +27,8 @@ public sealed record GrnDto(
public sealed record GrnSummaryDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, decimal TotalValue,
int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
int? PurchaseInvoiceId, string? PurchaseInvoiceNo,
string? PoDocNo = null, Dtos.Procurement.PoVendorDto? Vendor = null);
public sealed record CreatedLayerDto(
int LayerId, int ItemId, int WarehouseId, int? BatchId,
@@ -93,7 +94,8 @@ public sealed class CreateGrnRequest
public int? VendorId { get; set; }
[Required] public int WarehouseId { get; set; }
/// <summary>Owning branch for this GRN — warehouses are shared, so this is what actually
/// scopes the document (resolved via IBranchConfigService; required when branching is enabled).</summary>
/// scopes the document (resolved via IBranchConfigService; required when branching is enabled).
/// Ignored when <see cref="PoId"/> is set: a GRN against a purchase order always belongs to that order's branch.</summary>
[Required] public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
}
@@ -0,0 +1,71 @@
using System.ComponentModel.DataAnnotations;
namespace ERPCore.Dtos.OpeningBalances;
/// <summary>One cash or bank account as the opening-balance screen shows it. <c>Amount</c> is filled only once posted.</summary>
public sealed record OpeningCashBankAccountDto(
string AccountType, string AccountName, string? BankName, string? AccountNumber,
string GlAccountCode, string CurrencyCode, decimal? Amount);
public sealed record OpeningBalanceLineDto(string AccountCode, string AccountName, decimal Amount);
/// <summary>A posted opening balance (the record of the one-time entry).</summary>
public sealed record OpeningBalanceRunDto(
int OpeningBalanceRunId, string Kind, DateOnly CutoverDate, string? GlJournalNo,
decimal TotalAmount, string Currency, DateTime PostedAt, int PostedBy,
IReadOnlyList<OpeningBalanceLineDto> Lines);
/// <summary>The cash/bank screen's state: the accounts to enter amounts for, or the posted record once done.</summary>
public sealed record CashBankOpeningStateDto(
OpeningBalanceRunDto? Posted, IReadOnlyList<OpeningCashBankAccountDto> Accounts, string BaseCurrency);
public sealed class OpeningBalanceLineInput
{
/// <summary>The GL account code of a cash or bank account, as returned by the state endpoint.</summary>
[Required, StringLength(20)] public string GlAccountCode { get; set; } = string.Empty;
/// <summary>Opening balance, at most 2 decimals. Positive = money held; negative = an overdraft. Zero lines are ignored.</summary>
public decimal Amount { get; set; }
}
public sealed class PostCashBankOpeningRequest
{
/// <summary>The opening date (the GL entry date). Must fall in an open accounting period and not be in the future.</summary>
[Required] public DateOnly? CutoverDate { get; set; }
[Required, MinLength(1)] public List<OpeningBalanceLineInput> Lines { get; set; } = new();
}
// ---------------------------------------------------------------- opening stock
public sealed class OpeningStockLineInput
{
[Required] public int ItemId { get; set; }
[Required] public int WarehouseId { get; set; }
/// <summary>Quantity in the item's base UOM; more than 0, at most 4 decimals.</summary>
public decimal Qty { get; set; }
/// <summary>Cost of ONE base unit, 0 or more, at most 4 decimals — this becomes the FIFO layer's cost.</summary>
public decimal UnitCost { get; set; }
}
public sealed class PostOpeningStockRequest
{
/// <summary>The opening date (the stock layers' receipt date and the GL entry date). Not in the future.</summary>
[Required] public DateOnly? CutoverDate { get; set; }
[Required, MinLength(1), MaxLength(500)] public List<OpeningStockLineInput> Lines { get; set; } = new();
}
public sealed record OpeningStockLineDto(
int ItemId, string Sku, string ItemName, int WarehouseId, string WarehouseCode,
decimal Qty, decimal UnitCost, decimal Value);
public sealed record OpeningStockRunSummaryDto(
int OpeningBalanceRunId, DateOnly CutoverDate, string? GlJournalNo, decimal TotalValue, int LineCount,
DateTime PostedAt, int PostedBy);
public sealed record OpeningStockRunDto(
int OpeningBalanceRunId, DateOnly CutoverDate, string? GlJournalNo, decimal TotalValue, int LineCount,
DateTime PostedAt, int PostedBy, IReadOnlyList<OpeningStockLineDto> Lines);
@@ -5,9 +5,14 @@ namespace ERPCore.Dtos.Procurement;
// Responses (docs/11 §3.3) ------------------------------------------------------
/// <summary><c>Tax</c> is the rate (0..1); <c>TaxAmount</c> the stored amount for the line.</summary>
public sealed record PoLineDto(
int PoLineId, int ItemId, int WarehouseId,
decimal Qty, decimal UnitPrice, decimal Tax, decimal QtyReceived);
decimal Qty, decimal UnitPrice, decimal Tax, decimal QtyReceived, decimal TaxAmount = 0);
/// <summary>Vendor details shown on a purchase order. <c>Mobile</c>/<c>Email</c>/<c>Address</c> are null
/// unless the matching Procurement configuration switch is on.</summary>
public sealed record PoVendorDto(int VendorId, string Code, string Name, string? Mobile, string? Email, string? Address);
public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
@@ -15,11 +20,12 @@ public sealed record PurchaseOrderDto(
int PoId, string DocNo, int VendorId, int BranchId, int? RequisitionId, PurchaseOrderStatus Status,
bool ApprovalRequired, int CreatedBy, int? ApprovedBy, DateTime? ApprovedAt,
DateTime CreatedAt, DateTime? UpdatedAt,
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines);
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines, PoVendorDto? Vendor = null);
public sealed record PurchaseOrderSummaryDto(
int PoId, string DocNo, int VendorId, int BranchId, PurchaseOrderStatus Status,
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, PoTotalsDto Totals);
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, PoTotalsDto Totals,
PoVendorDto? Vendor = null);
// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
+3 -1
View File
@@ -11,4 +11,6 @@ namespace ERPCore.Dtos.Rbac;
/// entirely rather than merely restricting them by role.</summary>
public sealed record MeResponseDto(
string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes,
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled, bool WarehouseBranchingEnabled);
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled, bool WarehouseBranchingEnabled,
int? RoleId = null, bool HasRole = false,
IReadOnlyDictionary<string, int>? Permissions = null);
+1 -1
View File
@@ -5,7 +5,7 @@ namespace ERPCore.Dtos.Rbac;
public sealed record RoleDto(
int RoleId, string Code, string Name, bool IsSystemRole, EntityStatus Status,
DateTime CreatedAt, DateTime? UpdatedAt);
DateTime CreatedAt, DateTime? UpdatedAt, bool IsAdmin = false);
public sealed class CreateRoleRequest
{
+7
View File
@@ -84,3 +84,10 @@ public sealed record SendResetLinkResponse(DateTime? ExpiresAt);
/// <summary>AuthHex UserType lookup, for populating the create-user form's select (no local shadow — read-only passthrough).</summary>
public sealed record UserTypeOptionDto(Guid UserTypeId, string? Code, string? Description);
/// <summary>Outcome of the one-time copy of each user's role from AuthHex into the ERP's users table.</summary>
public sealed record BackfillRolesResultDto(
int Examined, int Assigned, int AlreadyHadRole, IReadOnlyList<BackfillRolesIssueDto> Issues);
/// <summary>A user the backfill could not give a role, and why — for an administrator to assign by hand.</summary>
public sealed record BackfillRolesIssueDto(int UserId, string Username, string Reason);
+14 -1
View File
@@ -6,7 +6,8 @@ namespace ERPCore.Dtos.Vendors;
/// <summary>Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).</summary>
public sealed record VendorDto(
int VendorId, string Code, string Name, string? Terms, string? TaxReg,
string Currency, EntityStatus Status, int BranchId, DateTime CreatedAt, DateTime? UpdatedAt);
string Currency, EntityStatus Status, int BranchId, DateTime CreatedAt, DateTime? UpdatedAt,
string? Mobile = null, string? Email = null, string? Address = null);
public sealed class CreateVendorRequest
{
@@ -15,6 +16,12 @@ public sealed class CreateVendorRequest
[StringLength(50)] public string? Terms { get; set; }
[StringLength(50)] public string? TaxReg { get; set; }
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
/// <summary>Only accepted while the vendor contact mode is Mobile (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(20)] public string? Mobile { get; set; }
/// <summary>Only accepted while the vendor contact mode is Email (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(200)] public string? Email { get; set; }
/// <summary>Only accepted while vendor address is enabled (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(500)] public string? Address { get; set; }
/// <summary>Optional — resolved via <see cref="Services.Interfaces.IBranchConfigService.ResolveBranchIdAsync"/>
/// when branching is enabled; ignored (defaulted) otherwise.</summary>
public int BranchId { get; set; }
@@ -27,6 +34,12 @@ public sealed class UpdateVendorRequest
[StringLength(50)] public string? Terms { get; set; }
[StringLength(50)] public string? TaxReg { get; set; }
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
/// <summary>Only accepted while the vendor contact mode is Mobile (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(20)] public string? Mobile { get; set; }
/// <summary>Only accepted while the vendor contact mode is Email (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(200)] public string? Email { get; set; }
/// <summary>Only accepted while vendor address is enabled (otherwise 422 CONFIG_DISABLED).</summary>
[StringLength(500)] public string? Address { get; set; }
public int BranchId { get; set; }
}