Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae7627fcf2 | |||
| 67150425e4 | |||
| 9e1aa57987 | |||
| 22f86451e3 | |||
| 4e84a15db7 |
@@ -1,5 +1,7 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
@@ -8,10 +10,12 @@ namespace ERPCore.Controllers;
|
||||
/// Base for the v1 API controllers. Centralises ETag / If-Match handling
|
||||
/// (docs/11-BACKEND-PHASE1.md §1.6) so concurrency behaviour is uniform.
|
||||
/// Each controller declares its own explicit lowercase <c>[Route]</c> to match
|
||||
/// the API contract paths (docs/11 §1.1).
|
||||
/// the API contract paths (docs/11 §1.1). Every v1 endpoint requires a valid
|
||||
/// AuthHex token satisfying the ERP door policy (docs/10 A.4).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Authorize(JwtAuthExtensions.ErpAccessPolicy)]
|
||||
public abstract class ApiControllerBase : ControllerBase
|
||||
{
|
||||
/// <summary>Parse a mandatory <c>If-Match</c> header, or 428 if absent/malformed.</summary>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using ERPCore.Dtos.Audit;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only audit trail (FR-X-02; auditor role). Extends the documented §11 API —
|
||||
/// the audit trail is required (AR-01 compensating control) and read access is the
|
||||
/// only way to consume it.
|
||||
/// </summary>
|
||||
[Route("api/v1/audit-logs")]
|
||||
public sealed class AuditLogsController : ApiControllerBase
|
||||
{
|
||||
private readonly IAuditService _audit;
|
||||
|
||||
public AuditLogsController(IAuditService audit) => _audit = audit;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<AuditLogDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<AuditLogDto>>> List(
|
||||
[FromQuery] string? entityType, [FromQuery] long? entityId, [FromQuery] long? userId,
|
||||
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _audit.ListLogsAsync(entityType, entityId, userId, from, to, query, ct));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using ERPCore.Dtos.Grn;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Goods-receipt endpoints (docs/11 §4).</summary>
|
||||
[Route("api/v1/grns")]
|
||||
public sealed class GrnsController : ApiControllerBase
|
||||
{
|
||||
private readonly IGrnService _grns;
|
||||
|
||||
public GrnsController(IGrnService grns) => _grns = grns;
|
||||
|
||||
[HttpGet("{grnId:long}")]
|
||||
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<GrnDto>> GetById(long grnId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _grns.GetAsync(grnId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>Create a Draft GRN against a PO or direct. Cost is PO-derived for PO lines.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<GrnDto>> Create([FromBody] CreateGrnRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _grns.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/grns/{dto.GrnId}", dto);
|
||||
}
|
||||
|
||||
/// <summary>Confirm: create FIFO layers + inbound ledger + update PO receipts (single UoW txn).</summary>
|
||||
[HttpPost("{grnId:long}/confirm")]
|
||||
[ProducesResponseType(typeof(GrnConfirmResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<GrnConfirmResultDto>> Confirm(
|
||||
long grnId, [FromHeader(Name = "Idempotency-Key")] string? idempotencyKey, CancellationToken ct)
|
||||
=> Ok(await _grns.ConfirmAsync(grnId, idempotencyKey, ct));
|
||||
|
||||
/// <summary>Release or reject an inspection-hold line (FR-GRN-05).</summary>
|
||||
[HttpPost("{grnId:long}/lines/{grnLineId:long}/release")]
|
||||
[ProducesResponseType(typeof(ReleaseLineResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ReleaseLineResultDto>> Release(
|
||||
long grnId, long grnLineId, [FromBody] ReleaseLineRequest request, CancellationToken ct)
|
||||
=> Ok(await _grns.ReleaseLineAsync(grnId, grnLineId, request.Action, ct));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using ERPCore.Dtos.Audit;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only GL-ready journal stubs (FR-STK-13; consumed by the Accounting phase).
|
||||
/// Data only — no posting in Phase 1.
|
||||
/// </summary>
|
||||
[Route("api/v1/journal-entries")]
|
||||
public sealed class JournalEntriesController : ApiControllerBase
|
||||
{
|
||||
private readonly IAuditService _audit;
|
||||
|
||||
public JournalEntriesController(IAuditService audit) => _audit = audit;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<JournalEntryStubDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<JournalEntryStubDto>>> List(
|
||||
[FromQuery] string? sourceDocType, [FromQuery] long? sourceDocId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _audit.ListJournalAsync(sourceDocType, sourceDocId, query, ct));
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-order endpoints (docs/11 §3.3).</summary>
|
||||
[Route("api/v1/purchase-orders")]
|
||||
public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseOrderService _pos;
|
||||
|
||||
public PurchaseOrdersController(IPurchaseOrderService pos) => _pos = pos;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<PurchaseOrderSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<PurchaseOrderSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] PurchaseOrderStatus? status, [FromQuery] long? vendorId, CancellationToken ct)
|
||||
=> Ok(await _pos.ListAsync(query, status, vendorId, ct));
|
||||
|
||||
[HttpGet("{poId:long}")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> GetById(long poId, CancellationToken ct)
|
||||
{
|
||||
var result = await _pos.GetAsync(poId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Create a PO — auto-approved on creation in Phase 1 (FR-PROC-04). Totals computed server-side.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Create([FromBody] CreatePurchaseOrderRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _pos.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/purchase-orders/{result.Value.PoId}", result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Edit while open (FR-PROC-05); requires <c>If-Match</c>. 409 PO_NOT_EDITABLE if closed.</summary>
|
||||
[HttpPut("{poId:long}")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Update(long poId, [FromBody] UpdatePurchaseOrderRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _pos.UpdateAsync(poId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.</summary>
|
||||
[HttpPost("{poId:long}/approve")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Approve(long poId, CancellationToken ct)
|
||||
=> Ok(await _pos.ApproveAsync(poId, ct));
|
||||
|
||||
/// <summary>Cancel — 409 if any goods have been received against the PO.</summary>
|
||||
[HttpPost("{poId:long}/cancel")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PurchaseOrderDto>> Cancel(long poId, [FromBody] CancelPurchaseOrderRequest request, CancellationToken ct)
|
||||
=> Ok(await _pos.CancelAsync(poId, request.Reason, ct));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-return endpoints (docs/11 §3.4).</summary>
|
||||
[Route("api/v1/purchase-returns")]
|
||||
public sealed class PurchaseReturnsController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseReturnService _returns;
|
||||
|
||||
public PurchaseReturnsController(IPurchaseReturnService returns) => _returns = returns;
|
||||
|
||||
/// <summary>Create + auto-post a return (outbound movement). 409 if return exceeds available stock.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(PurchaseReturnDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<PurchaseReturnDto>> Create([FromBody] CreatePurchaseReturnRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _returns.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/purchase-returns/{dto.ReturnId}", dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Reference;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Reason-code reference endpoints (docs/11 §6).</summary>
|
||||
[Route("api/v1/reason-codes")]
|
||||
public sealed class ReasonCodesController : ApiControllerBase
|
||||
{
|
||||
private readonly IReasonCodeService _codes;
|
||||
|
||||
public ReasonCodesController(IReasonCodeService codes) => _codes = codes;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<ReasonCodeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ReasonCodeDto>>> List(
|
||||
[FromQuery] ReasonContext? context, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _codes.ListAsync(context, query, ct));
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ReasonCodeDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ReasonCodeDto>> Create([FromBody] CreateReasonCodeRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _codes.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/reason-codes/{dto.ReasonCodeId}", dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-requisition endpoints (docs/11 §3.1).</summary>
|
||||
[Route("api/v1/requisitions")]
|
||||
public sealed class RequisitionsController : ApiControllerBase
|
||||
{
|
||||
private readonly IRequisitionService _requisitions;
|
||||
|
||||
public RequisitionsController(IRequisitionService requisitions) => _requisitions = requisitions;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<RequisitionSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<RequisitionSummaryDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _requisitions.ListAsync(query, ct));
|
||||
|
||||
[HttpGet("{requisitionId:long}")]
|
||||
[ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RequisitionDto>> GetById(long requisitionId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _requisitions.GetAsync(requisitionId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<RequisitionDto>> Create([FromBody] CreateRequisitionRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _requisitions.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/requisitions/{dto.RequisitionId}", dto);
|
||||
}
|
||||
|
||||
[HttpPost("{requisitionId:long}/submit")]
|
||||
[ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RequisitionDto>> Submit(long requisitionId, CancellationToken ct)
|
||||
=> Ok(await _requisitions.SubmitAsync(requisitionId, ct));
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>RFQ & quotation endpoints (docs/11 §3.2).</summary>
|
||||
[Route("api/v1/rfqs")]
|
||||
public sealed class RfqsController : ApiControllerBase
|
||||
{
|
||||
private readonly IRfqService _rfqs;
|
||||
|
||||
public RfqsController(IRfqService rfqs) => _rfqs = rfqs;
|
||||
|
||||
[HttpGet("{rfqId:long}")]
|
||||
[ProducesResponseType(typeof(RfqDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RfqDto>> GetById(long rfqId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _rfqs.GetAsync(rfqId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(RfqDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<RfqDto>> Create([FromBody] CreateRfqRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _rfqs.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/rfqs/{dto.RfqId}", dto);
|
||||
}
|
||||
|
||||
[HttpPost("{rfqId:long}/quotations")]
|
||||
[ProducesResponseType(typeof(VendorQuotationDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<VendorQuotationDto>> AddQuotation(long rfqId, [FromBody] CreateQuotationRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _rfqs.AddQuotationAsync(rfqId, request, ct);
|
||||
return Created($"/api/v1/rfqs/{rfqId}/quotations/{dto.QuotationId}", dto);
|
||||
}
|
||||
|
||||
[HttpGet("{rfqId:long}/comparison")]
|
||||
[ProducesResponseType(typeof(RfqComparisonDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RfqComparisonDto>> Comparison(long rfqId, CancellationToken ct)
|
||||
=> Ok(await _rfqs.GetComparisonAsync(rfqId, ct));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary>
|
||||
[Route("api/v1/stock-adjustments")]
|
||||
public sealed class StockAdjustmentsController : ApiControllerBase
|
||||
{
|
||||
private readonly IAdjustmentService _adjustments;
|
||||
|
||||
public StockAdjustmentsController(IAdjustmentService adjustments) => _adjustments = adjustments;
|
||||
|
||||
/// <summary>Create + auto-post an adjustment (mandatory reason code; decrease FIFO-consumes).</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(AdjustmentDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<AdjustmentDto>> Create([FromBody] CreateAdjustmentRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _adjustments.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/stock-adjustments/{dto.AdjustmentId}", dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary>
|
||||
[Route("api/v1/stock")]
|
||||
public sealed class StockController : ApiControllerBase
|
||||
{
|
||||
private readonly IStockService _stock;
|
||||
private readonly IReorderService _reorder;
|
||||
|
||||
public StockController(IStockService stock, IReorderService reorder)
|
||||
{
|
||||
_stock = stock;
|
||||
_reorder = reorder;
|
||||
}
|
||||
|
||||
[HttpGet("on-hand")]
|
||||
[ProducesResponseType(typeof(StockOnHandDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<StockOnHandDto>> OnHand([FromQuery] long itemId, [FromQuery] long warehouseId, CancellationToken ct)
|
||||
=> Ok(await _stock.GetOnHandAsync(itemId, warehouseId, ct));
|
||||
|
||||
[HttpGet("ledger")]
|
||||
[ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<StockLedgerRowDto>>> Ledger(
|
||||
[FromQuery] long? itemId, [FromQuery] long? warehouseId,
|
||||
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _stock.GetLedgerAsync(itemId, warehouseId, from, to, query, ct));
|
||||
|
||||
[HttpGet("valuation")]
|
||||
[ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<StockValuationDto>> Valuation([FromQuery] long itemId, [FromQuery] long warehouseId, CancellationToken ct)
|
||||
=> Ok(await _stock.GetValuationAsync(itemId, warehouseId, ct));
|
||||
|
||||
/// <summary>Items at/below their reorder point (FR-STK-10), computed on read.</summary>
|
||||
[HttpGet("reorder-alerts")]
|
||||
[ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts(
|
||||
[FromQuery] long? warehouseId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _reorder.GetAlertsAsync(warehouseId, query, ct));
|
||||
|
||||
/// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary>
|
||||
[HttpPost("reorder-alerts/{itemId:long}/requisition")]
|
||||
[ProducesResponseType(typeof(RequisitionDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<RequisitionDto>> SuggestRequisition(
|
||||
long itemId, [FromQuery] long warehouseId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _reorder.CreateSuggestedRequisitionAsync(itemId, warehouseId, ct);
|
||||
return Created($"/api/v1/requisitions/{dto.RequisitionId}", dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-count endpoints (docs/11 §5.6).</summary>
|
||||
[Route("api/v1/stock-counts")]
|
||||
public sealed class StockCountsController : ApiControllerBase
|
||||
{
|
||||
private readonly ICountService _counts;
|
||||
|
||||
public StockCountsController(ICountService counts) => _counts = counts;
|
||||
|
||||
[HttpGet("{countId:long}")]
|
||||
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CountDto>> GetById(long countId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _counts.GetAsync(countId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>Create a count with system quantities snapshotted (immutable).</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CountDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<CountDto>> Create([FromBody] CreateCountRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _counts.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/stock-counts/{dto.CountId}", dto);
|
||||
}
|
||||
|
||||
/// <summary>Enter counted quantities; variance = counted − system.</summary>
|
||||
[HttpPut("{countId:long}/counts")]
|
||||
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<CountDto>> EnterCounts(long countId, [FromBody] EnterCountsRequest request, CancellationToken ct)
|
||||
=> Ok(await _counts.EnterCountsAsync(countId, request, ct));
|
||||
|
||||
/// <summary>Post: emit a variance adjustment and close the count.</summary>
|
||||
[HttpPost("{countId:long}/post")]
|
||||
[ProducesResponseType(typeof(CountPostResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<CountPostResultDto>> Post(long countId, CancellationToken ct)
|
||||
=> Ok(await _counts.PostAsync(countId, ct));
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary>
|
||||
[Route("api/v1/stock-transfers")]
|
||||
public sealed class StockTransfersController : ApiControllerBase
|
||||
{
|
||||
private readonly ITransferService _transfers;
|
||||
|
||||
public StockTransfersController(ITransferService transfers) => _transfers = transfers;
|
||||
|
||||
[HttpGet("{transferId:long}")]
|
||||
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TransferDto>> GetById(long transferId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _transfers.GetAsync(transferId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<TransferDto>> Create([FromBody] CreateTransferRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _transfers.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/stock-transfers/{dto.TransferId}", dto);
|
||||
}
|
||||
|
||||
/// <summary>Dispatch: consume source FIFO layers into in-transit. 409 STOCK_NEGATIVE_BLOCKED if short.</summary>
|
||||
[HttpPost("{transferId:long}/dispatch")]
|
||||
[ProducesResponseType(typeof(DispatchResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<DispatchResultDto>> Dispatch(long transferId, CancellationToken ct)
|
||||
=> Ok(await _transfers.DispatchAsync(transferId, ct));
|
||||
|
||||
/// <summary>Receive: create the destination layer at the inherited cost (cost-preserving).</summary>
|
||||
[HttpPost("{transferId:long}/receive")]
|
||||
[ProducesResponseType(typeof(ReceiveResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<ActionResult<ReceiveResultDto>> Receive(long transferId, [FromBody] ReceiveTransferRequest request, CancellationToken ct)
|
||||
=> Ok(await _transfers.ReceiveAsync(transferId, request, ct));
|
||||
}
|
||||
@@ -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,23 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable audit trail entry (FR-X-02) — the compensating control for the deferred
|
||||
/// RBAC (02-SECURITY AR-01/B.3). One row per create/update/delete of an audited
|
||||
/// entity, capturing who / when / what changed (old→new in <see cref="ChangeSet"/>).
|
||||
/// Written automatically by <c>ErpDbContext.SaveChangesAsync</c>. Append-only at the
|
||||
/// app level; DB-role revocation of UPDATE/DELETE is deferred hardening (B.3).
|
||||
/// Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
public class AuditLog
|
||||
{
|
||||
public long AuditId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string EntityType { get; set; } = string.Empty;
|
||||
public long EntityId { get; set; }
|
||||
public AuditAction Action { get; set; }
|
||||
/// <summary>JSON change set: field→value (create/delete) or field→{old,new} (update).</summary>
|
||||
public string ChangeSet { get; set; } = "{}";
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Batch/lot for a batch-tracked item (FR-GRN-04, FR-WH-03). Expiry drives FEFO
|
||||
/// picking of perishables. Model: docs/10 Part C.4.
|
||||
/// </summary>
|
||||
public class Batch
|
||||
{
|
||||
public long BatchId { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public string BatchNo { get; set; } = string.Empty;
|
||||
public DateOnly? ExpiryDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Goods Receipt Note header (FR-GRN-01/02). Raised against a PO or direct
|
||||
/// (<see cref="PoId"/> null). On confirm each line creates a FIFO layer and posts
|
||||
/// an inbound ledger entry. Mutable aggregate with an <see cref="RowVersion"/>
|
||||
/// concurrency token (docs/10 C.10). Model: docs/10 Part C.3.
|
||||
/// </summary>
|
||||
public class Grn
|
||||
{
|
||||
public long GrnId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public long? PoId { get; set; }
|
||||
public PurchaseOrder? PurchaseOrder { get; set; }
|
||||
|
||||
public long VendorId { get; set; }
|
||||
public Vendor? Vendor { get; set; }
|
||||
|
||||
public long WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
public GrnStatus Status { get; set; } = GrnStatus.Draft;
|
||||
|
||||
public long CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? PostedAt { get; set; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token.</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<GrnLine> Lines { get; set; } = new List<GrnLine>();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// GRN line (FR-GRN-04..08). <see cref="UnitCost"/> is the PO-derived cost for
|
||||
/// PO-based receipts (client cost ignored — 02-SECURITY C.3) or the entered cost
|
||||
/// for direct receipts. <see cref="ReceivedValue"/> = qty × unitCost.
|
||||
/// <see cref="HoldStatus"/> gates issuability. Model: docs/10 Part C.3.
|
||||
/// </summary>
|
||||
public class GrnLine
|
||||
{
|
||||
public long GrnLineId { get; set; }
|
||||
|
||||
public long GrnId { get; set; }
|
||||
public Grn? Grn { get; set; }
|
||||
|
||||
public long? PoLineId { get; set; }
|
||||
public PoLine? PoLine { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public long UomId { get; set; }
|
||||
public Uom? Uom { get; set; }
|
||||
|
||||
public long? BinId { get; set; }
|
||||
public Bin? Bin { get; set; }
|
||||
|
||||
public long? BatchId { get; set; }
|
||||
public Batch? Batch { get; set; }
|
||||
|
||||
public decimal Qty { get; set; }
|
||||
public decimal UnitCost { get; set; }
|
||||
public decimal ReceivedValue { get; set; }
|
||||
public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// GL-ready journal entry emitted per stock movement (FR-STK-13) — data only, no
|
||||
/// posting in Phase 1 (the Accounting module consumes these later). One row per
|
||||
/// ledger entry, referencing the same source document polymorphically. Account
|
||||
/// codes are Phase-1 placeholders until a chart of accounts exists.
|
||||
/// Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
public class JournalEntryStub
|
||||
{
|
||||
public long JournalId { get; set; }
|
||||
public string SourceDocType { get; set; } = string.Empty;
|
||||
public long SourceDocId { get; set; }
|
||||
public string DebitAccount { get; set; } = string.Empty;
|
||||
public string CreditAccount { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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,32 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase return header (FR-PROC-08) — returns received goods to a vendor,
|
||||
/// generating an outbound stock movement. Auto-posts with a mandatory reason code.
|
||||
/// Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
public class PurchaseReturn
|
||||
{
|
||||
public long ReturnId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public long VendorId { get; set; }
|
||||
public Vendor? Vendor { get; set; }
|
||||
|
||||
public long WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
public long ReasonCodeId { get; set; }
|
||||
public ReasonCode? ReasonCode { get; set; }
|
||||
|
||||
public ReturnStatus Status { get; set; } = ReturnStatus.Posted;
|
||||
|
||||
public long CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public ICollection<PurchaseReturnLine> Lines { get; set; } = new List<PurchaseReturnLine>();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase-return line (FR-PROC-08) referencing the original GRN line for
|
||||
/// traceability. <see cref="Qty"/> is in base UOM. Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
public class PurchaseReturnLine
|
||||
{
|
||||
public long ReturnLineId { get; set; }
|
||||
|
||||
public long ReturnId { get; set; }
|
||||
public PurchaseReturn? Return { get; set; }
|
||||
|
||||
public long? GrnLineId { get; set; }
|
||||
public GrnLine? GrnLine { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public decimal Qty { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Configurable reason code for adjustments, returns and count variances
|
||||
/// (FR-X-04). Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
public class ReasonCode
|
||||
{
|
||||
public long ReasonCodeId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public ReasonContext Context { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Serial number for a serial-tracked item across its lifecycle (FR-WH-04).
|
||||
/// Model: docs/10 Part C.4.
|
||||
/// </summary>
|
||||
public class Serial
|
||||
{
|
||||
public long SerialId { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public string SerialNo { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "InStock";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Stock adjustment header (FR-STK-07) — the highest-risk feature in the phase
|
||||
/// (02-SECURITY C.5). Auto-posts in Phase 1 with a mandatory reason code and user
|
||||
/// stamp. Mutable aggregate with an <see cref="RowVersion"/> token (docs/10 C.10).
|
||||
/// Model: docs/10 Part C.6.
|
||||
/// </summary>
|
||||
public class StockAdjustment
|
||||
{
|
||||
public long AdjustmentId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public long WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
public long ReasonCodeId { get; set; }
|
||||
public ReasonCode? ReasonCode { get; set; }
|
||||
|
||||
public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted;
|
||||
|
||||
public long CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<StockAdjustmentLine> Lines { get; set; } = new List<StockAdjustmentLine>();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Adjustment line (FR-STK-07). <see cref="QtyDelta"/> is a signed base-UOM
|
||||
/// quantity: negative consumes FIFO layers, positive creates a layer at last cost.
|
||||
/// Model: docs/10 Part C.6.
|
||||
/// </summary>
|
||||
public class StockAdjustmentLine
|
||||
{
|
||||
public long AdjLineId { get; set; }
|
||||
|
||||
public long AdjustmentId { get; set; }
|
||||
public StockAdjustment? Adjustment { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public long? BinId { get; set; }
|
||||
public long? BatchId { get; set; }
|
||||
public long? SerialId { get; set; }
|
||||
|
||||
public decimal QtyDelta { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Cycle/full physical count header (FR-STK-08). System quantities are snapshotted
|
||||
/// at creation and are immutable once opened (02-SECURITY C.7); posting emits a
|
||||
/// variance adjustment. Mutable aggregate with an <see cref="RowVersion"/> token.
|
||||
/// Model: docs/10 Part C.6.
|
||||
/// </summary>
|
||||
public class StockCount
|
||||
{
|
||||
public long CountId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public long WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
public CountType CountType { get; set; }
|
||||
public CountStatus Status { get; set; } = CountStatus.Draft;
|
||||
|
||||
public long CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<StockCountLine> Lines { get; set; } = new List<StockCountLine>();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Count line (FR-STK-08). <see cref="SystemQty"/> is the immutable snapshot;
|
||||
/// <see cref="Variance"/> = counted − system (in base UOM). Model: docs/10 Part C.6.
|
||||
/// </summary>
|
||||
public class StockCountLine
|
||||
{
|
||||
public long CountLineId { get; set; }
|
||||
|
||||
public long CountId { get; set; }
|
||||
public StockCount? Count { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public long? BinId { get; set; }
|
||||
|
||||
public decimal SystemQty { get; set; }
|
||||
public decimal? CountedQty { get; set; }
|
||||
public decimal? Variance { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// FIFO cost layer — a quantity received at a specific unit cost, consumed
|
||||
/// oldest-first (FR-STK-02). Keyed per item **per warehouse**; quantities and
|
||||
/// <see cref="UnitCost"/> are in the item's base UOM. Answers valuation
|
||||
/// ("what's on hand and at what cost"). Model: docs/10 Part C.5.
|
||||
/// </summary>
|
||||
public class StockLayer
|
||||
{
|
||||
public long LayerId { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public long WarehouseId { get; set; }
|
||||
public Warehouse? Warehouse { get; set; }
|
||||
|
||||
public long? BatchId { get; set; }
|
||||
public Batch? Batch { get; set; }
|
||||
|
||||
public long? SerialId { get; set; }
|
||||
public Serial? Serial { get; set; }
|
||||
|
||||
/// <summary>Originating GRN line — carries the inspection hold status for this stock.</summary>
|
||||
public long? GrnLineId { get; set; }
|
||||
public GrnLine? GrnLine { get; set; }
|
||||
|
||||
public decimal QtyReceived { get; set; }
|
||||
public decimal QtyRemaining { get; set; }
|
||||
public decimal UnitCost { get; set; }
|
||||
public DateTime ReceiptDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable, append-only stock ledger (FR-STK-01, FR-X-05). One row per costed
|
||||
/// movement; answers history ("what moved, when, by whom"). The originating
|
||||
/// document is referenced polymorphically via
|
||||
/// <see cref="SourceDocType"/>/<see cref="SourceDocId"/> (no hard FK per type) so
|
||||
/// new transaction types write here without a schema change. Model: docs/10 Part C.5.
|
||||
/// </summary>
|
||||
public class StockLedger
|
||||
{
|
||||
public long LedgerId { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public long WarehouseId { get; set; }
|
||||
public long? BinId { get; set; }
|
||||
public long? BatchId { get; set; }
|
||||
public long? SerialId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
|
||||
public Direction Direction { get; set; }
|
||||
public decimal QtyBase { get; set; }
|
||||
public decimal UnitCost { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public decimal RunningBalance { get; set; }
|
||||
|
||||
public string SourceDocType { get; set; } = string.Empty;
|
||||
public long SourceDocId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Inter-warehouse stock transfer header (FR-STK-05/06). Dispatch consumes source
|
||||
/// FIFO layers into in-transit; receive creates the destination layer at the
|
||||
/// inherited cost (cost-preserving). Mutable aggregate with an
|
||||
/// <see cref="RowVersion"/> token (docs/10 C.10). Model: docs/10 Part C.6.
|
||||
/// </summary>
|
||||
public class StockTransfer
|
||||
{
|
||||
public long TransferId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public long SrcWarehouseId { get; set; }
|
||||
public Warehouse? SrcWarehouse { get; set; }
|
||||
|
||||
public long DestWarehouseId { get; set; }
|
||||
public Warehouse? DestWarehouse { get; set; }
|
||||
|
||||
public TransferStatus Status { get; set; } = TransferStatus.Draft;
|
||||
|
||||
public long CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<StockTransferLine> Lines { get; set; } = new List<StockTransferLine>();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Transfer line (FR-STK-05/06). <see cref="Qty"/> is in base UOM.
|
||||
/// <para>
|
||||
/// Deviation note: <see cref="UnitCost"/> and <see cref="QtyReceived"/> extend
|
||||
/// docs/10 Part C.6's <c>STOCK_TRANSFER_LINE</c> to make the transfer
|
||||
/// cost-preserving: at dispatch the value-weighted cost of the consumed source
|
||||
/// layers is stored here, and receive recreates the destination layer at that cost
|
||||
/// (supports partial receive via <see cref="QtyReceived"/>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class StockTransferLine
|
||||
{
|
||||
public long TransferLineId { get; set; }
|
||||
|
||||
public long TransferId { get; set; }
|
||||
public StockTransfer? Transfer { get; set; }
|
||||
|
||||
public long ItemId { get; set; }
|
||||
public Item? Item { get; set; }
|
||||
|
||||
public long? SrcBinId { get; set; }
|
||||
public long? DestBinId { get; set; }
|
||||
public long? BatchId { get; set; }
|
||||
public long? SerialId { get; set; }
|
||||
|
||||
public decimal Qty { get; set; }
|
||||
|
||||
/// <summary>Value-weighted unit cost of the consumed source layers (set at dispatch).</summary>
|
||||
public decimal? UnitCost { get; set; }
|
||||
|
||||
/// <summary>Quantity already received at the destination (partial-receive support).</summary>
|
||||
public decimal QtyReceived { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Application user (FR-X-01) — a **local shadow/projection** of an AuthHex identity.
|
||||
/// The local <see cref="UserId"/> (long) is what every `createdBy`/`requestedBy`/
|
||||
/// audit/ledger FK references; <see cref="AuthUserId"/> maps it to the AuthHex
|
||||
/// <c>UserId</c> (GUID) and is JIT-provisioned on first authenticated request
|
||||
/// (docs/10 A.4/C.7). A seeded <c>system</c> user (id 1, null AuthUserId) is the
|
||||
/// fallback actor for unauthenticated/system operations. Model: docs/10 Part C.7.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>Seeded fallback actor for unauthenticated/system operations.</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;
|
||||
|
||||
/// <summary>AuthHex identity (token <c>UserId</c> GUID); null for the seeded system user.</summary>
|
||||
public Guid? AuthUserId { get; set; }
|
||||
}
|
||||
@@ -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,13 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Stock-adjustment lifecycle (docs/10 §B.8.1). Phase 1 auto-posts, so
|
||||
/// <see cref="PendingApproval"/> is reserved for the future threshold-approval
|
||||
/// workflow (FR-STK-07). Stored as a string.
|
||||
/// </summary>
|
||||
public enum AdjustmentStatus
|
||||
{
|
||||
Draft,
|
||||
PendingApproval,
|
||||
Posted
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Kind of mutation recorded in the audit trail (FR-X-02). Stored as a string.</summary>
|
||||
public enum AuditAction
|
||||
{
|
||||
Create,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Stock-count lifecycle (docs/11 §8; docs/10 §B.8.1). Stored as a string.</summary>
|
||||
public enum CountStatus
|
||||
{
|
||||
Draft,
|
||||
Counted,
|
||||
Posted
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Physical-count scope (docs/11 §8; FR-STK-08). Stored as a string.</summary>
|
||||
public enum CountType
|
||||
{
|
||||
Cycle,
|
||||
Full
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Stock-ledger movement direction (docs/11 §8). Stored as a string.</summary>
|
||||
public enum Direction
|
||||
{
|
||||
In,
|
||||
Out
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Goods-receipt-note lifecycle (docs/11 §8; docs/10 §B.8.1). Stored as a string.</summary>
|
||||
public enum GrnStatus
|
||||
{
|
||||
Draft,
|
||||
Confirmed,
|
||||
Closed
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Inspection-hold state of received stock (docs/11 §8; FR-GRN-05). <see cref="OnHold"/>
|
||||
/// stock is on-hand but not issuable until released (FR-WH-07). Stored as a string.
|
||||
/// </summary>
|
||||
public enum HoldStatus
|
||||
{
|
||||
Available,
|
||||
OnHold,
|
||||
Rejected
|
||||
}
|
||||
@@ -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,9 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Where a reason code applies (FR-X-04; docs/10 §B.8.3). Stored as a string.</summary>
|
||||
public enum ReasonContext
|
||||
{
|
||||
Adjustment,
|
||||
Return,
|
||||
Count
|
||||
}
|
||||
@@ -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>Purchase-return lifecycle (docs/11 §3.4). Auto-posts in Phase 1. Stored as a string.</summary>
|
||||
public enum ReturnStatus
|
||||
{
|
||||
Draft,
|
||||
Posted
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>Stock-transfer lifecycle (docs/11 §8; docs/10 §B.8.1). Stored as a string.</summary>
|
||||
public enum TransferStatus
|
||||
{
|
||||
Draft,
|
||||
InTransit,
|
||||
Received,
|
||||
Closed
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Audit;
|
||||
|
||||
/// <summary>An audit-trail entry (FR-X-02). <c>ChangeSet</c> is the stored JSON, inlined.</summary>
|
||||
public sealed record AuditLogDto(
|
||||
long AuditId, long UserId, string EntityType, long EntityId, AuditAction Action, JsonElement ChangeSet, DateTime CreatedAt);
|
||||
|
||||
/// <summary>A GL-ready journal stub emitted per stock movement (FR-STK-13).</summary>
|
||||
public sealed record JournalEntryStubDto(
|
||||
long JournalId, string SourceDocType, long SourceDocId, string DebitAccount, string CreditAccount, decimal Amount);
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Grn;
|
||||
|
||||
// Responses (docs/11 §4) --------------------------------------------------------
|
||||
|
||||
public sealed record GrnLineDto(
|
||||
long GrnLineId, long? PoLineId, long ItemId, long UomId, long? BinId,
|
||||
decimal Qty, decimal UnitCost, decimal ReceivedValue, HoldStatus HoldStatus, long? BatchId);
|
||||
|
||||
public sealed record GrnDto(
|
||||
long GrnId, string DocNo, long? PoId, long VendorId, long WarehouseId, GrnStatus Status,
|
||||
long CreatedBy, DateTime CreatedAt, DateTime? PostedAt, IReadOnlyList<GrnLineDto> Lines);
|
||||
|
||||
public sealed record CreatedLayerDto(
|
||||
long LayerId, long ItemId, long WarehouseId, long? BatchId,
|
||||
decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate);
|
||||
|
||||
public sealed record GrnConfirmResultDto(
|
||||
long GrnId, GrnStatus Status, DateTime PostedAt,
|
||||
IReadOnlyList<CreatedLayerDto> CreatedLayers, IReadOnlyList<long> LedgerRefs, PurchaseOrderStatus? PoStatus);
|
||||
|
||||
public sealed record ReleaseLineResultDto(long GrnLineId, HoldStatus HoldStatus);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class BatchInput
|
||||
{
|
||||
[Required, StringLength(50)] public string BatchNo { get; set; } = string.Empty;
|
||||
public DateOnly? ExpiryDate { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateGrnLineInput
|
||||
{
|
||||
/// <summary>Set for a PO-based receipt; cost is then derived from the PO line (02-SECURITY C.3).</summary>
|
||||
public long? PoLineId { get; set; }
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Required] public long UomId { get; set; }
|
||||
public long? BinId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
/// <summary>Used only for direct (no-PO) receipts; ignored when <see cref="PoLineId"/> is set.</summary>
|
||||
[Range(0, double.MaxValue)] public decimal UnitCost { get; set; }
|
||||
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
|
||||
public BatchInput? Batch { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateGrnRequest
|
||||
{
|
||||
/// <summary>PO to receive against; null for a direct/emergency receipt (FR-GRN-02).</summary>
|
||||
public long? PoId { get; set; }
|
||||
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
|
||||
public long? VendorId { get; set; }
|
||||
[Required] public long WarehouseId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ReleaseLineRequest
|
||||
{
|
||||
/// <summary><c>Release</c> makes the stock available; <c>Reject</c> removes it from on-hand.</summary>
|
||||
[Required, RegularExpression("Release|Reject")]
|
||||
public string Action { get; set; } = "Release";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Procurement;
|
||||
|
||||
// Responses (docs/11 §3.3) ------------------------------------------------------
|
||||
|
||||
public sealed record PoLineDto(
|
||||
long PoLineId, long ItemId, long UomId, long WarehouseId,
|
||||
decimal Qty, decimal UnitPrice, decimal Tax, decimal QtyReceived);
|
||||
|
||||
public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
|
||||
|
||||
public sealed record PurchaseOrderDto(
|
||||
long PoId, string DocNo, long VendorId, long? RequisitionId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, long CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
|
||||
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines);
|
||||
|
||||
public sealed record PurchaseOrderSummaryDto(
|
||||
long PoId, string DocNo, long VendorId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, DateTime CreatedAt, PoTotalsDto Totals);
|
||||
|
||||
// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
|
||||
|
||||
public sealed class CreatePoLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Required] public long UomId { get; set; }
|
||||
[Required] public long WarehouseId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal UnitPrice { get; set; }
|
||||
[Range(0, 1)] public decimal Tax { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreatePurchaseOrderRequest
|
||||
{
|
||||
[Required] public long VendorId { get; set; }
|
||||
public long? RequisitionId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class UpdatePurchaseOrderRequest
|
||||
{
|
||||
[Required] public long VendorId { get; set; }
|
||||
public long? RequisitionId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CancelPurchaseOrderRequest
|
||||
{
|
||||
[StringLength(500)] public string? Reason { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Procurement;
|
||||
|
||||
// Responses (docs/11 §3.4) ------------------------------------------------------
|
||||
|
||||
public sealed record PurchaseReturnLineDto(long ReturnLineId, long? GrnLineId, long ItemId, decimal Qty);
|
||||
|
||||
public sealed record PurchaseReturnDto(
|
||||
long ReturnId, string DocNo, long VendorId, long WarehouseId, long ReasonCodeId, ReturnStatus Status,
|
||||
long CreatedBy, IReadOnlyList<PurchaseReturnLineDto> Lines, IReadOnlyList<long> LedgerRefs);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class CreatePurchaseReturnLineInput
|
||||
{
|
||||
/// <summary>Original GRN line, for traceability against the receipt.</summary>
|
||||
public long? GrnLineId { get; set; }
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreatePurchaseReturnRequest
|
||||
{
|
||||
[Required] public long VendorId { get; set; }
|
||||
[Required] public long WarehouseId { get; set; }
|
||||
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error.</summary>
|
||||
public long? ReasonCodeId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePurchaseReturnLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Procurement;
|
||||
|
||||
// Responses (docs/11 §3.1) ------------------------------------------------------
|
||||
|
||||
public sealed record RequisitionLineDto(long ReqLineId, long ItemId, decimal Qty, DateOnly? RequiredBy);
|
||||
|
||||
public sealed record RequisitionDto(
|
||||
long RequisitionId, string DocNo, RequisitionStatus Status, long RequestedBy,
|
||||
DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines);
|
||||
|
||||
public sealed record RequisitionSummaryDto(
|
||||
long RequisitionId, string DocNo, RequisitionStatus Status, long RequestedBy, DateTime CreatedAt);
|
||||
|
||||
// Requests — server sets docNo, status, requestedBy (audit actor), timestamps ----
|
||||
|
||||
public sealed class CreateRequisitionLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
public DateOnly? RequiredBy { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateRequisitionRequest
|
||||
{
|
||||
[Required, MinLength(1)] public List<CreateRequisitionLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Procurement;
|
||||
|
||||
// Responses (docs/11 §3.2) ------------------------------------------------------
|
||||
|
||||
public sealed record RfqLineDto(long RfqLineId, long ItemId, decimal Qty);
|
||||
|
||||
public sealed record RfqDto(
|
||||
long RfqId, string DocNo, long RequisitionId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines);
|
||||
|
||||
public sealed record QuotationLineDto(long ItemId, decimal UnitPrice, int LeadDays);
|
||||
|
||||
public sealed record VendorQuotationDto(
|
||||
long QuotationId, long RfqId, long VendorId, IReadOnlyList<QuotationLineDto> Lines);
|
||||
|
||||
/// <summary>Per-item, per-vendor price matrix for <c>GET /rfqs/{id}/comparison</c>.</summary>
|
||||
public sealed record RfqComparisonCellDto(long VendorId, long QuotationId, decimal UnitPrice, int LeadDays);
|
||||
public sealed record RfqComparisonRowDto(long ItemId, decimal Qty, IReadOnlyList<RfqComparisonCellDto> Quotes);
|
||||
public sealed record RfqComparisonDto(long RfqId, IReadOnlyList<long> VendorIds, IReadOnlyList<RfqComparisonRowDto> Rows);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class CreateRfqLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateRfqRequest
|
||||
{
|
||||
[Required] public long RequisitionId { get; set; }
|
||||
/// <summary>Vendors the RFQ is issued to (validated for existence; quotations reference them).</summary>
|
||||
public List<long> VendorIds { get; set; } = new();
|
||||
[Required, MinLength(1)] public List<CreateRfqLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CreateQuotationLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal UnitPrice { get; set; }
|
||||
[Range(0, int.MaxValue)] public int LeadDays { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateQuotationRequest
|
||||
{
|
||||
[Required] public long VendorId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateQuotationLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Reference;
|
||||
|
||||
/// <summary>Reason code (docs/11 §6).</summary>
|
||||
public sealed record ReasonCodeDto(long ReasonCodeId, string Code, string Description, ReasonContext Context);
|
||||
|
||||
public sealed class CreateReasonCodeRequest
|
||||
{
|
||||
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
|
||||
[Required, StringLength(200)] public string Description { get; set; } = string.Empty;
|
||||
[Required, EnumDataType(typeof(ReasonContext))] public ReasonContext Context { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Stock;
|
||||
|
||||
// Responses (docs/11 §5.5) ------------------------------------------------------
|
||||
|
||||
public sealed record AdjustmentLineDto(long AdjLineId, long ItemId, long? BinId, long? BatchId, decimal QtyDelta);
|
||||
|
||||
public sealed record AdjustmentDto(
|
||||
long AdjustmentId, string DocNo, long WarehouseId, long ReasonCodeId, AdjustmentStatus Status,
|
||||
long CreatedBy, DateTime CreatedAt, IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<long> LedgerRefs);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class CreateAdjustmentLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
public long? BinId { get; set; }
|
||||
public long? BatchId { get; set; }
|
||||
/// <summary>Signed base-UOM delta: negative consumes FIFO layers, positive adds stock.</summary>
|
||||
public decimal QtyDelta { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateAdjustmentRequest
|
||||
{
|
||||
[Required] public long WarehouseId { get; set; }
|
||||
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary>
|
||||
public long? ReasonCodeId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Stock;
|
||||
|
||||
// Responses (docs/11 §5.6) ------------------------------------------------------
|
||||
|
||||
public sealed record CountLineDto(long CountLineId, long ItemId, long? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
|
||||
|
||||
public sealed record CountDto(
|
||||
long CountId, string DocNo, long WarehouseId, CountType CountType, CountStatus Status, IReadOnlyList<CountLineDto> Lines);
|
||||
|
||||
public sealed record CountPostResultDto(long CountId, CountStatus Status, long? AdjustmentId, IReadOnlyList<long> LedgerRefs);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class CreateCountRequest
|
||||
{
|
||||
[Required] public long WarehouseId { get; set; }
|
||||
[Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; }
|
||||
[Required, MinLength(1)] public List<long> ItemIds { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class EnterCountLineInput
|
||||
{
|
||||
[Required] public long CountLineId { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal CountedQty { get; set; }
|
||||
}
|
||||
|
||||
public sealed class EnterCountsRequest
|
||||
{
|
||||
[Required, MinLength(1)] public List<EnterCountLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ERPCore.Dtos.Stock;
|
||||
|
||||
/// <summary>An item at/below its reorder point (docs/11 §5.7; FR-STK-10). Computed on read.</summary>
|
||||
public sealed record ReorderAlertDto(
|
||||
long ItemId, long WarehouseId, decimal Available,
|
||||
decimal ReorderPoint, decimal ReorderQty, decimal SuggestedRequisitionQty);
|
||||
@@ -0,0 +1,22 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Stock;
|
||||
|
||||
/// <summary>Stock enquiry (docs/11 §5.1). available = onHand − onHold − reserved − inTransit(out).</summary>
|
||||
public sealed record StockOnHandDto(
|
||||
long ItemId, long WarehouseId, decimal OnHand, decimal Available,
|
||||
decimal OnHold, decimal InTransit, decimal Reserved, DateTime AsOf);
|
||||
|
||||
/// <summary>A stock-ledger row (docs/11 §5.2).</summary>
|
||||
public sealed record StockLedgerRowDto(
|
||||
long LedgerId, long ItemId, long WarehouseId, long? BinId, long? BatchId, long? SerialId,
|
||||
Direction Direction, decimal QtyBase, decimal UnitCost, decimal Value, decimal RunningBalance,
|
||||
string SourceDocType, long SourceDocId, long UserId, DateTime CreatedAt);
|
||||
|
||||
/// <summary>An open FIFO layer in a valuation (docs/11 §5.3).</summary>
|
||||
public sealed record StockValuationLayerDto(long LayerId, decimal QtyRemaining, decimal UnitCost, decimal Value, DateTime ReceiptDate);
|
||||
|
||||
/// <summary>Valuation of on-hand stock from open FIFO layers (docs/11 §5.3).</summary>
|
||||
public sealed record StockValuationDto(
|
||||
long ItemId, long WarehouseId, IReadOnlyList<StockValuationLayerDto> Layers,
|
||||
decimal TotalQty, decimal TotalValue, string Currency, string CostingMethod);
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Stock;
|
||||
|
||||
// Responses (docs/11 §5.4) ------------------------------------------------------
|
||||
|
||||
public sealed record TransferLineDto(
|
||||
long TransferLineId, long ItemId, long? SrcBinId, long? DestBinId, long? BatchId, decimal Qty, decimal QtyReceived);
|
||||
|
||||
public sealed record TransferDto(
|
||||
long TransferId, string DocNo, long SrcWarehouseId, long DestWarehouseId,
|
||||
TransferStatus Status, IReadOnlyList<TransferLineDto> Lines);
|
||||
|
||||
public sealed record ConsumedLayerDto(long LayerId, decimal QtyConsumed, decimal UnitCost);
|
||||
|
||||
public sealed record DispatchResultDto(
|
||||
long TransferId, TransferStatus Status, IReadOnlyList<ConsumedLayerDto> ConsumedLayers, IReadOnlyList<long> LedgerRefs);
|
||||
|
||||
public sealed record TransferCreatedLayerDto(long LayerId, long WarehouseId, decimal QtyReceived, decimal UnitCost);
|
||||
|
||||
public sealed record ReceiveResultDto(
|
||||
long TransferId, TransferStatus Status, IReadOnlyList<TransferCreatedLayerDto> CreatedLayers, IReadOnlyList<long> LedgerRefs);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class CreateTransferLineInput
|
||||
{
|
||||
[Required] public long ItemId { get; set; }
|
||||
public long? SrcBinId { get; set; }
|
||||
public long? DestBinId { get; set; }
|
||||
public long? BatchId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateTransferRequest
|
||||
{
|
||||
[Required] public long SrcWarehouseId { get; set; }
|
||||
[Required] public long DestWarehouseId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ReceiveTransferLineInput
|
||||
{
|
||||
[Required] public long TransferLineId { get; set; }
|
||||
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReceiveTransferRequest
|
||||
{
|
||||
[Required, MinLength(1)] public List<ReceiveTransferLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Claim type names emitted by the AuthHex IdP (see its <c>JwtTokenHelper</c>).
|
||||
/// AuthHex uses no standard <c>sub</c>/<c>nameid</c>; identity is the custom
|
||||
/// <see cref="UserId"/> (GUID). These are read verbatim (JWT bearer is configured
|
||||
/// with <c>MapInboundClaims = false</c>).
|
||||
/// </summary>
|
||||
public static class AuthHexClaims
|
||||
{
|
||||
public const string UserId = "UserId";
|
||||
public const string UserTypeId = "UserTypeId";
|
||||
public const string UserTypeCode = "UserTypeCode";
|
||||
public const string RoleId = "RoleId";
|
||||
public const string RoleCode = "RoleCode";
|
||||
public const string Nic = "NIC";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using ERPCore.Domain.Entities;
|
||||
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
@@ -28,4 +29,6 @@ public sealed class CurrentUser : ICurrentUser
|
||||
return string.IsNullOrWhiteSpace(sub) ? SystemActor : sub;
|
||||
}
|
||||
}
|
||||
|
||||
public long AuditUserId => long.TryParse(UserId, out var id) ? id : User.SystemUserId;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ public interface ICurrentUser
|
||||
/// <summary>The audit actor identity (token `sub`), or "system" when unauthenticated.</summary>
|
||||
string UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Numeric audit actor for stamping document <c>createdBy</c>/<c>requestedBy</c> FKs.
|
||||
/// Resolves the token <c>sub</c> to a user id; falls back to the seeded system
|
||||
/// user (<see cref="Entities.User.SystemUserId"/>) while auth is deferred (§6).
|
||||
/// </summary>
|
||||
long AuditUserId { get; }
|
||||
|
||||
/// <summary>True when the request carries an authenticated principal.</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
}
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// JWT bearer wiring. Authentication only — RBAC/authorization policies are
|
||||
/// deferred for Phase 1; the validated principal exists solely so that
|
||||
/// <see cref="ICurrentUser"/> can stamp the audit actor.
|
||||
/// Auth wiring for ERPCore as a **resource server** for the external AuthHex IdP
|
||||
/// (docs/10 A.4). Validates AuthHex's **RS256** tokens against AuthHex's RSA public
|
||||
/// key (configured statically — no JWKS), issuer <c>AuthHex</c>, audience
|
||||
/// <c>AuthHexClient</c>. A single door policy (<see cref="ErpAccessPolicy"/>) admits
|
||||
/// only ERP <c>UserType</c>/<c>Role</c> holders when those codes are configured;
|
||||
/// per-endpoint RBAC stays deferred. Identity → audit actor is resolved by
|
||||
/// <see cref="ShadowUserClaimsTransformation"/> + <see cref="ICurrentUser"/>.
|
||||
/// </summary>
|
||||
public static class JwtAuthExtensions
|
||||
{
|
||||
/// <summary>Authorization policy applied to every v1 controller (via ApiControllerBase).</summary>
|
||||
public const string ErpAccessPolicy = "ErpAccess";
|
||||
|
||||
public static IServiceCollection AddErpJwtAuth(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
var issuer = config["Jwt:Issuer"];
|
||||
var audience = config["Jwt:Audience"];
|
||||
var signingKey = config["Jwt:SigningKey"] ?? string.Empty;
|
||||
var issuer = config["Auth:Issuer"];
|
||||
var audience = config["Auth:Audience"];
|
||||
var publicKeyXml = config["Auth:RsaPublicKeyXml"]
|
||||
?? throw new InvalidOperationException("Auth:RsaPublicKeyXml (AuthHex RSA public key) is not configured.");
|
||||
var requiredUserType = config["Auth:RequiredUserTypeCode"];
|
||||
var requiredRole = config["Auth:RequiredRoleCode"];
|
||||
|
||||
// AuthHex publishes no JWKS; the RSA public key is configured statically.
|
||||
var rsa = RSA.Create();
|
||||
rsa.FromXmlString(publicKeyXml);
|
||||
var signingKey = new RsaSecurityKey(rsa);
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
// Keep AuthHex's claim names verbatim (UserId, UserTypeCode, RoleCode …).
|
||||
options.MapInboundClaims = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
@@ -28,12 +45,26 @@ public static class JwtAuthExtensions
|
||||
ValidAudience = audience,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
|
||||
IssuerSigningKey = signingKey,
|
||||
ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
|
||||
ClockSkew = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(ErpAccessPolicy, policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
// Door gate: only enforce a UserType/Role when configured (AuthHex is a
|
||||
// shared IdP). Empty config = require a valid ERP token only.
|
||||
if (!string.IsNullOrWhiteSpace(requiredUserType))
|
||||
policy.RequireClaim(AuthHexClaims.UserTypeCode, requiredUserType);
|
||||
if (!string.IsNullOrWhiteSpace(requiredRole))
|
||||
policy.RequireClaim(AuthHexClaims.RoleCode, requiredRole);
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Security.Claims;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Infra.Persistence;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Maps an authenticated AuthHex principal to ERPCore's local identity (docs/10 A.4/A.5).
|
||||
/// AuthHex tokens carry the user as a custom <c>UserId</c> (GUID) claim and no
|
||||
/// <c>sub</c>/<c>nameid</c>. This transformation JIT-provisions a local shadow
|
||||
/// <see cref="User"/> (keyed by <c>auth_user_id</c>) and injects the local
|
||||
/// <c>long</c> id as <see cref="ClaimTypes.NameIdentifier"/>, so
|
||||
/// <see cref="ICurrentUser"/>/<c>AuditUserId</c> resolve the real user unchanged.
|
||||
/// Idempotent — <see cref="IClaimsTransformation"/> may run several times per request.
|
||||
/// </summary>
|
||||
public sealed class ShadowUserClaimsTransformation : IClaimsTransformation
|
||||
{
|
||||
private readonly ErpDbContext _db;
|
||||
|
||||
public ShadowUserClaimsTransformation(ErpDbContext db) => _db = db;
|
||||
|
||||
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
if (principal.Identity is not ClaimsIdentity identity || !identity.IsAuthenticated)
|
||||
return principal;
|
||||
if (identity.HasClaim(c => c.Type == ClaimTypes.NameIdentifier))
|
||||
return principal; // already resolved this request
|
||||
|
||||
var raw = principal.FindFirstValue(AuthHexClaims.UserId);
|
||||
if (!Guid.TryParse(raw, out var authUserId))
|
||||
return principal; // no mappable identity → CurrentUser falls back to system
|
||||
|
||||
var nic = principal.FindFirstValue(AuthHexClaims.Nic);
|
||||
var localId = await ResolveOrProvisionAsync(authUserId, nic);
|
||||
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, localId.ToString()));
|
||||
return principal;
|
||||
}
|
||||
|
||||
private async Task<long> ResolveOrProvisionAsync(Guid authUserId, string? nic)
|
||||
{
|
||||
var existing = await _db.Users.AsNoTracking()
|
||||
.Where(u => u.AuthUserId == authUserId)
|
||||
.Select(u => u.UserId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existing != 0) return existing;
|
||||
|
||||
var label = string.IsNullOrWhiteSpace(nic) ? authUserId.ToString() : nic.Trim();
|
||||
var user = new User
|
||||
{
|
||||
AuthUserId = authUserId,
|
||||
Username = label,
|
||||
DisplayName = string.IsNullOrWhiteSpace(nic) ? "AuthHex User" : nic.Trim(),
|
||||
Status = EntityStatus.Active
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_db.Users.Add(user);
|
||||
await _db.SaveChangesAsync();
|
||||
return user.UserId;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// Lost a race (unique auth_user_id) — the row now exists; re-read it.
|
||||
_db.Entry(user).State = EntityState.Detached;
|
||||
return await _db.Users.AsNoTracking()
|
||||
.Where(u => u.AuthUserId == authUserId)
|
||||
.Select(u => u.UserId)
|
||||
.FirstAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Auditing;
|
||||
|
||||
/// <summary>A mutation captured before save, awaiting its (possibly generated) key.</summary>
|
||||
public sealed record PendingAudit(EntityEntry Entry, string EntityType, AuditAction Action, string ChangeSet, long CapturedId, bool IsAdded);
|
||||
|
||||
/// <summary>
|
||||
/// Builds audit-trail rows from the EF change tracker (FR-X-02). High-volume /
|
||||
/// derived / self-referential tables are excluded (the stock ledger is itself the
|
||||
/// stock movement audit). Change sets are captured <b>before</b> save so old→new is
|
||||
/// accurate; generated keys for inserts are read <b>after</b> save.
|
||||
/// </summary>
|
||||
public static class AuditScribe
|
||||
{
|
||||
private static readonly HashSet<Type> Excluded =
|
||||
[
|
||||
typeof(AuditLog), typeof(JournalEntryStub), typeof(NumberSequence),
|
||||
typeof(StockLedger), typeof(StockLayer),
|
||||
];
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
public static List<PendingAudit> Capture(ChangeTracker tracker)
|
||||
{
|
||||
var pending = new List<PendingAudit>();
|
||||
foreach (var entry in tracker.Entries())
|
||||
{
|
||||
if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted)) continue;
|
||||
if (Excluded.Contains(entry.Entity.GetType())) continue;
|
||||
|
||||
var action = entry.State switch
|
||||
{
|
||||
EntityState.Added => AuditAction.Create,
|
||||
EntityState.Deleted => AuditAction.Delete,
|
||||
_ => AuditAction.Update,
|
||||
};
|
||||
|
||||
var changeSet = BuildChangeSet(entry, action);
|
||||
if (action == AuditAction.Update && changeSet == "{}") continue; // only concurrency token touched, etc.
|
||||
|
||||
var isAdded = entry.State == EntityState.Added;
|
||||
pending.Add(new PendingAudit(entry, entry.Entity.GetType().Name, action, changeSet, isAdded ? 0 : ReadKey(entry), isAdded));
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
public static AuditLog ToLog(PendingAudit p, long userId, DateTime now) => new()
|
||||
{
|
||||
UserId = userId,
|
||||
EntityType = p.EntityType,
|
||||
EntityId = p.IsAdded ? ReadKey(p.Entry) : p.CapturedId,
|
||||
Action = p.Action,
|
||||
ChangeSet = p.ChangeSet,
|
||||
CreatedAt = now,
|
||||
};
|
||||
|
||||
private static long ReadKey(EntityEntry entry)
|
||||
{
|
||||
var pk = entry.Metadata.FindPrimaryKey();
|
||||
if (pk is null || pk.Properties.Count != 1) return 0;
|
||||
var value = entry.Property(pk.Properties[0].Name).CurrentValue;
|
||||
return value is null ? 0 : Convert.ToInt64(value);
|
||||
}
|
||||
|
||||
private static string BuildChangeSet(EntityEntry entry, AuditAction action)
|
||||
{
|
||||
var set = new Dictionary<string, object?>();
|
||||
foreach (var p in entry.Properties)
|
||||
{
|
||||
if (p.Metadata.IsPrimaryKey()) continue;
|
||||
if (p.Metadata.Name == nameof(Item.RowVersion)) continue;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case AuditAction.Create when p.CurrentValue is not null:
|
||||
set[p.Metadata.Name] = p.CurrentValue;
|
||||
break;
|
||||
case AuditAction.Delete:
|
||||
set[p.Metadata.Name] = p.OriginalValue;
|
||||
break;
|
||||
case AuditAction.Update when p.IsModified && !Equals(p.OriginalValue, p.CurrentValue):
|
||||
set[p.Metadata.Name] = new Dictionary<string, object?> { ["old"] = p.OriginalValue, ["new"] = p.CurrentValue };
|
||||
break;
|
||||
}
|
||||
}
|
||||
return JsonSerializer.Serialize(set, Json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AuditLog> builder)
|
||||
{
|
||||
builder.ToTable("audit_logs");
|
||||
builder.HasKey(a => a.AuditId);
|
||||
|
||||
builder.Property(a => a.EntityType).IsRequired().HasMaxLength(80);
|
||||
builder.Property(a => a.Action).HasConversion<string>().HasMaxLength(10).IsRequired();
|
||||
builder.Property(a => a.ChangeSet).IsRequired().HasColumnType("jsonb");
|
||||
builder.Property(a => a.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(a => a.UserId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(a => new { a.EntityType, a.EntityId });
|
||||
builder.HasIndex(a => a.CreatedAt);
|
||||
builder.HasIndex(a => a.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class JournalEntryStubConfiguration : IEntityTypeConfiguration<JournalEntryStub>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<JournalEntryStub> builder)
|
||||
{
|
||||
builder.ToTable("journal_entry_stubs");
|
||||
builder.HasKey(j => j.JournalId);
|
||||
|
||||
builder.Property(j => j.SourceDocType).IsRequired().HasMaxLength(10);
|
||||
builder.Property(j => j.DebitAccount).IsRequired().HasMaxLength(20);
|
||||
builder.Property(j => j.CreditAccount).IsRequired().HasMaxLength(20);
|
||||
builder.Property(j => j.Amount).HasPrecision(18, 4);
|
||||
|
||||
builder.HasIndex(j => new { j.SourceDocType, j.SourceDocId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class BatchConfiguration : IEntityTypeConfiguration<Batch>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Batch> builder)
|
||||
{
|
||||
builder.ToTable("batches");
|
||||
builder.HasKey(b => b.BatchId);
|
||||
|
||||
builder.Property(b => b.BatchNo).IsRequired().HasMaxLength(50);
|
||||
|
||||
builder.HasOne(b => b.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Batch number unique within an item.
|
||||
builder.HasIndex(b => new { b.ItemId, b.BatchNo }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SerialConfiguration : IEntityTypeConfiguration<Serial>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Serial> builder)
|
||||
{
|
||||
builder.ToTable("serials");
|
||||
builder.HasKey(s => s.SerialId);
|
||||
|
||||
builder.Property(s => s.SerialNo).IsRequired().HasMaxLength(100);
|
||||
builder.Property(s => s.Status).IsRequired().HasMaxLength(20);
|
||||
|
||||
builder.HasOne(s => s.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(s => new { s.ItemId, s.SerialNo }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class GrnConfiguration : IEntityTypeConfiguration<Grn>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Grn> builder)
|
||||
{
|
||||
builder.ToTable("grns");
|
||||
builder.HasKey(g => g.GrnId);
|
||||
|
||||
builder.Property(g => g.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(g => g.DocNo).IsUnique();
|
||||
|
||||
builder.Property(g => g.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(g => g.CreatedAt).IsRequired();
|
||||
builder.Property(g => g.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(g => g.Status);
|
||||
builder.HasIndex(g => g.PoId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GrnLineConfiguration : IEntityTypeConfiguration<GrnLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GrnLine> builder)
|
||||
{
|
||||
builder.ToTable("grn_lines");
|
||||
builder.HasKey(l => l.GrnLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
builder.Property(l => l.UnitCost).HasPrecision(18, 6);
|
||||
builder.Property(l => l.ReceivedValue).HasPrecision(18, 4);
|
||||
builder.Property(l => l.HoldStatus).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
|
||||
builder.HasOne(l => l.Grn).WithMany(g => g.Lines).HasForeignKey(l => l.GrnId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.PoLine).WithMany().HasForeignKey(l => l.PoLineId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Uom).WithMany().HasForeignKey(l => l.UomId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Bin).WithMany().HasForeignKey(l => l.BinId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Batch).WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class NumberSequenceConfiguration : IEntityTypeConfiguration<NumberSequence>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NumberSequence> builder)
|
||||
{
|
||||
builder.ToTable("number_sequences");
|
||||
builder.HasKey(s => s.SequenceId);
|
||||
|
||||
builder.Property(s => s.DocType).HasColumnName("doc_type").IsRequired().HasMaxLength(10);
|
||||
builder.Property(s => s.Year).HasColumnName("year").IsRequired();
|
||||
builder.Property(s => s.LastNumber).HasColumnName("last_number").IsRequired();
|
||||
|
||||
// One counter per (doc type, year); also the ON CONFLICT target for atomic issue.
|
||||
builder.HasIndex(s => new { s.DocType, s.Year }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class PurchaseOrderConfiguration : IEntityTypeConfiguration<PurchaseOrder>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PurchaseOrder> builder)
|
||||
{
|
||||
builder.ToTable("purchase_orders");
|
||||
builder.HasKey(p => p.PoId);
|
||||
|
||||
builder.Property(p => p.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(p => p.DocNo).IsUnique();
|
||||
|
||||
builder.Property(p => p.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(p => p.CreatedAt).IsRequired();
|
||||
|
||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||
builder.Property(p => p.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(p => p.Vendor)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.VendorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(p => p.Requisition)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.RequisitionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(p => p.Creator)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.CreatedBy)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(p => p.Status);
|
||||
builder.HasIndex(p => p.VendorId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PoLineConfiguration : IEntityTypeConfiguration<PoLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PoLine> builder)
|
||||
{
|
||||
builder.ToTable("po_lines");
|
||||
builder.HasKey(l => l.PoLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
builder.Property(l => l.UnitPrice).HasPrecision(18, 4);
|
||||
builder.Property(l => l.Tax).HasPrecision(9, 4);
|
||||
builder.Property(l => l.QtyReceived).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.PurchaseOrder)
|
||||
.WithMany(p => p.Lines)
|
||||
.HasForeignKey(l => l.PoId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(l => l.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(l => l.Uom)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.UomId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(l => l.Warehouse)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.WarehouseId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class PurchaseReturnConfiguration : IEntityTypeConfiguration<PurchaseReturn>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PurchaseReturn> builder)
|
||||
{
|
||||
builder.ToTable("purchase_returns");
|
||||
builder.HasKey(r => r.ReturnId);
|
||||
|
||||
builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(r => r.DocNo).IsUnique();
|
||||
|
||||
builder.Property(r => r.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
|
||||
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);
|
||||
builder.HasOne(r => r.ReasonCode).WithMany().HasForeignKey(r => r.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(r => r.Creator).WithMany().HasForeignKey(r => r.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PurchaseReturnLineConfiguration : IEntityTypeConfiguration<PurchaseReturnLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PurchaseReturnLine> builder)
|
||||
{
|
||||
builder.ToTable("purchase_return_lines");
|
||||
builder.HasKey(l => l.ReturnLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Return).WithMany(r => r.Lines).HasForeignKey(l => l.ReturnId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.GrnLine).WithMany().HasForeignKey(l => l.GrnLineId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class ReasonCodeConfiguration : IEntityTypeConfiguration<ReasonCode>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ReasonCode> builder)
|
||||
{
|
||||
builder.ToTable("reason_codes");
|
||||
builder.HasKey(r => r.ReasonCodeId);
|
||||
|
||||
builder.Property(r => r.Code).IsRequired().HasMaxLength(20);
|
||||
builder.Property(r => r.Description).IsRequired().HasMaxLength(200);
|
||||
builder.Property(r => r.Context).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
|
||||
builder.HasIndex(r => new { r.Context, r.Code }).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class RequisitionConfiguration : IEntityTypeConfiguration<Requisition>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Requisition> builder)
|
||||
{
|
||||
builder.ToTable("requisitions");
|
||||
builder.HasKey(r => r.RequisitionId);
|
||||
|
||||
builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(r => r.DocNo).IsUnique();
|
||||
|
||||
builder.Property(r => r.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(r => r.Requester)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.RequestedBy)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(r => r.Status);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RequisitionLineConfiguration : IEntityTypeConfiguration<RequisitionLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RequisitionLine> builder)
|
||||
{
|
||||
builder.ToTable("requisition_lines");
|
||||
builder.HasKey(l => l.ReqLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Requisition)
|
||||
.WithMany(r => r.Lines)
|
||||
.HasForeignKey(l => l.RequisitionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(l => l.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class RfqConfiguration : IEntityTypeConfiguration<Rfq>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Rfq> builder)
|
||||
{
|
||||
builder.ToTable("rfqs");
|
||||
builder.HasKey(r => r.RfqId);
|
||||
|
||||
builder.Property(r => r.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(r => r.DocNo).IsUnique();
|
||||
|
||||
builder.Property(r => r.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(r => r.Requisition)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.RequisitionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RfqLineConfiguration : IEntityTypeConfiguration<RfqLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RfqLine> builder)
|
||||
{
|
||||
builder.ToTable("rfq_lines");
|
||||
builder.HasKey(l => l.RfqLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Rfq)
|
||||
.WithMany(r => r.Lines)
|
||||
.HasForeignKey(l => l.RfqId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(l => l.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VendorQuotationConfiguration : IEntityTypeConfiguration<VendorQuotation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<VendorQuotation> builder)
|
||||
{
|
||||
builder.ToTable("vendor_quotations");
|
||||
builder.HasKey(q => q.QuotationId);
|
||||
|
||||
builder.Property(q => q.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne(q => q.Rfq)
|
||||
.WithMany(r => r.Quotations)
|
||||
.HasForeignKey(q => q.RfqId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(q => q.Vendor)
|
||||
.WithMany()
|
||||
.HasForeignKey(q => q.VendorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// One quotation per vendor per RFQ.
|
||||
builder.HasIndex(q => new { q.RfqId, q.VendorId }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class VendorQuotationLineConfiguration : IEntityTypeConfiguration<VendorQuotationLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<VendorQuotationLine> builder)
|
||||
{
|
||||
builder.ToTable("vendor_quotation_lines");
|
||||
builder.HasKey(l => l.QuotationLineId);
|
||||
|
||||
builder.Property(l => l.UnitPrice).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Quotation)
|
||||
.WithMany(q => q.Lines)
|
||||
.HasForeignKey(l => l.QuotationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(l => l.Item)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration<StockAdjustment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockAdjustment> builder)
|
||||
{
|
||||
builder.ToTable("stock_adjustments");
|
||||
builder.HasKey(a => a.AdjustmentId);
|
||||
|
||||
builder.Property(a => a.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(a => a.DocNo).IsUnique();
|
||||
|
||||
builder.Property(a => a.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(a => a.CreatedAt).IsRequired();
|
||||
builder.Property(a => a.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(a => a.Creator).WithMany().HasForeignKey(a => a.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StockAdjustmentLineConfiguration : IEntityTypeConfiguration<StockAdjustmentLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockAdjustmentLine> builder)
|
||||
{
|
||||
builder.ToTable("stock_adjustment_lines");
|
||||
builder.HasKey(l => l.AdjLineId);
|
||||
|
||||
builder.Property(l => l.QtyDelta).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Adjustment).WithMany(a => a.Lines).HasForeignKey(l => l.AdjustmentId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Bin>().WithMany().HasForeignKey(l => l.BinId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Batch>().WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Serial>().WithMany().HasForeignKey(l => l.SerialId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class StockLayerConfiguration : IEntityTypeConfiguration<StockLayer>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockLayer> builder)
|
||||
{
|
||||
builder.ToTable("stock_layers");
|
||||
builder.HasKey(l => l.LayerId);
|
||||
|
||||
builder.Property(l => l.QtyReceived).HasPrecision(18, 4);
|
||||
builder.Property(l => l.QtyRemaining).HasPrecision(18, 4);
|
||||
builder.Property(l => l.UnitCost).HasPrecision(18, 6);
|
||||
builder.Property(l => l.ReceiptDate).IsRequired();
|
||||
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Warehouse).WithMany().HasForeignKey(l => l.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Batch).WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.Serial).WithMany().HasForeignKey(l => l.SerialId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(l => l.GrnLine).WithMany().HasForeignKey(l => l.GrnLineId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// FIFO consumption orders by receipt date then layer id, scoped per item+warehouse.
|
||||
builder.HasIndex(l => new { l.ItemId, l.WarehouseId, l.ReceiptDate, l.LayerId });
|
||||
builder.HasIndex(l => l.GrnLineId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StockLedgerConfiguration : IEntityTypeConfiguration<StockLedger>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockLedger> builder)
|
||||
{
|
||||
// Append-only (FR-STK-01/FR-X-05): the app never updates/deletes ledger rows.
|
||||
// DB-level revocation of UPDATE/DELETE is a deferred hardening step (02-SECURITY B.3).
|
||||
builder.ToTable("stock_ledger");
|
||||
builder.HasKey(l => l.LedgerId);
|
||||
|
||||
builder.Property(l => l.Direction).HasConversion<string>().HasMaxLength(5).IsRequired();
|
||||
builder.Property(l => l.QtyBase).HasPrecision(18, 4);
|
||||
builder.Property(l => l.UnitCost).HasPrecision(18, 6);
|
||||
builder.Property(l => l.Value).HasPrecision(18, 4);
|
||||
builder.Property(l => l.RunningBalance).HasPrecision(18, 4);
|
||||
builder.Property(l => l.SourceDocType).IsRequired().HasMaxLength(10);
|
||||
builder.Property(l => l.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasOne<Item>().WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Warehouse>().WithMany().HasForeignKey(l => l.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<User>().WithMany().HasForeignKey(l => l.UserId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Bin>().WithMany().HasForeignKey(l => l.BinId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Batch>().WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Serial>().WithMany().HasForeignKey(l => l.SerialId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Time-series query paths (NFR-06) and polymorphic source tracing.
|
||||
builder.HasIndex(l => new { l.ItemId, l.WarehouseId, l.LedgerId });
|
||||
builder.HasIndex(l => new { l.ItemId, l.WarehouseId, l.CreatedAt });
|
||||
builder.HasIndex(l => new { l.SourceDocType, l.SourceDocId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class StockCountConfiguration : IEntityTypeConfiguration<StockCount>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockCount> builder)
|
||||
{
|
||||
builder.ToTable("stock_counts");
|
||||
builder.HasKey(c => c.CountId);
|
||||
|
||||
builder.Property(c => c.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(c => c.DocNo).IsUnique();
|
||||
|
||||
builder.Property(c => c.CountType).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(c => c.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(c => c.CreatedAt).IsRequired();
|
||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(c => c.Warehouse).WithMany().HasForeignKey(c => c.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(c => c.Creator).WithMany().HasForeignKey(c => c.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(c => c.Status);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StockCountLineConfiguration : IEntityTypeConfiguration<StockCountLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockCountLine> builder)
|
||||
{
|
||||
builder.ToTable("stock_count_lines");
|
||||
builder.HasKey(l => l.CountLineId);
|
||||
|
||||
builder.Property(l => l.SystemQty).HasPrecision(18, 4);
|
||||
builder.Property(l => l.CountedQty).HasPrecision(18, 4);
|
||||
builder.Property(l => l.Variance).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Count).WithMany(c => c.Lines).HasForeignKey(l => l.CountId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Bin>().WithMany().HasForeignKey(l => l.BinId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class StockTransferConfiguration : IEntityTypeConfiguration<StockTransfer>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockTransfer> builder)
|
||||
{
|
||||
builder.ToTable("stock_transfers");
|
||||
builder.HasKey(t => t.TransferId);
|
||||
|
||||
builder.Property(t => t.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(t => t.DocNo).IsUnique();
|
||||
|
||||
builder.Property(t => t.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(t => t.CreatedAt).IsRequired();
|
||||
builder.Property(t => t.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(t => t.SrcWarehouse).WithMany().HasForeignKey(t => t.SrcWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(t => t.DestWarehouse).WithMany().HasForeignKey(t => t.DestWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(t => t.Status);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StockTransferLineConfiguration : IEntityTypeConfiguration<StockTransferLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StockTransferLine> builder)
|
||||
{
|
||||
builder.ToTable("stock_transfer_lines");
|
||||
builder.HasKey(l => l.TransferLineId);
|
||||
|
||||
builder.Property(l => l.Qty).HasPrecision(18, 4);
|
||||
builder.Property(l => l.UnitCost).HasPrecision(18, 6);
|
||||
builder.Property(l => l.QtyReceived).HasPrecision(18, 4);
|
||||
|
||||
builder.HasOne(l => l.Transfer).WithMany(t => t.Lines).HasForeignKey(l => l.TransferId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(l => l.Item).WithMany().HasForeignKey(l => l.ItemId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Bin>().WithMany().HasForeignKey(l => l.SrcBinId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Bin>().WithMany().HasForeignKey(l => l.DestBinId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Batch>().WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne<Serial>().WithMany().HasForeignKey(l => l.SerialId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.ToTable("users");
|
||||
builder.HasKey(u => u.UserId);
|
||||
|
||||
builder.Property(u => u.Username).IsRequired().HasMaxLength(100);
|
||||
builder.HasIndex(u => u.Username).IsUnique();
|
||||
builder.Property(u => u.DisplayName).IsRequired().HasMaxLength(200);
|
||||
builder.Property(u => u.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
|
||||
// Maps the local shadow user to its AuthHex identity (unique; NULL for the
|
||||
// system user — Postgres allows multiple NULLs in a unique index).
|
||||
builder.Property(u => u.AuthUserId).HasColumnName("auth_user_id");
|
||||
builder.HasIndex(u => u.AuthUserId).IsUnique();
|
||||
|
||||
// Seeded fallback audit actor while auth is deferred (§6).
|
||||
builder.HasData(new User
|
||||
{
|
||||
UserId = User.SystemUserId,
|
||||
Username = "system",
|
||||
DisplayName = "System",
|
||||
Status = EntityStatus.Active
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Infra.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent startup seeding of configurable reference data (docs/10 §B.8.3).
|
||||
/// Reason codes are seeded at runtime (not via <c>HasData</c>) so the identity
|
||||
/// sequence advances normally and later admin <c>POST /reason-codes</c> calls
|
||||
/// cannot collide with seeded ids.
|
||||
/// </summary>
|
||||
public static class DataSeeder
|
||||
{
|
||||
private static readonly (string Code, string Description, ReasonContext Context)[] StandardReasonCodes =
|
||||
[
|
||||
("DMG", "Damage", ReasonContext.Adjustment),
|
||||
("THEFT", "Theft/Loss", ReasonContext.Adjustment),
|
||||
("VAR", "Count Variance", ReasonContext.Adjustment),
|
||||
("EXP", "Expiry Write-off", ReasonContext.Adjustment),
|
||||
("SYS", "System Correction", ReasonContext.Adjustment),
|
||||
("DEF", "Defective", ReasonContext.Return),
|
||||
("WRONG", "Wrong Item", ReasonContext.Return),
|
||||
("OVER", "Over-supply", ReasonContext.Return),
|
||||
("QREJ", "Quality Reject", ReasonContext.Return),
|
||||
];
|
||||
|
||||
public static async Task SeedAsync(ErpDbContext db, CancellationToken ct = default)
|
||||
{
|
||||
var existing = await db.ReasonCodes
|
||||
.Select(r => new { r.Context, r.Code })
|
||||
.ToListAsync(ct);
|
||||
var have = existing.Select(x => (x.Context, x.Code)).ToHashSet();
|
||||
|
||||
var toAdd = StandardReasonCodes
|
||||
.Where(r => !have.Contains((r.Context, r.Code)))
|
||||
.Select(r => new ReasonCode { Code = r.Code, Description = r.Description, Context = r.Context })
|
||||
.ToList();
|
||||
|
||||
if (toAdd.Count == 0) return;
|
||||
|
||||
db.ReasonCodes.AddRange(toAdd);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.Persistence.Auditing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Infra.Persistence;
|
||||
@@ -8,11 +10,15 @@ namespace ERPCore.Infra.Persistence;
|
||||
/// <see cref="IEntityTypeConfiguration{TEntity}"/> configurations are added under
|
||||
/// Domain/Entities and Infra/Persistence/Configurations as they are implemented.
|
||||
/// The authoritative schema lives in docs/10-BACKEND-PHASE1.md — do not invent it here.
|
||||
/// Every save writes an immutable audit trail (FR-X-02) via <see cref="AuditScribe"/>.
|
||||
/// </summary>
|
||||
public class ErpDbContext : DbContext
|
||||
{
|
||||
public ErpDbContext(DbContextOptions<ErpDbContext> options) : base(options)
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public ErpDbContext(DbContextOptions<ErpDbContext> options, ICurrentUser currentUser) : base(options)
|
||||
{
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
// --- Master Data (docs/10 Part C.1) ---
|
||||
@@ -25,6 +31,51 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<Bin> Bins => Set<Bin>();
|
||||
|
||||
// --- Cross-cutting (docs/10 Part C.7) ---
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<NumberSequence> NumberSequences => Set<NumberSequence>();
|
||||
|
||||
// --- Procurement (docs/10 Part C.2) ---
|
||||
public DbSet<Requisition> Requisitions => Set<Requisition>();
|
||||
public DbSet<RequisitionLine> RequisitionLines => Set<RequisitionLine>();
|
||||
public DbSet<Rfq> Rfqs => Set<Rfq>();
|
||||
public DbSet<RfqLine> RfqLines => Set<RfqLine>();
|
||||
public DbSet<VendorQuotation> VendorQuotations => Set<VendorQuotation>();
|
||||
public DbSet<VendorQuotationLine> VendorQuotationLines => Set<VendorQuotationLine>();
|
||||
public DbSet<PurchaseOrder> PurchaseOrders => Set<PurchaseOrder>();
|
||||
public DbSet<PoLine> PoLines => Set<PoLine>();
|
||||
|
||||
// --- Goods Receipt (docs/10 Part C.3) ---
|
||||
public DbSet<Grn> Grns => Set<Grn>();
|
||||
public DbSet<GrnLine> GrnLines => Set<GrnLine>();
|
||||
|
||||
// --- Batch / Serial (docs/10 Part C.4) ---
|
||||
public DbSet<Batch> Batches => Set<Batch>();
|
||||
public DbSet<Serial> Serials => Set<Serial>();
|
||||
|
||||
// --- Stock core: FIFO layers + immutable ledger (docs/10 Part C.5) ---
|
||||
public DbSet<StockLayer> StockLayers => Set<StockLayer>();
|
||||
public DbSet<StockLedger> StockLedger => Set<StockLedger>();
|
||||
|
||||
// --- Stock transactions (docs/10 Part C.6) ---
|
||||
public DbSet<StockTransfer> StockTransfers => Set<StockTransfer>();
|
||||
public DbSet<StockTransferLine> StockTransferLines => Set<StockTransferLine>();
|
||||
public DbSet<StockAdjustment> StockAdjustments => Set<StockAdjustment>();
|
||||
public DbSet<StockAdjustmentLine> StockAdjustmentLines => Set<StockAdjustmentLine>();
|
||||
public DbSet<StockCount> StockCounts => Set<StockCount>();
|
||||
public DbSet<StockCountLine> StockCountLines => Set<StockCountLine>();
|
||||
|
||||
// --- Purchase returns (docs/10 Part C.2) ---
|
||||
public DbSet<PurchaseReturn> PurchaseReturns => Set<PurchaseReturn>();
|
||||
public DbSet<PurchaseReturnLine> PurchaseReturnLines => Set<PurchaseReturnLine>();
|
||||
|
||||
// --- Reference data (docs/10 Part C.7) ---
|
||||
public DbSet<ReasonCode> ReasonCodes => Set<ReasonCode>();
|
||||
|
||||
// --- Cross-cutting: audit trail + GL-ready journal (docs/10 Part C.7) ---
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
public DbSet<JournalEntryStub> JournalEntryStubs => Set<JournalEntryStub>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@@ -33,4 +84,39 @@ public class ErpDbContext : DbContext
|
||||
// (Infra/Persistence/Configurations/*).
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ErpDbContext).Assembly);
|
||||
}
|
||||
|
||||
// Audit trail (FR-X-02): capture mutations before save (accurate old→new), then
|
||||
// write the log rows once inserts have their generated keys. A second base save
|
||||
// persists the logs without re-auditing them.
|
||||
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken ct = default)
|
||||
{
|
||||
var pending = AuditScribe.Capture(ChangeTracker);
|
||||
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
WriteAuditLogs(pending);
|
||||
await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
var pending = AuditScribe.Capture(ChangeTracker);
|
||||
var result = base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
if (pending.Count > 0)
|
||||
{
|
||||
WriteAuditLogs(pending);
|
||||
base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void WriteAuditLogs(IReadOnlyList<PendingAudit> pending)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var userId = _currentUser.AuditUserId;
|
||||
foreach (var p in pending)
|
||||
AuditLogs.Add(AuditScribe.ToLog(p, userId, now));
|
||||
}
|
||||
}
|
||||
|
||||
+987
@@ -0,0 +1,987 @@
|
||||
// <auto-generated />
|
||||
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.Infra.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ErpDbContext))]
|
||||
[Migration("20260710090753_AddProcurement")]
|
||||
partial class AddProcurement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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.Bin", b =>
|
||||
{
|
||||
b.Property<long>("BinId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("BinId"));
|
||||
|
||||
b.Property<string>("BinType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<long>("WarehouseId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("BinId");
|
||||
|
||||
b.HasIndex("WarehouseId", "Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("bins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.Property<long>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("CategoryId"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<long?>("ParentId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||
{
|
||||
b.Property<long>("ItemId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ItemId"));
|
||||
|
||||
b.Property<long>("BaseUomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CategoryId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("DefaultVendorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Sku")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.Property<string>("TaxClass")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("TrackingMode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("ItemId");
|
||||
|
||||
b.HasIndex("BaseUomId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("DefaultVendorId");
|
||||
|
||||
b.HasIndex("Sku")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||
{
|
||||
b.Property<long>("ReorderId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReorderId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("ReorderPoint")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("ReorderQty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("WarehouseId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("ReorderId");
|
||||
|
||||
b.HasIndex("WarehouseId");
|
||||
|
||||
b.HasIndex("ItemId", "WarehouseId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("item_reorders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b =>
|
||||
{
|
||||
b.Property<long>("SequenceId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceId"));
|
||||
|
||||
b.Property<string>("DocType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)")
|
||||
.HasColumnName("doc_type");
|
||||
|
||||
b.Property<long>("LastNumber")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("last_number");
|
||||
|
||||
b.Property<int>("Year")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("year");
|
||||
|
||||
b.HasKey("SequenceId");
|
||||
|
||||
b.HasIndex("DocType", "Year")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("number_sequences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b =>
|
||||
{
|
||||
b.Property<long>("PoLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("PoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("QtyReceived")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<decimal>("Tax")
|
||||
.HasPrecision(9, 4)
|
||||
.HasColumnType("numeric(9,4)");
|
||||
|
||||
b.Property<decimal>("UnitPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("UomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("WarehouseId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("PoLineId");
|
||||
|
||||
b.HasIndex("ItemId");
|
||||
|
||||
b.HasIndex("PoId");
|
||||
|
||||
b.HasIndex("UomId");
|
||||
|
||||
b.HasIndex("WarehouseId");
|
||||
|
||||
b.ToTable("po_lines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
|
||||
{
|
||||
b.Property<long>("PoId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("PoId"));
|
||||
|
||||
b.Property<bool>("ApprovalRequired")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long?>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("VendorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("PoId");
|
||||
|
||||
b.HasIndex("CreatedBy");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RequisitionId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("VendorId");
|
||||
|
||||
b.ToTable("purchase_orders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b =>
|
||||
{
|
||||
b.Property<long>("RequisitionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RequisitionId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequestedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("RequisitionId");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RequestedBy");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("requisitions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b =>
|
||||
{
|
||||
b.Property<long>("ReqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ReqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<DateOnly?>("RequiredBy")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<long>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("ReqLineId");
|
||||
|
||||
b.HasIndex("ItemId");
|
||||
|
||||
b.HasIndex("RequisitionId");
|
||||
|
||||
b.ToTable("requisition_lines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b =>
|
||||
{
|
||||
b.Property<long>("RfqId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DocNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<long>("RequisitionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("RfqId");
|
||||
|
||||
b.HasIndex("DocNo")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RequisitionId");
|
||||
|
||||
b.ToTable("rfqs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b =>
|
||||
{
|
||||
b.Property<long>("RfqLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("RfqLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("Qty")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.Property<long>("RfqId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("RfqLineId");
|
||||
|
||||
b.HasIndex("ItemId");
|
||||
|
||||
b.HasIndex("RfqId");
|
||||
|
||||
b.ToTable("rfq_lines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
|
||||
{
|
||||
b.Property<long>("UomId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UomId"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("UomId");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("uoms", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||
{
|
||||
b.Property<long>("ConversionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("ConversionId"));
|
||||
|
||||
b.Property<decimal>("Factor")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("numeric(18,6)");
|
||||
|
||||
b.Property<long>("FromUomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ToUomId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("ConversionId");
|
||||
|
||||
b.HasIndex("FromUomId");
|
||||
|
||||
b.HasIndex("ToUomId");
|
||||
|
||||
b.HasIndex("ItemId", "FromUomId", "ToUomId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("uom_conversions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<long>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("UserId"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("UserId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
UserId = 1L,
|
||||
DisplayName = "System",
|
||||
Status = "Active",
|
||||
Username = "system"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b =>
|
||||
{
|
||||
b.Property<long>("VendorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("VendorId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("character varying(3)")
|
||||
.HasDefaultValue("LKR");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.Property<string>("TaxReg")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Terms")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("VendorId");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("vendors", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
|
||||
{
|
||||
b.Property<long>("QuotationId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationId"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("RfqId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("VendorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("QuotationId");
|
||||
|
||||
b.HasIndex("VendorId");
|
||||
|
||||
b.HasIndex("RfqId", "VendorId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("vendor_quotations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b =>
|
||||
{
|
||||
b.Property<long>("QuotationLineId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("QuotationLineId"));
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("LeadDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("QuotationId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("UnitPrice")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("numeric(18,4)");
|
||||
|
||||
b.HasKey("QuotationLineId");
|
||||
|
||||
b.HasIndex("ItemId");
|
||||
|
||||
b.HasIndex("QuotationId");
|
||||
|
||||
b.ToTable("vendor_quotation_lines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||
{
|
||||
b.Property<long>("WarehouseId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("WarehouseId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("WarehouseId");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("warehouses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||
.WithMany("Bins")
|
||||
.HasForeignKey("WarehouseId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Warehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Category", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom")
|
||||
.WithMany()
|
||||
.HasForeignKey("BaseUomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor")
|
||||
.WithMany()
|
||||
.HasForeignKey("DefaultVendorId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("BaseUom");
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("DefaultVendor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany("ReorderSettings")
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||
.WithMany()
|
||||
.HasForeignKey("WarehouseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("Warehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder")
|
||||
.WithMany("Lines")
|
||||
.HasForeignKey("PoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Uom", "Uom")
|
||||
.WithMany()
|
||||
.HasForeignKey("UomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse")
|
||||
.WithMany()
|
||||
.HasForeignKey("WarehouseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("PurchaseOrder");
|
||||
|
||||
b.Navigation("Uom");
|
||||
|
||||
b.Navigation("Warehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.User", "Creator")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedBy")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition")
|
||||
.WithMany()
|
||||
.HasForeignKey("RequisitionId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor")
|
||||
.WithMany()
|
||||
.HasForeignKey("VendorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Creator");
|
||||
|
||||
b.Navigation("Requisition");
|
||||
|
||||
b.Navigation("Vendor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.User", "Requester")
|
||||
.WithMany()
|
||||
.HasForeignKey("RequestedBy")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Requester");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition")
|
||||
.WithMany("Lines")
|
||||
.HasForeignKey("RequisitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("Requisition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition")
|
||||
.WithMany()
|
||||
.HasForeignKey("RequisitionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Requisition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq")
|
||||
.WithMany("Lines")
|
||||
.HasForeignKey("RfqId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("Rfq");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
|
||||
.WithMany()
|
||||
.HasForeignKey("FromUomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany("UomConversions")
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Uom", "ToUom")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToUomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("FromUom");
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("ToUom");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq")
|
||||
.WithMany("Quotations")
|
||||
.HasForeignKey("RfqId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor")
|
||||
.WithMany()
|
||||
.HasForeignKey("VendorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Rfq");
|
||||
|
||||
b.Navigation("Vendor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.VendorQuotation", "Quotation")
|
||||
.WithMany("Lines")
|
||||
.HasForeignKey("QuotationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Item");
|
||||
|
||||
b.Navigation("Quotation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Item", b =>
|
||||
{
|
||||
b.Navigation("ReorderSettings");
|
||||
|
||||
b.Navigation("UomConversions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
|
||||
{
|
||||
b.Navigation("Lines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b =>
|
||||
{
|
||||
b.Navigation("Lines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b =>
|
||||
{
|
||||
b.Navigation("Lines");
|
||||
|
||||
b.Navigation("Quotations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
|
||||
{
|
||||
b.Navigation("Lines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b =>
|
||||
{
|
||||
b.Navigation("Bins");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcurement : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "number_sequences",
|
||||
columns: table => new
|
||||
{
|
||||
SequenceId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
doc_type = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
year = table.Column<int>(type: "integer", nullable: false),
|
||||
last_number = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_number_sequences", x => x.SequenceId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_users", x => x.UserId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "requisitions",
|
||||
columns: table => new
|
||||
{
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
RequestedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_requisitions", x => x.RequisitionId);
|
||||
table.ForeignKey(
|
||||
name: "FK_requisitions_users_RequestedBy",
|
||||
column: x => x.RequestedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "purchase_orders",
|
||||
columns: table => new
|
||||
{
|
||||
PoId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
ApprovalRequired = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_purchase_orders", x => x.PoId);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_orders_requisitions_RequisitionId",
|
||||
column: x => x.RequisitionId,
|
||||
principalTable: "requisitions",
|
||||
principalColumn: "RequisitionId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_orders_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_orders_vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "vendors",
|
||||
principalColumn: "VendorId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "requisition_lines",
|
||||
columns: table => new
|
||||
{
|
||||
ReqLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
RequiredBy = table.Column<DateOnly>(type: "date", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_requisition_lines", x => x.ReqLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_requisition_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_requisition_lines_requisitions_RequisitionId",
|
||||
column: x => x.RequisitionId,
|
||||
principalTable: "requisitions",
|
||||
principalColumn: "RequisitionId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "rfqs",
|
||||
columns: table => new
|
||||
{
|
||||
RfqId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
RequisitionId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_rfqs", x => x.RfqId);
|
||||
table.ForeignKey(
|
||||
name: "FK_rfqs_requisitions_RequisitionId",
|
||||
column: x => x.RequisitionId,
|
||||
principalTable: "requisitions",
|
||||
principalColumn: "RequisitionId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "po_lines",
|
||||
columns: table => new
|
||||
{
|
||||
PoLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
PoId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UomId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitPrice = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
Tax = table.Column<decimal>(type: "numeric(9,4)", precision: 9, scale: 4, nullable: false),
|
||||
QtyReceived = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_po_lines", x => x.PoLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_po_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_po_lines_purchase_orders_PoId",
|
||||
column: x => x.PoId,
|
||||
principalTable: "purchase_orders",
|
||||
principalColumn: "PoId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_po_lines_uoms_UomId",
|
||||
column: x => x.UomId,
|
||||
principalTable: "uoms",
|
||||
principalColumn: "UomId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_po_lines_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "rfq_lines",
|
||||
columns: table => new
|
||||
{
|
||||
RfqLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RfqId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_rfq_lines", x => x.RfqLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_rfq_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_rfq_lines_rfqs_RfqId",
|
||||
column: x => x.RfqId,
|
||||
principalTable: "rfqs",
|
||||
principalColumn: "RfqId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "vendor_quotations",
|
||||
columns: table => new
|
||||
{
|
||||
QuotationId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RfqId = table.Column<long>(type: "bigint", nullable: false),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_vendor_quotations", x => x.QuotationId);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotations_rfqs_RfqId",
|
||||
column: x => x.RfqId,
|
||||
principalTable: "rfqs",
|
||||
principalColumn: "RfqId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotations_vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "vendors",
|
||||
principalColumn: "VendorId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "vendor_quotation_lines",
|
||||
columns: table => new
|
||||
{
|
||||
QuotationLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
QuotationId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UnitPrice = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
LeadDays = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_vendor_quotation_lines", x => x.QuotationLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotation_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_vendor_quotation_lines_vendor_quotations_QuotationId",
|
||||
column: x => x.QuotationId,
|
||||
principalTable: "vendor_quotations",
|
||||
principalColumn: "QuotationId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "users",
|
||||
columns: new[] { "UserId", "DisplayName", "Status", "Username" },
|
||||
values: new object[] { 1L, "System", "Active", "system" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_number_sequences_doc_type_year",
|
||||
table: "number_sequences",
|
||||
columns: new[] { "doc_type", "year" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_ItemId",
|
||||
table: "po_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_PoId",
|
||||
table: "po_lines",
|
||||
column: "PoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_UomId",
|
||||
table: "po_lines",
|
||||
column: "UomId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_po_lines_WarehouseId",
|
||||
table: "po_lines",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_CreatedBy",
|
||||
table: "purchase_orders",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_DocNo",
|
||||
table: "purchase_orders",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_RequisitionId",
|
||||
table: "purchase_orders",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_Status",
|
||||
table: "purchase_orders",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_orders_VendorId",
|
||||
table: "purchase_orders",
|
||||
column: "VendorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisition_lines_ItemId",
|
||||
table: "requisition_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisition_lines_RequisitionId",
|
||||
table: "requisition_lines",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_DocNo",
|
||||
table: "requisitions",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_RequestedBy",
|
||||
table: "requisitions",
|
||||
column: "RequestedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_requisitions_Status",
|
||||
table: "requisitions",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfq_lines_ItemId",
|
||||
table: "rfq_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfq_lines_RfqId",
|
||||
table: "rfq_lines",
|
||||
column: "RfqId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfqs_DocNo",
|
||||
table: "rfqs",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_rfqs_RequisitionId",
|
||||
table: "rfqs",
|
||||
column: "RequisitionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_Username",
|
||||
table: "users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotation_lines_ItemId",
|
||||
table: "vendor_quotation_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotation_lines_QuotationId",
|
||||
table: "vendor_quotation_lines",
|
||||
column: "QuotationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotations_RfqId_VendorId",
|
||||
table: "vendor_quotations",
|
||||
columns: new[] { "RfqId", "VendorId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_vendor_quotations_VendorId",
|
||||
table: "vendor_quotations",
|
||||
column: "VendorId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "number_sequences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "po_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "requisition_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "rfq_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "vendor_quotation_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "purchase_orders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "vendor_quotations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "rfqs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "requisitions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1480
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,434 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStockAndGrn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "batches",
|
||||
columns: table => new
|
||||
{
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BatchNo = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
ExpiryDate = table.Column<DateOnly>(type: "date", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_batches", x => x.BatchId);
|
||||
table.ForeignKey(
|
||||
name: "FK_batches_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "grns",
|
||||
columns: table => new
|
||||
{
|
||||
GrnId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
PoId = table.Column<long>(type: "bigint", nullable: true),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
PostedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_grns", x => x.GrnId);
|
||||
table.ForeignKey(
|
||||
name: "FK_grns_purchase_orders_PoId",
|
||||
column: x => x.PoId,
|
||||
principalTable: "purchase_orders",
|
||||
principalColumn: "PoId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grns_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grns_vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "vendors",
|
||||
principalColumn: "VendorId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grns_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "serials",
|
||||
columns: table => new
|
||||
{
|
||||
SerialId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SerialNo = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_serials", x => x.SerialId);
|
||||
table.ForeignKey(
|
||||
name: "FK_serials_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "grn_lines",
|
||||
columns: table => new
|
||||
{
|
||||
GrnLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
GrnId = table.Column<long>(type: "bigint", nullable: false),
|
||||
PoLineId = table.Column<long>(type: "bigint", nullable: true),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UomId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitCost = table.Column<decimal>(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
ReceivedValue = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
HoldStatus = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_grn_lines", x => x.GrnLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_batches_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "batches",
|
||||
principalColumn: "BatchId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_bins_BinId",
|
||||
column: x => x.BinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_grns_GrnId",
|
||||
column: x => x.GrnId,
|
||||
principalTable: "grns",
|
||||
principalColumn: "GrnId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_po_lines_PoLineId",
|
||||
column: x => x.PoLineId,
|
||||
principalTable: "po_lines",
|
||||
principalColumn: "PoLineId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_grn_lines_uoms_UomId",
|
||||
column: x => x.UomId,
|
||||
principalTable: "uoms",
|
||||
principalColumn: "UomId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_ledger",
|
||||
columns: table => new
|
||||
{
|
||||
LedgerId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: true),
|
||||
SerialId = table.Column<long>(type: "bigint", nullable: true),
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Direction = table.Column<string>(type: "character varying(5)", maxLength: 5, nullable: false),
|
||||
QtyBase = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitCost = table.Column<decimal>(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
Value = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
RunningBalance = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
SourceDocType = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
SourceDocId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_ledger", x => x.LedgerId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_batches_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "batches",
|
||||
principalColumn: "BatchId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_bins_BinId",
|
||||
column: x => x.BinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_serials_SerialId",
|
||||
column: x => x.SerialId,
|
||||
principalTable: "serials",
|
||||
principalColumn: "SerialId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_ledger_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_layers",
|
||||
columns: table => new
|
||||
{
|
||||
LayerId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: true),
|
||||
SerialId = table.Column<long>(type: "bigint", nullable: true),
|
||||
GrnLineId = table.Column<long>(type: "bigint", nullable: true),
|
||||
QtyReceived = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
QtyRemaining = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitCost = table.Column<decimal>(type: "numeric(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
ReceiptDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_layers", x => x.LayerId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_layers_batches_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "batches",
|
||||
principalColumn: "BatchId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_layers_grn_lines_GrnLineId",
|
||||
column: x => x.GrnLineId,
|
||||
principalTable: "grn_lines",
|
||||
principalColumn: "GrnLineId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_layers_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_layers_serials_SerialId",
|
||||
column: x => x.SerialId,
|
||||
principalTable: "serials",
|
||||
principalColumn: "SerialId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_layers_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_batches_ItemId_BatchNo",
|
||||
table: "batches",
|
||||
columns: new[] { "ItemId", "BatchNo" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_BatchId",
|
||||
table: "grn_lines",
|
||||
column: "BatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_BinId",
|
||||
table: "grn_lines",
|
||||
column: "BinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_GrnId",
|
||||
table: "grn_lines",
|
||||
column: "GrnId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_ItemId",
|
||||
table: "grn_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_PoLineId",
|
||||
table: "grn_lines",
|
||||
column: "PoLineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grn_lines_UomId",
|
||||
table: "grn_lines",
|
||||
column: "UomId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_CreatedBy",
|
||||
table: "grns",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_DocNo",
|
||||
table: "grns",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_PoId",
|
||||
table: "grns",
|
||||
column: "PoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_Status",
|
||||
table: "grns",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_VendorId",
|
||||
table: "grns",
|
||||
column: "VendorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_grns_WarehouseId",
|
||||
table: "grns",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_serials_ItemId_SerialNo",
|
||||
table: "serials",
|
||||
columns: new[] { "ItemId", "SerialNo" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_layers_BatchId",
|
||||
table: "stock_layers",
|
||||
column: "BatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_layers_GrnLineId",
|
||||
table: "stock_layers",
|
||||
column: "GrnLineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_layers_ItemId_WarehouseId_ReceiptDate_LayerId",
|
||||
table: "stock_layers",
|
||||
columns: new[] { "ItemId", "WarehouseId", "ReceiptDate", "LayerId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_layers_SerialId",
|
||||
table: "stock_layers",
|
||||
column: "SerialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_layers_WarehouseId",
|
||||
table: "stock_layers",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_BatchId",
|
||||
table: "stock_ledger",
|
||||
column: "BatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_BinId",
|
||||
table: "stock_ledger",
|
||||
column: "BinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_ItemId_WarehouseId_CreatedAt",
|
||||
table: "stock_ledger",
|
||||
columns: new[] { "ItemId", "WarehouseId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_ItemId_WarehouseId_LedgerId",
|
||||
table: "stock_ledger",
|
||||
columns: new[] { "ItemId", "WarehouseId", "LedgerId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_SerialId",
|
||||
table: "stock_ledger",
|
||||
column: "SerialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_SourceDocType_SourceDocId",
|
||||
table: "stock_ledger",
|
||||
columns: new[] { "SourceDocType", "SourceDocId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_UserId",
|
||||
table: "stock_ledger",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_ledger_WarehouseId",
|
||||
table: "stock_ledger",
|
||||
column: "WarehouseId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_layers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_ledger");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "grn_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "serials");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "batches");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "grns");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1847
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStockTransactions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "reason_codes",
|
||||
columns: table => new
|
||||
{
|
||||
ReasonCodeId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Code = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Context = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_reason_codes", x => x.ReasonCodeId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_transfers",
|
||||
columns: table => new
|
||||
{
|
||||
TransferId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
SrcWarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DestWarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_transfers", x => x.TransferId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfers_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfers_warehouses_DestWarehouseId",
|
||||
column: x => x.DestWarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfers_warehouses_SrcWarehouseId",
|
||||
column: x => x.SrcWarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_adjustments",
|
||||
columns: table => new
|
||||
{
|
||||
AdjustmentId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ReasonCodeId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_adjustments", x => x.AdjustmentId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustments_reason_codes_ReasonCodeId",
|
||||
column: x => x.ReasonCodeId,
|
||||
principalTable: "reason_codes",
|
||||
principalColumn: "ReasonCodeId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustments_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustments_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_transfer_lines",
|
||||
columns: table => new
|
||||
{
|
||||
TransferLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
TransferId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SrcBinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
DestBinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: true),
|
||||
SerialId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
UnitCost = table.Column<decimal>(type: "numeric(18,6)", precision: 18, scale: 6, nullable: true),
|
||||
QtyReceived = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_transfer_lines", x => x.TransferLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_batches_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "batches",
|
||||
principalColumn: "BatchId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_bins_DestBinId",
|
||||
column: x => x.DestBinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_bins_SrcBinId",
|
||||
column: x => x.SrcBinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_serials_SerialId",
|
||||
column: x => x.SerialId,
|
||||
principalTable: "serials",
|
||||
principalColumn: "SerialId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_transfer_lines_stock_transfers_TransferId",
|
||||
column: x => x.TransferId,
|
||||
principalTable: "stock_transfers",
|
||||
principalColumn: "TransferId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_adjustment_lines",
|
||||
columns: table => new
|
||||
{
|
||||
AdjLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AdjustmentId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
BatchId = table.Column<long>(type: "bigint", nullable: true),
|
||||
SerialId = table.Column<long>(type: "bigint", nullable: true),
|
||||
QtyDelta = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_adjustment_lines", x => x.AdjLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustment_lines_batches_BatchId",
|
||||
column: x => x.BatchId,
|
||||
principalTable: "batches",
|
||||
principalColumn: "BatchId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustment_lines_bins_BinId",
|
||||
column: x => x.BinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustment_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustment_lines_serials_SerialId",
|
||||
column: x => x.SerialId,
|
||||
principalTable: "serials",
|
||||
principalColumn: "SerialId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_adjustment_lines_stock_adjustments_AdjustmentId",
|
||||
column: x => x.AdjustmentId,
|
||||
principalTable: "stock_adjustments",
|
||||
principalColumn: "AdjustmentId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_reason_codes_Context_Code",
|
||||
table: "reason_codes",
|
||||
columns: new[] { "Context", "Code" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustment_lines_AdjustmentId",
|
||||
table: "stock_adjustment_lines",
|
||||
column: "AdjustmentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustment_lines_BatchId",
|
||||
table: "stock_adjustment_lines",
|
||||
column: "BatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustment_lines_BinId",
|
||||
table: "stock_adjustment_lines",
|
||||
column: "BinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustment_lines_ItemId",
|
||||
table: "stock_adjustment_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustment_lines_SerialId",
|
||||
table: "stock_adjustment_lines",
|
||||
column: "SerialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustments_CreatedBy",
|
||||
table: "stock_adjustments",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustments_DocNo",
|
||||
table: "stock_adjustments",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustments_ReasonCodeId",
|
||||
table: "stock_adjustments",
|
||||
column: "ReasonCodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_adjustments_WarehouseId",
|
||||
table: "stock_adjustments",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_BatchId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "BatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_DestBinId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "DestBinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_ItemId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_SerialId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "SerialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_SrcBinId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "SrcBinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfer_lines_TransferId",
|
||||
table: "stock_transfer_lines",
|
||||
column: "TransferId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfers_CreatedBy",
|
||||
table: "stock_transfers",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfers_DestWarehouseId",
|
||||
table: "stock_transfers",
|
||||
column: "DestWarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfers_DocNo",
|
||||
table: "stock_transfers",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfers_SrcWarehouseId",
|
||||
table: "stock_transfers",
|
||||
column: "SrcWarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_transfers_Status",
|
||||
table: "stock_transfers",
|
||||
column: "Status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_adjustment_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_transfer_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_adjustments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_transfers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "reason_codes");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2134
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCountsAndReturns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "purchase_returns",
|
||||
columns: table => new
|
||||
{
|
||||
ReturnId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
VendorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ReasonCodeId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_purchase_returns", x => x.ReturnId);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_returns_reason_codes_ReasonCodeId",
|
||||
column: x => x.ReasonCodeId,
|
||||
principalTable: "reason_codes",
|
||||
principalColumn: "ReasonCodeId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_returns_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_returns_vendors_VendorId",
|
||||
column: x => x.VendorId,
|
||||
principalTable: "vendors",
|
||||
principalColumn: "VendorId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_returns_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_counts",
|
||||
columns: table => new
|
||||
{
|
||||
CountId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
WarehouseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CountType = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_counts", x => x.CountId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_counts_users_CreatedBy",
|
||||
column: x => x.CreatedBy,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_counts_warehouses_WarehouseId",
|
||||
column: x => x.WarehouseId,
|
||||
principalTable: "warehouses",
|
||||
principalColumn: "WarehouseId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "purchase_return_lines",
|
||||
columns: table => new
|
||||
{
|
||||
ReturnLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ReturnId = table.Column<long>(type: "bigint", nullable: false),
|
||||
GrnLineId = table.Column<long>(type: "bigint", nullable: true),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Qty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_purchase_return_lines", x => x.ReturnLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_return_lines_grn_lines_GrnLineId",
|
||||
column: x => x.GrnLineId,
|
||||
principalTable: "grn_lines",
|
||||
principalColumn: "GrnLineId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_return_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_purchase_return_lines_purchase_returns_ReturnId",
|
||||
column: x => x.ReturnId,
|
||||
principalTable: "purchase_returns",
|
||||
principalColumn: "ReturnId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "stock_count_lines",
|
||||
columns: table => new
|
||||
{
|
||||
CountLineId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CountId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BinId = table.Column<long>(type: "bigint", nullable: true),
|
||||
SystemQty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
CountedQty = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: true),
|
||||
Variance = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_stock_count_lines", x => x.CountLineId);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_count_lines_bins_BinId",
|
||||
column: x => x.BinId,
|
||||
principalTable: "bins",
|
||||
principalColumn: "BinId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_count_lines_items_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalTable: "items",
|
||||
principalColumn: "ItemId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_stock_count_lines_stock_counts_CountId",
|
||||
column: x => x.CountId,
|
||||
principalTable: "stock_counts",
|
||||
principalColumn: "CountId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_return_lines_GrnLineId",
|
||||
table: "purchase_return_lines",
|
||||
column: "GrnLineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_return_lines_ItemId",
|
||||
table: "purchase_return_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_return_lines_ReturnId",
|
||||
table: "purchase_return_lines",
|
||||
column: "ReturnId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_returns_CreatedBy",
|
||||
table: "purchase_returns",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_returns_DocNo",
|
||||
table: "purchase_returns",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_returns_ReasonCodeId",
|
||||
table: "purchase_returns",
|
||||
column: "ReasonCodeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_returns_VendorId",
|
||||
table: "purchase_returns",
|
||||
column: "VendorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_purchase_returns_WarehouseId",
|
||||
table: "purchase_returns",
|
||||
column: "WarehouseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_count_lines_BinId",
|
||||
table: "stock_count_lines",
|
||||
column: "BinId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_count_lines_CountId",
|
||||
table: "stock_count_lines",
|
||||
column: "CountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_count_lines_ItemId",
|
||||
table: "stock_count_lines",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_counts_CreatedBy",
|
||||
table: "stock_counts",
|
||||
column: "CreatedBy");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_counts_DocNo",
|
||||
table: "stock_counts",
|
||||
column: "DocNo",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_counts_Status",
|
||||
table: "stock_counts",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stock_counts_WarehouseId",
|
||||
table: "stock_counts",
|
||||
column: "WarehouseId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "purchase_return_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_count_lines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "purchase_returns");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "stock_counts");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2222
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAuditAndJournal : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "audit_logs",
|
||||
columns: table => new
|
||||
{
|
||||
AuditId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
EntityType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
EntityId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Action = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
ChangeSet = table.Column<string>(type: "jsonb", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_audit_logs", x => x.AuditId);
|
||||
table.ForeignKey(
|
||||
name: "FK_audit_logs_users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "journal_entry_stubs",
|
||||
columns: table => new
|
||||
{
|
||||
JournalId = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SourceDocType = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
SourceDocId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DebitAccount = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreditAccount = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Amount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_journal_entry_stubs", x => x.JournalId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_audit_logs_CreatedAt",
|
||||
table: "audit_logs",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_audit_logs_EntityType_EntityId",
|
||||
table: "audit_logs",
|
||||
columns: new[] { "EntityType", "EntityId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_audit_logs_UserId",
|
||||
table: "audit_logs",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_journal_entry_stubs_SourceDocType_SourceDocId",
|
||||
table: "journal_entry_stubs",
|
||||
columns: new[] { "SourceDocType", "SourceDocId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "audit_logs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "journal_entry_stubs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2229
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAuthUserId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "auth_user_id",
|
||||
table: "users",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "users",
|
||||
keyColumn: "UserId",
|
||||
keyValue: 1L,
|
||||
column: "auth_user_id",
|
||||
value: null);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_auth_user_id",
|
||||
table: "users",
|
||||
column: "auth_user_id",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_users_auth_user_id",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "auth_user_id",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user