# Vendor & Storefront build plan

> Living document. Update the **Status** column as items are completed.
> Status legend: `TODO` · `IN PROGRESS` · `DONE` · `DEFERRED`
>
> **Source of truth for product scope:** `PRD_FoodStack_v2.md` (Sections 4.x state machines, 5.x features, 6.4 vendor page inventory, 7.4 real-time, 8.x AI). Citations of the form *(PRD §x.y)* in this doc point back to that file.

---

## 0. Where we are today

### Vendor backend — already built
| Page | Route | Notes |
| --- | --- | --- |
| Dashboard | `vendor.dashboard` | KPIs, setup checklist, recent orders, top items, low stock, sparkline |
| KYC | `vendor.kyc.index` | Submit / delete documents |
| Profile show + edit | `vendor.profile.*` | Owner profile only |
| Menu categories CRUD | `vendor.categories.*` | |
| Menu items CRUD + toggle | `vendor.items.*` | Has availability toggle |

### Storefront — already built (skeleton)
| Page | Route | Notes |
| --- | --- | --- |
| Storefront home | `storefront.home` | Vendor landing on subdomain |
| Storefront menu | `storefront.menu` | Browse items by category |
| Item detail | `storefront.item` | Single item page |

### Customer area — already built (skeleton)
| Page | Route | Notes |
| --- | --- | --- |
| Dashboard | `dashboard` | Basic shell |
| Orders list | `customer.orders` | List of past orders |
| Order detail | `customer.orders.show` | Read-only |
| Profile edit | `profile.edit` | Built-in Breeze profile |

### Models that already exist (and matter to this plan)
`Order`, `OrderItem`, `Payment`, `Refund`, `MenuCategory`, `MenuItem`, `OfflineSale`, `InventoryLog`, `Customer`, `ChatConversation`, `ChatMessage`, `AIUsageLog`, `AIReport`, `VendorSubscription`. The PRD §7.5 / §7.6 schema is therefore mostly *already there* — most work is wiring UI, services, and contracts on top.

### Models we'll add as the plan ships
`VendorOperatingHours` (A.1) · `VendorDeliveryZone` (A.1) · `VendorExpense` (**A.6 — new, vendor-private, no admin involvement**) · `Cart`, `CartItem` (B.1) · `Review`, `CustomerAddress` (Phase D).

---

## 1. Sequencing rationale

The four phases below are ordered by dependency, not by importance:

- **Phase A** (Vendor store setup) gives a vendor the controls to configure their store. No live orders required.
- **Phase B** (Storefront purchase flow) is the big one — it produces the *real orders* the rest of the system reacts to.
- **Phase C** (Vendor live ops) only matters once orders exist, so it depends on Phase B.
- **Phase D** (Customer post-order polish) only matters once orders are completed, so it depends on Phase C.

We can park items inside a phase as `DEFERRED` if they aren't pulling weight yet (e.g. multi-vendor cart, AI weekly reports).

> **Out of plan for v1:** vendor staff invitations / role matrix (PRD §5.7) — owner-only operation for v1. Spatie roles `vendor_manager`, `vendor_kitchen`, `vendor_cashier` exist in code but no UI to invite or manage them.

---

## 2. PHASE A — Vendor store setup

**Goal:** A vendor can fully configure their storefront so it's ready to take its first real order. *(PRD §5.1, §5.3, §5.6, §5.10)*

