# ๐Ÿšฉ **SPRINT 1 โ€” NEW UPDATES FROM 09-08-2026** > # **โฌ› THIS IS THE NEW-UPDATES REGISTER โฌ›** > # **SPRINT 1 ยท FROM 09-08-2026** > > **Everything below is business behaviour that did NOT exist in the knowledge base before this sprint.** > **Full business context for each item lives in `BUSINESS_KNOWLEDGE_BASE.md`, where the same items carry the inline flag ๐Ÿšฉ SPRINT 1 ยท 09-08-2026.** > **Known defects and load risks stay in `ISSUES_TRACKER.md`.** | | | |---|---| | **Sprint** | **Sprint 1** | | **Updates from** | **09-08-2026** | | **Code state documented** | branch `dev`, up to 2026-08-25 (`ec29bb9`) | | **Documented on** | 2026-08-29 | | **Previous KB baseline** | 2026-07-11 | --- # **PART A โ€” SPRINT 1 UPDATES (from 09-08-2026)** --- ## ๐Ÿšฉ A1 โ€” Kitchen preparation time on every order **Date:** 2026-08-09 โ†’ 2026-08-11 ยท **Where:** `items.preparation_time`, `orders.preparation_time`, `OrderService::calculateOrderPreparationTime`, `Helpers::orderTime` **What the business gets:** every ticket now carries how long the kitchen needs for it, and the screens show it as a clock. - Each menu item has a **preparation time in minutes** (`items.preparation_time`, default **10**, editable from the dashboard, 1โ€“1440). - The **order's** preparation time = the **slowest meal on the ticket**, scaled by how many meals it holds: - **1โ€“3 meals โ†’ ร—1** - **4โ€“8 meals โ†’ ร—2** - **9+ meals โ†’ ร—2.5** - rounded up. Free (price 0) components still count towards the meal count. - It is recomputed whenever the order is **edited**. - Rendered on every order payload as **`order_time`** in `MM:00` form (20 minutes โ†’ `"20:00"`). - Orders created before 2026-08-08 have no value and read `00:00`. **Watch out:** it is an *estimate only* โ€” nothing measures against it, and editing an order silently replaces the estimate the kitchen already saw. --- ## ๐Ÿšฉ A2 โ€” Measured order timings (`inProgressTime`, `actual_order_time`) **Date:** 2026-08-07 โ†’ 2026-08-09 ยท **Where:** `Order::inProgressStatusLog`, `Order::inProgressToPendingStatusLog`, order resources Built on the status activity log (Part B, B5): - **`inProgressTime`** โ€” the wall-clock time (`h:i:s A`) the order was last moved to kitchen status `inProgress`. Null until the kitchen starts it. - **`actual_order_time`** โ€” the `time_between` of the latest `inProgress โ†’ pending` kitchen transition: **how long the kitchen actually took**, as opposed to the estimate in A1. Both appear on the POS order list, the kitchen list and the external-orders tab. --- ## ๐Ÿšฉ A3 โ€” Loyalty-points orders from the mobile app **Date:** 2026-08-09 ยท **Where:** `PlatformSeeder`, `MobileOrderStoreRequest`, `OrderService::mapMobileOrderToStoreRequest` - A third external tender, **"Loyalty Points"**, is seeded and mapped to the Asparagus platform under the pivot type **`loyalty-points`**. - When the mobile app sends **`payed_with_loyalty_points = true`**, the routing type is **overridden to `loyalty-points`** regardless of the `type` field, so the order books against that tender and appears under it in every money report. - The redeemed **`points`** are stored in the order's external payload and shown to the cashier. **Business gap to close:** Dinex records the points but **owns no balance** โ€” it neither validates nor deducts them. Reconciliation lives entirely in the mobile app. **Also in this commit:** the platform seeder switched to `firstOrCreate` for the platform record, so re-seeding no longer rotates the Asparagus platform's credentials. --- ## ๐Ÿšฉ A4 โ€” Mobile orders are matched by SKU (not `mapper_slug`) **Date:** 2026-08-02 (contract), enforced through Sprint 1 ยท **Where:** `items.sku`, `MobileOrderStoreRequest`, `OrderService::mapMobileOrderToStoreRequest` - **`items.sku` (unique, nullable) is now the identifier the mobile app orders by.** An unknown SKU rejects the whole order: *"No item found with SKU: โ€ฆ"*. - `mapper_slug` survives only as the key used to *match* an existing POS item during the SKU sync, and for the MYOM placeholders (A5). - **`POST /v1/pos/items/mobile/check-mapper-slugs`** reconciles the two catalogs: for every platform item whose `mapper_slug` matches a POS item, it **writes the platform's SKU onto that POS item** and reports back what the POS holds. --- ## ๐Ÿšฉ A5 โ€” "Make your own meal" (MYOM) from the mobile app โ€” **temporary mapping** **Date:** 2026-08-13 โ†’ 2026-08-15 ยท **Where:** `order_items.custom_name`, `TempMYOMMapping` seeder, `OrderService`, `OrderItemResource` A composed meal ordered in the app arrives as lines carrying a **`group`** number, and is handled as follows: - The line is bound to a **seeded placeholder item `myom-N`** (`mapper_slug`), where **N counts composed meals within the same group** โ€” first โ†’ `myom-1`, second โ†’ `myom-2`, โ€ฆ - **The platform supplies the line's price and name.** The stored display name becomes **`"-"`** (e.g. `MYOM 1-Chicken bowl`), saved in the new column **`order_items.custom_name`** and shown instead of the item name everywhere the line appears. - `groupId` keeps the components of one meal together on the ticket. - **10 placeholder items** (`MYOM 1`โ€ฆ`MYOM 10`, price 0, category 49) are seeded by `TempMYOMMapping`. **Two business consequences, both important:** 1. **The server-side "the client cannot invent prices" guarantee does not hold for MYOM lines** โ€” the platform's price is trusted and flows straight into the invoice, the DSR items report and all revenue reports. 2. A composed meal is **not traceable to the components the customer actually chose** โ€” only a joined display name survives. **This is explicitly temporary** pending a real composed-meal model. Do not delete or renumber the `myom-*` items; a group holding more than 10 meals is rejected. --- ## ๐Ÿšฉ A6 โ€” Delivery instructions passed through to Noon **Date:** 2026-08-11 ยท **Where:** `external_orders_data.data['delivery_instructions']`, `NoonIntegrationService`, `ExternalOrderResource` The customer's delivery instructions from the mobile app are stored with the order, shown to the cashier on the external tab, and **sent to Noon as `delivery_notes`** on the create-task payload. Nothing sanitises or truncates the text before it reaches the courier. --- ## ๐Ÿšฉ A7 โ€” Meal plan and richer external-order context **Date:** 2026-08-08 โ†’ 2026-08-11 ยท **Where:** `external_orders_data.data`, `KitchenOrderResource`, `ExternalOrderResource` The stored platform payload now also carries **`plan`** (the customer's meal-plan / package name), **`type`** (the routing type) and **`points`**. The kitchen list shows `plan` **for external orders only**; the external tab shows plan, points, delivery instructions and the external order type. **Related fix (2026-08-11/12):** the `type` echoed back to the platform on every status callback is now read from this stored payload rather than derived from the payment-type pivot โ€” the platform gets back exactly the type it sent, which is what its own order matching keys on. --- ## ๐Ÿšฉ A8 โ€” Mobile orders are pinned to their own branch **Date:** 2026-08-10 ยท **Where:** `PlatformAuthMiddleware`, `BranchScope` A platform order belongs to the branch named by **`branch_slug`**, which is usually **not** the branch of the platform's related user. The middleware now resolves that branch, merges it into the request and **re-registers `BranchScope` pinned to it**. **What this fixed:** the order-number sequence for a mobile order was previously drawn from the related user's branch โ€” or from *every* branch at once when that user had no branch โ€” producing wrong or duplicated order numbers on mobile orders. **Same batch:** `BranchScope` now treats **admin/superadmin as branch-unscoped** (head office is cross-branch by design), an explicitly passed branch id applies to everyone, and the POS pre-login picker lists **branch staff only** (admins excluded). --- ## ๐Ÿšฉ A9 โ€” Best-selling item chart is real **Date:** 2026-08-10 ยท **Where:** `DashboardChartsRepository::topItemSelling` `topItemSellings` used to echo the top-*branch* payload. It is now a real query: **the item with the highest total quantity sold** in the period, excluding cancelled and returned orders, returned as `{item_name, total_quantity_sold}`. *(Still open in the same response: `totalSales` returns the delivered order **count** โ€” the front end must read `total_sales`.)* --- ## ๐Ÿšฉ A10 โ€” Dashboard period filter rework, incl. a real "day" **Date:** 2026-08-13 ยท **Where:** `ReportsRepository`, `DashboardChartsRepository` Applies to **every report and chart**: - **`day` is the business day, not the calendar day.** It now matches orders whose `related_date` is today **plus the day that is still open**, whichever calendar dates its orders were created on. *Previously `day` had no case of its own and fell through to `week` โ€” "today" reported a whole week.* - The open day is identified exactly as the POS closes one (`closed_at IS NULL`, `day_closed = 0`, `related_date IS NULL`) and never by `created_at`, so a shift running past midnight is not half-counted and the dashboard agrees with the DSR the branch prints. - The open day is added to **every** period, custom ranges included, **but only for branches whose open day actually belongs to that window** โ€” decided per branch by the day's **opening** order. A branch that opened on the 31st is no longer counted again in the next month. --- ## ๐Ÿšฉ A11 โ€” Platform can create menu items in the POS **Date:** 2026-08-25 ยท **Where:** `POST /v1/pos/items/mobile/items-creation`, `Pos\ItemController::ItemCreationMobile` The mobile app can now push an item the POS is missing: - `updateOrCreate` **keyed on `sku`** โ€” name, price, slug, `mapper_slug = slug`, `is_custom = 0`, `preparation_time = 10`, `created_by = 1`. - A newly created item gets the default image; the item is then **synced to exactly one category** resolved by `category_slug`. - An unknown `category_slug` **rolls the whole thing back** and answers `{status:false, message:"not found category in POS : "}`. **Watch out:** - The response is **HTTP 200 with a `status` flag**, not an error status code. - **Re-posting the same SKU overwrites the name, price and category of a live menu item** โ€” no approval step, no audit beyond `created_by = 1`. **Enabling fix in the same commit:** the role and permission middleware now read `$request->user()` (the authenticated user) instead of the request attribute `$request->user`, so `api.role` / `api.permission` also work on platform-authenticated routes. --- # **PART B โ€” DELIVERED EARLIER IN THE SAME CYCLE (2026-07-13 โ†’ 2026-08-08)** *Not part of the 09-08-2026 batch, but also new since the knowledge base baseline of 2026-07-11 โ€” and now folded into it with plain dated notes.* ## B1 โ€” Discounts can no longer produce a negative invoice **2026-07-13** ยท An `amount` discount larger than the order net now floors the subtotal at **0** (0 subtotal โ‡’ 0 VAT โ‡’ 0 total) instead of producing a negative invoice. The entered discount value is still recorded as given, so **a reported discount can exceed the sales it was applied to**. ## B2 โ€” Day close is blocked by pending external orders **2026-07-15** ยท Committing a day close is refused while any order is still `inReview`: *"There are pending external orders in review, please approve or reject them first"*. **Not overridable by `close_orders=1`** โ€” sweeping an unapproved mobile order into a closed day lost the order and left the customer's app without an outcome. Operational consequence: **one forgotten mobile order blocks the branch's day close.** ## B3 โ€” Seen / unseen badge on incoming mobile orders **2026-07-27** ยท New flag `external_orders_data.is_seen` (existing orders back-filled to seen) and endpoint **`POST /v1/pos/orders/change-is-seen-external-order`** (`order_ids[]`, `is_seen`) so the cashier's external tab can badge unread orders. The lookup deliberately ignores branch scope โ€” the flag belongs to the order, not the viewing branch โ€” and unknown ids or orders without external data raise an error rather than silently succeeding. ## B4 โ€” Order status activity log **2026-07-27** ยท New table `active_order_status_activity_logs`, written by an `OrderObserver`: two rows at creation (one per status axis) and one row per later change of `status` or `operation_status`, recording `from_status`, `to_status`, **`time_between`** (how long the previous status lasted) and `updated_by`. This is what makes A2 possible. *Limits:* `updated_by` is null for queue/console writes, and `time_between` is a TIME column, so a status lasting over 24 h does not record truthfully. ## B5 โ€” Order History report + invoice ZIP **2026-08-07** ยท Two new head-office endpoints: - **`GET /v1/dashboard/reports/orders-history`** โ€” the order-by-order ledger behind the aggregates: order number, branch, timestamp, business date, order type, cashier, all payment methods, status, net, discount, sub total, additional fees, delivery fee, VAT, total, invoice link. Shared period presets (`day|week|month|6month|year|custom`, default `month`) with the resolved window echoed back as `filters`; filters on `branches[]` and `status[]` (enum names **or** stored codes); paging `page`/`per_page` (default 25, max 500); `export=1` writes a CSV of the **whole** selection. - **`POST /v1/dashboard/reports/orders-history/invoices`** โ€” zips the invoice PDFs of the picked orders, or of every order matching the current filters. One folder per branch, `_order_.pdf`, non-ASCII branch names preserved. **Caps: 10 000 invoices per download, 100 missing invoices regenerated per request**, handles flushed every 1 000; anything still missing is reported in `missing_invoices`. **Risk to flag:** the ZIPs land on the public disk at guessable paths and bundle **customer names, phones and addresses**. ## B6 โ€” SKU on items + catalog reconciliation **2026-07-29 โ†’ 2026-08-02** ยท `items.sku` (unique, nullable) added, `ItemsSkuSeeder` back-fill, and the `check-mapper-slugs` endpoint (see A4). ## B7 โ€” Item preparation time (the input to A1) **2026-08-08** ยท `items.preparation_time` (default 10, `Item::DEFAULT_PREPARATION_TIME`), dashboard create/update validation `1..1440`, `ItemsPreparationTimeSeeder` back-fill, and the field exposed on the item resources. ## B8 โ€” Performance and correctness fixes carried in this cycle **2026-07-10** (tracked in `ISSUES_TRACKER.md`): outbound platform/Noon calls moved onto **queued jobs** (HL-1), order-number allocation locked (BL-8), Noon leftovers wired up โ€” polling now syncs, `cancelNoonOrder` recalls a cancelled delivery, webhook status regressions rejected (BL-9), approve/reject/deliver input validated (BL-10), the raw-exception leak in `storeMobileOrder` removed (BL-7), duplicate repositories deleted (DC-1). > โš  **Operational consequence of HL-1: a running queue worker is now a production dependency.** Without one (on a non-`sync` driver) mobile customers never learn their order's outcome and no courier is ever booked โ€” with no user-visible error. --- # **PART C โ€” WHAT TO VERIFY BEFORE / AFTER RELEASE** | # | Check | Why | |---|---|---| | 1 | Migrations applied: `sku`, `preparation_time` (items + orders), `custom_name`, `is_seen`, `active_order_status_activity_logs` | The features silently no-op or fatal without them | | 2 | Seeders run: `ItemsSkuSeeder`, `ItemsPreparationTimeSeeder`, `TempMYOMMapping`, `PlatformSeeder` (Loyalty Points), `ChangeIsSeenStatusExternalOrderTableSeeder` | SKU-less items cannot be ordered from the app; missing `myom-*` items reject composed meals | | 3 | Queue workers running for `default` **and** `noon` | Platform callbacks and Noon dispatch/recall depend on them | | 4 | `add_custom_name_to_order_items_table.php` has **no timestamp prefix** โ€” confirm it ran on every environment | Ordering on a fresh install is not guaranteed | | 5 | Day close on a branch with a pending `inReview` order | Confirm the new blocker is understood by branch staff โ€” there is no override | | 6 | Dashboard `day` period against the branch's printed DSR | The two must now agree, including across midnight | | 7 | A mobile MYOM order end to end | Confirm the composed name, the platform price and the group on the invoice | | 8 | `items-creation` with an existing SKU | Confirm the business accepts that it **overwrites** the live item's name, price and category |