# Company warehouse (مستودع الشركة) — feature brief

Input for writing the spec. Every decision below was agreed with the user (grilling session,
2026-10-29, revised 2026-10-31); nothing is implemented yet. Numbers in brackets (Q1…Q29) are the
questions they answer, kept so a reviewer can trace a rule back to its decision.

**Revision 2026-10-31: no order split.** The first version split a confirmed order with pieces
in both places into two orders (Q22, Q25-Q27). The supervisor confirmed that **every order is
gathered in our warehouse before it goes to the customer**, so pieces in two places are normal
and nothing has to be split: each order line only records how many of its pieces come from our
shelf (`order_details.warehouse_qty`). The split is kept for the record in section 4.

## 1. Problem

The company has a physical warehouse. Sellers leave some of their goods in it. When an order
arrives for a product we hold, those pieces are already in the warehouse, so the captain only
collects the rest from the seller's shop. Today nothing records what is on the shelves, so staff
recount the whole warehouse after orders take goods out. The goal: the system knows, per shop /
product / variant, how many pieces we hold, moves that number with the orders, and makes counting
a check instead of a full recount.

**The order flow does not change**: same orders, same statuses, same amounts; the seller still
confirms; stock is still taken on confirmation and given back on cancel / return / back to
pending. The warehouse is a layer on top of it.

## 2. Terms (proposed for `CONTEXT.md`)

- **Warehouse** — the company's storage. There is exactly one, so the system has no list of
  warehouses (Q1, revised 2026-10-31).
- **Warehouse Stock** — pieces of one shop's product variant held in the warehouse. Always a
  **part of** the seller's stock (`product_stocks.qty`), never extra (Q2).
- **Warehouse Movement** — one row per change of Warehouse Stock (fill, take out, count
  correction, order take, order give-back, return in, transfer). The stock is explained by its
  movements; nobody types over the number (Q7).
- **Warehouse pieces** of an order line — `order_details.warehouse_qty`: how many of the line's
  pieces come from our shelf. The rest come from the seller's shop. 0 = all from the shop (every
  line today).
- **Warehouse Order** — an order whose live pieces all come from the warehouse, so nothing has to
  be collected from the seller (how delivery uses that: deferred, 3.4).

The Child Order keeps its definition (one row per seller per checkout): no glossary change, no
ADR.

## 3. Decisions

### 3.1 Warehouse and its stock
1. **One warehouse, nothing to set up** (revised 2026-10-31): no `warehouses` table, no address,
   no map point, no settings screen. The warehouse simply exists; its stock is the
   `warehouse_stocks` table. The address / map point (Q23) was only for Logistic pickup and moves
   to the deferred 3.4. If a second warehouse ever comes, one migration adds a `warehouse_id`
   (default = the current one) to the stock, movement and count tables.
2. The warehouse is divided by shop. **Every shop can use it**: seller shops, partner stores
   (Dawana, CLOI, Itel) and the in-house store (Q12).
3. Quantities are **per variant** (a product without variants has one row) (Q3). Key the rows
   by `product_id` + variant text. That is what the rest of the system already keys by
   (`carts.variation`, `order_details.variation`, back-in-stock requests), so an order line finds
   its Warehouse Stock by text.
   - **Stock ids are stable now** (fixed 2026-10-29, in `dev` since 2026-10-31). Before the fix,
     every product form deleted and recreated every `product_stocks` row on each save, so the id
     changed every time. That was why this brief first ruled out keying by `product_stocks.id`.
     Now every form (admin, seller web, seller app, the three digital ones) goes through
     `ProductStockService::store()`, which matches the rows **by variant text** and updates them in
     place:
     - an existing variant keeps its row id;
     - a new variant gets a new row;
     - a variant the form no longer sends is deleted through the model, so stock events fire;
     - a second row of the same variant (old data) is deleted.

     The only `stocks()->delete()` calls left are:
     - product deletion (`ProductService::destroy`, `ProductController::deleteProduct`, seller web /
       app `destroy`, `SellerController::destroy`), which 3.1.7 refuses while the warehouse holds
       pieces;
     - partner "link existing product" (`PartnerCatalogWriter.php:364`), which replaces the rows
       once when a hand-entered product is linked to the partner.

     The Excel import only creates new products. So the spec **may also store
     `product_stock_id`** as a foreign key next to the text key, which makes joins easier. The text
     stays the key that order lines and carts use.
   - **A rename is still a removal**: `store()` matches rows by text, so renaming a value ("أحمر" →
     "احمر"), or adding or removing an attribute, changes the variant text. The old row is deleted
     and a new row is created. So a rename is refused while the warehouse holds pieces of the old
     variant, like Q15. The message says: take the pieces out, change the product, fill again.
     Because the refusal happens before the save, it can be one check in front of
     `ProductStockService::store()`, since all the forms call it. Carrying pieces across a rename
     would need `store()` to tell a rename apart from a removal plus an addition. That is not
     needed here; revisit it if renames turn out to be common.