### A.1 Settings hub & branding

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A1.1 | **Vendor settings hub** (`vendor.settings.index`) — Alpine-tabbed page (Branding · Identity · Fulfillment · Hours · Tax · Publish · Bank), 4-tile status snapshot, hash-deep-linkable tabs | DONE | §5.1, §6.4 (`/vendor/storefront`) | `Vendor\SettingController` + view |
| A1.2 | **Branding tab** — logo upload (with live preview + remove), cover image upload (3:1 with preview), tagline (160 char), brand colour picker with live swatch, storefront description | DONE | §5.1 | Image uploads via `Storage::disk('public')` |
| A1.3 | **Business identity tab** — business name, type (individual/business), CAC/registration number, cuisine, currency, timezone, website, Instagram/Facebook/X handles | DONE | §5.1 | Reuses existing `vendors` columns |
| A1.4 | **Fulfillment defaults tab** — pickup/delivery card toggles (validates ≥1 enabled), prep time minutes (1-240), minimum order amount, default delivery fee, delivery radius | DONE | §5.1, §5.2 | New columns: `accept_pickup`, `accept_delivery`, `prep_time_minutes` |
| A1.5 | **Operating hours tab** — 7-day grid, per-day open toggle + 24h time inputs, persists to existing `business_hours` JSON column | DONE | §5.1, §4.4 | View + JSON column (deferred dedicated table to v2) |
| A1.6 | **Open/closed quick toggle** — green/amber button in settings hero strip, flips `vendors.is_open` with a flash message, audited via activity logs | DONE | §5.1, §4.4 | `vendor.settings.toggle-open` action |
| A1.7 | **Delivery zones** — per-zone fee, area definition (city/postal/polygon), min order per zone | TODO | §5.10 (delivery context) | New `vendor_delivery_zones` table + tab |
| A1.8 | **Bank account / Paystack subaccount tab** — bank fields, "verify account name" via Paystack name-resolution, create subaccount | TODO | §5.10 | View + service call (currently a read-only stub linking to profile editor) |
| A1.9 | **Tax & service charges tab** — VAT %, service charge %, packaging fee, with a "how the math works at checkout" cheatsheet | DONE | §5.10 | New columns: `tax_percentage`, `service_charge_percentage`, `packaging_fee` |
| A1.10 | **Publish tab** — slug editor with live `https://<slug>.<domain>` preview + uniqueness validation, closed-message editor, "going-live needs three things" reminder banner | DONE | §5.1 | Tab + slug regex validation |

### A.2 Menu extensions (beyond CRUD already built)

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A2.1 | **Duplicate item** — clone existing item as a draft | TODO | §5.3, §6.4 (`/menu/items/{item}/duplicate`) | Action route + button |
| A2.2 | **Bulk restock / start-of-day reset** — multi-select reset of `stock_qty` and `is_available` | TODO | §5.3, §6.4 (`/menu/bulk-restock`) | Page + action |
| A2.3 | **Daily-special flag** — `is_daily_special` column + storefront highlight | TODO | §5.3 | Migration + UI |
| A2.4 | **Menu cache invalidation** — menu mutations bust the AI snapshot cache and refresh in ≤30s | TODO | §4.5, §5.3 | Eloquent observer / event |

### A.3 Inventory

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A3.1 | **Stock levels overview** (`vendor.inventory.index`) — KPI strip, filter chips (all/tracked/untracked/low/out), search, per-row Adjust modal (delta or set, source = restock/adjustment/waste/return), start/stop tracking | DONE | §5.6, §6.4 | View |
| A3.2 | **Inventory history** (`vendor.inventory.history`) — filter by item / source / date, full audit feed of `inventory_logs` | DONE | §5.6, §6.4 | View |
| A3.3 | **Bulk restock** (`vendor.inventory.bulk` / `bulk.update`) — start-of-day reset, "add" or "set" mode, one form, audited | DONE | §5.6 | View + action |
| A3.4 | **Auto sold-out at zero** — when `track_stock` and `stock_qty == 0`, optionally flip `is_available=false` | TODO | §5.6 | Service hook |
| A3.5 | **Low-stock daily digest email** | TODO | §5.6 | Scheduled job + Mailable |

### A.4 Vendor AI / chatbot config

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A4.1 | **Chatbot settings** (`vendor.chatbot.settings`) — greeting, closed-state message, minimum order, dietary tags | TODO | §5.2, §6.4 | View + form |
| A4.2 | **AI usage panel** — current month tokens / cost vs ceiling, lite-mode status | TODO | §8.3 | Reads `ai_usage_logs` |

### A.5 Notifications preferences

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A5.1 | **Vendor notifications page** (`vendor.notifications.index`) — toggle channels (email, browser push, sound) per event | TODO | §5.4, §6.4 | View + simple JSON column |

### A.6 Expenses tracker (vendor-private)

