Files
ERP-core/Backend/PROGRESS.md
T
ImanThiyanga 22f86451e3 feat: Add new services and interfaces for GRN, Purchase Return, Reason Code, Reorder, Stock, and Transfer functionalities
- Implemented IGrnService for managing goods receipts including retrieval, creation, and confirmation.
- Created IPurchaseReturnService for handling purchase return operations.
- Added IReasonCodeService for managing reason codes with listing and creation capabilities.
- Developed IReorderService for fetching reorder alerts and creating suggested requisitions.
- Introduced IStockMutator for applying stock changes and posting ledger entries.
- Established IStockService for stock inquiries, ledger retrieval, and valuation.
- Created ITransferService for managing inter-warehouse transfers including dispatch and receiving operations.
- Implemented PurchaseReturnService to handle purchase return logic and stock adjustments.
- Developed ReasonCodeService for listing and creating reason codes.
- Created ReorderService for fetching reorder alerts and generating requisitions.
- Implemented FifoCostingService for FIFO cost-layer management and ledger writing.
- Developed StockMutator for applying stock deltas and posting ledger entries.
- Created StockService for stock inquiries and ledger management.
- Implemented TransferService for managing inter-warehouse transfers with dispatch and receive functionalities.
2026-07-14 10:20:38 +05:30

