HOCO B2B
SRSAPI referenceBack-office endpoints

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.

PermissionUnlocks on this page
stock.viewLevels, movements, inventory sessions (read), warehouses (read)
stock.createGoods receipts
stock.updateAdjustments, thresholds, opening and counting inventory sessions
stock.validateValidating an inventory session (posts the differences)
stock.administerCreating and editing warehouses
pricing.viewStock 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.

MethodPathOperationAccessAppsRelease
GET/admin/stockadmin.stock.levels
Stock by variant and warehouse (physical, reserved, available, threshold, alert)
stock.viewBOV1
GET/admin/stock/movementsadmin.stock.movements
Movement journal with filters
stock.viewBOV1
POST/admin/stock/receiptsadmin.stock.receipt
Record goods received
stock.createBOV1
POST/admin/stock/adjustmentsadmin.stock.adjust
Adjust stock with a reason
stock.updateBOV1
PUT/admin/stock/thresholdsadmin.stock.thresholds
Set alert thresholds (bulk)
stock.updateBOV1
GET · POST · PUT/admin/inventory-sessions[/{sessionId}[/lines]]admin.inventory.crud
Inventory sessions: open, count lines, review differences
stock.updateBOV1
POST/admin/inventory-sessions/{sessionId}/validateadmin.inventory.validate
Validate a session: posts adjustment movements
stock.validateBOV1
GET · POST · PATCH/admin/warehouses[/{warehouseId}]admin.warehouses.crud
Warehouses
stock.administerBOV1

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;
}
TypequantityreservedDeltaPosted by
RECEIPT+0admin.stock.receipt, stock import (ADD)
ADJUSTMENT±0admin.stock.adjust, stock import (SET)
INVENTORY±0admin.inventory.validate
RESERVATION0+Order reaching stock.reserveOn; line increase on a reserved order
RESERVATION_RELEASE0−Refusal, cancellation, expiry, line decrease — automatic
SHIPMENT−−Order reaching stock.deductOn
RETURN+0Return reception, door refusal (automatic)
TRANSFER_OUT / TRANSFER_IN− / +0Reserved 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