**Why here:** independent of orders/payments — vendors can start tracking from day one, even before going live. **Admin has zero involvement**; data never leaves the vendor's account, never feeds platform reports, never affects commission.

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| A6.1 | **Schema** — `vendor_expenses`: one row per line item (vendor_id, recorded_by_user_id, expense_number, expense_date, item_name, qty, unit, unit_price, total, category, payment_method, supplier, notes, timestamps + softDeletes) | DONE | (vendor-only addition) | Migration + `VendorExpense` model with `BelongsToVendor` |
| A6.2 | **Expenses ledger** (`vendor.expenses.index`) — grouped by date, KPI strip (today/week/month/all), top-categories bar chart for current month, filter by date range / category / supplier / search | DONE | — | View |
| A6.3 | **Quick-log form** (`vendor.expenses.create`) — date defaults today, live `qty × unit_price` total, **category is a free-text input with `<datalist>` suggestions** (built-in + ones the vendor has typed before), supplier datalist, "save and log another" toggle | DONE | — | View + controller |
| A6.4 | **Expense detail** (`vendor.expenses.show`) — read-only view of the line, soft-delete confirm | DONE | — | View |
| A6.5 | **Edit expense** (`vendor.expenses.edit`) — full edit, audited via `activity_logs` | DONE | — | View + controller |
| A6.6 | **Soft delete** (`vendor.expenses.destroy`) — keeps the row recoverable | DONE | — | Action |
| A6.7 | **Dashboard "Profit this week" card** — Revenue (online+offline paid) · Expenses this week · Net + margin %, deep-link to expense ledger / log expense | DONE | — | Dashboard widget |
| A6.8 | **Profitability section** on `vendor.analytics.index` — *Net = Revenue (online + offline) − Expenses*, with a "by category" breakdown | TODO | (extends C.2 analytics) | Analytics widget |

**Field-level decisions (locked):**
- **Schema shape:** one row per line item — matches how vendors think and log ("₦1000 pepper, ₦8000 turkey on Monday" → 2 rows, both dated Monday).
- **Receipts:** no receipt upload in v1 — keeps the form light. Easy to add later as a nullable `receipt_path` column.
- **Categories:** free-text field with a `<datalist>` suggestion list of `ingredients · packaging · utilities · staff · rent · transport · marketing · equipment · other`. Vendor can type anything else.
- **Admin involvement:** none. Model is `BelongsToVendor`-scoped, no admin route, no admin sidebar entry, never appears in `admin.*` pages or platform-level reports.
- **Multi-currency:** locked to `vendor.currency` per row; cross-currency tracking is deferred.

**Future hook (not on the v1 roadmap):** if AI weekly reports later want benchmark sentences like *"Your ingredient spend is 35% of revenue, vendors of your size average 28%"*, a nightly job will roll expenses into an **anonymous, vendor-id-stripped** `expense_aggregates` table grouped by `(category, vendor_size_bucket, week)`. Admin/AI then read aggregates only — never `vendor_expenses` directly.

**Pre-flight before Phase A:** review `vendors` table for already-existing columns we plan to reuse so we don't duplicate (`is_open`, `business_hours`, `commission_percentage`, `bank_*`, `paystack_subaccount_code`, `ai_lite_mode`).

---

## 3. PHASE B — Storefront purchase flow

**Goal:** A visitor on `slug.foodstack.africa` (or `slug.localhost:8000` in dev) can browse → cart → register/guest → checkout → pay → receive confirmation → track the order. *(PRD §5.10, §4.1–4.3, §7.4)*

### B.1 Cart

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| B1.1 | **Schema** — `carts` (id, user_id nullable, session_token nullable, vendor_id, currency, totals snapshot) and `cart_items` (cart_id, menu_item_id, qty, unit_price, modifiers JSON) | TODO | §5.10 | 2 migrations + 2 models |
| B1.2 | **Cart resolver service** — returns active cart for `auth()->id()` or `session('cart_token')`, lazy-creates one | TODO | §5.10 | `App\Services\CartService` |
| B1.3 | **Add-to-cart action** — Alpine button on `storefront.menu` and `storefront.item`, posts to `storefront.cart.add` | TODO | §5.10 | Controller + JS |
| B1.4 | **Mini-cart drawer** — slide-out cart in storefront layout, count badge | TODO | §5.10 | Layout partial |
| B1.5 | **Cart page** (`storefront.cart`) — line-items, qty +/-, remove, subtotal, fulfillment-type picker | TODO | §5.10 | Full page |
| B1.6 | **One-vendor-per-cart guard** — switching vendors clears the cart with a confirm prompt | TODO | §5.10 | Service-level rule |

