HOCO B2B
SRSDatabase design

Stock

Warehouses, stock levels, the movement ledger and inventory sessions — physical, reserved and available quantities, back-office only.

Stock per variant and warehouse, and the ledger that explains every change (cahier §4.3, §8 — STK). Prisma file: stock.prisma.

  • Three numbers, two stored. physicalQty and reservedQty live on StockLevel; available = physical − reserved (cahier §8) is computed, never stored.
  • Back-office only. Quantities, thresholds and movements are visible only to stock.* permission holders. Resellers, guests and delegates receive StockAvailability — IN_STOCK or OUT_OF_STOCK — and nothing else (STK-07).
  • Automatic movements (STK-03): reservation at confirmation (VALIDATED), deduction at SHIPPED, release on refusal or cancellation (and re-entry of anything already deducted), re-entry at return reception — each posted in the same transaction as the order, return or inventory change, with the reference of that document.
  • Global or per warehouse (STK-01): the setting stock.mode (GLOBAL = one default warehouse, PER_WAREHOUSE) changes the screens, not the schema.
Business event (default settings)Movements writtenphysicalreserved
Receipt of 200RECEIPT +200+200—
Order confirmed, 30 unitsRESERVATION +30—+30
Same order shippedRESERVATION_RELEASE −30, SHIPMENT −30−30−30
Order cancelled while READYRESERVATION_RELEASE −30—−30
Return received, 2 resaleableRETURN +2+2—
Inventory finds 4 missingINVENTORY −4−4—

Tables

Warehouse

A stock location (STK-01). In global stock mode (setting stock.mode = GLOBAL) there is exactly one, the default warehouse. Table warehouses · written by inventory (admin.warehouses.crud, BO-37).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
codeString @db.VarChar(20)——Unique.
nameString———
addressStringyes——
wilayaCodeString @db.VarChar(2)yes—FK → Wilaya.code · Restrict.
communeIdString @db.Uuidyes—FK → Commune · Restrict.
isDefaultBoolean—falseWarehouse used to reserve and ship when no other rule applies.
isActiveBoolean—true—
archivedAtDateTimeyes—Refused while physical or reserved stock remains.
createdAtDateTime—now()—
updatedAtDateTime—@updatedAt—

Constraints and indexes Prisma cannot express (raw SQL in the migration):

CREATE UNIQUE INDEX warehouses_one_default ON warehouses (is_default) WHERE is_default AND archived_at IS NULL;

Example row:

{
  "id": "01a15db0-5680-7dc8-bb36-70c311a4c34e",
  "code": "ALG-BEZ",
  "name": "Dépôt Bab Ezzouar",
  "address": "Zone d'activité, Bab Ezzouar",
  "wilayaCode": "16",
  "communeId": null,
  "isDefault": true,
  "isActive": true,
  "archivedAt": null,
  "createdAt": "2026-10-21T09:00:00.000Z",
  "updatedAt": "2026-10-21T09:00:00.000Z"
}

StockLevel

Current quantities of one variant in one warehouse (STK-02). Table stock_levels · written by inventory (only writer, always together with a StockMovement).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
variantIdString @db.Uuid——FK → ProductVariant · Restrict.
warehouseIdString @db.Uuid——FK → Warehouse · Restrict.
physicalQtyInt—0Units on the shelf.
reservedQtyInt—0Units promised to VALIDATED…READY orders (default reservation setting).
alertThresholdIntyes—Back-office low-stock alert (STK-05) when available ≤ threshold; null → setting stock.defaultAlertThreshold. Never exposed to resellers.
lastMovementAtDateTimeyes——
createdAtDateTime—now()—
updatedAtDateTime—@updatedAt—
  • Relations: variant → ProductVariant (variantId, onDelete: Restrict) · warehouse → Warehouse (warehouseId, onDelete: Restrict).
  • Keys & indexes: @@unique([variantId, warehouseId]) · @@index([warehouseId]).
  • Derived, not stored: available = physicalQty − reservedQty (cahier §8). Not stored because it is a pure function of two columns that always change in the same statement: a third column could only drift. It is computed in SQL (expression index above) and in the API. It may be negative after an authorised over-stock validation (STK-08).
  • Derived, not stored: global stock of a variant = Σ over warehouses; the reseller-facing StockAvailability — IN_STOCK when the global available quantity is above 0, else OUT_OF_STOCK. Quantities never leave the back-office (decision of 17 Oct, STK-07): no reseller, delegate or guest response carries a number.
  • Concurrency: quantities change only through UPDATE stock_levels SET reserved_qty = reserved_qty + $q … WHERE id = $id RETURNING physical_qty, reserved_qty inside the business transaction, so no optimistic version is needed; the returned values become the movement's balances.