QueryTypeValues / defaultNotes
viewstringwarehouse (default when stock.multiWarehouse) | globalglobal sums sellable warehouses, one row per variant
warehouseIdUuid[]repeatablewarehouse view
alertLevelstring[]OK | LOW | OUTRepeatable — the Alertes tab sends LOW and OUT
brandId / categoryIdUuid[]repeatableCategory includes sub-categories
productStatusProductStatus[]default all except ARCHIVED
negativeAvailablebooleanOverridden orders left available below 0
qstringmin 2 charsSKU, barcode (exact), product name
sortstringavailable (default), physical, reserved, sku, productName, lastMovementAt, stockValueHtstockValueHt needs pricing.view
order / page / pageSizeasc (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

StatusCodeWhen
400VALIDATION_FAILEDUnknown alert level, sort field or view
403PERMISSION_DENIEDCaller lacks stock.view; sort=stockValueHt without pricing.view

Rules & side effects

  1. Read-only. Levels are maintained in the same transaction as each movement, so the list is exact at read time.
  2. Live: stock.alert whenever a level changes alertLevel.

Used by BO-33 · BO-07 · BO-35

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

QueryTypeValues / defaultNotes
variantIdUuid
warehouseIdUuid[]repeatable
typeStockMovementType[]repeatable
referenceType / referenceIdstring / UuidORDER, RECEIPT, ADJUSTMENT, INVENTORY, RETURN, IMPORTAll movements of one order, receipt…
actorIdUuidsystem for automatic movements
from / tostringdate or timestampOn at
qstringmin 2 charsSKU, product name, order or return number
sortstringat (only)
order / page / pageSizedesc (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

StatusCodeWhen
400VALIDATION_FAILEDUnknown type, bad dates
403PERMISSION_DENIEDCaller lacks stock.view

Rules & side effects

  1. Read-only. Exportable through admin.exports.create (LIST key stock-movements, permission stock.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

StatusCodeWhen
400VALIDATION_FAILEDNo line, duplicate variant, quantity ≤ 0, receivedAt out of range
403PERMISSION_DENIEDCaller lacks stock.create; unitCost without pricing.view
404NOT_FOUNDUnknown warehouse or variant
422WAREHOUSE_INACTIVEThe warehouse is inactive

Rules & side effects

  1. All-or-nothing; one RECEIPT movement per line (physical += quantity), grouped by receiptId. unitCost is stored on the movement for valuation; it does not change the variant's purchasePrice.
  2. A product put OUT_OF_STOCK by the system goes back to ACTIVE when available becomes positive; resellers see « En stock » at their next read.
  3. Audit CREATE (entity StockMovement, label the supplier reference). SSE: stock.alert with level: "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

StatusCodeWhen
400VALIDATION_FAILEDdelta 0, both delta and newPhysical, negative newPhysical
403PERMISSION_DENIEDCaller lacks stock.update
404NOT_FOUNDUnknown warehouse or variant
409INVENTORY_SESSION_CONFLICTThe variant is in an OPEN or IN_REVIEW inventory session of this warehouse — count it there
422REASON_REQUIREDEmpty reason
422STOCK_BELOW_RESERVEDThe new physical would be below the reserved quantity — details: { lines: [{ sku, newPhysical, reserved }] }; release or modify the orders first
422WAREHOUSE_INACTIVEThe warehouse is inactive

Rules & side effects

  1. All-or-nothing; one ADJUSTMENT movement per line with the reason. Available reaching 0 sets the product OUT_OF_STOCK when every variant is out.
  2. Audit CREATE with 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.
  3. SSE: stock.alert for every level that changes alertLevel; notifications stock.low and stock.out (in-app, e-mail digest) to stock managers per the notifications matrix.

Used by BO-35 · BO-33

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

StatusCodeWhen
400VALIDATION_FAILEDMore than 1 000 items, negative threshold, duplicate pair
403PERMISSION_DENIEDCaller lacks stock.update
404NOT_FOUNDUnknown variant or warehouse

Rules & side effects

  1. All-or-nothing. Thresholds do not move stock (no movement row); alertLevel is recomputed. Audit one UPDATE row per changed StockLevel.
  2. SSE: stock.alert for each raised or cleared alert.

Used by BO-33 · BO-28

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

QueryTypeValues / defaultNotes
statusstring[]OPEN | IN_REVIEW | VALIDATED | CANCELLEDRepeatable
warehouseIdUuid
from / tostring (date)On openedAt
sort / order / page / pageSizeopenedAt (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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.view

Rules & side effects

  1. Read-only (the bundle's stock.update applies to writes; reading needs stock.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

QueryTypeValues / defaultNotes
linesstringALL (default) | UNCOUNTED | WITH_DIFFERENCE
qstringSKU, barcode, name
page / pageSizenumber1 / 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.view
404NOT_FOUNDUnknown session

Rules & side effects

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

StatusCodeWhen
400VALIDATION_FAILEDPARTIAL without lists, empty scope result
403PERMISSION_DENIEDCaller lacks stock.update
404NOT_FOUNDUnknown warehouse, category, brand or variant
409INVENTORY_SESSION_CONFLICTAnother OPEN or IN_REVIEW session of the warehouse overlaps the scope — details: { sessionId }
422WAREHOUSE_INACTIVEInactive warehouse

Rules & side effects

  1. One InventoryLine per non-archived variant in scope with expectedQty = physical at opening. Orders keep flowing (no stock freeze); manual adjustments on variants in scope are refused until the session ends.
  2. Audit CREATE on InventorySession.

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

StatusCodeWhen
400VALIDATION_FAILEDNegative quantity, neither variantId nor barcode
403PERMISSION_DENIEDCaller lacks stock.update
404NOT_FOUNDUnknown session
409INVENTORY_TRANSITION_NOT_ALLOWEDSession not OPEN
422VARIANT_NOT_IN_SCOPEVariant or barcode not in the session — details: { variantIds }

Rules & side effects

  1. No stock movement while counting. Audit: one UPDATE row per call (lines listed in changes).

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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.update
404NOT_FOUNDUnknown session
409INVENTORY_TRANSITION_NOT_ALLOWEDMove not allowed (VALIDATED and CANCELLED are final)
409VERSION_CONFLICTStale version
422REASON_REQUIREDRecount or cancellation without reason

Rules & side effects

  1. OPEN → IN_REVIEW freezes counts; uncounted lines stay null (decided at validation). CANCELLED posts nothing.
  2. Audit STATUS_CHANGE. No notification (the session list shows sessions IN_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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.validate
404NOT_FOUNDUnknown session
409INVENTORY_TRANSITION_NOT_ALLOWEDSession not IN_REVIEW
409VERSION_CONFLICTStale version
422INVENTORY_LINES_INCOMPLETEUncounted lines with uncounted: REJECT — details: { uncounted: 4 }

Rules & side effects

  1. For every line with a non-zero difference: INVENTORY movement of counted − (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.
  2. All-or-nothing; VALIDATED is final (a new session corrects a wrong count).
  3. Audit VALIDATE with the summary; product statuses and alertLevel recomputed; SSE stock.alert for 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

QueryTypeValues / defaultNotes
statusstringACTIVE | 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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.view

Rules & side effects

  1. Read-only (reading relaxed to stock.view for filters and pickers — Assumption). stockValueHt only with pricing.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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.view
404NOT_FOUNDUnknown warehouse

Rules & side effects

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

StatusCodeWhen
400VALIDATION_FAILEDCode not matching ^WH-[A-Z0-9]{2,10}$
403PERMISSION_DENIEDCaller lacks stock.administer
409DUPLICATE_VALUECode or name already used

Rules & side effects

  1. A second sellable warehouse is accepted only when stock.multiWarehouse = true (422 MULTI_WAREHOUSE_DISABLED otherwise). Audit CREATE.

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

StatusCodeWhen
403PERMISSION_DENIEDCaller lacks stock.administer
404NOT_FOUNDUnknown warehouse
409RESOURCE_IN_USEDeactivating a warehouse with physical stock, reservations or an open inventory session; unsetting the only default
409VERSION_CONFLICTStale version
422MULTI_WAREHOUSE_DISABLEDMaking a second warehouse sellable while stock.multiWarehouse = false

Rules & side effects

  1. Changing sellable changes reseller availability immediately (availability is the sum over sellable warehouses); products may switch between ACTIVE and OUT_OF_STOCK. Audit UPDATE.

Used by BO-37

On this page