### B.2 Checkout

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| B2.1 | **Step 1 — Contact** — name, email, phone; auto-filled if logged in; "log in or guest" branch | TODO | §5.10 | Form |
| B2.2 | **Step 2 — Fulfillment** — pickup time slot OR delivery address with zone validation (uses A1.7) | TODO | §5.10 | Form |
| B2.3 | **Step 3 — Payment method** — only show gateways enabled in admin settings | TODO | §5.10 | Form |
| B2.4 | **Review & place** — reads cart, calculates fees/tax (uses A1.9), creates `order` + `payment` (status `pending`) | TODO | §5.10, §4.1 | Controller |
| B2.5 | **Server-side recompute** — never trust the browser; recalculate totals from cart_items + vendor settings on submit | TODO | §5.10 | Service |

### B.3 Payments (Paystack subaccount split)

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| B3.1 | **Paystack initialize with subaccount** — funds settle to vendor's subaccount, platform commission auto-deducted at gateway | TODO | §5.10 | `PaystackService::initialize()` |
| B3.2 | **Paystack callback / webhook** — verify reference, mark `payment` paid, `order.payment_status` paid, `food_status` pending | TODO | §5.10, §4.3 | Webhook + return-URL handler |
| B3.3 | **Failure / cancel return** — handle declined / cancelled with retry option | TODO | §5.10 | View + controller |
| B3.4 | **Idempotency** — avoid double-marking on webhook + return URL race | TODO | §5.10 | Lock or DB unique on reference |
| B3.5 | **Refund flow uses subaccount balance** — surface "insufficient subaccount balance" state when applicable | TODO | §5.10 | Admin refund controller hook |

### B.4 Post-order

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| B4.1 | **Order confirmation page** — `storefront.order.confirm/{reference}` with thank-you + tracking link + estimate | TODO | §5.10 | View |
| B4.2 | **Public order tracking** — `storefront.order.track/{token}` (no auth, public token already on `orders`) with live status | TODO | §5.10, §6.2 | View + lightweight polling |
| B4.3 | **Order placed → email customer** — `OrderPlaced` mailable | TODO | §5.10 | Mailable + queue job |
| B4.4 | **New order → email vendor** — `NewOrderForVendor` mailable (fallback when no active session) | TODO | §5.4 | Mailable + queue job |

### B.5 Edge & empty states

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| B5.1 | "Store closed" banner when outside operating hours or `is_open=false` | TODO | §5.1, §4.4 | Storefront layout |
| B5.2 | "Vendor not yet verified" lockout for unverified KYC — preview allowed, paid orders blocked | TODO | §5.1 | Storefront middleware |
| B5.3 | Item unavailable / out of stock chip on item card; bot must filter out (server-side) | TODO | §5.2, §5.3, §5.6 | Component |
| B5.4 | Cart cleared for inactive items at checkout time | TODO | §5.10 | Pre-checkout sweep |

---

## 4. PHASE C — Vendor live ops

**Goal:** Vendors run their day from the dashboard. Depends on Phase B producing real orders. *(PRD §5.4, §5.5, §5.8, §5.9, §5.11, §6.4, §7.4, §8)*

### C.1 Orders (online + offline, unified)

