Stock
Back-office stock operations — levels per variant and warehouse, the movement journal, receipts, adjustments, alert thresholds, inventory sessions and warehouses.
These operations control commercial availability (cahier §4.3, §8 « Stock disponible = stock physique − stock réservé ; contrôle au panier et à la validation »): stock per warehouse or global, reserved quantities, alert thresholds, a journal of every movement, goods receipts, adjustments with reasons and inventory sessions (STK-01 to STK-06). Screens: stock overview (BO-33), movements journal (BO-34), receipt / adjustment (BO-35), inventory sessions (BO-36), warehouses (BO-37) and the stock dashboard (BO-07).
Quantities never leave the back-office. Resellers and guests only ever see IN_STOCK or OUT_OF_STOCK (decision of 17 Oct 2026, STK-07). Low-stock is a back-office notion: each level has an alertLevel — OUT (available ≤ 0), LOW (0 < available ≤ alert threshold) or OK.
Movements are automatic where they can be. Order reservations, deductions and releases, cancellations and return receptions post their movements themselves (orders, lifecycles); this page adds the manual ones — receipts, adjustments, inventories — and reads the journal. Every variation is journaled (cahier §4.3 « Les variations de stock doivent être journalisées », STK-04); a StockLevel is never written without its StockMovement.
Who uses it. The Responsable stock (cahier §3: « Produits, catégories, variantes, quantités, inventaires et alertes »); the Super administrateur; commercial and delivery staff read levels.
| Permission | Unlocks on this page |
|---|---|
stock.view | Levels, movements, inventory sessions (read), warehouses (read) |
stock.create | Goods receipts |
stock.update | Adjustments, thresholds, opening and counting inventory sessions |
stock.validate | Validating an inventory session (posts the differences) |
stock.administer | Creating and editing warehouses |
pricing.view | Stock valuation fields (stockValueHt, receipt unitCost) |
Settings used (settings keys): stock.reserveOn (default VALIDATED), stock.deductOn (default SHIPPED), stock.lowThresholdDefault (10), stock.multiWarehouse (false — one sellable warehouse).
Audit. CREATE on receipts and adjustments (with the reason), UPDATE on thresholds and warehouses, STATUS_CHANGE / VALIDATE on inventory sessions; each StockMovement row itself records actor, app, reason and reference.
Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); UserSummary and FileRef are shared schemas; quantities are integers (units); editable records carry version; the dashboard sends an Idempotency-Key on receipts and adjustments (recommended — a double click must not double the stock). 401, 429 and 500 apply everywhere and are not repeated.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /admin/stock | admin.stock.levelsStock by variant and warehouse (physical, reserved, available, threshold, alert) | stock.view | BO | V1 |
| GET | /admin/stock/movements | admin.stock.movementsMovement journal with filters | stock.view | BO | V1 |
| POST | /admin/stock/receipts | admin.stock.receiptRecord goods received | stock.create | BO | V1 |
| POST | /admin/stock/adjustments | admin.stock.adjustAdjust stock with a reason | stock.update | BO | V1 |
| PUT | /admin/stock/thresholds | admin.stock.thresholdsSet alert thresholds (bulk) | stock.update | BO | V1 |
| GET · POST · PUT | /admin/inventory-sessions[/{sessionId}[/lines]] | admin.inventory.crudInventory sessions: open, count lines, review differences | stock.update | BO | V1 |
| POST | /admin/inventory-sessions/{sessionId}/validate | admin.inventory.validateValidate a session: posts adjustment movements | stock.validate | BO | V1 |
| GET · POST · PATCH | /admin/warehouses[/{warehouseId}] | admin.warehouses.crudWarehouses | stock.administer | BO | V1 |
Schemas
StockLevel
interface StockLevel {
variantId: Uuid;
sku: string;
barcode?: string;
productId: Uuid;
productName: string;
variantName: string;
brand: { id: Uuid; name: string };
category: { id: Uuid; name: string; parentId: Uuid | null };
productStatus: ProductStatus;
warehouse: { id: Uuid; code: string; name: string } | null; // null in the global view (sum of sellable warehouses)
physical: number; // on the shelves
reserved: number; // held by orders between stock.reserveOn and stock.deductOn
available: number; // physical − reserved (negative only after a stock override)
alertThreshold: number;
alertLevel: "OK" | "LOW" | "OUT";
availability: "IN_STOCK" | "OUT_OF_STOCK"; // what resellers see (global, sellable warehouses)
lastMovementAt: Iso | null;
lastReceiptAt: Iso | null;
stockValueHt?: Money; // physical × purchasePrice — only with pricing.view
}StockMovement
interface StockMovement {
id: Uuid;
at: Iso;
type: StockMovementType; // RECEIPT, ADJUSTMENT, INVENTORY, RESERVATION, RESERVATION_RELEASE, SHIPMENT, RETURN, TRANSFER_OUT, TRANSFER_IN
variant: { id: Uuid; sku: string; productName: string; variantName: string };
warehouse: { id: Uuid; code: string; name: string };
quantity: number; // signed change of physical stock (0 for reservations)
reservedDelta: number; // signed change of reserved stock
physicalAfter: number;
reservedAfter: number;
availableAfter: number;
reference: { type: "ORDER" | "RECEIPT" | "ADJUSTMENT" | "INVENTORY" | "RETURN" | "IMPORT"; id: Uuid; number?: string };
reason?: string;
unitCost?: Money; // RECEIPT, HT — only with pricing.view
actor: UserSummary | null; // null = system (order state machine, jobs)
app: ClientApp;
}| Type | quantity | reservedDelta | Posted by |
|---|---|---|---|
RECEIPT | + | 0 | admin.stock.receipt, stock import (ADD) |
ADJUSTMENT | ± | 0 | admin.stock.adjust, stock import (SET) |
INVENTORY | ± | 0 | admin.inventory.validate |
RESERVATION | 0 | + | Order reaching stock.reserveOn; line increase on a reserved order |
RESERVATION_RELEASE | 0 | − | Refusal, cancellation, expiry, line decrease — automatic |
SHIPMENT | − | − | Order reaching stock.deductOn |
RETURN | + | 0 | Return reception, door refusal (automatic) |
TRANSFER_OUT / TRANSFER_IN | − / + | 0 | Reserved for transfers between warehouses (no V1 operation) |
Levels and journal
GET /admin/stock
admin.stock.levels · Permission stock.view · Apps BO · Release V1 · Requirements STK-01 · STK-02 · STK-05
Purpose. Stock by variant and warehouse — physical, reserved, available, threshold and alert — for the stock overview and its alert tabs.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
view | string | warehouse (default when stock.multiWarehouse) | global | global sums sellable warehouses, one row per variant |
warehouseId | Uuid[] | repeatable | warehouse view |
alertLevel | string[] | OK | LOW | OUT | Repeatable — the Alertes tab sends LOW and OUT |
brandId / categoryId | Uuid[] | repeatable | Category includes sub-categories |
productStatus | ProductStatus[] | default all except ARCHIVED | |
negativeAvailable | boolean | Overridden orders left available below 0 | |
q | string | min 2 chars | SKU, barcode (exact), product name |
sort | string | available (default), physical, reserved, sku, productName, lastMovementAt, stockValueHt | stockValueHt needs pricing.view |
order / page / pageSize | asc (default) · 1 · 50 (max 200) |
Response 200 OK
{
"items": [
{
"variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "barcode": "6974316280521",
"productId": "0192b215-6f70-7a81-9b2c-3d4e5f6a7b80", "productName": "Chargeur GaN 50 W 2 ports", "variantName": "Blanc",
"brand": { "id": "01928a01-2b3c-7d4e-9f5a-6b7c8d9e0f12", "name": "ACEFAST" },
"category": { "id": "01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f", "name": "Chargeurs", "parentId": null },
"productStatus": "ACTIVE",
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"physical": 46, "reserved": 40, "available": 6, "alertThreshold": 20, "alertLevel": "LOW", "availability": "IN_STOCK",
"lastMovementAt": "2026-11-03T07:15:00Z", "lastReceiptAt": "2026-10-28T08:40:00Z", "stockValueHt": 6900000
},
{
"variantId": "0192b21a-9c0d-7e1f-a2b3-4c5d6e7f8a9b", "sku": "BF-BH20-BK", "barcode": "6941991101234",
"productId": "0192b21a-8b9c-7d0e-9f1a-2b3c4d5e6f70", "productName": "Support voiture magnétique", "variantName": "Noir",
"brand": { "id": "01928a01-3c4d-7e5f-8a6b-7c8d9e0f1a23", "name": "BOROFONE" },
"category": { "id": "01928b01-4d5e-7f6a-9b7c-8d9e0f1a2b34", "name": "Supports voiture", "parentId": null },
"productStatus": "ACTIVE",
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"physical": 312, "reserved": 15, "available": 297, "alertThreshold": 30, "alertLevel": "OK", "availability": "IN_STOCK",
"lastMovementAt": "2026-11-03T09:52:30Z", "lastReceiptAt": "2026-10-15T09:00:00Z", "stockValueHt": 9360000
}
],
"total": 641,
"page": 1,
"pageSize": 50,
"summary": { "out": 8, "low": 15, "negativeAvailable": 0, "stockValueHt": 412870000 }
}Field notes. stockValueHt (rows and summary) — only with pricing.view; 46 × 150 000 = 6 900 000. availability is the global reseller view: IN_STOCK while the sum of available over sellable warehouses is above 0.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown alert level, sort field or view |
| 403 | PERMISSION_DENIED | Caller lacks stock.view; sort=stockValueHt without pricing.view |
Rules & side effects
- Read-only. Levels are maintained in the same transaction as each movement, so the list is exact at read time.
- Live:
stock.alertwhenever a level changesalertLevel.
GET /admin/stock/movements
admin.stock.movements · Permission stock.view · Apps BO · Release V1 · Requirements STK-04
Purpose. The movement journal with filters — the answer to "why is this quantity what it is?".
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
variantId | Uuid | ||
warehouseId | Uuid[] | repeatable | |
type | StockMovementType[] | repeatable | |
referenceType / referenceId | string / Uuid | ORDER, RECEIPT, ADJUSTMENT, INVENTORY, RETURN, IMPORT | All movements of one order, receipt… |
actorId | Uuid | system for automatic movements | |
from / to | string | date or timestamp | On at |
q | string | min 2 chars | SKU, product name, order or return number |
sort | string | at (only) | |
order / page / pageSize | desc (default) · 1 · 50 (max 200) |
Response 200 OK — Page<StockMovement>
{
"items": [
{
"id": "01930171-4d5e-7f6a-8b7c-8d9e0f1a2b3c", "at": "2026-11-03T09:52:30Z", "type": "RESERVATION",
"variant": { "id": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "productName": "Chargeur GaN 50 W 2 ports", "variantName": "Blanc" },
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"quantity": 0, "reservedDelta": 6, "physicalAfter": 46, "reservedAfter": 46, "availableAfter": 0,
"reference": { "type": "ORDER", "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158" },
"actor": { "id": "01929e66-3d4e-7f5a-8b6c-7d8e9f0a1b2c", "displayName": "Sofiane Hamdi" }, "app": "DASHBOARD"
},
{
"id": "01930090-5e6f-7a7b-9c8d-9e0f1a2b3c4d", "at": "2026-10-28T08:40:00Z", "type": "RECEIPT",
"variant": { "id": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "productName": "Chargeur GaN 50 W 2 ports", "variantName": "Blanc" },
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"quantity": 40, "reservedDelta": 0, "physicalAfter": 52, "reservedAfter": 34, "availableAfter": 18,
"reference": { "type": "RECEIPT", "id": "01930090-4d5e-7f6a-8b7c-8d9e0f1a2b3c" }, "reason": "Arrivage conteneur ACEFAST n° 14", "unitCost": 150000,
"actor": { "id": "01929e88-5f6a-7b7c-8d8e-9f0a1b2c3d4e", "displayName": "Amine Saadi" }, "app": "DASHBOARD"
}
],
"total": 2318,
"page": 1,
"pageSize": 50
}Field notes. unitCost — only with pricing.view. The journal is append-only: corrections are new movements, never edits.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown type, bad dates |
| 403 | PERMISSION_DENIED | Caller lacks stock.view |
Rules & side effects
- Read-only. Exportable through
admin.exports.create(LISTkeystock-movements, permissionstock.export).
Used by BO-34 · BO-28 (variant stock tab) · BO-39
Receipts, adjustments and thresholds
POST /admin/stock/receipts
admin.stock.receipt · Permission stock.create · Apps BO · Release V1 · Requirements STK-04
Purpose. Records goods received into a warehouse (« Réception de marchandise »).
Request
interface StockReceiptBody {
warehouseId: Uuid;
receivedAt?: Iso; // default now; not in the future, ≤ 7 days back
supplierReference?: string; // delivery note / container number, ≤ 80
note?: string;
lines: { variantId: Uuid; quantity: number; unitCost?: Money }[]; // 1–500 lines, quantity ≥ 1; unitCost HT needs pricing.view
}{
"warehouseId": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011",
"supplierReference": "Conteneur ACEFAST n° 15",
"lines": [
{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "quantity": 120, "unitCost": 150000 },
{ "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "quantity": 500 }
]
}Response 201 Created
{
"receiptId": "01930180-6f7a-7b8c-9d0e-1f2a3b4c5d6e",
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"receivedAt": "2026-11-04T08:05:00Z",
"supplierReference": "Conteneur ACEFAST n° 15",
"createdBy": { "id": "01929e88-5f6a-7b7c-8d8e-9f0a1b2c3d4e", "displayName": "Amine Saadi" },
"movements": [
{ "id": "01930180-7a8b-7c9d-8e0f-2a3b4c5d6e7f", "type": "RECEIPT", "sku": "AF-A29-50W-WH", "quantity": 120, "physicalAfter": 166, "reservedAfter": 46, "availableAfter": 120 },
{ "id": "01930180-8b9c-7d0e-9f1a-3b4c5d6e7f80", "type": "RECEIPT", "sku": "HC-X14-CC-1M-BK", "quantity": 500, "physicalAfter": 1740, "reservedAfter": 160, "availableAfter": 1580 }
],
"alertsCleared": [{ "sku": "AF-A29-50W-WH", "from": "OUT", "to": "OK" }]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | No line, duplicate variant, quantity ≤ 0, receivedAt out of range |
| 403 | PERMISSION_DENIED | Caller lacks stock.create; unitCost without pricing.view |
| 404 | NOT_FOUND | Unknown warehouse or variant |
| 422 | WAREHOUSE_INACTIVE | The warehouse is inactive |
Rules & side effects
- All-or-nothing; one
RECEIPTmovement per line (physical += quantity), grouped byreceiptId.unitCostis stored on the movement for valuation; it does not change the variant'spurchasePrice. - A product put
OUT_OF_STOCKby the system goes back toACTIVEwhen available becomes positive; resellers see « En stock » at their next read. - Audit
CREATE(entityStockMovement, label the supplier reference). SSE:stock.alertwithlevel: "OK"for each cleared alert.
Used by BO-35
POST /admin/stock/adjustments
admin.stock.adjust · Permission stock.update · Apps BO · Release V1 · Requirements STK-06
Purpose. Corrects stock outside an inventory — breakage, loss, a counting error found on the shelf — always with a reason.
Request
interface StockAdjustmentBody {
warehouseId: Uuid;
reason: string; // 3–500 chars, required
lines: ({ variantId: Uuid; delta: number } | { variantId: Uuid; newPhysical: number })[]; // 1–200; delta ≠ 0
}{
"warehouseId": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011",
"reason": "Carton écrasé à la réception — 4 supports inutilisables.",
"lines": [{ "variantId": "0192b21a-9c0d-7e1f-a2b3-4c5d6e7f8a9b", "delta": -4 }]
}Response 201 Created — { "adjustmentId": "01930181-9c0d-7e1f-8a2b-3c4d5e6f7a8b", "movements": [{ "id": "…", "type": "ADJUSTMENT", "sku": "BF-BH20-BK", "quantity": -4, "physicalAfter": 308, "reservedAfter": 15, "availableAfter": 293 }], "alertsRaised": [] }
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | delta 0, both delta and newPhysical, negative newPhysical |
| 403 | PERMISSION_DENIED | Caller lacks stock.update |
| 404 | NOT_FOUND | Unknown warehouse or variant |
| 409 | INVENTORY_SESSION_CONFLICT | The variant is in an OPEN or IN_REVIEW inventory session of this warehouse — count it there |
| 422 | REASON_REQUIRED | Empty reason |
| 422 | STOCK_BELOW_RESERVED | The new physical would be below the reserved quantity — details: { lines: [{ sku, newPhysical, reserved }] }; release or modify the orders first |
| 422 | WAREHOUSE_INACTIVE | The warehouse is inactive |
Rules & side effects
- All-or-nothing; one
ADJUSTMENTmovement per line with the reason. Available reaching 0 sets the productOUT_OF_STOCKwhen every variant is out. - Audit
CREATEwith the reason (cahier §3 « inventaires et ajustements avec motifs »). A reason reference list (STOCK_ADJUSTMENT_REASON) is not part of the canonical lists — Assumption: free text in V1. - SSE:
stock.alertfor every level that changesalertLevel; notificationsstock.lowandstock.out(in-app, e-mail digest) to stock managers per the notifications matrix.
PUT /admin/stock/thresholds
admin.stock.thresholds · Permission stock.update · Apps BO · Release V1 · Requirements STK-02
Purpose. Sets alert thresholds in bulk (per variant, in one warehouse or all).
Request
interface StockThresholdsBody {
items: { variantId: Uuid; warehouseId?: Uuid | null; alertThreshold: number }[]; // 1–1 000; warehouseId null/absent = every warehouse; threshold 0–100 000
}{ "items": [
{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "alertThreshold": 30 },
{ "variantId": "0192b210-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "warehouseId": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "alertThreshold": 200 }
] }Response 200 OK — { "updated": 2, "alertsRaised": [{ "sku": "AF-A29-50W-WH", "warehouseCode": "WH-ORN", "from": "OK", "to": "LOW" }], "alertsCleared": [] }
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | More than 1 000 items, negative threshold, duplicate pair |
| 403 | PERMISSION_DENIED | Caller lacks stock.update |
| 404 | NOT_FOUND | Unknown variant or warehouse |
Rules & side effects
- All-or-nothing. Thresholds do not move stock (no movement row);
alertLevelis recomputed. Audit oneUPDATErow per changedStockLevel. - SSE:
stock.alertfor each raised or cleared alert.
Inventory sessions
An inventory session counts a warehouse (or part of it) and posts the differences as INVENTORY movements when validated (cahier §3 « inventaires », STK-06). Lifecycle (InventorySessionStatus, not yet in enums.json): OPEN (counting) → IN_REVIEW (counts frozen, differences reviewed) → VALIDATED; IN_REVIEW → OPEN to recount; OPEN / IN_REVIEW → CANCELLED. Expected quantities are a snapshot of physical stock at opening; selling continues during the count, so the difference posted at validation is counted − (expected + movements since opening) (BR-19) — after validation physical stock equals the count.
interface InventorySession {
id: Uuid;
label: string; // "Inventaire chargeurs — novembre"
warehouse: { id: Uuid; code: string; name: string };
status: "OPEN" | "IN_REVIEW" | "VALIDATED" | "CANCELLED";
scope: { kind: "FULL" | "PARTIAL"; categoryIds: Uuid[]; brandIds: Uuid[]; variantIds: Uuid[] };
blind: boolean; // counters do not see expected quantities
openedAt: Iso; openedBy: UserSummary;
submittedAt?: Iso; validatedAt?: Iso; validatedBy?: UserSummary; cancelledAt?: Iso;
note?: string;
stats: { lines: number; counted: number; withDifference: number; unitsPlus: number; unitsMinus: number; movementsSinceOpen: number };
differenceValueHt?: Money; // Σ difference × purchasePrice — only with pricing.view
version: number;
}
interface InventoryLine {
variantId: Uuid; sku: string; barcode?: string; productName: string; variantName: string;
expectedQty?: number; // snapshot at opening — hidden from counters of a blind session until IN_REVIEW
countedQty: number | null; // null = not counted yet
difference: number | null; // countedQty − (expectedQty + movementsSinceOpen)
movementsSinceOpen: number; // net physical change posted on this variant since opening (information)
countedBy?: UserSummary; countedAt?: Iso; note?: string;
}GET /admin/inventory-sessions
admin.inventory.crud (list) · Permission stock.view · Apps BO · Release V1 · Requirements STK-06
Purpose. Lists inventory sessions.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | string[] | OPEN | IN_REVIEW | VALIDATED | CANCELLED | Repeatable |
warehouseId | Uuid | ||
from / to | string (date) | On openedAt | |
sort / order / page / pageSize | openedAt (default) · desc · 1 · 25 (max 200) |
Response 200 OK
{
"items": [
{
"id": "01930190-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "label": "Inventaire chargeurs — novembre",
"warehouse": { "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran" },
"status": "IN_REVIEW",
"scope": { "kind": "PARTIAL", "categoryIds": ["01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f"], "brandIds": [], "variantIds": [] },
"blind": true,
"openedAt": "2026-11-05T06:30:00Z", "openedBy": { "id": "01929e88-5f6a-7b7c-8d8e-9f0a1b2c3d4e", "displayName": "Amine Saadi" },
"submittedAt": "2026-11-05T11:10:00Z",
"stats": { "lines": 64, "counted": 64, "withDifference": 3, "unitsPlus": 2, "unitsMinus": 7, "movementsSinceOpen": 5 },
"differenceValueHt": -510000,
"version": 9
}
],
"total": 4,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.view |
Rules & side effects
- Read-only (the bundle's
stock.updateapplies to writes; reading needsstock.view— Assumption).
Used by BO-36
GET /admin/inventory-sessions/{sessionId}
admin.inventory.crud (get) · Permission stock.view · Apps BO · Release V1 · Requirements STK-06
Purpose. A session with a page of its lines — the counting sheet and the difference review.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
lines | string | ALL (default) | UNCOUNTED | WITH_DIFFERENCE | |
q | string | SKU, barcode, name | |
page / pageSize | number | 1 / 100 (max 500) | Lines sorted by brand, then SKU |
Response 200 OK — the InventorySession plus lines: Page<InventoryLine>:
{
"id": "01930190-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"label": "Inventaire chargeurs — novembre",
"status": "IN_REVIEW",
"blind": true,
"lines": {
"items": [
{ "variantId": "0192b215-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "sku": "AF-A29-50W-WH", "barcode": "6974316280521", "productName": "Chargeur GaN 50 W 2 ports", "variantName": "Blanc",
"expectedQty": 166, "countedQty": 157, "difference": -3, "movementsSinceOpen": -6,
"countedBy": { "id": "01929e99-6a7b-7c8d-9e9f-0a1b2c3d4e5f", "displayName": "Walid Rahmani" }, "countedAt": "2026-11-05T09:41:00Z", "note": "3 boîtes vides trouvées." }
],
"total": 3, "page": 1, "pageSize": 100
},
"stats": { "lines": 64, "counted": 64, "withDifference": 3, "unitsPlus": 2, "unitsMinus": 7, "movementsSinceOpen": 5 },
"version": 9
}Field notes. In a blind session expectedQty and difference are omitted while OPEN for callers without stock.validate; differenceValueHt only with pricing.view.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.view |
| 404 | NOT_FOUND | Unknown session |
Rules & side effects
- Read-only.
Used by BO-36
POST /admin/inventory-sessions
admin.inventory.crud (create) · Permission stock.update · Apps BO · Release V1 · Requirements STK-06
Purpose. Opens a session for a whole warehouse or part of it and snapshots the expected quantities.
Request
interface OpenInventoryBody {
label: string;
warehouseId: Uuid;
scope: { kind: "FULL" | "PARTIAL"; categoryIds?: Uuid[]; brandIds?: Uuid[]; variantIds?: Uuid[] }; // PARTIAL needs at least one list
blind?: boolean; // default true
note?: string;
}{ "label": "Inventaire chargeurs — novembre", "warehouseId": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "scope": { "kind": "PARTIAL", "categoryIds": ["01928b01-0a1b-7c2d-8e3f-4a5b6c7d8e9f"] }, "blind": true }Response 201 Created — the InventorySession (status OPEN, stats.lines 64, stats.counted 0, version 1).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | PARTIAL without lists, empty scope result |
| 403 | PERMISSION_DENIED | Caller lacks stock.update |
| 404 | NOT_FOUND | Unknown warehouse, category, brand or variant |
| 409 | INVENTORY_SESSION_CONFLICT | Another OPEN or IN_REVIEW session of the warehouse overlaps the scope — details: { sessionId } |
| 422 | WAREHOUSE_INACTIVE | Inactive warehouse |
Rules & side effects
- One
InventoryLineper non-archived variant in scope withexpectedQty= physical at opening. Orders keep flowing (no stock freeze); manual adjustments on variants in scope are refused until the session ends. - Audit
CREATEonInventorySession.
Used by BO-36
PUT /admin/inventory-sessions/{sessionId}/lines
admin.inventory.crud (count lines) · Permission stock.update · Apps BO · Release V1 · Requirements STK-06
Purpose. Records counted quantities (keyboard or barcode scanner), several lines at a time; a later count of the same line replaces the previous one.
Request
interface InventoryCountBody {
lines: { variantId?: Uuid; barcode?: string; countedQty: number; note?: string }[]; // 1–500; variantId or barcode; countedQty ≥ 0
version?: number;
}{ "lines": [{ "barcode": "6974316280521", "countedQty": 163, "note": "3 boîtes vides trouvées." }, { "variantId": "0192b215-8b9c-7d0e-9f1a-2b3c4d5e6f70", "countedQty": 58 }] }Response 200 OK — { "updated": 2, "stats": { "lines": 64, "counted": 41, "withDifference": 2, "unitsPlus": 2, "unitsMinus": 3, "movementsSinceOpen": 2 }, "version": 6 } (blind sessions: differences counted but not detailed for counters).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Negative quantity, neither variantId nor barcode |
| 403 | PERMISSION_DENIED | Caller lacks stock.update |
| 404 | NOT_FOUND | Unknown session |
| 409 | INVENTORY_TRANSITION_NOT_ALLOWED | Session not OPEN |
| 422 | VARIANT_NOT_IN_SCOPE | Variant or barcode not in the session — details: { variantIds } |
Rules & side effects
- No stock movement while counting. Audit: one
UPDATErow per call (lines listed inchanges).
Used by BO-36
PUT /admin/inventory-sessions/{sessionId}
admin.inventory.crud (update) · Permission stock.update · Apps BO · Release V1 · Requirements STK-06
Purpose. Changes the label or note, submits the counts for review, sends the session back to counting, or cancels it.
Request
interface UpdateInventoryBody {
label?: string;
note?: string | null;
status?: "IN_REVIEW" | "OPEN" | "CANCELLED";
reason?: string; // required for OPEN (recount) and CANCELLED
version: number;
}{ "status": "IN_REVIEW", "version": 8 }Response 200 OK — the InventorySession with its new status.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.update |
| 404 | NOT_FOUND | Unknown session |
| 409 | INVENTORY_TRANSITION_NOT_ALLOWED | Move not allowed (VALIDATED and CANCELLED are final) |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | REASON_REQUIRED | Recount or cancellation without reason |
Rules & side effects
OPEN→IN_REVIEWfreezes counts; uncounted lines staynull(decided at validation).CANCELLEDposts nothing.- Audit
STATUS_CHANGE. No notification (the session list shows sessionsIN_REVIEW).
Used by BO-36
POST /admin/inventory-sessions/{sessionId}/validate
admin.inventory.validate · Permission stock.validate · Apps BO · Release V1 · Requirements STK-06
Purpose. Validates a reviewed session: posts one INVENTORY movement per line with a difference.
Request
interface ValidateInventoryBody {
uncounted?: "REJECT" | "ZERO" | "EXPECTED"; // default REJECT: refuse while lines are uncounted
note?: string;
version: number;
}{ "uncounted": "REJECT", "note": "Écarts expliqués : boîtes vides et casse.", "version": 9 }Response 200 OK
{
"session": { "id": "01930190-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "status": "VALIDATED", "validatedAt": "2026-11-05T12:02:00Z", "validatedBy": { "id": "01929e66-3d4e-7f5a-8b6c-7d8e9f0a1b2c", "displayName": "Sofiane Hamdi" }, "version": 10 },
"movements": [
{ "id": "01930191-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "type": "INVENTORY", "sku": "AF-A29-50W-WH", "quantity": -3, "physicalAfter": 157, "reservedAfter": 40, "availableAfter": 117 }
],
"summary": { "linesAdjusted": 3, "unitsPlus": 2, "unitsMinus": 7, "differenceValueHt": -510000 }
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.validate |
| 404 | NOT_FOUND | Unknown session |
| 409 | INVENTORY_TRANSITION_NOT_ALLOWED | Session not IN_REVIEW |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | INVENTORY_LINES_INCOMPLETE | Uncounted lines with uncounted: REJECT — details: { uncounted: 4 } |
Rules & side effects
- For every line with a non-zero difference:
INVENTORYmovement ofcounted − (expected + movements since opening)(here: 166 at opening, 6 units shipped during the count so physical is 160, counted 157 → −3 → physical 157). A count below the reserved quantity is accepted (it is the reality): available becomes negative and the stock dashboard lists the orders to arbitrate. - All-or-nothing;
VALIDATEDis final (a new session corrects a wrong count). - Audit
VALIDATEwith the summary; product statuses andalertLevelrecomputed; SSEstock.alertfor each changed level.
Used by BO-36
Warehouses
interface Warehouse {
id: Uuid;
code: string; // unique, immutable: "WH-ORN"
name: string;
address?: { wilayaCode: string; communeId?: Uuid; addressLine?: string };
isDefault: boolean; // receives stock and serves orders by default
sellable: boolean; // counts towards reseller availability
status: "ACTIVE" | "INACTIVE";
variantCount: number; // variants with physical > 0
units: number; // Σ physical
stockValueHt?: Money; // only with pricing.view
createdAt: Iso; updatedAt: Iso; version: number;
}GET /admin/warehouses
admin.warehouses.crud (list) · Permission stock.view · Apps BO · Release V1 · Requirements STK-01
Purpose. Lists warehouses (global stock or stock per warehouse, cahier §4.3).
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | string | ACTIVE | INACTIVE |
Response 200 OK
{
"items": [
{ "id": "01928c01-1111-7aaa-8bbb-0c0d0e0f1011", "code": "WH-ORN", "name": "Dépôt Oran", "address": { "wilayaCode": "31", "addressLine": "Zone industrielle Es Senia, lot 42" }, "isDefault": true, "sellable": true, "status": "ACTIVE", "variantCount": 598, "units": 48210, "stockValueHt": 412870000, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 },
{ "id": "01928c01-2222-7bbb-8ccc-1d1e1f202122", "code": "WH-ALG", "name": "Dépôt Alger", "address": { "wilayaCode": "16", "addressLine": "Rouiba, zone d'activités" }, "isDefault": false, "sellable": false, "status": "ACTIVE", "variantCount": 0, "units": 0, "stockValueHt": 0, "createdAt": "2026-10-20T08:00:00Z", "updatedAt": "2026-10-20T08:00:00Z", "version": 1 }
],
"total": 2,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.view |
Rules & side effects
- Read-only (reading relaxed to
stock.viewfor filters and pickers — Assumption).stockValueHtonly withpricing.view.
Used by BO-37 · BO-33 · BO-35 · BO-36
GET /admin/warehouses/{warehouseId}
admin.warehouses.crud (get) · Permission stock.view · Apps BO · Release V1 · Requirements STK-01
Purpose. One warehouse.
Response 200 OK — a Warehouse.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.view |
| 404 | NOT_FOUND | Unknown warehouse |
Rules & side effects
- Read-only.
Used by BO-37
POST /admin/warehouses
admin.warehouses.crud (create) · Permission stock.administer · Apps BO · Release V1 · Requirements STK-01
Purpose. Adds a warehouse.
Request
interface CreateWarehouseBody { code: string; name: string; address?: Warehouse["address"]; sellable?: boolean; isDefault?: boolean }{ "code": "WH-ALG", "name": "Dépôt Alger", "address": { "wilayaCode": "16", "addressLine": "Rouiba, zone d'activités" }, "sellable": false }Response 201 Created — the Warehouse; StockLevel rows at 0 are created for every variant.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Code not matching ^WH-[A-Z0-9]{2,10}$ |
| 403 | PERMISSION_DENIED | Caller lacks stock.administer |
| 409 | DUPLICATE_VALUE | Code or name already used |
Rules & side effects
- A second sellable warehouse is accepted only when
stock.multiWarehouse = true(422 MULTI_WAREHOUSE_DISABLEDotherwise). AuditCREATE.
Used by BO-37
PATCH /admin/warehouses/{warehouseId}
admin.warehouses.crud (update) · Permission stock.administer · Apps BO · Release V1 · Requirements STK-01
Purpose. Edits a warehouse, makes it the default, changes whether it is sellable, or deactivates it.
Request
interface UpdateWarehouseBody { name?: string; address?: Warehouse["address"] | null; sellable?: boolean; isDefault?: boolean; status?: "ACTIVE" | "INACTIVE"; version: number }{ "sellable": true, "version": 1 }Response 200 OK — the updated Warehouse.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks stock.administer |
| 404 | NOT_FOUND | Unknown warehouse |
| 409 | RESOURCE_IN_USE | Deactivating a warehouse with physical stock, reservations or an open inventory session; unsetting the only default |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | MULTI_WAREHOUSE_DISABLED | Making a second warehouse sellable while stock.multiWarehouse = false |
Rules & side effects
- Changing
sellablechanges reseller availability immediately (availability is the sum over sellable warehouses); products may switch betweenACTIVEandOUT_OF_STOCK. AuditUPDATE.
Used by BO-37
Catalog & pricing
Back-office catalog operations — brands, category tree, filterable attributes, products and variants with their three customer-type prices, visibility, price history, product import and export.
Orders & delivery
Back-office order processing — queue of orders to confirm, controls, confirmation with overrides, refusal, controlled modification, transitions, cancellation, documents, returns, the delivery board, proof of delivery and carriers.