Constraints and indexes Prisma cannot express (raw SQL in the migration):

ALTER TABLE stock_levels ADD CONSTRAINT stock_levels_non_negative CHECK (physical_qty >= 0 AND reserved_qty >= 0);
-- low-stock and out-of-stock alerts (STK-05, BO-07)
CREATE INDEX stock_levels_available ON stock_levels ((physical_qty - reserved_qty));

Example row:

{
  "id": "01a18298-9480-7cf6-b4ac-fb61fc816b49",
  "variantId": "01a18298-9480-7f48-8522-52331c7620e9",
  "warehouseId": "01a15db0-5680-7dc8-bb36-70c311a4c34e",
  "physicalQty": 1240,
  "reservedQty": 85,
  "alertThreshold": 100,
  "lastMovementAt": "2026-11-08T14:05:00.000Z",
  "createdAt": "2026-10-28T13:00:00.000Z",
  "updatedAt": "2026-11-08T14:05:00.000Z"
}

StockMovement

Append-only stock ledger: every variation is journaled (cahier §4.3, STK-04). Table stock_movements · written by inventory (only writer; triggered by orders, fulfillment, inventory sessions, receipts, adjustments, V2 returns).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
variantIdString @db.Uuid——FK → ProductVariant · Restrict.
warehouseIdString @db.Uuid——FK → Warehouse · Restrict.
typeStockMovementType———
quantityInt——Signed delta of the bucket the type moves: reserved for RESERVATION (+) / RESERVATION_RELEASE (−); physical for every other type (RECEIPT +, SHIPMENT −, RETURN +, TRANSFER_OUT −, TRANSFER_IN +, ADJUSTMENT/INVENTORY ±). Never 0.
physicalAfterInt——Balance after the movement (physical).
reservedAfterInt——Balance after the movement (reserved).
refTypeString @db.VarChar(32)yes—ORDER, INVENTORY_SESSION, RETURN_REQUEST, TRANSFER, IMPORT.
refIdString @db.Uuidyes—Id of the source document (no FK: polymorphic). Transfers share one generated id for the OUT/IN pair.
referenceString @db.VarChar(60)yes—External document number (supplier delivery note on a receipt).
reasonStringyes—Required for ADJUSTMENT (STK-06).
actorUserIdString @db.Uuidyes—FK → User · Restrict. null for system movements (order transitions).
createdAtDateTime—now()—
  • Relations: variant → ProductVariant (variantId, onDelete: Restrict) · warehouse → Warehouse (warehouseId, onDelete: Restrict) · actorUser → User (actorUserId, onDelete: Restrict).
  • Keys & indexes: @@index([variantId, warehouseId, createdAt(sort: Desc)]) · @@index([refType, refId]) · @@index([type, createdAt]).
  • Enums: StockMovementType.
  • Automatic flows with the default settings (stock.reserveOn = VALIDATED, stock.deductOn = SHIPPED), all in the transaction of the business change (STK-03): order confirmed VALIDATED → RESERVATION (+q); CANCELLED or REFUSED → RESERVATION_RELEASE (−q) for what was reserved and, if quantities were already deducted, RETURN (+q) for them; SHIPPED → RESERVATION_RELEASE (−q) then SHIPMENT (−q); return reception (admin.returns.receive) → RETURN (+restockQty) per ReturnItem with refType = RETURN_REQUEST.
  • Ledger order is createdAt, then id (UUIDv7 is time-ordered).

Constraints and indexes Prisma cannot express (raw SQL in the migration):

ALTER TABLE stock_movements ADD CONSTRAINT stock_movements_non_zero CHECK (quantity <> 0);

Example row:

{
  "id": "01a1bb7a-0ae0-7d69-92ae-77c64156bcf1",
  "variantId": "01a18298-9480-7f48-8522-52331c7620e9",
  "warehouseId": "01a15db0-5680-7dc8-bb36-70c311a4c34e",
  "type": "RESERVATION",
  "quantity": 20,
  "physicalAfter": 1240,
  "reservedAfter": 85,
  "refType": "ORDER",
  "refId": "01a1ac4b-a100-74df-8237-614783cfeacd",
  "reference": null,
  "reason": null,
  "actorUserId": null,
  "createdAt": "2026-11-08T14:05:00.000Z"
}

