# 🚩 **SPRINT 2 — DRAFT UPDATES FROM 2026-09-08**

> # **⬛ DRAFT — NOT YET FINALIZED ⬛**
> # **SPRINT 2 · FROM 2026-09-08**
>
> **Everything below is business behaviour that did NOT exist in the knowledge base before this period.**
> **Full business context for each item is already folded into `BUSINESS_KNOWLEDGE_BASE.md`, where the same items carry the inline flag 🚩 Draft · &lt;date&gt;.**
> **This register is a draft** — written directly from the code diff (no separate business sign-off pass like Sprint 1 had). Treat dates/framing as best-effort until reviewed.

| | |
|---|---|
| **Sprint** | **Sprint 2 (draft)** |
| **Updates from** | **2026-09-08** |
| **Code state documented** | branch `main`, up to 2026-09-16/17 (`7c4d737`) |
| **Documented on** | 2026-09-24 |
| **Previous baseline** | Sprint 1, code through 2026-08-25 (`ec29bb9`) — see `SPRINT_1_UPDATES_09-08-2026.md` |

**Note on "last week":** as of the documentation date (2026-09-24), the repository has had **zero commits in the literal preceding 7 days** — the last actual commit is 2026-09-16/17. This register covers the most recent *active* stretch of development instead: 2026-09-08 → 2026-09-17 (there were no commits at all between the Sprint 1 cutoff of 2026-08-25 and 2026-09-08).

---

## D1 — Ingredient creation & SKU sync from the mobile app
**Date:** 2026-09-08 → 2026-09-09 · **Where:** `Pos\IngredientController::IngredientCreationCC`, `ingredients.sku`, `ingredients.unit` enum

- New platform-authenticated endpoint **`POST /pos/ingredient/cc/ingredient-creation`** lets the platform push ingredient data into the POS: `updateOrCreate` **keyed on `sku`** (`name_en`, `name_ar`, `unit`).
- **`ingredients.sku`** (nullable string) added — **unlike `items.sku`, it carries no unique constraint**, so duplicate ingredient SKUs are possible.
- `unit` enum gained **`kg`** alongside the existing `pcs, gram, ml, liter`.
- The platform sends short unit codes (`gm`, `pcs`, `ml`, `kg`, `ltr`) which are remapped to the internal enum values. **Risk:** an unrecognized code is an *undefined-array-key* PHP warning, not a caught exception — it can silently store a `null` unit rather than failing loudly, unless the DB enum constraint itself rejects it.
- **No request validation** on this endpoint — name/sku presence isn't checked before the DB write.

---

## D2 — Platform can cancel its own mobile order
**Date:** 2026-09-10 · **Where:** `POST /pos/orders/mobile/cancel`, `OrderService::cancelMobileOrder`

The platform can now cancel an order it previously submitted: `order_id` + `type` locate the `ExternalOrder` (by `data->type` + `external_order_id`), then the underlying `Order` (scoped to `created_by = platform's related user`, bypassing branch scope). On match, **both** `status` and `operation_status` are set to `cancelled`.

**Watch out:** a lookup miss (wrong id, wrong type, or the order no longer belongs to that platform's user) **fails silently** — the endpoint returns `false` and logs it, with no error surfaced to the platform beyond that boolean. There's no guard against cancelling an order that's already `completed`/paid.

---

## D3 — Kitchen status "pending" renamed to "ready" (internal), "pending" kept as an API alias
**Date:** 2026-09-14 → 2026-09-17 · **Where:** `OperationStatusEnum`, `OrderRepository::kitchenOrderList`, `OrderController::kitchenChangeStatus`, `Order::inProgressToPendingStatusLog`

`OperationStatusEnum::PENDING` (stored value `'pending'`, code 3) is renamed to `OperationStatusEnum::READY` (stored value `'ready'`). Everything internal — the kitchen-list query, the activity-log lookup that powers `actual_order_time` (Sprint 1, A2) — now reads `ready`.

**Backward compatibility:** `kitchen-change-status` still accepts `pending` as an input value and silently remaps it to `ready` before it reaches the service, so existing POS/kitchen-app clients that send `pending` keep working unchanged. Only the *internal* vocabulary changed.

---

## D4 — Day close gains a third, non-overridable guard: undelivered mobile orders
**Date:** 2026-09-13 → 2026-09-16 · **Where:** `DailySalesRepository::dsr`

Alongside the existing "active orders" guard (overridable via `close_orders=1`) and Sprint 1's "pending inReview orders" guard (not overridable), there is now a **third guard, also not overridable**: if any in-scope order has an external-order row with `is_delivered = 0`, day close is refused —
*"There are many mobile orders that have not been marked as delivered. Please mark them as delivered so you can close the day."*

This checks **every** external order in scope regardless of financial status — an *approved* delivery order Noon hasn't finished yet (or that the cashier hasn't manually marked) also blocks the close. D6's auto-deliver-on-webhook reduces how often this needs a manual click, but there's currently **no override or escape hatch** if a delivery genuinely can't be marked delivered (failed dispatch, no linked outlet, etc.) — worth flagging operationally.

