# 11 · BACKEND — Phase 1 API Reference (Inventory & Supply Chain)

> **Authoritative for:** the REST API contract — every endpoint with complete request/response bodies, the error catalog, and enums.
> **Navigation:** you arrived from `00-CORE.md`. Business rules, architecture, and the entity model are in `10-BACKEND-PHASE1.md`. Frontend consumers follow `20-FRONTEND.md`. Record work in `Backend/PROGRESS.md`.
> **Consistency:** field names match the entity model in `10-BACKEND-PHASE1.md Part C`. This document should match the Swashbuckle-generated OpenAPI; an OpenAPI 3.1 YAML can be produced from it.

---

## 1. Conventions

### 1.1 Base URL & versioning
```
https://{host}/api/v1
```
Path-based versioning. Breaking changes bump the major version.

### 1.2 Authentication & authorization
```
Authorization: Bearer <access_token>
```
- Every endpoint requires a valid **Bearer JWT**; unauthenticated → `401`. Tokens are issued by the **external AuthHex IdP** (not ERPCore) — **RS256**, issuer `AuthHex`, audience `AuthHexClient`. ERPCore validates them against AuthHex's static RSA public key (no JWKS) and admits only holders of the configured ERP `UserType`/`Role` (door policy) → otherwise `403`.
- **Per-endpoint RBAC is NOT enforced in Phase 1** (FR-X-01): any ERP-admitted user may call any endpoint.
- The **audit actor** is AuthHex's custom **`UserId` (GUID)** claim, mapped to a local shadow user (`int`). Clients never send `createdBy`; the server derives it (docs/10 A.4).

### 1.3 Content type & encoding
`application/json`, UTF-8, **camelCase**. Timestamps ISO 8601 UTC (`2026-07-07T09:30:00Z`); dates `YYYY-MM-DD`. Base currency **LKR** in Phase 1.

### 1.4 List envelope
```json
{ "items": [ /* ... */ ],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 137, "totalPages": 7 } }
```

### 1.5 Pagination / filtering / sorting
`page` (1-based, default 1) · `pageSize` (default 20, max 200) · `sort` (`name` / `-createdAt`) · `q` (free text) · resource filters documented per endpoint.

### 1.6 Concurrency & idempotency
Mutable resources expose `ETag` (EF `RowVersion`); `PUT`/`PATCH` send `If-Match` → `412` on mismatch. Transactional POSTs (GRN confirm, transfer dispatch/receive, adjustment) accept an optional `Idempotency-Key` header.

### 1.7 Status codes
`200` read/update · `201` created (+`Location`) · `204` no content · `400` validation · `401` unauth · `404` not found · `409` domain conflict · `412` ETag mismatch · `422` semantically invalid.

### 1.8 Error format (RFC 7807)
```json
{ "type": "https://errors.erp.local/validation",
  "title": "One or more validation errors occurred.",
  "status": 400, "traceId": "00-6f1c...-01",
  "errors": { "sku": ["The sku field is required."], "lines": ["At least one line is required."] } }
```
Domain errors add a stable `code` (catalog §7):
```json
{ "type": "https://errors.erp.local/insufficient-stock",
  "title": "Insufficient stock to fulfil the issue.",
  "status": 409, "code": "STOCK_NEGATIVE_BLOCKED",
  "detail": "Available 4 < requested 10 for item ITM-1001 at WH-MAIN.", "traceId": "00-9a2f...-01" }
```

---

## 2. Master Data

### 2.0 Auth — **external (AuthHex IdP); not an ERPCore endpoint**
> **Superseded (2026-07-14).** ERPCore no longer exposes `/auth/login`. Login, registration and recovery are owned by the
> separate **AuthHex** service (e.g. `POST /api/loginUser` with `{ identifier, password }`), which returns an **RS256** JWT
> (issuer `AuthHex`, audience `AuthHexClient`; claims `UserId` (GUID), `UserTypeCode`, `RoleCode`, `NIC`, …). ERPCore only
> **validates** that Bearer token and provisions a local shadow user (docs/10 A.4). The old shape is retained here for history:
```json
POST {AuthHex}/api/loginUser   →   { "identifier": "…", "password": "••••••••" }
// 200 → an RS256 access token (Bearer). Bad credentials → 401. Token → ERPCore Authorization: Bearer <token>.
```