4. Warehouse Stock is part of the seller's stock: filling the warehouse does **not** add
   sellable pieces (Q2). The seller cannot lower their stock below what we hold (web panel,
   seller app, admin form: refuse with a message).
   - **Three numbers per variant, two stored** (agreed 2026-10-31):

     | Number | Stored | Example |
     |---|---|---|
     | **Total for sale** (what customers can buy) | `product_stocks.qty`, as today | 5 |
     | **In Kapitano's warehouse** | `warehouse_stocks.qty` | 3 |
     | **At the seller's shop** | never stored: total − warehouse | 2 |

     The shop number is worked out, not stored: sellers edit the total themselves, and a stored
     third number would drift from the other two.
   - **What each movement does to them:**

     | Movement | Warehouse | Seller's shop | Total for sale |
     |---|---|---|---|
     | Fill (the seller brings 2 pieces from the shop) | 3 → 5 | 2 → 0 | 5, unchanged |
     | Take out, back to the seller's shop (2 pieces) | 3 → 1 | 2 → 4 | 5, unchanged |
     | Take out, damaged / lost (1 piece) | 3 → 2 | 2, unchanged | 5 → 4 |
     | Fill of 4 when the shop part is only 2 (Q11: the total rises to the warehouse amount) | 3 → 7 | 2 → 0 | 5 → 7 |
     | Order of 4 marked "جاهز للشحن" | 3 → 0 | 2 → 1 | 5 → 1 |

     Pieces that move between our warehouse and the seller's shop never change the total: raising
     it as well would count the same pieces twice (customers could buy 7 when 5 exist).
   - **All three are shown wherever stock is shown**: the warehouse stock page, the seller's
     warehouse view (3.5), and next to the quantity on the admin and seller product forms (and the
     seller inventory screen): "5 for sale: 3 in Kapitano's warehouse, 2 in your shop".
5. Filling more pieces than the site stock shows **raises the site stock** to the warehouse
   amount, recorded in the history (Q11).
6. Partner sync: after a sync the site stock is **the larger of the partner's number and the
   warehouse number**, so we can still sell what we hold when the partner says 0 (Q14). Touch
   points: `PartnerCatalogVariants::quantity()` / `PartnerCatalogWriter::syncStocks()`.
   Built 2026-11-01 (`WarehouseProductPaths`), with three rules the brief did not spell out: a
   variant the partner stops listing also keeps the pieces we hold on sale; a value the partner
   renames (same partner variant) carries its pieces to the new text; linking a hand-entered
   product is refused while we hold pieces of a variant the partner does not list (texts compared
   exactly, as on the forms), and the link, which hands the product to the partner's shop, writes a
   `transfer` row like a move.
7. Removing a variant that has Warehouse Stock, or deleting such a product, is **refused**
   until the pieces are taken out. Unpublishing stays allowed (Q15).
8. Product transfer (`ProductTransferService`, admin product lists → "transfer"): **move**
   carries the Warehouse Stock to the new owner's part (history row); **clone** starts at 0
   (Q16).

### 3.2 The warehouse section (admin)

Screens and rules agreed on 2026-10-29; take out, owner, shelf timing, seller view, cancelled shop pieces and the put back tick decided on
2026-10-31 (3.2.5). The table layout below is the proposal the spec starts from.

#### 3.2.1 Data

| Table | One row per | Columns |
|---|---|---|
| `warehouse_stocks` | product + variant | `product_id`, `variant` (`''` = no variants), `product_stock_id` (nullable link, 3.1.3), `qty` (pieces we hold), unique (`product_id`, `variant`) |
| `warehouse_movements` | change of a `warehouse_stocks.qty` | `warehouse_stock_id`, `type`, `qty_change` (+ / −), `qty_after`, `seller_id` (owner at that moment), `reason`, `note`, `order_id` / `order_detail_id` (order types), `count_id` (counts), `site_qty_before` / `site_qty_after` (when the move also changed `product_stocks.qty`), `user_id` (staff), `created_at` |
| `warehouse_counts` + `warehouse_count_lines` | count session / variant counted | session: shop, status (open / saved / cancelled), started and saved by / at; line: `warehouse_stock_id`, expected at start, counted, difference |

- **The shop is not stored on the stock row; it is the product's owner** (`products.user_id`).
  A product transfer **move** then carries its pieces to the new owner by itself, and a **clone**
  starts at 0 by itself (Q16). A `transfer` movement (change 0) records the owner change for the
  history. Movements keep `seller_id` so old history still reads right after a transfer.
- **The number is never typed over** (Q7): `qty` changes only by writing a movement, in one
  transaction with `lockForUpdate()` on the stock row (the same lock confirmation takes, section 4).
  `qty` = the sum of the row's movements; the nightly check (6.4) compares the two.
