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.
physicalQtyandreservedQtylive 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 receiveStockAvailability—IN_STOCKorOUT_OF_STOCK— and nothing else (STK-07). - Automatic movements (STK-03): reservation at confirmation (
VALIDATED), deduction atSHIPPED, 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 written | physical | reserved |
|---|---|---|---|
| Receipt of 200 | RECEIPT +200 | +200 | — |
| Order confirmed, 30 units | RESERVATION +30 | — | +30 |
| Same order shipped | RESERVATION_RELEASE −30, SHIPMENT −30 | −30 | −30 |
Order cancelled while READY | RESERVATION_RELEASE −30 | — | −30 |
| Return received, 2 resaleable | RETURN +2 | +2 | — |
| Inventory finds 4 missing | INVENTORY −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).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
code | String @db.VarChar(20) | — | — | Unique. |
name | String | — | — | — |
address | String | yes | — | — |
wilayaCode | String @db.VarChar(2) | yes | — | FK → Wilaya.code · Restrict. |
communeId | String @db.Uuid | yes | — | FK → Commune · Restrict. |
isDefault | Boolean | — | false | Warehouse used to reserve and ship when no other rule applies. |
isActive | Boolean | — | true | — |
archivedAt | DateTime | yes | — | Refused while physical or reserved stock remains. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
wilaya→ Wilaya (wilayaCode,onDelete: Restrict) ·commune→ Commune (communeId,onDelete: Restrict). - Referenced by: StockLevel.
warehouseId, StockMovement.warehouseId, InventorySession.warehouseId, Order.warehouseId. - Keys & indexes:
codeunique.
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).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
variantId | String @db.Uuid | — | — | FK → ProductVariant · Restrict. |
warehouseId | String @db.Uuid | — | — | FK → Warehouse · Restrict. |
physicalQty | Int | — | 0 | Units on the shelf. |
reservedQty | Int | — | 0 | Units promised to VALIDATED…READY orders (default reservation setting). |
alertThreshold | Int | yes | — | Back-office low-stock alert (STK-05) when available ≤ threshold; null → setting stock.defaultAlertThreshold. Never exposed to resellers. |
lastMovementAt | DateTime | yes | — | — |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @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_STOCKwhen the global available quantity is above 0, elseOUT_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_qtyinside the business transaction, so no optimisticversionis 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).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
variantId | String @db.Uuid | — | — | FK → ProductVariant · Restrict. |
warehouseId | String @db.Uuid | — | — | FK → Warehouse · Restrict. |
type | StockMovementType | — | — | — |
quantity | Int | — | — | 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. |
physicalAfter | Int | — | — | Balance after the movement (physical). |
reservedAfter | Int | — | — | Balance after the movement (reserved). |
refType | String @db.VarChar(32) | yes | — | ORDER, INVENTORY_SESSION, RETURN_REQUEST, TRANSFER, IMPORT. |
refId | String @db.Uuid | yes | — | Id of the source document (no FK: polymorphic). Transfers share one generated id for the OUT/IN pair. |
reference | String @db.VarChar(60) | yes | — | External document number (supplier delivery note on a receipt). |
reason | String | yes | — | Required for ADJUSTMENT (STK-06). |
actorUserId | String @db.Uuid | yes | — | FK → User · Restrict. null for system movements (order transitions). |
createdAt | DateTime | — | 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 confirmedVALIDATED→RESERVATION(+q);CANCELLEDorREFUSED→RESERVATION_RELEASE(−q) for what was reserved and, if quantities were already deducted,RETURN(+q) for them;SHIPPED→RESERVATION_RELEASE(−q) thenSHIPMENT(−q); return reception (admin.returns.receive) →RETURN(+restockQty) per ReturnItem withrefType = RETURN_REQUEST. - Ledger order is
createdAt, thenid(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).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
warehouseId | String @db.Uuid | — | — | FK → Warehouse · Restrict. |
name | String | — | — | — |
status | InventorySessionStatus | — | OPEN | — |
categoryId | String @db.Uuid | yes | — | FK → Category · Restrict. Optional scope. |
brandId | String @db.Uuid | yes | — | FK → Brand · Restrict. Optional scope. |
note | String | yes | — | — |
openedById | String @db.Uuid | — | — | FK → User · Restrict. |
validatedById | String @db.Uuid | yes | — | FK → User · Restrict. |
validatedAt | DateTime | yes | — | — |
cancelledAt | DateTime | yes | — | — |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @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 oneINVENTORYmovement 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.
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
sessionId | String @db.Uuid | — | — | FK → InventorySession · Cascade. |
variantId | String @db.Uuid | — | — | FK → ProductVariant · Restrict. |
expectedQty | Int | yes | — | Physical quantity read when the count was saved. |
countedQty | Int | yes | — | null = not counted yet. |
adjustmentQty | Int | yes | — | Delta actually posted at validation, recomputed against the physical quantity at that moment. |
countedById | String @db.Uuid | yes | — | FK → User · Restrict. |
countedAt | DateTime | yes | — | — |
note | String | yes | — | — |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @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"
}Catalog and pricing
Brands, the category tree, products and variants with their three customer-type prices, filterable attributes, visibility rules, favorites and price history; V2 price agreements.
Orders and delivery
Server-side cart, orders with snapshots and totals in centimes, status and change history, deliveries and carriers, and V1 returns.