### 2.1 Items
#### `GET /items`
Query: `q`, `status` (`Active|Inactive`), `categoryId`, `trackingMode` (`None|Batch|Serial`), + paging.
**200 OK**
```json
{ "items": [ { "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
      "categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
      "itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD", "status": "Active" } ],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
```

#### `GET /items/{itemId}` → **200 OK** (header `ETag: "AAAAAAAAB9E="`)
```json
{ "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
  "description": "Grade 8.8 zinc-plated hex bolt", "categoryId": 12, "baseUomId": 1,
  "defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
  "status": "Active", "reorder": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 } ],
  "createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-07-01T10:15:00Z" }
```

#### `POST /items`
```json
{ "sku": "ITM-1002", "name": "Steel Nut M8", "description": "Grade 8 zinc-plated hex nut",
  "categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
  "itemType": "Stocked", "trackingMode": "None", "taxClass": "STD" }
```
**201 Created** — `Location: /api/v1/items/1002`
```json
{ "itemId": 1002, "sku": "ITM-1002", "name": "Steel Nut M8", "categoryId": 12, "baseUomId": 1,
  "defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "None", "taxClass": "STD",
  "status": "Active", "createdAt": "2026-07-07T09:30:00Z" }
```
`400` → `code: SKU_DUPLICATE` if SKU exists.

#### `PUT /items/{itemId}`
Full update; requires `If-Match`. → **200 OK** updated resource; `412` on ETag mismatch.

#### `PATCH /items/{itemId}/status`
```json
{ "status": "Inactive" }
```
**204 No Content**. Masters are deactivated, not hard-deleted (FR-MD-08); hard `DELETE` of a referenced master → `409 MASTER_IN_USE`.

#### `PUT /items/{itemId}/reorder`
```json
{ "settings": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 },
                { "warehouseId": 2, "reorderPoint": 100, "reorderQty": 400 } ] }
```
**200 OK** → persisted settings array.

### 2.2 Units of Measure
#### `GET /uoms` · `POST /uoms`
```json
{ "name": "Box-12" }
```
**201 Created** → `{ "uomId": 7, "name": "Box-12" }`

#### `PUT /items/{itemId}/uom-conversions`
```json
{ "conversions": [ { "fromUom": 7, "toUom": 1, "factor": 12 } ] }
```
**200 OK**
```json
{ "itemId": 1001, "baseUomId": 1,
  "conversions": [ { "conversionId": 33, "fromUom": 7, "toUom": 1, "factor": 12 } ] }
```

### 2.3 Categories
#### `POST /categories`
```json
{ "name": "Fasteners", "parentId": 3 }
```
**201 Created** → `{ "categoryId": 12, "name": "Fasteners", "parentId": 3 }`
`GET /categories?tree=true` returns a nested tree.

### 2.4 Vendors
#### `POST /vendors`
```json
{ "code": "VN-005", "name": "Lanka Steel Traders (Pvt) Ltd", "terms": "NET30",
  "taxReg": "134567890-7000", "currency": "LKR" }
```
**201 Created**
```json
{ "vendorId": 5, "code": "VN-005", "name": "Lanka Steel Traders (Pvt) Ltd", "terms": "NET30",
  "taxReg": "134567890-7000", "currency": "LKR", "status": "Active", "createdAt": "2026-07-07T09:31:00Z" }
```
`GET /vendors`, `GET /vendors/{id}`, `PUT /vendors/{id}`, `PATCH /vendors/{id}/status` follow the Item pattern.

### 2.5 Warehouses & Bins
#### `POST /warehouses`
```json
{ "code": "WH-MAIN", "name": "Main Warehouse - Negombo" }
```
**201 Created** → `{ "warehouseId": 1, "code": "WH-MAIN", "name": "Main Warehouse - Negombo" }`

#### `POST /warehouses/{warehouseId}/bins`
```json
{ "code": "A-01-01", "binType": "Shelf" }
```
**201 Created** → `{ "binId": 45, "warehouseId": 1, "code": "A-01-01", "binType": "Shelf" }`
`GET /warehouses/{warehouseId}/bins` lists bins.