- **Movement types:**

  | Type | Change | Written by |
  |---|---|---|
  | `fill` | + | Fill screen |
  | `take_out` | − | Take out screen, with a reason (3.2.2.3) |
  | `count` | ± | Saving a count session |
  | `order_take` | − | Confirmation (3.3.2), line quantity raised (3.3.8) |
  | `order_give_back` | + | The line's warehouse pieces given back: back to pending, cancel, line quantity lowered or line deleted (3.6) |
  | `cancel_in` | + | The line's **shop** pieces of a cancelled order that were already brought into our warehouse (3.6) |
  | `return_in` | + | Return received, whatever the source (3.3.7, 3.6) |
  | `transfer` | 0 | Product transfer, move (owner change only) |

#### 3.2.2 Screens

All under a new admin menu section **Warehouse (المستودع)**.

**Telling variants apart on the shelf** (checked 2026-10-31): 2,336 products have variants
(5.9 on average, up to 105). Colours are stored as English CSS names ("CornflowerBlue-XL",
"PaleVioletRed-44", "DarkSlateGray"), which nobody at a shelf can match by name. So every screen
that lists variants (fill, take out, count and its printed sheet, To prepare and its pick list)
shows, next to the variant text:
- the **colour swatch** and the **variant's photo** (else the product's);
- the **SKU** when there is one (83% of variant rows have it).

The fill screen warns when a product has variants that differ only by capitals, spaces or
hyphens ("Black-38" / "black 38"), so pieces are not filled under the wrong one. None exist
today (re-checked 2026-10-31; an earlier count of 58 was products with two **identical** rows of
one variant, old data that the in-place product save removes the next time the product is
saved): the warning is a guard for new ones.

1. **Stock (main page)**: one row per variant held, filtered by shop and searchable by product.
   Columns: total for sale (`product_stocks.qty`), in the warehouse (`qty`), at the seller's shop
   (total − warehouse, 3.1.4), held for orders ready to ship (see 3.2.3), last movement. A total per shop. Excel export.
2. **Fill**: choose shop → search that shop's products → for each variant, its site stock and
   warehouse stock next to a quantity box; optional note; save writes one `fill` per variant.
   A fill that takes the warehouse above the site stock **raises the site stock** to it, after a
   confirmation that lists those variants (Q11); the movement keeps the site stock before / after.
   Then the printable receipt (6.6).
3. **Take out**: same picker; quantity ≤ what we hold; required reason (Q7) from a list, and the
   reason decides the site stock (decided 2026-10-31):
   - **back to the seller's shop**: the pieces move from our warehouse to the seller's shop, so the
     warehouse goes down and the seller's shop goes up by the same number; the total for sale
     (`product_stocks.qty`) does not change (3.1.4);
   - **damaged** / **lost**: `product_stocks.qty` goes down by the same number (the pieces no
     longer exist anywhere), through Eloquent inside `SellerStockUpdates::quietly()`; the movement
     keeps the site stock before / after. Who pays for them is the business question in section 6.

   No "other" reason (decided 2026-10-31): every piece that leaves either goes to the seller or no
   longer exists, so the reason alone decides the total. An optional note can be added to any take
   out. Then the printable receipt.
4. **Count a shop** (Q18): lists the shop's variants with a box for what is on the shelf; the
   expected number can be hidden while counting; printable as a count sheet. Starting a count
   stores each variant's expected number; saving writes one `count` movement per difference, and
   a difference needs a reason (Q7). Orders that move during the count must not be "corrected"
   back (section 4): the saved difference is *counted − (expected at start + movements of that row
   since the start)*.
   - **Expected = free + held** (6.1). Next to each expected number the screen lists the orders
     holding pieces of that variant: "3 expected: 0 free + 3 held by order #123, ready to ship since
     28/10".
   - **A shortfall from held pieces is not a correction.** The held pieces were already taken from
     `qty` at "جاهز للشحن", so a correction cannot take them again: a negative difference larger
     than the free number is **refused**, and the screen points to the holding orders instead
     ("found 0 — if #123 already left, move it to في الطريق"). Once the order is moved, the
     expected number drops and the count saves with no difference. Only the part of a shortfall
     that the free pieces cover is saved as a correction (`qty` never goes below 0).
5. **History** (Q7): every movement, filtered by shop, product, variant, type, staff member and
   dates; Excel export.
6. **To prepare** (Q29): orders **ready to ship** (جاهز للشحن) with warehouse pieces, one block
   per order (order code, date, product, variant, warehouse quantity); printable as a
   pick list. See 3.2.3. Below it, the **put back** list: pieces of cancelled orders that came
   back into the building and are not on the shelf yet; staff tick them "on the shelf" (3.6).

#### 3.2.3 When pieces leave the shelf: the order statuses we already have (decided 2026-10-31)

