Files
ERP-core/Backend/PROGRESS.md
ImanThiyanga 67150425e4 Add migration to add auth_user_id column to users table
- Introduced a new column 'auth_user_id' of type UUID to the 'users' table.
- Updated existing user data to set 'auth_user_id' to null for UserId 1.
- Created a unique index on 'auth_user_id' to enforce uniqueness.
- Implemented rollback functionality to remove the column and index if needed.
2026-07-14 16:30:45 +05:30

26 KiB
Raw Permalink Blame History

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

  • Solution + Web API project (net10.0), packages restored (00-CORE §5.4)
  • Folder structure per 00-CORE §5.3
  • ErpDbContext + Npgsql wired; InitialCreate migration created and applied (2026-07-10, 8 master-data tables). /healthHealthy.
  • Serilog, JWT, Swagger, HealthChecks, ProblemDetails in Program.cs — JWT bearer now validates RS256 tokens from the external AuthHex IdP (issuer AuthHex / audience AuthHexClient / static RSA public key). v1 endpoints [Authorize]-gated via the ErpAccess door policy (§6); /health, /api/meta, Swagger stay anonymous.
  • IUnitOfWork + UnitOfWork (transaction boundary)
  • Generic repository base + interfaces
  • ICurrentUser (audit stamp from token identity claim nameid/sub) — with AuthHex the actor comes from the UserId GUID → local shadow user (nameid injected by the §6 provisioning step)
  • 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. Flipped [x] on 2026-07-14 — the §6 security gate (00-CORE §8: auth control 02-SECURITY B.1 + audit trail B.3) is now met (AuthHex RS256 validation + [Authorize] door policy + shadow-user provisioning, and the audit trail). The dated smoke-test notes in §2–§5 that reference a pending "§6 gate" are historical.

  • 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).

  • 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.
  • 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.

6. Cross-cutting

Status: COMPLETE. Audit trail, doc numbering, reason codes, JournalEntryStub, negative-stock block, and now authentication (external AuthHex IdP integration) are all done + verified. The §6 security gate (NFR-03 auth + AR-01 audit) is met — §1–§5 flipped [~][x] (2026-07-14). FEFO pick-ordering is the only intentional deferral.

  • 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.
  • 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: external AuthHex IdP integration (2026-07-14) — ERPCore is a resource server. JwtAuthExtensions validates RS256 against AuthHex's RSA public key (config Auth:RsaPublicKeyXmlRsaSecurityKey; MapInboundClaims=false), issuer AuthHex, audience AuthHexClient (no JWKS → static key). [Authorize(ErpAccess)] on ApiControllerBase gates every v1 endpoint; the ErpAccess policy RequireAuthenticatedUser + optional RequireClaim(UserTypeCode/RoleCode) from Auth:RequiredUserTypeCode/RequiredRoleCode (empty ⇒ any valid ERP token — AuthHex is ERP-dedicated). Shadow-user JIT provisioning: ShadowUserClaimsTransformation (IClaimsTransformation) maps the token's UserId GUID → a local users row (auth_user_id unique; Username/DisplayName = NIC), idempotent, and injects the local long id as nameid so ICurrentUser.AuditUserId resolves the real actor. Migration AddAuthUserId. Verified: no token→401; /health,/api/meta,Swagger anonymous; valid token→200; shadow user provisioned (User 2, Username=NIC, AuthUserId=GUID); item Create audited as the shadow user (id 2, not system); re-request reuses the same user; door gate → 403 on UserType mismatch, 200 on match.
  • 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).
  • Negative-stock policy enforcement (default block) — enforced in FifoCostingService.ConsumeAsync409 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.
  • 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

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; /healthHealthy.
  • 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 availablerelease → 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]superseded 2026-07-14, now external AuthHex IdP integration; see the next entry) 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).

2026-07-14 — Auth architecture change: external AuthHex IdP (docs-only pass)

  • Plan changed: auth is no longer a local POST /auth/login inside ERPCore. A separate AuthHex IdP (runs on :5011, source at c:\Users\WAS\Documents\Developments\ERP_Auth_Service\) owns login/registration/recovery; ERPCore becomes a resource server that only validates AuthHex tokens. Updated docs/10-BACKEND-PHASE1.md (header, A.4 auth/audit-actor, A.5 DI, B.2.3, FR-X-01, NFR-03, C.7 USER, C.9, B.8.4 decision #10) and this file. No code changed this pass.
  • Decisions (confirmed): (1) identity = shadow-user JIT provisioning — add auth_user_id GUID (unique) to users, keep all long FKs; (2) authorization = door-gate to an ERP UserType/Role, per-endpoint RBAC still deferred; (3) scope = docs only now, code integration is a follow-up.
  • Confirmed AuthHex facts: RS256 (RSA 2048; ERPCore needs the static public key — no JWKS), issuer AuthHex, audience AuthHexClient, lifetime 1000 min prod / 60 min dev; claims UserId(GUID)/UserTypeCode/RoleCode/NIC/jti/iat (no sub/nameid); BCrypt password hashing; login POST /api/loginUser {identifier,password}.
  • Open blockers (resolve before the code phase): exact ERP UserTypeCode/RoleCode for the door gate (must exist in AuthHex); RSA public-key distribution + rotation process (no JWKS); shadow-user Username/DisplayName source (token has no name); secrets hygiene in AuthHex config (private key/SMTP/DB in plaintext); docs/11 §2.0 still documents /auth/login (now AuthHex-owned) — recommend a follow-up annotation.

2026-07-14 (2) — Auth code integration: AuthHex resource server (§6 COMPLETE → §1–§5 flipped [x])

  • RS256 validation: JwtAuthExtensions rewritten — RsaSecurityKey from Auth:RsaPublicKeyXml (AuthHex public key), ValidIssuer=AuthHex, ValidAudience=AuthHexClient, ValidAlgorithms=[RS256], MapInboundClaims=false (keeps UserId/UserTypeCode/RoleCode verbatim). appsettings.json JwtAuth (public key + issuer/audience + RequiredUserTypeCode/RequiredRoleCode); removed the HS256 dev signing key.
  • Door policy ErpAccess: RequireAuthenticatedUser + optional RequireClaim(UserTypeCode/RoleCode) when configured (AuthHex is ERP-dedicated → empty default = any valid token). [Authorize(ErpAccess)] on ApiControllerBase; MetaController/health/Swagger stay anonymous.
  • Shadow-user provisioning: ShadowUserClaimsTransformation (IClaimsTransformation, scoped) maps token UserId GUID → local users row (auth_user_id unique, Username/DisplayName=NIC), idempotent w/ race-safe re-read, injects local long id as nameid. User.AuthUserId (Guid?) + AuthHexClaims consts + migration AddAuthUserId.
  • Verified (minted AuthHex-shaped RS256 token, signed with AuthHex's real private key): no token→401; /health,/api/meta,Swagger→200 anon; valid token→200; POST item→201 audited as shadow user id 2 (Username=NIC, AuthUserId=GUID), not system; repeat request reuses user (1 provision); door gate RequiredUserTypeCode=WAREHOUSE → ERP-type token 403, WAREHOUSE-type token 200. Build clean; migration applied.
  • §6 COMPLETE. Only intentional deferral left in Phase 1: FEFO pick-ordering (§6, [~]). Follow-ups: set the real ERP Auth:RequiredUserTypeCode/RoleCode for production; secure the RSA key rotation process.