---

## 3. Procurement

### 3.1 Requisitions
#### `POST /requisitions`
```json
{ "lines": [ { "itemId": 1001, "qty": 5000, "requiredBy": "2026-07-20" },
             { "itemId": 1002, "qty": 8000, "requiredBy": "2026-07-20" } ] }
```
**201 Created** (`requestedBy` from token)
```json
{ "requisitionId": 210, "docNo": "PR-2026-00210", "status": "Draft", "requestedBy": 17,
  "createdAt": "2026-07-07T09:35:00Z",
  "lines": [ { "reqLineId": 501, "itemId": 1001, "qty": 5000, "requiredBy": "2026-07-20" },
             { "reqLineId": 502, "itemId": 1002, "qty": 8000, "requiredBy": "2026-07-20" } ] }
```
`POST /requisitions/{id}/submit` → **200 OK** `status: "Submitted"`.

### 3.2 RFQs & Quotations
#### `POST /rfqs`
```json
{ "requisitionId": 210, "vendorIds": [5, 8, 11],
  "lines": [ { "itemId": 1001, "qty": 5000 }, { "itemId": 1002, "qty": 8000 } ] }
```
**201 Created**
```json
{ "rfqId": 88, "docNo": "RFQ-2026-00088", "requisitionId": 210, "status": "Open",
  "lines": [ { "rfqLineId": 701, "itemId": 1001, "qty": 5000 },
             { "rfqLineId": 702, "itemId": 1002, "qty": 8000 } ] }
```

#### `POST /rfqs/{rfqId}/quotations`
```json
{ "vendorId": 5, "lines": [ { "itemId": 1001, "unitPrice": 12.50, "leadDays": 7 },
                            { "itemId": 1002, "unitPrice": 6.20, "leadDays": 7 } ] }
```
**201 Created**
```json
{ "quotationId": 140, "rfqId": 88, "vendorId": 5,
  "lines": [ { "itemId": 1001, "unitPrice": 12.50, "leadDays": 7 },
             { "itemId": 1002, "unitPrice": 6.20, "leadDays": 7 } ] }
```
`GET /rfqs/{rfqId}/comparison` → vendor-by-line price matrix.

### 3.3 Purchase Orders
> **Phase 1:** `approvalRequired` defaults `false` → PO **auto-approved on creation**. Approve endpoint exists but is a no-op unless enabled (FR-PROC-04). PO **freely editable while open** (Option B, FR-PROC-05).

#### `POST /purchase-orders`
```json
{ "vendorId": 5, "requisitionId": 210,
  "lines": [ { "itemId": 1001, "uomId": 1, "warehouseId": 1, "qty": 5000, "unitPrice": 12.50, "tax": 0.18 },
             { "itemId": 1002, "uomId": 1, "warehouseId": 1, "qty": 8000, "unitPrice": 6.20, "tax": 0.18 } ] }
```
**201 Created** — `Location: /api/v1/purchase-orders/342`
```json
{ "poId": 342, "docNo": "PO-2026-00342", "vendorId": 5, "requisitionId": 210,
  "status": "Approved", "approvalRequired": false, "createdBy": 17, "createdAt": "2026-07-07T09:40:00Z",
  "totals": { "subTotal": 112100.00, "tax": 20178.00, "grandTotal": 132278.00, "currency": "LKR" },
  "lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "warehouseId": 1, "qty": 5000, "unitPrice": 12.50, "tax": 0.18, "qtyReceived": 0 },
             { "poLineId": 901, "itemId": 1002, "uomId": 1, "warehouseId": 1, "qty": 8000, "unitPrice": 6.20, "tax": 0.18, "qtyReceived": 0 } ] }
```

`GET /purchase-orders?status=Approved&vendorId=5` → list envelope of PO summaries.

#### `PUT /purchase-orders/{poId}`
Edit while open (not FullyReceived/Closed/Cancelled); requires `If-Match`. → **200 OK** updated resource; `409 PO_NOT_EDITABLE` if closed.

#### `POST /purchase-orders/{poId}/approve` → **200 OK** (no-op in Phase 1; transitions PendingApproval→Approved when enabled).