> Online orders and offline sales now live under one mental model — "a sale happened." The `OfflineSale` and `Order` tables stay separate (different state machines, different schemas) but the **UI surfaces them together** under `vendor.orders.*` with a source filter.

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C1.1 | **Orders board** (`vendor.orders.index`) — combined list of online + offline; filter chip **All · Online · Offline** (default = All); badge per row; search + date range; Kanban view available for the Online tab only | TODO | §5.4, §5.5, §4.1, §4.2 | Polymorphic list, live polling on online rows |
| C1.2 | **Online order detail** (`vendor.orders.show`) — items, customer, address, payment, advance/cancel along legal state machine, internal notes | TODO | §5.4, §4.1 | Mirror admin order page but vendor-scoped |
| C1.3 | **Log offline sale** (`vendor.orders.offline.create`) — single-sale form: items + qty, payment method, optional customer; "+ Log offline sale" button on the orders board | TODO | §5.5 | Form, atomic with inventory deduction |
| C1.4 | **Bulk end-of-day offline entry** (`vendor.orders.offline.bulk`) — table-style multi-row input | TODO | §5.5, §6.4 | Form |
| C1.5 | **Offline sale detail** (`vendor.orders.offline.show`) — read-only summary, "Reverse this sale" action | TODO | §5.5 | View |
| C1.6 | **Offline sale reversal** (`vendor.orders.offline.reverse`) — creates a negative-quantity offline sale, audited | TODO | §5.5 | Action |
| C1.7 | **Kitchen queue** (`vendor.orders.queue`) — large fonts, audible alerts, minimal UI; **online orders only** (offline sales already happened) | TODO | §3.3, §6.4 | Standalone page |
| C1.8 | **Real-time updates** — `OrderPlaced` → `vendor.{vendor_id}.orders` channel via Reverb / Echo; 15s polling fallback | TODO | §7.4 | Broadcasting setup |
| C1.9 | **In-session order alerts** — sound + browser push; email if no active session | TODO | §5.4 | Front-end + Mailable |

**UI/route decisions (locked):**
- **Filter default:** *All* — matches how the vendor dashboard already reports revenue.
- **Detail routes:** two explicit routes (`vendor.orders.show/{order}` for online, `vendor.orders.offline.show/{sale}` for offline) — cleaner controllers and authorisation than a single polymorphic route.
- **Combined pagination:** interleaved by date — most useful for "what happened today."

### C.2 Vendor analytics

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C2.1 | **Analytics overview** (`vendor.analytics.index`) — range chips (7/30/90/MTD) with prev-window deltas, KPI strip (revenue, orders, AOV, expenses, net + margin), stacked daily revenue bar chart (online vs offline), channel split, top items leaderboards (revenue + units, online + offline combined), profitability section with expenses-by-category breakdown from A.6 | DONE | §5.8 + A.6.8 | View |
| C2.2 | **Peak-hours heatmap** (P2) | TODO | §5.8 | View |
| C2.3 | **AI weekly reports archive** (`vendor.analytics.reports`) — `ai_reports` table; batch-API generated Mondays 06:00 vendor-local | TODO | §5.9 | View + scheduled job |

### C.3 Customers

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C3.1 | **Customers list** (`vendor.customers.index`) — phone-keyed CRM book derived from orders + offline sales, KPI strip (total/repeat/new this month/lifetime spend/avg spend), filter chips (All/Repeat/New/Lapsed), search + sort, paginated table with repeat/lapsed badges, top-5 leaderboard sidebar, vendor-private privacy banner | DONE | §6.4 | View |
| C3.2 | **Customer detail** (`vendor.customers.show`) — profile header (name/phone/email + repeat/lapsed badges), 4 stat tiles (orders/spend/AOV/customer since), favourite items leaderboard, interleaved online + offline order timeline with status & payment badges, call/email quick actions | DONE | §6.4 | View |

### C.4 Money & subscription

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C4.1 | **Vendor transactions** (`vendor.transactions.index` + `show`) — KPI strip (today/week/lifetime/records), filter by status / gateway / date / search, read-only payment detail with refund history & "Request refund" deep link | DONE | §6.4 | View |
| C4.2 | **Vendor refunds + request flow** (`vendor.refunds.*`: index, create, store, show) — vendor picks a paid order, requests full or partial refund (with quick-fill chips, max=remaining refundable), refund status tabs, status timeline, admin processes via existing admin flow; refunds carry new `requested_by_user_id` so admin sees a "Vendor" badge on requests they didn't raise themselves | DONE | §5.10 | Controller + view + migration |
| C4.3 | **Vendor payouts / earnings ledger** (`vendor.payouts.index` + `show`) — "How you get paid" callout (Paystack T+1 split-payment), 4-tier KPI strip (today/week/month/lifetime, each showing net + gross + commission + paid order count), live daily earnings ledger from paid orders with 30-day totals row, platform commission ledger from `payouts` table with status filter, fine-print sidebar (commission rate, `min_payout_amount`, settlement cadence), bank snapshot panel; read-only `show` view with breakdown, settlement target & failure reasons | DONE | §6.4 | View |
| C4.4 | **Subscription view** (`vendor.subscription.index`) — current plan hero (status pill, price/cycle, commission, trial countdown, period dates), feature checklist, **usage meters** for paid orders / AI messages / staff seats with red→amber→blue tone bands and "Unlimited" handling, full plan comparison row (Starter / Growth / Pro) with current-plan and "Most popular" badges, last 10 subscription history rows, "Self-serve coming" notice with mailto contact-admin links | DONE | §5.11, §6.4 | View |