123 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backend — PROGRESS (Phase 1: Inventory & Supply Chain)
Legend: `[ ]` not started · `[~]` in progress · `[x]` done
Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md` (API)
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
## 0. Bootstrap
- [x] Solution + Web API project (`net10.0`), packages restored (00-CORE §5.4)
- [x] Folder structure per 00-CORE §5.3
- [x] `ErpDbContext` + Npgsql wired; `InitialCreate` migration **created and applied** (2026-07-10, 8 master-data tables). `/health``Healthy`.
- [x] Serilog, JWT, Swagger, HealthChecks, ProblemDetails in `Program.cs` (JWT bearer *validated*; endpoints not yet `[Authorize]`-gated — see §6 auth note)
- [x] `IUnitOfWork` + `UnitOfWork` (transaction boundary)
- [x] Generic repository base + interfaces
- [x] `ICurrentUser` (audit stamp from token `sub`)
- [x] ProblemDetails middleware + domain exception → `code` mapping (System/Errors; full §7 catalog added to `ErrorCodes`)
## 1. Master Data
> Code complete for all items below (2026-07-09). **Live smoke test PASSED against Postgres (2026-07-10):** create/get/list/update/status/reorder/uom-conversions across all 5 controllers; ETag round-trip 200 / stale→412 / missing→428; SKU_DUPLICATE→400; bad reference→422; missing-field→400 ValidationProblemDetails; category `?tree=true` nesting; `pageSize=9999` clamped to 200; deactivate via PATCH status→204. Still `[~]` (not `[x]`) for **one** reason: the **security gate** (00-CORE §8) — the foundational auth control (02-SECURITY B.1) and the audit trail (B.3, the AR-01 compensating control) land in §6. Flip to `[x]` once §6 auth+audit are wired.
- [x] Item: entity + config + enums (ItemType, TrackingMode; EntityStatus added) — `xmin`/RowVersion concurrency token (Npgsql), unique SKU
- [~] Item: repository (generic) + service + controller (CRUD, narrow DTOs, ETag/If-Match→412, SKU_DUPLICATE, reference validation) — server-controlled fields excluded (02-SECURITY C.1)
- [~] UOM + UOM conversions (`GET/POST /uoms`, `PUT /items/{id}/uom-conversions` full-replace upsert)
- [~] Category (hierarchy, `GET /categories?tree=true` nested build, parent-exists validation)
- [~] Vendor (CRUD, ETag/If-Match, unique code, deactivate via `PATCH /vendors/{id}/status`)
- [~] Warehouse + Bin (`/warehouses`, nested `/warehouses/{id}/bins`, bin code unique per warehouse)
- [~] Item reorder settings (`PUT /items/{id}/reorder` full-replace upsert, warehouse-exists validation)
## 2. Procurement
> Requisition/RFQ/PO implemented 2026-07-10. **Live smoke test PASSED** (docNo `PR/RFQ/PO-2026-#####` gap-controlled + incrementing, requestedBy/createdBy = seeded system user, RFQ comparison matrix, duplicate quotation→409, PO auto-approve + server-computed totals matching the spec example `112100/20178/132278`, edit-while-open with recomputed totals + ETag 200/412, PO_NOT_EDITABLE→409 on cancelled, cancel→200, bad reference→422). Same `[~]` reason as §1: the §6 security gate (auth + audit) is not yet wired.
- [~] Requisition (+ lines) + submit (`POST /requisitions`, `/{id}/submit`, list, get)
- [~] RFQ + quotations + comparison (`POST /rfqs`, `/{id}/quotations` [one per vendor], `GET /{id}/comparison` matrix)
- [~] Purchase Order: create (auto-approve, `approvalRequired` flag), edit-while-open (If-Match), approve (no-op), cancel
- [~] Purchase Return (outbound movement, reason code) — `POST /purchase-returns` auto-posts an outbound FIFO consume via shared `StockMutator`; mandatory Return-context reason (`REASON_CODE_REQUIRED`→400, wrong context→422), references the GRN line for traceability, over-return→`409 STOCK_NEGATIVE_BLOCKED`. Verified. (Cumulative return-vs-received cap still relies on the stock-availability guard.)
> **Deviation (recorded):** `VendorQuotation` is modelled as header + `VendorQuotationLine` (per-item pricing) to satisfy the API contract (docs/11 §3.2); docs/10 Part C.2's scalar `VENDOR_QUOTATION(unit_price, lead_days)` with no item ref cannot represent it. Update the ER model doc to match.
> **RFQ `vendorIds`** are validated for existence but not persisted (no RFQ↔vendor link in the model); quotations reference vendors directly.
## 3. Goods Receipt
> Implemented + **smoke test PASSED** 2026-07-13 (see §4 note for the shared stock verification). Same `[~]` reason as §1/§2: the §6 auth+audit gate.
- [~] GRN create (against PO / direct), over-receipt tolerance — `unitCost` **PO-derived server-side** (client `999` verified ignored → PO price used, 02-SECURITY C.3); direct receipt requires `vendorId` + entered cost (AR-04); over-receipt → `422 OVER_RECEIPT_TOLERANCE` (verified at open-qty boundary); batch created/reused per (item, batchNo). Serial capture deferred.
- [~] GRN confirm → FIFO layer + ledger + PO `qtyReceived` (single UoW txn) — verified: layers+ledger posted, running balance, PO → PartiallyReceived/FullyReceived, UOM→base conversion (10 Box-12 → 120 base @10). **Idempotent** re-confirm verified (no double-post). Note: `Idempotency-Key` accepted but idempotency is resource-state based (already-Confirmed replays existing result); a keyed idempotency store is deferred.
- [~] Inspection hold release / reject — Release fully verified (OnHold excluded from `available`, then released). Reject removes on-hand + posts a reversing ledger entry; formal link to a Purchase Return is deferred (§3.4).
## 4. Stock Core
> Implemented + **live smoke test PASSED** 2026-07-13: receive→confirm creates FIFO layers + inbound ledger (qtyBase/unitCost/value/runningBalance correct), on-hand/valuation/ledger queries correct, OnHold excluded from `available`, UOM→base conversion applied. Same `[~]` gate (§6 auth+audit).
- [x] StockLayer + StockLedger entities/config — ledger **append-only at the app level** (never updated/deleted); DB-role `UPDATE`/`DELETE` revoke is deferred hardening (02-SECURITY B.3). Layers keyed per item **per warehouse**, base-UOM qty + unit cost; ledger polymorphic source (`sourceDocType`/`sourceDocId`), time-series indexes.
- [~] `FifoCostingService` — inbound layer + ledger posting + valuation **and oldest-first consume with row lock** (`SELECT … FOR UPDATE`, on-hold/expired exclusion, negative-stock block) all implemented + verified 2026-07-13 via §5. Blended cost on multi-layer consume verified (700@10 + 100@12 → 10.25).
- [~] Stock enquiry (onHand / available / onHold / inTransit) — onHand/available/onHold **and inTransit** now live + verified (inTransit = outstanding InTransit-transfer qty out of this warehouse). `reserved` stays a 0 stub until Sales.
- [x] Ledger query · Valuation query — `GET /stock/ledger` (item/warehouse/from/to + paging), `GET /stock/valuation` (open layers, totals, FIFO); both verified.
## 5. Stock Transactions
> **All four §5 features implemented + live smoke test PASSED 2026-07-13** (Adjustment, Transfer, Count, Reorder alerts); Purchase Return (§3.4) also done this pass. Same `[~]` gate (§6 auth+audit).
- [~] Transfer: create → dispatch (consume source FIFO row-locked → In-Transit) → receive (dest layer, **cost-preserving**) — verified: dispatch reduces source onHand + reports inTransit; receive creates dest layer at inherited cost (300 @12 → dest value 3600); `destWarehouseId != srcWarehouseId`→422; dispatch short→`409 STOCK_NEGATIVE_BLOCKED`. Partial receive supported (`QtyReceived`).
- [~] Adjustment (auto-post, mandatory reason code) — **highest-risk feature (02-SECURITY C.5)**: `REASON_CODE_REQUIRED`→400, non-Adjustment reason→422, decrease FIFO-consumes (blended cost, negative→409), increase creates a layer at last cost. All verified.
- [~] Count (cycle/full → enter counts → variance → post) — create snapshots systemQty (immutable), enter sets counted+variance→Counted, post emits a variance `StockAdjustment` via shared `StockMutator` + closes the count. Verified: variance 15 (post→on-hand 485) and +10 increase; re-post→409.
- [~] Reorder alerts (query) + suggest requisition — `GET /stock/reorder-alerts` (available ≤ ROP, computed on read) + `POST …/{itemId}/requisition` (draft PR at suggested qty). Verified.
syte
## 6. Cross-cutting
> **Status:** audit trail, doc numbering, reason codes, JournalEntryStub, negative-stock block all **done**. The **one** remaining item is authentication. **Auth-enforcement gap (open):** JWT bearer *validation* is wired, but no token issuer exists yet and controllers are **not** `[Authorize]`-gated, so endpoints are currently open. This is the AR-01/NFR-03 control surface — gate all v1 endpoints (fallback authorization policy) in the same change as `POST /auth/login`, then re-run the 02-SECURITY B.1 checklist and flip §1–§5 items to `[x]`. (The AR-01 **audit** compensating control is now in place.)
- [x] Audit log on every mutation (who/when/old→new) — `AuditLog` (jsonb `changeSet`), written by an `ErpDbContext.SaveChanges` override (`AuditScribe`): Create captures the field set, Update captures **only changed fields as {old,new}**, Delete captures the prior row; PK/RowVersion excluded; ledger/layer/seq/self/journal excluded. Actor from `ICurrentUser` (system=1 until auth). Read via `GET /audit-logs`. **Verified** (Item create+update old→new; StockAdjustment create). This is the **AR-01 compensating control** (02-SECURITY B.3) — app-level append-only; DB-role UPDATE/DELETE revoke still deferred.
- [x] Document numbering sequences (per type, per year) — `NumberSequence` + `NumberSequenceService` (atomic `INSERT … ON CONFLICT … RETURNING` inside the doc's UoW txn; gap-controlled). Verified issuing + incrementing PR/RFQ/PO.
- [~] Auth: simple in-app login → JWT (`POST /auth/login`) — foundation only: `User` table + seeded `system` user (id 1) exist and `ICurrentUser.AuditUserId` stamps docs; login endpoint + `[Authorize]` still pending.
- [x] JournalEntryStub emitted per stock movement (data only) — `JournalEntryStub` written in `FifoCostingService.PostLedgerAsync` for every ledger entry (In → Dr Inventory `1300` / Cr Clearing `2100`; Out reverses; amount = movement value). Placeholder accounts until a chart of accounts exists. Read via `GET /journal-entries`. **Verified** (GRN In 700, ADJ Out 70).
- [x] Negative-stock policy enforcement (default block) — enforced in `FifoCostingService.ConsumeAsync``409 STOCK_NEGATIVE_BLOCKED` (verified). Per-item override still a config stub.
- [~] FEFO picking for perishables; block expired / on-hold issue — **issue-block done + verified** (`ONHOLD_NOT_ISSUABLE`, `EXPIRED_BATCH_BLOCKED`; on-hold/expired layers excluded from consume). FEFO *pick ordering* (oldest-expiry first) not yet built.
- [x] Reason codes (FR-X-04) — `ReasonCode` entity + `GET/POST /reason-codes`; standard set (docs/10 §B.8.3) seeded idempotently at startup (`DataSeeder`). Verified.
## Deferred (Phase 2+ — do NOT build now, hooks only)
- [ ] Vendor invoice + three-way match
- [ ] Reservation/allocation fulfilment
- [ ] RBAC policy enforcement + approval workflow activation
## Done
<!-- move [x] items here with date + note if the active list grows long -->
### 2026-07-09 — Bootstrap verified + Master Data (§1) implemented
- Bootstrap scaffolding confirmed against 00-CORE §5 (solution, packages, `Program.cs` wiring, UoW, generic repo, `ICurrentUser`, ProblemDetails handler). Added enum-as-string JSON (`JsonStringEnumConverter`) and registered the 5 master-data services.
- Domain: 3 enums (`ItemType`, `TrackingMode`, `EntityStatus`) + 8 entities (Category, Uom, UomConversion, Item, ItemReorder, Vendor, Warehouse, Bin) with one `IEntityTypeConfiguration` each; FKs `Restrict` (masters deactivate, not cascade-delete), unique indexes (SKU, vendor/warehouse code, uom name, bin code per-warehouse), decimal precision, `xmin` concurrency token on Item/Vendor.
- API: 5 controllers, lowercase routes matching `docs/11 §2` exactly (verified via generated `swagger.json`). ETag/If-Match (428 if missing, 412 on mismatch), narrow request DTOs (no over-posting), `PagedResponse<T>` list envelope (§1.4), `PageQuery` with pageSize clamp ≤200 (B.6).
- Migration `InitialCreate` generated (`xmin` correctly produces no DDL — uses the PG system column).
- **Verified:** `dotnet build` clean (0 warn/0 err); app boots (`Now listening… Application started`); `/api/meta` 200; `swagger.json` 200 with all 13 master-data paths; DI resolves controller→service→repo→DbContext (a DB-backed call reaches Npgsql, failing only on creds).
- **Blocked / follow-ups:** (1) auth enforcement + audit trail — §6 (the remaining security gate for `[x]`); (2) no `DELETE` master endpoints — `MASTER_IN_USE` code reserved until transaction tables exist (deactivate-only per FR-MD-08); (3) minor: bad-enum bind error leaks the CLR type name in `detail` (02-SECURITY B.5) — fine in Dev, tidy before prod.
### 2026-07-10 — Migration applied + live smoke test PASSED
- `dotnet ef database update` applied `InitialCreate` to local Postgres; `/health``Healthy`.
- End-to-end curl smoke across all 5 controllers — all green: warehouse/bin create+list; uom create; category + child + `?tree=true` nesting; vendor create + PUT (If-Match 200 / stale 412 / missing 428); item create (201, referencing category/uom/vendor) + GET (ETag header) + list/filter `q` + `pageSize=9999`→clamped 200; reorder PUT; uom-conversions PUT; full item PUT with fresh ETag→200; PATCH status Inactive→204; duplicate SKU→400 `SKU_DUPLICATE`; bad reference→422; missing required→400 ValidationProblemDetails; bad enum→400. Concurrency token (`xmin`) confirmed incrementing per mutation.
- Note: local dev DB now holds smoke-test rows (warehouse/bin/uom×2/category×2/vendor/item, item left Inactive). Reset any time with `dotnet ef database drop -f && dotnet ef database update`.
### 2026-07-10 — Procurement (§2, minus returns) + cross-cutting foundations
- Cross-cutting: `User` entity (+ seeded `system` user via `HasData`), `ICurrentUser.AuditUserId` (numeric actor, system fallback), `NumberSequence` + `NumberSequenceService` (atomic per-type/per-year doc numbers issued inside the UoW txn).
- Procurement: enums (RequisitionStatus, RfqStatus, PurchaseOrderStatus); 8 entities (Requisition/Line, Rfq/Line, VendorQuotation/Line, PurchaseOrder/Line) + configs; DTOs; 3 services; 3 controllers (`/requisitions`, `/rfqs`, `/purchase-orders`). PO carries the `xmin` ETag token; totals computed server-side; create/edit wrapped in `IUnitOfWork.ExecuteInTransactionAsync` so the reserved doc number rolls back with the doc.
- Migration `AddProcurement` generated + applied (10 tables incl. users/number_sequences; system-user seed; PO `xmin` emits no DDL).
- **Verified:** build clean; app boots; full procurement smoke green (see §2 note) — requisition→submit, RFQ→quotation→comparison, PO create/get/edit/approve/cancel, numbering increment, error paths 409/412/422.
- **Deferred/next:** Purchase Return (needs GRN+stock), then §3 GRN, §4 Stock Core (FIFO/ledger), §5 stock transactions. Auth/audit (§6) still the gate for flipping §1/§2 to `[x]`.
- Housekeeping: an empty user-created migration `20260709124415_initial` sits between InitialCreate and AddProcurement (applied, harmless no-op; emits a cosmetic CS8981 lowercase-name warning).
### 2026-07-13 — Stock Core (§4) + Goods Receipt (§3, minus purchase return)
- Entities: Batch, Serial, StockLayer (FIFO), StockLedger (append-only), Grn/GrnLine + configs; enums Direction, HoldStatus, GrnStatus. DbSets + migration `AddStockAndGrn` (6 tables) applied.
- Services: `FifoCostingService` (Services/Stock — inbound layer, ledger post, on-hand, valuation), `StockService` (enquiry/ledger/valuation), `GrnService` (create with PO-derived cost + over-receipt + batch resolve + UOM→base; confirm in `ExecuteInTransactionAsync`; release/reject). Controllers `/stock`, `/grns`.
- **Verified against Postgres:** PO→GRN receive (client cost ignored, PO price used) → confirm → 2 FIFO layers + 2 ledger rows (running balance) → PO FullyReceived; on-hand 5000 / valuation 62500 / ledger In; **idempotent re-confirm** (on-hand stayed 5000); over-receipt `422` at the open-qty boundary, exact-fill `201`; batch receive **OnHold** → excluded from `available`**release** → available; **UOM conversion** 10×Box-12 → 120 base units @10 (value 1200).
- **Deferred (next):** §5 stock transactions — Transfer (dispatch/receive, in-transit, cost-preserving), Adjustment (auto-post + reason code), Count, Reorder alerts — which is where the FIFO **oldest-first consume + row lock** lands; then Purchase Return (§3.4) and GRN reject→return linkage. `ReasonCode` (§6) needed for adjustments/returns. Auth/audit (§6) remains the `[x]` gate.
### 2026-07-13 (2) — Stock Transactions (§5: Adjustment, Transfer, Reorder) + FIFO consume + Reason codes
- **FIFO consume engine** in `FifoCostingService.ConsumeAsync`: oldest-first, row-locked via `SELECT … FOR UPDATE` (raw SQL, no LINQ composed on top so the lock reaches PG; `{batchId}::bigint` cast avoids a 42P18 null-param error), excludes on-hold + expired layers, throws `STOCK_NEGATIVE_BLOCKED`/`ONHOLD_NOT_ISSUABLE`/`EXPIRED_BATCH_BLOCKED`, returns consumed segments for cost-preserving moves.
- Entities: ReasonCode, StockAdjustment/Line, StockTransfer/Line (+ `UnitCost`/`QtyReceived` extension on the transfer line for cost-preservation) + configs; enums TransferStatus, AdjustmentStatus, ReasonContext. Migration `AddStockTransactions` (5 tables) applied.
- Services/controllers: `ReasonCodeService` (`/reason-codes`, startup seed via `DataSeeder`), `AdjustmentService` (`/stock-adjustments`), `TransferService` (`/stock-transfers` create/dispatch/receive), `ReorderService` (`/stock/reorder-alerts` + suggest-requisition). `StockService.GetOnHandAsync` now computes real inTransit.
- **Verified against Postgres:** adjustment decrease FIFO-consume with blended cost 10.25 across two layers, negative→409, `REASON_CODE_REQUIRED`→400, wrong-context reason→422, increase-at-last-cost; transfer create→dispatch (source onHand↓, inTransit↑, consumedLayers)→receive (dest layer cost-preserved @12, value 3600), dest==src→422, dispatch-short→409; reorder alerts list + draft-PR suggestion; reason codes seeded (5 Adjustment + 4 Return).
- **Deferred (next):** §5 **Count** (create snapshot → enter counts → post variance via the Adjustment engine), **Purchase Return** (§3.4, + GRN reject→return linkage), FEFO pick ordering, `JournalEntryStub`. Auth/audit (§6) remains the `[x]` gate.
### 2026-07-13 (3) — Count (§5.6) + Purchase Return (§3.4) + shared StockMutator
- **`StockMutator`** (Services/Stock): shared signed-delta poster (negative → FIFO consume; positive → layer at last cost) + ledger, run inside the caller's txn. Adjustment/Count-post/Return all delegate to it → one code path for stock-affecting postings.
- Refactored `AdjustmentService` onto `StockMutator` and added an **intermediate `SaveChanges`** so the header id is flushed before ledger posting — fixes a latent bug where new-in-txn documents wrote `sourceDocId=0` (verified: `ADJ` ledger now `sourceDocId=5`). Also fixed `ledgerRefs:[0]` by mapping ledger ids **after** commit (Count + Return).
- Entities: StockCount/Line (+ CountType, CountStatus), PurchaseReturn/Line (+ ReturnStatus) + configs. Migration `AddCountsAndReturns` (4 tables) applied.
- Services/controllers: `CountService` (`/stock-counts` create/enter/post), `PurchaseReturnService` (`/purchase-returns`).
- **Verified against Postgres:** count snapshot 500 → counted 485 → post variance 15 (on-hand→485, adjustmentId+ledgerRefs), positive variance +10, re-post→409; return 100 outbound (on-hand→385), `REASON_CODE_REQUIRED`→400, non-Return reason→422, over-return→409 `STOCK_NEGATIVE_BLOCKED`.
- **§5 is now complete.** Remaining Phase-1 backend: §6 (auth/login + `[Authorize]`, audit trail, `JournalEntryStub`, negative-stock per-item override, FEFO pick ordering). Auth/audit is still the gate for flipping §1–§5 `[~]``[x]`.
### 2026-07-13 (4) — §6 audit trail + JournalEntryStub (auth deferred by request)
- **Audit trail (FR-X-02):** `AuditLog` (jsonb `changeSet`) written by an `ErpDbContext.SaveChanges/Async` override via `AuditScribe` — captures before save (accurate old→new), writes rows after inserts get their keys. Create = field set, Update = only changed fields `{old,new}`, Delete = prior row; excludes PK/RowVersion and the ledger/layer/sequence/journal/self tables. Actor from `ICurrentUser` (system=1). `ErpDbContext` now takes `ICurrentUser` (design-time migration still works via DI).
- **JournalEntryStub (FR-STK-13):** emitted for every ledger entry in `FifoCostingService.PostLedgerAsync` (In → Dr `1300`/Cr `2100`; Out reverses; amount = value). Placeholder GL accounts.
- Read endpoints (auditor role, beyond documented §11): `GET /audit-logs` (entityType/entityId/userId/from/to), `GET /journal-entries` (sourceDocType/sourceDocId). `AuditService`. Migration `AddAuditAndJournal` (2 tables, jsonb) applied.
- **Verified against Postgres:** item Create logged full field set (userId 1); item Update logged only `Name` + `UpdatedAt` as `{old,new}`; GRN confirm → journal In Dr1300/Cr2100 amount 700; adjustment decrease → journal Out Dr2100/Cr1300 amount 70; StockAdjustment Create audited.
- **Only auth remains for Phase 1.** Everything else in §6 is done. Auth (`POST /auth/login` + global `[Authorize]`) is intentionally deferred per request; wiring it is what flips §1–§5 `[~]``[x]`. FEFO pick-ordering left as a documented deferral (would conflict with FIFO-costing integrity without a physical/cost layer split); negative-stock stays the resolved global block (open-decision #2).