#### `POST /purchase-orders/{poId}/cancel`
```json
{ "reason": "Duplicate order" }
```
**200 OK** `status: "Cancelled"`; `409` if any receipt exists.

### 3.4 Purchase Returns
#### `POST /purchase-returns`
```json
{ "vendorId": 5, "warehouseId": 1, "reasonCodeId": 22,
  "lines": [ { "grnLineId": 1300, "itemId": 1001, "qty": 200 } ] }
```
**201 Created**
```json
{ "returnId": 61, "docNo": "PRET-2026-00061", "vendorId": 5, "warehouseId": 1, "reasonCodeId": 22,
  "status": "Posted", "createdBy": 17,
  "lines": [ { "returnLineId": 120, "grnLineId": 1300, "itemId": 1001, "qty": 200 } ],
  "ledgerRefs": [ 55021 ] }
```
`409 STOCK_NEGATIVE_BLOCKED` if return qty exceeds available.

---

## 4. Goods Receipt (GRN)
> On **confirm**, each line creates a **FIFO cost layer** and posts an **inbound ledger** entry (FR-GRN-06). Goods may land `holdStatus: "OnHold"` (not issuable) until released.

### 4.1 `POST /grns`
Against a PO (lines default from open PO lines) or direct (`poId: null`, by permission).
```json
{ "poId": 342, "warehouseId": 1,
  "lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45, "qty": 5000,
      "unitCost": 12.50, "holdStatus": "OnHold",
      "batch": { "batchNo": "B-2607", "expiryDate": "2028-07-01" } } ] }
```
**201 Created** — status `Draft`
```json
{ "grnId": 780, "docNo": "GRN-2026-00780", "poId": 342, "vendorId": 5, "warehouseId": 1,
  "status": "Draft", "createdBy": 17,
  "lines": [ { "grnLineId": 1300, "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45,
      "qty": 5000, "unitCost": 12.50, "receivedValue": 62500.00, "holdStatus": "OnHold", "batchId": 410 } ] }
```
`422 OVER_RECEIPT_TOLERANCE` if qty exceeds open PO qty beyond tolerance.

### 4.2 `POST /grns/{grnId}/confirm`
Header: optional `Idempotency-Key`. **200 OK** — creates FIFO layers + ledger; updates PO line `qtyReceived`.
```json
{ "grnId": 780, "status": "Confirmed", "postedAt": "2026-07-07T10:05:00Z",
  "createdLayers": [ { "layerId": 9001, "itemId": 1001, "warehouseId": 1, "batchId": 410,
      "qtyReceived": 5000, "qtyRemaining": 5000, "unitCost": 12.50, "receiptDate": "2026-07-07T10:05:00Z" } ],
  "ledgerRefs": [ 55010 ], "poStatus": "Fully Received" }
```
Received `OnHold` → the layer is **not** available until released.

### 4.3 `POST /grns/{grnId}/lines/{grnLineId}/release`
```json
{ "action": "Release" }
```
**200 OK** → `{ "grnLineId": 1300, "holdStatus": "Available" }`
`action: "Reject"` moves the quantity to a return workflow instead.

---

## 5. Stock Management

### 5.1 `GET /stock/on-hand?itemId=1001&warehouseId=1`
```json
{ "itemId": 1001, "warehouseId": 1, "onHand": 5000, "available": 0, "onHold": 5000,
  "inTransit": 0, "reserved": 0, "asOf": "2026-07-07T10:06:00Z" }
```
`available = onHand − onHold − reserved − inTransit(out)`. `reserved` always `0` in Phase 1 (stub, FR-STK-11).

### 5.2 `GET /stock/ledger?itemId=1001&warehouseId=1&from=2026-07-01&to=2026-07-07`
```json
{ "items": [ { "ledgerId": 55010, "itemId": 1001, "warehouseId": 1, "binId": 45, "batchId": 410,
      "serialId": null, "direction": "In", "qtyBase": 5000, "unitCost": 12.50, "value": 62500.00,
      "runningBalance": 5000, "sourceDocType": "GRN", "sourceDocId": 780, "userId": 17,
      "createdAt": "2026-07-07T10:05:00Z" } ],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
```