### C.5 Reviews (depends on D.3 reviews schema)

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C5.1 | **Read & respond to reviews** (`vendor.reviews.index`) — list, write a public reply | TODO | (post-PRD addition) | View |

### C.6 AI chatbot conversations

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C6.1 | **Browse recent chats** (`vendor.chatbot.conversations`) — last N customer chats with this vendor's bot | TODO | §5.2, §6.4 | View |
| C6.2 | **AI usage breach handling** — when monthly cost ceiling hits 100%, flip `ai_lite_mode` and surface in dashboard | TODO | §8.3 | Service + UI banner |

### C.7 Dashboard refinements (now that orders/offline/AI/expenses all exist)

| # | Task | Status | PRD ref | Deliverable |
| --- | --- | --- | --- | --- |
| C7.1 | **Online vs offline split** on KPI strip | DONE | §5.8 | (See dashboard view) |
| C7.2 | **AI lite-mode banner** when `vendor.ai_lite_mode=true` | DONE | §8.3 | (See dashboard view) |
| C7.3 | **"Expenses this week" KPI tile** on dashboard (drives from A.6) | TODO | (A.6 hook) | Dashboard widget |
| C7.4 | **Quick actions deep-links** to Phase C pages once they ship (kitchen queue, log offline sale, log expense, analytics, AI reports) | TODO | §6.4 | Dashboard tweaks |

---

## 5. PHASE D — Customer post-order polish

**Goal:** Customers feel taken care of after the order; reviews feed Phase C.5.

| # | Task | Status | Deliverable |
| --- | --- | --- | --- |
| D1 | **Saved addresses** — `customer_addresses` table, CRUD on `customer.addresses.*` | TODO | Migration + CRUD |
| D2 | **Reorder** — clone order items into a fresh cart from a past order | TODO | Action button + service call |
| D3 | **Reviews schema** — `reviews` table (vendor_id, user_id, order_id, rating, body, vendor_reply, replied_at) | TODO | Migration + model |
| D4 | **Leave a review** — flow opens once `food_status=completed`, edit window e.g. 14 days | TODO | View + controller |
| D5 | **My reviews** — list reviews left by the customer | TODO | View |
| D6 | **Account security** — change password, list active sessions, logout-everywhere | TODO | Reuses Laravel session table |
| D7 | **Notifications inbox** — Laravel notifications: order status, refund updates, vendor replies | TODO | `notifications` table already exists in default Laravel |
| D8 | **Favourite vendors** — pin / follow vendors | DEFERRED | Nice-to-have |

---

## 6. Cross-cutting concerns

- **Money math** — always integer minor units (kobo) or `decimal(12,2)` consistently across cart, checkout, payment, refund. Never `float`.
- **Currency consistency** — every cart, order and payment locks to the vendor's currency at creation time. Don't rely on `setting('default_currency')` after the fact.
- **Time zones** — operating-hours, pickup-time slots, AI weekly reports must use the vendor's `timezone`, not the server's.
- **Subdomain auth** — make sure cart sessions persist across `slug.foodstack.africa` and `foodstack.africa` if customers ever cross domains. Set `SESSION_DOMAIN=.foodstack.africa` (`.localhost` in dev) if not already.
- **Activity logs** — every state transition (order accepted, refund requested, payout processed, AI lite mode toggled) writes an `activity_logs` row.
- **Email templates** — once Phase B mailables exist, the admin email-templates editor (currently stubs) becomes worth building (PRD §5.10, §5.12).
- **AI freshness contract (PRD §4.5)** — menu mutation → bot reflects in **≤30s**; subdomain resolves in **≤30s** of signup; chat → DB → vendor dashboard **≤5s** p95.
- **State machine naming** — codebase uses `status` + `food_status` (`received`, `confirmed`, `preparing`, `ready`, `out_for_delivery`, `delivered`, `completed`, `cancelled`). PRD §4.1 uses `pending`/`accepted`/`in_progress`/`ready`/`completed`/`cancelled`/`refunded`/`failed_payment`. Add a small mapping table when wiring orders UI so labels stay consistent across vendor / admin / storefront.