No new "pulled" mark: the warehouse follows the stages the delivery screen
(`OrderDeliveryController`, `OrderFollowUp::DELIVERY_STAGES`) already uses.

- **Taken at "جاهز للشحن"**: the Confirm Orders team's confirmation
  (`OrderFollowUpController::confirm`, `order_follow_ups.status = ready`) is what moves the order
  from `pending` to `confirmed`, and so what takes the stock (`OrderStockUtility`) and the
  warehouse pieces (3.3.1). The seller's own confirmation only stamps `seller_confirmed_at`.
- **On the shelf while ready to ship**: the order is in the stage "جاهز للشحن" while
  `delivery_status` is `confirmed` or `picked_up`. Its warehouse pieces are no longer in `qty` but
  still on the shelf: the stock page shows them as "held for orders ready to ship", "To prepare"
  lists them, and a count expects them (6.1).
- **Off the shelf at "في الطريق"**: once the order is `on_the_way`, its pieces have left.
  `delivered` counts as left too, for an order that jumps straight to delivered: in the status
  history (19/09-28/10) **10% of delivered orders never passed "في الطريق"** (137 of 1,367).
- **If staff forget "في الطريق"**: the system cannot see the shelf; it believes
  *on the shelf = free + held*, and a piece stops being "held" only when its order's status moves
  on. If the order leaves but its status stays `confirmed` / `picked_up`, the system still
  believes the pieces are on the shelf. Seller A, 3 × M in the warehouse, order #123 for 3 × M:

  | Day | Physically | Order status | Free | Held | System believes on the shelf | Really on the shelf |
  |---|---|---|---|---|---|---|
  | Mon | nothing yet | `pending` | 3 | 0 | 3 | 3 |
  | Mon | Confirm Orders marks "جاهز للشحن" | `confirmed` | 0 | 3 | 3 | 3 |
  | Tue | the 3 pieces leave with the captain | **still `confirmed`** (forgotten) | 0 | 3 | **3** | **0** |

  From Tuesday the difference of 3 is **the gap**. No piece is lost and no stock number is wrong:
  only the order's status is out of date. But "To prepare" still lists #123 (staff look for pieces
  that left), the stock page shows 3 "held", and the next count expects 3 and finds 0. If that −3
  were saved as a count correction, the same 3 pieces would be taken **a second time** (they
  already left `qty` on Monday). Two safeguards:
  - **Held too long**: the stock page and "To prepare" flag an order held longer than **N days**
    ("#123 held for 5 days — check whether it already left"). It reads `orders.confirmed_at`
    (stamped by `Order::saving` since 2026-10-28): still `confirmed` / `picked_up` and
    `confirmed_at` older than N days. N is a setting, **4 days** to start: from "جاهز للشحن" to
    "في الطريق" the median is 1 day, 90% of orders take at most 3.1 days, 95% at most 4 days, the
    longest 8 days (status history 19/09-28/10, 1,303 orders). So 4 days flags about 5% of
    orders; on 28/10, 1 of the 47 orders ready to ship was older (9.7 days). Re-check it later
    in Reports → Order Status Times (`OrderStatusTimes::matrix()`).
  - **The count** finds the gap and refuses to "correct" it (3.2.2.4): the fix is moving the order
    to "في الطريق", never a count correction, which would take the same pieces twice.
- **Coming back** (cancelled, back to pending, returned): see 3.6, by where the pieces came from.

#### 3.2.4 Permissions

New Staff Roles section "Warehouse": **view warehouse** (stock, history, to prepare, receipts)
and **manage warehouse** (fill, take out, count, tick "on the shelf"), given to "full access" only (Q9).

#### 3.2.5 Decided 2026-10-31

1. **Take out and the site stock**: back to the seller's shop moves pieces from the warehouse to
   the shop and leaves the total for sale alone; damaged / lost lower the total (3.1.4, 3.2.2.3).
   Every screen shows the three numbers: total for sale, in the warehouse, at the seller's shop.
2. **The owner comes from the product** (`products.user_id`), no shop column on the stock row
   (3.2.1).
3. **No "pulled" mark**: pieces leave the shelf at "في الطريق", as the existing statuses say
   (3.2.3).
4. **The seller sees their warehouse stock from rollout step 1** (3.5, 6.5): read only, so it can
   be shown before orders use the warehouse.
5. **Shop pieces of a cancelled order that already reached us go into our warehouse**
   (`cancel_in`, 3.6): cancelled after `picked_up` or after "في الطريق". The seller gets them
   back only through a take out "back to the seller's shop".
6. ~~**The "put back" tick** (3.6): pieces that come back from a cancelled order wait on the "put
   back" list until staff tick them "on the shelf"; until then a count shows them apart.~~
   Reversed 2026-11-01: no put back list (see 3.6).