### 5.3 `GET /stock/valuation?itemId=1001&warehouseId=1`
```json
{ "itemId": 1001, "warehouseId": 1,
  "layers": [ { "layerId": 9001, "qtyRemaining": 5000, "unitCost": 12.50, "value": 62500.00,
      "receiptDate": "2026-07-07T10:05:00Z" } ],
  "totalQty": 5000, "totalValue": 62500.00, "currency": "LKR", "costingMethod": "FIFO" }
```

### 5.4 Transfers (in-transit)
> create → dispatch → receive. Dispatch consumes source FIFO layers into in-transit; receive creates the destination layer at inherited cost (cost-preserving, FR-STK-06).

#### `POST /stock-transfers`
```json
{ "srcWarehouseId": 1, "destWarehouseId": 2,
  "lines": [ { "itemId": 1001, "srcBinId": 45, "destBinId": 90, "batchId": 410, "qty": 1000 } ] }
```
**201 Created**
```json
{ "transferId": 55, "docNo": "TRF-2026-00055", "srcWarehouseId": 1, "destWarehouseId": 2,
  "status": "Draft",
  "lines": [ { "transferLineId": 300, "itemId": 1001, "srcBinId": 45, "destBinId": 90, "batchId": 410, "qty": 1000 } ] }
```

#### `POST /stock-transfers/{id}/dispatch` → **200 OK** (status `InTransit`)
```json
{ "transferId": 55, "status": "InTransit",
  "consumedLayers": [ { "layerId": 9001, "qtyConsumed": 1000, "unitCost": 12.50 } ],
  "ledgerRefs": [ 55033 ] }
```
`409 STOCK_NEGATIVE_BLOCKED` if source available < requested.

#### `POST /stock-transfers/{id}/receive`
```json
{ "lines": [ { "transferLineId": 300, "qty": 1000 } ] }
```
**200 OK** (status `Received`)
```json
{ "transferId": 55, "status": "Received",
  "createdLayers": [ { "layerId": 9040, "warehouseId": 2, "qtyReceived": 1000, "unitCost": 12.50 } ],
  "ledgerRefs": [ 55034 ] }
```

### 5.5 Adjustments (auto-post)
> Reason code mandatory. Decrease consumes FIFO layers; increase creates a layer at supplied/last cost (FR-STK-07).

#### `POST /stock-adjustments`
```json
{ "warehouseId": 1, "reasonCodeId": 4,
  "lines": [ { "itemId": 1001, "binId": 45, "batchId": 410, "qtyDelta": -15 } ] }
```
**201 Created**
```json
{ "adjustmentId": 77, "docNo": "ADJ-2026-00077", "warehouseId": 1, "reasonCodeId": 4,
  "status": "Posted", "createdBy": 17, "createdAt": "2026-07-07T10:20:00Z",
  "lines": [ { "adjLineId": 210, "itemId": 1001, "binId": 45, "batchId": 410, "qtyDelta": -15 } ],
  "ledgerRefs": [ 55050 ] }
```
`400 REASON_CODE_REQUIRED` if `reasonCodeId` omitted.

### 5.6 Counts
#### `POST /stock-counts`
```json
{ "warehouseId": 1, "countType": "Cycle", "itemIds": [1001, 1002] }
```
**201 Created** — status `Draft`, system quantities snapshotted
```json
{ "countId": 30, "docNo": "CNT-2026-00030", "warehouseId": 1, "countType": "Cycle", "status": "Draft",
  "lines": [ { "countLineId": 400, "itemId": 1001, "binId": 45, "systemQty": 4985, "countedQty": null, "variance": null } ] }
```

#### `PUT /stock-counts/{id}/counts`
```json
{ "lines": [ { "countLineId": 400, "countedQty": 4980 } ] }
```
**200 OK** → `{ "lines": [ { "countLineId": 400, "systemQty": 4985, "countedQty": 4980, "variance": -5 } ] }`

#### `POST /stock-counts/{id}/post` → **200 OK** (posts variance adjustment, closes count)
```json
{ "countId": 30, "status": "Posted", "adjustmentId": 78, "ledgerRefs": [ 55060 ] }
```