---

## 7. Recommended starting point

Inside Phase A, work in this exact order. Each step is self-contained and immediately testable.

1. **A1.1 → A1.4** — settings hub shell with the three "no new schema" tabs (branding, identity, fulfillment defaults). Reuses existing `vendors` columns.
2. **A1.5 + A1.6** — operating hours (one migration, one model, one tab) + open/closed quick toggle.
3. **A1.7** — delivery zones (one migration, one model, one tab, one validator the checkout will call later).
4. **A1.8 + A1.9 + A1.10** — bank/Paystack tab, tax/service charges tab, publish toggle.
5. **A.2 menu extensions** (duplicate, bulk-restock, daily-special).
6. **A.3 inventory overview**.
7. **A.4 chatbot settings + A.5 notifications preferences**.
8. **A.6 expenses tracker** — independent of orders and dashboard, can also ship in parallel with steps 1–7 if we want a quick early win. Vendor-only, no admin work.

After that we cross into Phase B, top-to-bottom (cart → checkout → Paystack → confirmation → tracking → emails → edge states), then Phase C unlocks once orders exist.

---

## 8. Open decisions

| Decision | Options | Default if not chosen |
| --- | --- | --- |
| Cart for guests? | (a) Session-only · (b) Session + force-login at checkout · (c) Always require account | **(b)** — easier for customers, clean conversion at checkout |
| Multi-vendor cart? | (a) One vendor per cart (clear-on-switch) · (b) Multi-vendor cart with split checkout | **(a)** for v1; (b) is a lot of work |
| Delivery-zone shape? | (a) City + postal code list · (b) Drawn polygon on map · (c) Radius from vendor pin | **(a)** for v1 — no map dependency |
| Payment gateways for v1? | Paystack only / + Stripe / + Flutterwave | **Paystack only with subaccount split** (PRD §5.10) |
| Order numbering? | (a) DB id · (b) Random reference · (c) Per-vendor sequence | **(b)** for customer-facing reference + (a) internally |
| Real-time channel? | (a) Reverb (preferred) · (b) Pusher · (c) Polling only for v1 | **(a)** Reverb + 15s polling fallback (PRD §7.4) |
| Offline sale UI placement? | (a) separate `vendor.offline-sales.*` · (b) merged into `vendor.orders.*` with source filter | **(b)** — vendors think of every transaction as one "sale" |
| Combined orders pagination? | (a) interleaved by date · (b) per-tab paginators | **(a)** for the "what happened today" view |
| Expenses tracker schema? | (a) one row per line item · (b) one row per shopping trip with JSON `items[]` | **(a)** — matches "₦1000 pepper, ₦8000 turkey on Monday" mental model |
| Expenses receipts? | (a) optional upload · (b) no upload in v1 | **(b)** — keeps the form light, can add `receipt_path` later |
| Expenses categories? | (a) admin-managed list · (b) fixed enum · (c) free-text + suggestion datalist | **(c)** — vendor types anything; suggestions cover the common cases |

---

## 9. Out of scope for v1

These are intentionally not on the roadmap and should not block earlier phases.

- **Vendor staff invitations / role matrix** (PRD §5.7) — owner-only operation in v1. Spatie roles `vendor_manager`, `vendor_kitchen`, `vendor_cashier` exist in code but no invite/edit/role-gating UI is being built.
- **WhatsApp ordering** — explicitly deferred by the PRD (out of scope for v1).
- **Multi-branch / custom domains.**
- **Self-service subscription upgrades** (admin manually upgrades in v1; PRD §5.11).