### 3.3 Orders
1. The warehouse is used at **confirmation**, the same moment the seller's stock is taken today
   (`OrderStockUtility::moveLines()`), never at placement (Q4). In practice that is the Confirm
   Orders team marking the order "جاهز للشحن" (3.2.3); it covers every seller and the in-house
   store (Q19).
2. For each live line: `warehouse_qty = min(ordered, Warehouse Stock)`, stored on the line and
   taken from the warehouse; the rest comes from the shop (Q5). The seller's
   `product_stocks.qty` is taken as today for the whole line (warehouse pieces are part of it).
3. **The order is not split and no amount moves** (revised 2026-10-31, replaces Q22, Q25, Q26).
   A line with pieces in both places stays one line; an order with lines in both places stays one
   order. Price, tax, purchase price, coupon, shipping, seller balance, invoices and every report
   stay exactly as today: `warehouse_qty` only says where the pieces are.
4. The seller still confirms as before. If the seller marks a product **unavailable**, it is
   unavailable as today and the warehouse pieces stay on the shelf (no take). The seller's order
   page shows which pieces we hold and how many they must hand over (Q10).
5. **Back to pending** gives the line's `warehouse_qty` back to the warehouse and sets it to 0.
   Confirming again decides again from the Warehouse Stock of that moment; whatever the
   warehouse no longer holds comes from the shop (replaces Q27: no refusal needed, since nothing
   was split).
6. **Cancelled** lines and orders give their `warehouse_qty` back to the warehouse (Q8); shop
   pieces already brought into our warehouse go into it too. Every case: 3.6.
7. **Returned** goods go into the warehouse under that seller when staff mark the return
   received, **whether they came from the warehouse or from the shop** (Q8, Q17). Single return
   path: `OrderStockUtility::returnLines()`. Every case: 3.6.
8. **Staff changing a confirmed line** (quantity change, line delete — both through
   `OrderStockUtility`): a lower quantity keeps `warehouse_qty ≤ quantity`, giving the difference
   back to the warehouse; a higher quantity takes from the warehouse what it holds, the rest from
   the shop; a deleted line gives everything back.
9. Orders placed before `stock_on_confirm_since` (old rule, stock taken at placement) **never
   touch the warehouse** (Q28).
10. Customer order edits happen before the seller answers, so before any warehouse take: no
    change there.

### 3.4 Delivery and Logistic — deferred (2026-10-31)

Left for a later round, on the user's request. The warehouse section (3.1-3.3, 3.5) does not wait
for it: it records where each piece comes from and reads the order statuses we already have
(3.2.3). What is known so far, to start from later:

- Every order is gathered in our warehouse before it goes to the customer (supervisor,
  2026-10-31): the captain collects only the shop's pieces; the warehouse pieces join them there
  (Q6).
- Kapitano Logistic (on in production, Q20) takes one parcel per order with one pickup point,
  today always the seller's shop (`OrderPayloadMapper.php:67`, `ShopBranchResolver`), and every
  line as an item. Ideas to check with the Logistic team: an order with every piece in the
  warehouse picks up at the warehouse or nowhere (Q24; the warehouse's address / map point
  would be added then, Q23); a mixed order picks up at the
  shop with a note naming the pieces already in the warehouse.
- The delivery screens ("توصيل الطلبات", `OrderDeliveryController`) and the order pages show where
  each product comes from (Q21); the admin and seller order pages get it in the first version
  (3.5), the delivery screens later.

### 3.5 Seller side
- Seller panel: read-only view of their Warehouse Stock per product / variant with its history
  (Q13), from rollout step 1 (decided 2026-10-31).
- Their order list / page shows, per product, how many pieces we hold and how many they hand
  over.

### 3.6 Cancelled and returned orders, by where the pieces came from (decided 2026-10-31)

Each order line has **W** pieces from our warehouse (`warehouse_qty`) and **S** from the seller's
shop (the rest). So a line is one of three kinds:

- **warehouse only**: S = 0;
- **shop only**: W = 0 (every line today, and any line whose variant the warehouse did not hold);
- **both**: W and S above 0.

**One rule covers all of them: the books follow where the pieces physically are.**
- The **total for sale** moves exactly as today's stock rule moves it (`OrderStockUtility`):
  given back on a cancel at any status except after `delivered`, and on a return.
- The **warehouse** number goes up for every piece that is, or comes back, in our building: the
  W pieces (`order_give_back`), the S pieces once the captain brought them in (`cancel_in`), and
  every returned piece (`return_in`, Q17).
- The **seller's shop** number is total − warehouse, so it follows by itself: it goes up only for
  pieces that are really back at the seller.