---

## D5 — Category `code` field; mobile item-creation now targets it (not `slug`)
**Date:** 2026-09-16 · **Where:** `categories.code`, `Pos\ItemController::ItemCreationMobile`

- New column **`categories.code`** (string, unique, nullable) — a platform-facing category identifier distinct from the existing menu-navigation `slug`.
- The mobile item-creation endpoint (`POST /pos/items/mobile/items-creation`, added in Sprint 1 as A11) now resolves the target category by **`category_code`** instead of `category_slug`. Same failure behavior as before: an unmatched code rolls back with `{status:false, message:"not found category in POS : <value>"}` on HTTP 200.

---

## D6 — Noon integration: error tracking, a real webhook bug fix, and automatic delivery handover
**Date:** 2026-09-16 → 2026-09-17 · **Where:** `NoonIntegrationService`, `order_trackers.error_msg`, `NoonWebhookRequest`, `VerifyNoonWebhookSignature`

Several changes land together here, all in the Noon delivery path:

1. **Dispatch failures are now recorded with a message.** `order_trackers` gained an `error_msg` column. Any non-`200`/non-`successful` response from Noon's create-task call now stores a tracker with status `error` and a human-readable message (parsed from Noon's `error`/`message`/`error_message`/`detail`/`errors` response keys, or the raw body). This is surfaced to the cashier on the external-orders list as `orderTrackerDetail.error`. Previously only a *thrown exception* produced an error tracker — a clean 4xx/5xx response with no exception left the order silently undispatched.
2. **Dispatch success check tightened:** now requires **both** HTTP 200 **and** body `status: 'successful'` (was just the body field, which could warn on a missing body).
3. **Real bug fixed in the inbound webhook handler:** `changeOrderStatusNoonWebHook` was reading a payload key `mp_task_nr` that the request validation never actually used — the validated (and presumably Noon-sent) field has always been `order_nr`. Every inbound webhook was silently dropped with a "no tracker found" warning until this line was corrected. **This means Noon status webhooks may not have been reliably processed before this fix** — worth checking `integration_logs` history around/before 2026-09-16 for gaps.
4. **Malformed validation rule fixed:** the webhook's `unique:order_trackers,provider_reference_number,provider` rule (previously flagged as malformed — wrong use of `unique`'s third parameter) is now `exists:order_trackers,provider_reference_number` — an honest "the tracker must already exist" check.
5. **Environment name correction:** the webhook's fail-open check for `local`/`testing` environments now checks `local`/`test` — this app's actual non-production environment name is `test`, so the old check likely never matched in practice.
6. **Automatic delivery handover:** when a webhook moves a tracker to `delivered` for the first time, the same transaction now also sets `external_orders_data.is_delivered = 1` and calls `OrderService::deliverExternalOrder()` — the platform's "completed" callback fires automatically, and the cashier no longer has to manually mark a Noon-delivered order as handed over. This directly reduces how often D4's new close-day guard trips.
   - Implementation note worth flagging: this reuses the **inbound webhook request object itself** (via PHP's case-insensitive function matching, `Request()` resolves to the global `request()` helper) with a property bolted on (`->id = $order->id`), rather than building a clean request/DTO for `deliverExternalOrder`. Works today; fragile if that method ever reads other request fields.

---

# What to verify before/after this period is reviewed

| # | Check | Why |
|---|---|---|
| 1 | Migrations applied: `sku`+`kg` on ingredients, `code` on categories, `error_msg` on order_trackers | Same silent-no-op risk pattern as Sprint 1 |
| 2 | Any POS/kitchen client hardcoded to expect `operation_status = 'pending'` in a *response* (not just sending it as input) | D3's rename is input-compatible but **response bodies now say `ready`**, not `pending` |
| 3 | `integration_logs` around 2026-09-08 → 2026-09-16 for a gap in processed Noon webhooks | D6.3 — webhooks may have been silently dropped before the `order_nr` fix |
| 4 | A branch that cannot get a delivery order marked `is_delivered` | D4 has no override — confirm this is an accepted operational risk or needs one |
| 5 | `ingredients.sku` duplicates across ingredients | No unique constraint (unlike `items.sku`) — confirm this is intentional |
| 6 | Mobile item-creation calls using `category_slug` (old param) | D5 changed the expected param to `category_code` — old-shaped requests will now 404-equivalent on "category not found" |