InventorySession

A physical count of a warehouse, or of one category/brand in it (STK-06). Table inventory_sessions · written by inventory (admin.inventory.*, BO-36).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
warehouseIdString @db.Uuid——FK → Warehouse · Restrict.
nameString———
statusInventorySessionStatus—OPEN—
categoryIdString @db.Uuidyes—FK → Category · Restrict. Optional scope.
brandIdString @db.Uuidyes—FK → Brand · Restrict. Optional scope.
noteStringyes——
openedByIdString @db.Uuid——FK → User · Restrict.
validatedByIdString @db.Uuidyes—FK → User · Restrict.
validatedAtDateTimeyes——
cancelledAtDateTimeyes——
createdAtDateTime—now()—
updatedAtDateTime—@updatedAt—
  • Relations: warehouse → Warehouse (warehouseId, onDelete: Restrict) · category → Category (categoryId, onDelete: Restrict) · brand → Brand (brandId, onDelete: Restrict) · openedBy → User (openedById, onDelete: Restrict) · validatedBy → User (validatedById, onDelete: Restrict).
  • Referenced by: InventoryLine.sessionId.
  • Keys & indexes: @@index([warehouseId, status]).
  • Enums: InventorySessionStatus (proposed).
  • Validation (stock.validate) posts one INVENTORY movement per line whose difference is not 0, in one transaction.

Constraints and indexes Prisma cannot express (raw SQL in the migration):

CREATE UNIQUE INDEX inventory_sessions_one_open ON inventory_sessions (warehouse_id) WHERE status = 'OPEN';

Example row:

{
  "id": "01a1de1c-ecc0-71a5-9f71-18662eb6c7fa",
  "warehouseId": "01a15db0-5680-7dc8-bb36-70c311a4c34e",
  "name": "Inventaire câbles — novembre 2026",
  "status": "VALIDATED",
  "categoryId": "01a15db0-5680-7eea-aabe-b4ba8c5fd08d",
  "brandId": null,
  "note": null,
  "openedById": "01a19f89-c802-701b-83ec-271e6091a1ed",
  "validatedById": "01a19f89-c802-701b-83ec-271e6091a1ed",
  "validatedAt": "2026-11-15T16:10:00.000Z",
  "cancelledAt": null,
  "createdAt": "2026-11-15T07:30:00.000Z",
  "updatedAt": "2026-11-15T16:10:00.000Z"
}

InventoryLine

The count of one variant in an inventory session. Table inventory_lines · written by inventory.

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
sessionIdString @db.Uuid——FK → InventorySession · Cascade.
variantIdString @db.Uuid——FK → ProductVariant · Restrict.
expectedQtyIntyes—Physical quantity read when the count was saved.
countedQtyIntyes—null = not counted yet.
adjustmentQtyIntyes—Delta actually posted at validation, recomputed against the physical quantity at that moment.
countedByIdString @db.Uuidyes—FK → User · Restrict.
countedAtDateTimeyes——
noteStringyes——
createdAtDateTime—now()—
updatedAtDateTime—@updatedAt—
  • Relations: session → InventorySession (sessionId, onDelete: Cascade) · variant → ProductVariant (variantId, onDelete: Restrict) · countedBy → User (countedById, onDelete: Restrict).
  • Keys & indexes: @@unique([sessionId, variantId]).
  • Derived, not stored: difference shown during the count = countedQty − expectedQty.

Example row:

{
  "id": "01a1de7a-4f00-7431-905e-17b23cd344b9",
  "sessionId": "01a1de1c-ecc0-71a5-9f71-18662eb6c7fa",
  "variantId": "01a18298-9480-7f48-8522-52331c7620e9",
  "expectedQty": 1240,
  "countedQty": 1236,
  "adjustmentQty": -4,
  "countedById": "01a19f89-c802-701b-83ec-271e6091a1ed",
  "countedAt": "2026-11-15T09:12:00.000Z",
  "note": "2 câbles abîmés retirés",
  "createdAt": "2026-11-15T09:12:00.000Z",
  "updatedAt": "2026-11-15T16:10:00.000Z"
}

On this page