| Moment the line is cancelled / returned | Where the pieces are | Warehouse | Total for sale | Seller's shop | "Put back" list |
|---|---|---|---|---|---|
| Cancelled before "جاهز للشحن" (`pending`) | nothing was taken | — | — | — | no |
| Cancelled, or sent back to pending, while ready to ship and **not collected** (`confirmed`) | W still on our shelf, S still at the seller | +W | +(W+S) | +S | no |
| Cancelled after the captain **collected the shop's pieces** (`picked_up`) | W on our shelf, S brought into our warehouse | +(W+S) | +(W+S) | — | the S pieces |
| Cancelled after **"في الطريق"** (`on_the_way`), the goods come back with the captain | all back in our warehouse | +(W+S) | +(W+S) | — | all W+S |
| Cancelled after **`delivered`** | with the customer | — | — | — | no: nothing comes back (today's rule); to bring goods back, staff use a return |
| **Returned** (customer return received or staff return), n pieces | back in our warehouse | +n | +n | — | no (received in hand) |

"Sent back to pending" from `picked_up` or `on_the_way` works like a cancel at that moment. A
line cancelled on its own while the order goes on follows the same table.

**Worked example per kind of line** (seller A, T-shirt M, an order for 3 × M):

| Kind | Before the order (total / warehouse / shop) | At "جاهز للشحن" | Cancelled while `confirmed` | Cancelled at `picked_up` or after "في الطريق" | Returned (all 3) |
|---|---|---|---|---|---|
| Warehouse only (W 3, S 0) | 6 / 3 / 3 | 3 / 0 / 3 | 6 / 3 / 3 | 6 / 3 / 3, 3 to put back if after في الطريق | 6 / 3 / 3 |
| Shop only (W 0, S 3) | 3 / 0 / 3 | 0 / 0 / 0 | 3 / 0 / 3 (never left the seller) | 3 / **3** / 0, 3 to put back | 3 / **3** / 0 |
| Both (W 2, S 1) | 6 / 2 / 4 | 3 / 0 / 3 | 6 / 2 / 4 | 6 / **3** / 3, 1 (or 3) to put back | 6 / **3** / 3 |

In bold: shop pieces that ended up in our warehouse. They stay ours to hold, like any returned
piece (Q17). If the seller wants them back, staff record a take out "back to the seller's shop".

**The "put back" list**: pieces given back by a cancel after they physically left our shelf or
the seller (the rows above). They are counted in the warehouse number at once, but staff still
have to put them on the shelf; until they tick them "on the shelf", the count screen shows them
apart ("3 expected, 3 still to be put back") and refuses a shortfall they explain, like held
pieces (3.2.2.4).

**Reversed 2026-11-01: no "put back" list.** Built, then dropped before release at the business's
request ("no need for this one"): pieces a cancel brings into our building are free warehouse pieces
at once, with no tick "on the shelf"; a count expects free + held only. The "Put back" column of the
table above no longer applies.

**Checked against the system (2026-10-31, local copy, status history 19/09-28/10)**:
- `picked_up` is set when the captain collects the goods at the pickup point, the seller's shop
  (Kapitano Logistic or the delivery boy app); from then on the goods are "with the captain"
  (`CancelGuard::IN_TRANSIT`). With Logistic on, the shop **refuses** to cancel an order in
  `picked_up` / `on_the_way` and sends staff to Kapitano dispatch, so a cancel at that stage
  happens only after dispatch has the goods back. The shop pieces therefore come back to us, not
  to the seller: the `picked_up` row stands.
- It happens: in those 6 weeks, `picked_up` → cancelled 22, `on_the_way` → cancelled 21,
  `on_the_way` → returned 36 (a failed delivery recorded as a return, the most common way goods
  come back), `delivered` → returned 8.
- Between `picked_up` and `on_the_way` the median is 2 hours, but a quarter of orders wait a day
  or more, which fits goods held in our building before they go out.

## 4. Things the spec must handle

- **Stock writes outside the seller**: warehouse fills that raise `product_stocks.qty` must run
  inside `SellerStockUpdates::quietly()` (not a seller update) and go through Eloquent so
  `ProductStockUpdated` / back-in-stock alerts fire.
- **Concurrency**: two confirmations racing for the last warehouse piece. Lock the warehouse row
  inside the confirmation transaction (as `OrderStockUtility` does for stock).
- **Counting while orders move**: a count saved after an order took pieces must not "correct"
  them back. Store the expected number at count start, or count against movements since then.
- **`warehouse_qty` on a line that is split later** (`ReturnRequests::splitLine()`, a partial
  customer return): the two lines share it so their sum stays the original. Returned pieces go
  into the warehouse whatever their source (3.3.7), so the share only matters for the record.
- Management read-only views (`sqlupdates/kapitano_reports_readonly_access_2026-10-04.sql`):
  decide whether to expose warehouse tables.
- Deploy: migrations (tables, `order_details.warehouse_qty`, permissions, Arabic strings in
  `translations.json`), then `php artisan cache:clear`. Nothing changes until a warehouse is
  created and filled.
- **Dropped alternatives, kept for the record**:
  - *Split at confirmation* (agreed 2026-10-29, dropped 2026-10-31): a mixed order became two
    orders of the same seller linked by `orders.split_from_order_id`, the line's price / tax /
    purchase price divided per piece, and every per-seller count and lookup in web, admin, seller,
    `Api/V2` and reports had to treat the two as one. Needed only if each parcel had to be picked
    up at one place and go straight to the customer. Not needed, since every order is gathered in
    the warehouse.
  - *Serve each order from one place only* (option A, 2026-10-31, never adopted): the warehouse
    used only when it holds every piece of the order. Superseded the same day for the same reason.
  - *One order, two Logistic parcels* (Q22b): rejected because it moves the risk into the
    Logistic layer.

## 5. Walkthrough (the expected behaviour end to end)

Seller A's T-shirt, size M: site stock **5**, of which **3** are in the warehouse. A customer
orders **4 × M** from seller A and one product from seller B.

1. **Placed** — checkout #500, order #123 (A, 4 × M), order #125 (B). No stock moves; the
   customer can still edit until the seller answers.
2. **Seller A confirms #123** (their order page notes "3 of these pieces are in Kapitano's
   warehouse — hand over 1"). Nothing moves yet, as today: the seller's answer is recorded.
   - Had the seller marked the T-shirt **unavailable**: cancelled as today, warehouse keeps 3,
     `warehouse_qty` stays 0.
3. **Confirm Orders marks #123 "جاهز للشحن"** (`pending` → `confirmed`). In the same transaction:
   - seller stock 5 → **1** (today's rule);
   - warehouse 3 → **0**, one movement row naming the order;
   - the line's `warehouse_qty` = **3**;
   - order, line, prices, totals: unchanged.
4. **Who sees what**
   - Customer: nothing changes — one order from seller A, same total, same pushes.
   - Seller A: one order, "1 from your shop, 3 from Kapitano's warehouse"; reports and dashboard
     as today; balance credits the purchase price of all 4 pieces.
   - Warehouse staff: #123 on "To prepare" (3 × T-shirt M); the stock page shows M:
     0 free + 3 held for orders ready to ship; a count of seller A expects 3 on the shelf.
   - Admin order page: where each product comes from.
5. **"في الطريق"** — #123 goes `on_the_way`: it leaves "To prepare" and a count now expects 0 on
   the shelf. (Delivery itself: deferred, 3.4.)
6. **Afterwards**
   - 2 pieces returned → warehouse 0 → 2 (whatever their source); seller stock and balance move
     back as today.
   - #123 cancelled while ready to ship → warehouse 0 → 3; the pieces never left the shelf.
   - #123 cancelled after "في الطريق" → warehouse 0 → 3; shown under "put back" until staff
     return the 3 pieces to the shelf.
   - #123 sent back to Confirm Orders (`pending`) → warehouse 0 → 3, `warehouse_qty` 0; marking it
     ready again takes again (fewer if the warehouse holds fewer by then; the rest from the shop).
7. **All in the warehouse** — had the customer ordered 3 × M: #123 is a Warehouse Order (every
   piece from the shelf). **None in the warehouse**: exactly as today.

**Cost**: the warehouse tables, one column on `order_details` (`warehouse_qty`), the warehouse
step in confirmation and in every give-back path, and the screens. No new orders, no amount
moves, no report changes.

## 6. Further recommendations (agreed 2026-10-29, updated 2026-10-31)

### Must have (wrong numbers or breakage otherwise)

1. **Counts expect the pieces of orders ready to ship.** The warehouse number drops at
   "جاهز للشحن", but the pieces stay on the shelf until "في الطريق" (3.2.3). The count screen
   expects *Warehouse Stock + `warehouse_qty` of live lines whose order is `confirmed` or
   `picked_up`*, and shows both numbers apart, or every count in that time writes a false
   correction. A shortfall the held pieces explain is refused, not corrected (3.2.2.4).
2. **A rename is a removal** — see 3.1.3.
3. **Old seller-app versions must not break.** The website refuses a stock below the warehouse
   amount with a message (Q2). The seller app in sellers' phones does not know that refusal, and
   an error could stop the whole product save. For `Api/V2/Seller`, **raise** the posted stock to
   the warehouse amount and answer with a note instead of refusing.
4. **Nightly consistency check** (scheduled command): every Warehouse Stock row has its variant
   and is ≤ the site stock, and its `qty` equals the sum of its movements; problems are listed on
   the warehouse page. Many paths rewrite `product_stocks` (forms, partner sync, import,
   zeroing), and this finds a broken one within a day instead of at the next physical count.

### Rollout

5. **Switch + pilot shops**, like Logistic's allow-list: orders use the warehouse only for chosen
   shops at first.
   - Step 1: warehouse screens (fill, take out, count, history, seller view, receipts). No effect
     on orders.
   - Step 2: orders take from the warehouse for one or two pilot shops.
   - Step 3: every shop.

### Cheap and useful

6. **Printed receipts** for goods in (fill) and out (take out): products, quantities, date, staff
   member, signed by both sides. The proof when a seller disputes a quantity.
7. **Slow-moving stock report**: pieces with no sale in 30 / 60 days, per seller, so staff can
   ask the seller to take them back.

### Business questions (not in the first version)

- Storage fees: if charged, they could later come out of the seller's balance.
- Liability for damaged / lost pieces: decides what a "damaged" take-out means for the seller.
- Faster delivery: warehouse products can arrive sooner; a "fast delivery" badge could sell them
  and bring sellers to the warehouse.

## 7. Code map (where this lands)

| Area | Files |
|---|---|
| Stock on confirm / cancel / return / line change | `app/Utility/OrderStockUtility.php` (`moveLines`, `movable`, `returnLines`, quantity change, line delete), `product_restock()` |
| Seller confirm | `app/Http/Controllers/Seller/OrderController.php::confirm`, WhatsApp accept page, seller app confirm |
| Line split by a partial return | `app/Services/ReturnRequests/ReturnRequests.php::splitLine` (shares `warehouse_qty`) |
| Logistic pickup / note (deferred, 3.4) | `app/Services/Logistic/OrderPayloadMapper.php` (`map`, `note`), `ShopBranchResolver.php`, `PickupPointService.php` |
| Product forms (variant removal / rename refusal) | `app/Services/ProductStockService.php::store()` (rows updated in place by variant text since 2026-10-29), product delete paths (`ProductService::destroy`, admin / seller / seller-app `destroy`) |
| Partner sync | `PartnerCatalogVariants::quantity`, `PartnerCatalogWriter::syncStocks` |
| Transfer | `app/Services/ProductTransferService.php` (`clone`, `move`) |
| Seller stock record | `app/Services/SellerStockUpdates.php` |
| Order pages (where each product comes from) | `backend/sales/show*.blade.php`, seller order page; delivery screens (`OrderDeliveryController`) deferred |
| Warehouse section (new) | admin controllers + views under `backend/warehouse/`, `App\Services\Warehouse\*` (stock, movements, counts), Staff Roles section |

## 8. Tests to pin

### Warehouse stock and movements

- `qty` always equals the sum of its movements and never goes below 0.
- Fill moves pieces shop → warehouse with the total unchanged; a fill above the total raises the
  total to the warehouse amount.
- Take out, back to the seller's shop: warehouse down, shop number up, total unchanged. Damaged /
  lost: the total goes down too. No other reason is accepted.
- The shop number is always total − warehouse.

### Counts

- Count corrections and the count screen; a count saved while an order took pieces does not
  correct them back.
- An order ready to ship is in what a count expects; it leaves that at "في الطريق", or when it
  jumps straight to `delivered`.
- A shortfall larger than the free number is refused, names the holding orders, and saves once
  the order is moved to "في الطريق".
- An order still ready to ship after N days (`confirmed_at`, 4 by default) is flagged; one
  younger is not.
- Variant rows on the fill, count and To prepare screens carry the colour swatch, photo and SKU;
  the fill screen warns on variants that differ only by capitals, spaces or hyphens.

### To prepare

- An order ready to ship with warehouse pieces is on "To prepare" and leaves it at "في الطريق".
- "Put back" lists the S pieces of an order cancelled at `picked_up` and all pieces of one
  cancelled after "في الطريق"; nothing for an order cancelled while `confirmed`; ticking "on the
  shelf" removes them from the list and from what a count shows apart.

### Products and sellers

- The seller cannot go below the warehouse amount (web, admin); the seller app posting a stock
  below it is raised, not refused.
- Variant removal and variant rename are refused while the warehouse holds pieces; a product save
  (price only) keeps the Warehouse Stock matched by variant text.
- Partner sync keeps the larger of the partner's number and the warehouse number.
- Transfer move writes a `transfer` row and the history keeps the old owner; transfer clone starts
  at 0.

### Orders

- Confirmation ("جاهز للشحن") sets `warehouse_qty` (all, part, none) and takes from the warehouse;
  the seller's own confirmation takes nothing.
- A mixed order keeps its lines, amounts, coupon, shipping and seller balance unchanged.
- Seller unavailable leaves the warehouse untouched.
- Back to pending and re-confirm.
- Every row of the 3.6 table, for each kind of line (warehouse only, shop only, both): cancelled
  at `pending`, `confirmed`, `picked_up`, `on_the_way`, `delivered`, and returned; warehouse,
  total and shop numbers as in the worked example.
- A staff quantity change or line delete on a confirmed line moves the warehouse.
- A partial return split shares `warehouse_qty`.
- Old-rule orders ignore the warehouse.
- Two concurrent confirmations for the last piece: only one gets it.

### Nightly check

- Reports an orphan row, a row above the total, and a `qty` that differs from its movements.

(Logistic payload tests: with 3.4, later.)