### 5.7 Reorder alerts
#### `GET /stock/reorder-alerts?warehouseId=1`
Items at/below ROP (FR-STK-10); computed on read, no stored entity.
```json
{ "items": [ { "itemId": 1002, "warehouseId": 1, "available": 90, "reorderPoint": 100,
      "reorderQty": 400, "suggestedRequisitionQty": 400 } ],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
```
`POST /stock/reorder-alerts/{itemId}/requisition?warehouseId=1` → creates a draft requisition for the suggested qty.

---

## 6. Reference Data
#### `GET /reason-codes?context=Adjustment`
```json
{ "items": [ { "reasonCodeId": 4, "code": "DMG", "description": "Damage", "context": "Adjustment" },
             { "reasonCodeId": 22, "code": "QREJ", "description": "Quality Reject", "context": "Return" } ],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 2, "totalPages": 1 } }
```
`POST /reason-codes` (admin). Number sequences are server-managed; no write API in Phase 1.

---

## 7. Domain Error Catalog
| `code` | HTTP | When |
|---|---|---|
| `SKU_DUPLICATE` | 400 | Item SKU already exists. |
| `MASTER_IN_USE` | 409 | Hard delete of a referenced master (use deactivate). |
| `PO_NOT_EDITABLE` | 409 | Editing a PO that is fully received / closed / cancelled. |
| `OVER_RECEIPT_TOLERANCE` | 422 | GRN qty exceeds PO open qty beyond tolerance. |
| `STOCK_NEGATIVE_BLOCKED` | 409 | Issue/transfer/adjustment would drive available stock negative. |
| `EXPIRED_BATCH_BLOCKED` | 409 | Issue/pick of an expired batch. |
| `ONHOLD_NOT_ISSUABLE` | 409 | Issue against on-hold/quarantined stock. |
| `REASON_CODE_REQUIRED` | 400 | Adjustment/return without a reason code. |
| `CONCURRENCY_CONFLICT` | 412 | ETag / RowVersion mismatch. |
| `IDEMPOTENCY_REPLAY` | 200 | Duplicate `Idempotency-Key`; original result returned. |

Example (`409`, `application/problem+json`):
```json
{ "type": "https://errors.erp.local/onhold-not-issuable",
  "title": "Stock is on inspection hold and cannot be issued.",
  "status": 409, "code": "ONHOLD_NOT_ISSUABLE",
  "detail": "5000 units of ITM-1001 at WH-MAIN are OnHold; release via GRN inspection before issue.",
  "traceId": "00-1b7c...-01" }
```

---

## 8. Enumerations
| Enum | Values |
|---|---|
| `itemType` | `Stocked`, `NonStocked`, `Service` |
| `trackingMode` | `None`, `Batch`, `Serial` |
| `holdStatus` | `Available`, `OnHold`, `Rejected` |
| `direction` (ledger) | `In`, `Out` |
| PO `status` | `Draft`, `PendingApproval`, `Approved`, `PartiallyReceived`, `FullyReceived`, `Closed`, `Cancelled` |
| GRN `status` | `Draft`, `Confirmed`, `Closed` |
| Transfer `status` | `Draft`, `InTransit`, `Received`, `Closed` |
| Count `status` | `Draft`, `Counted`, `Posted` |
| `countType` | `Cycle`, `Full` |

---

## 9. Implementation notes (ASP.NET Core)
- Serve via **Swashbuckle**; annotate controllers with `[ProducesResponseType]` per status so generated OpenAPI matches this document.
- Use **`ProblemDetails` / `ValidationProblemDetails`** for all errors (§1.8) — framework default.
- Map `ETag`/`If-Match` to EF `[Timestamp] byte[] RowVersion`.
- Wrap every stock-affecting operation (GRN confirm, transfer dispatch/receive, adjustment, return, count post) in **one** UoW transaction; FIFO layer consumption locks affected layer rows (NFR-02). See `10-BACKEND-PHASE1.md Part A`.
- Derive audit actor from `User.FindFirst("sub")`, never from the body.
- Deferred (Phase 2+): vendor invoice + 3-way match, reservation/allocation, RBAC policy attributes — all additive, no breaking change to these routes.

---

*End of 11-BACKEND-PHASE1.md. Model & rules: `10-BACKEND-PHASE1.md`. Record work: `Backend/PROGRESS.md`.*
