# FoodStack Africa — Product Requirements Document

**Version:** 2.0
**Date:** May 2026
**Status:** Draft (supersedes `food1.md` and `food2.md`)
**Tech direction:** Laravel monolith, building on the existing `foodproject` codebase
**Out of scope for v1:** WhatsApp Business API integration, multi-branch, custom domains

---

## Table of Contents

1. [Product Overview](#1-product-overview)
2. [Goals & Success Metrics](#2-goals--success-metrics)
3. [User Personas](#3-user-personas)
4. [Canonical Definitions (Glossary & State Machines)](#4-canonical-definitions)
5. [Feature Requirements](#5-feature-requirements)
6. [Page Inventory — Admin, Vendor, Public](#6-page-inventory)
7. [Technical Architecture (Laravel)](#7-technical-architecture-laravel)
8. [AI Architecture & Cost Model](#8-ai-architecture--cost-model)
9. [Pricing & Business Model](#9-pricing--business-model)
10. [Development Roadmap](#10-development-roadmap)
11. [Codebase Migration Plan — What to Change, Add, Remove](#11-codebase-migration-plan)
12. [Risks, Compliance & Open Questions](#12-risks-compliance--open-questions)

---

## 1. Product Overview

FoodStack Africa is a **multi-tenant SaaS** that gives every African food vendor a branded storefront on a custom subdomain, an AI chatbot trained on their live menu, real-time order management, and a unified record of online and offline sales — built on a Laravel monolith.

**Core value proposition:** A food vendor signs up, completes a 10-minute onboarding wizard, and is immediately live with a public storefront and AI menu assistant — without writing code, without paying marketplace commissions, and with full ownership of their customers and data.

### 1.1 Problem Statement (revised — WhatsApp-free)

African food businesses operate without reliable digital infrastructure:

- Menus change daily; static websites don't fit.
- They have no affordable unified tool for orders, inventory, staff, and analytics.
- **Offline sales (cash, walk-ins) are never captured digitally,** creating revenue blind spots.
- Customers have to call, DM, or walk in for "what's available today?" — repetitive, low-leverage work for vendors.
- Existing marketplaces (Chowdeck, Glovo) take 15–25% commission and own the customer relationship.

### 1.2 Solution (v1, no WhatsApp)

Each vendor gets:

- A public storefront at `{slug}.foodstack.africa` (subdomain auto-routed by Laravel).
- An embedded **AI menu chatbot** that answers customer questions strictly from their live menu data.
- A **vendor dashboard** for menu, orders, inventory, staff, offline sales, and analytics.
- A **direct payments flow**: customers pay via Paystack and the funds are settled directly into the vendor's own bank account in real time (Paystack split payments / subaccounts). FoodStack only takes its platform commission as the split share. A manual "pay on delivery / bank transfer" option remains as a fallback.
- Weekly **AI-generated business reports** delivered by email.

WhatsApp ordering is intentionally deferred: we focus on shipping a strong web-first product before paying Meta/360dialog per-conversation fees and dealing with WABA approval timelines.

---

## 2. Goals & Success Metrics

### 2.1 Business Goals (Year 1)

- Acquire **400 paying vendors** within 12 months of public launch (revised from 500 to account for no WhatsApp acquisition channel in v1).
- Reach **₦12M MRR** by end of Year 1.
- Maintain vendor NPS ≥ 50.
- Establish FoodStack as the default *non-marketplace* SaaS for Nigerian food vendors.

### 2.2 Product Goals

- Vendor signup → live storefront in **≤ 10 minutes**, p95.
- Daily menu update completable in **≤ 2 minutes**, p95.
- AI chatbot answer accuracy **≥ 90%** at launch (measured against a fixed 200-question evaluation set rebuilt monthly).
- Order placed → visible in vendor dashboard in **≤ 5 seconds**, p95.
- Storefront page load on 4G **≤ 2.5 seconds**, p95 (Core Web Vitals: LCP < 2.5s, CLS < 0.1).

> **Note:** Earlier drafts contradicted themselves (3s vs 10s, "immediately" vs 60s). All latency targets above are now canonical; everything else in the PRD must reference them.

### 2.3 KPIs

| KPI | Target M3 | Target M6 | Target M12 |
|---|---|---|---|
| Active vendors | 50 | 150 | 400 |
| Paying vendors (% of active) | 15% | 30% | 50% |
| Total orders processed/month | 1,000 | 5,000 | 25,000 |
| Vendor monthly churn | < 12% | < 8% | < 5% |
| AI chatbot accuracy | 86% | 90% | 92% |
| Avg orders/vendor/day | 5 | 8 | 15 |
| Offline sale logs/vendor/month | 10 | 20 | 50 |
| AI cost / paying vendor / month | ≤ ₦1,500 | ≤ ₦1,200 | ≤ ₦1,000 |

---

## 3. User Personas

### 3.1 Chidinma — The Home Caterer

> **Profile:** Age 34, Lagos. Runs a home catering business from her kitchen. Takes orders via WhatsApp/phone daily. Menu changes based on what she can source from the market each morning. Has 80+ regular customers but struggles to keep up with order messages and often loses track of who ordered what.

**Pain Points:**
- Spends 2+ hours per day manually replying to order messages.
- Customers frequently ask "what's available today?" — she answers the same question 30+ times daily.
- No way to know which meals sell best or which customers bring the most revenue.
- Loses offline cash sales data — has no record of walk-in orders.

**Goals:**
- Get orders automatically without constant manual replies.
- Let customers self-serve on today's menu.
- See a clear summary of her sales at the end of each week.

### 3.2 Emeka — The Restaurant Owner

> **Profile:** Age 42, Abuja. Owns a mid-size restaurant with 8 staff. Uses paper order books. Has attempted using Chowdeck but dislikes losing a percentage of each sale to a marketplace. Wants to own his customer relationships and data.

**Pain Points:**
- High commission fees from third-party marketplaces eat into margins.
- Staff management is chaotic — no attendance tracking or role-based access.
- No insight into inventory levels until items physically run out.
- Cannot track which staff member processed which order in case of disputes.

**Goals:**
- Own a branded digital presence customers associate with his restaurant, not a marketplace.
- Give each staff member appropriate and limited access.
- Monitor sales and inventory remotely on his phone.

### 3.3 Tunde — The Kitchen Staff

Cook at Emeka's restaurant. Mid-30s. Phone-only, 4G. Needs to see incoming orders, mark them in-progress and ready, and update inventory levels — without seeing revenue numbers. Spends his shift in the kitchen, glances at a tablet on the wall.

**Pain:** Paper tickets get lost / misread. Cooks duplicate work or skip orders.
**Goals:** A clean order queue, large fonts, audible alerts, no clutter.

---

## 4. Canonical Definitions

This section exists to prevent the contradictions that plagued earlier drafts. **All other sections MUST reference these definitions.**

### 4.1 Order State Machine

```
                ┌─────────┐
                │ pending │ ← created by storefront / AI bot / staff
                └────┬────┘
       (vendor)     │     (vendor / auto-timeout 30min)
        accepts     │      rejects
            ┌───────┴───────┐
            ▼               ▼
      ┌──────────┐    ┌──────────┐
      │ accepted │    │ rejected │ ← terminal
      └─────┬────┘    └──────────┘
   (kitchen)│
   in-prog  ▼
      ┌─────────────┐
      │ in_progress │
      └──────┬──────┘
             │ ready / ready_for_pickup
             ▼
      ┌────────┐
      │ ready  │
      └────┬───┘
           │ delivered / picked_up
           ▼
      ┌───────────┐
      │ completed │ ← terminal
      └───────────┘

Side-states (any non-terminal state can transition to):
  - cancelled       (customer-initiated)
  - failed_payment  (payment attempt failed; auto-cancellable after 15min)
  - refunded        (post-completion; via Refund flow)
```

States: `pending`, `accepted`, `rejected`, `in_progress`, `ready`, `completed`, `cancelled`, `failed_payment`, `refunded`.

### 4.2 Order Source

`web` | `chatbot` | `offline_walkin` | `offline_phone` | `staff_entered`.

(WhatsApp explicitly excluded for v1; reserve the `whatsapp` value in the enum for future use.)

### 4.3 Payment Status (separate from order status)

`unpaid`, `paying` (gateway in flight), `paid`, `failed`, `refunded`, `partially_refunded`.

### 4.4 Vendor Lifecycle

`pending_kyc` → `active` ⇄ `suspended` → `closed` (terminal).
`active` vendors have a separate **operational** flag: `is_open` (today's storefront state, toggleable).

### 4.5 AI Latency / Freshness Contract

- A menu change saved by a vendor MUST be reflected in chatbot responses within **30 seconds** (single canonical number).
- Subdomain routing for a new vendor MUST resolve within **30 seconds** of signup (achieved via Laravel route domain matching, not DNS provisioning).
- Order placement (chat → DB → vendor dashboard) MUST be **≤ 5 seconds** end-to-end at p95.

---

## 5. Feature Requirements

Each feature has user stories and acceptance criteria. Phase column indicates MVP (P1), growth (P2), or scale (P3) per Section 10.

### 5.1 Vendor Onboarding & Storefront [P1]

**User stories**

- As a new vendor, I sign up with email + phone, complete a 6-step wizard (business profile → KYC docs → branding → first menu category → first 3 menu items → preview & publish), and go live in one session.
- As a vendor, my storefront URL is `{slug}.foodstack.africa` and is auto-provisioned from my chosen slug.
- As a vendor, I can toggle "Open" / "Closed" with a custom closed message.

**Acceptance criteria**

- Wizard p95 completion time: ≤ 10 minutes (instrumented).
- Subdomain resolves within 30 seconds of signup (Section 4.5).
- KYC documents (NIN/BVN proof or business registration) are required *before* the first transaction — vendor can preview but cannot accept paid orders.
- Storefront is mobile-first responsive (≥ 360px viewport).

### 5.2 AI Menu Chatbot [P1]

**Differentiator and core risk.** See Section 8 for full AI architecture.

**User stories**

- As a customer on the storefront, I chat in plain English/Pidgin and ask "what's available today?", "how much is jollof?", "do you have anything spicy?".
- As a vendor, my menu changes are reflected in chatbot answers within 30 seconds (canonical).
- As a vendor, I can configure: greeting message, closed-state message, minimum order value, custom dietary tags.
- As a customer, I can place an order through the chat (the bot collects name, phone, delivery address) and proceed to payment.

**Acceptance criteria**

- Bot's responses cite **only** the vendor's current menu data; offers only items where `is_available = true` AND `stock_qty > 0`.
- Out of stock items are filtered server-side at the prompt-build stage, not relied on the model to refuse.
- Each conversation logs `vendor_id`, `model_used`, `input_tokens`, `output_tokens`, `cached_tokens`, `cost_estimate` to `ai_usage_logs` for cost tracking.
- A confidence-low or unknown-question fallback returns: *"I'm not sure — please call/message the vendor directly at [phone]."*
- A hard guardrail rejects any prompt that attempts injection (e.g., "ignore previous instructions") via a sanitizer middleware.
- AI accuracy ≥ 90% on the canonical evaluation set (Section 2.2).

### 5.3 Menu Management [P1]

**User stories**

- Create menu categories (Rice Dishes, Soups, Drinks, etc.) — per-vendor, scoped.
- Add menu items with: name, description, photo, price, currency, optional dietary tags, optional add-ons (combo extras), `is_available`, `stock_qty`, `low_stock_threshold`, optional `available_from` and `available_until` (time of day).
- Bulk actions: mark all as restocked at start of day; duplicate item; mark item sold-out.
- Daily-special flag separate from normal availability — visually highlighted on storefront.

**Acceptance criteria**

- All menu mutations invalidate the AI prompt cache for that vendor and trigger a cache rebuild.
- Image uploads are stored via Laravel filesystem (S3 in prod, local in dev), resized to 800×800 and 200×200 thumbnails.

### 5.4 Order Management Dashboard [P1]

**User stories**

- Real-time order list (live updated via Laravel Echo / Reverb / Pusher).
- Filter by status, source (Section 4.2), date range, search by customer name/phone.
- Detail view: full order data, status transitions only along the legal state machine (Section 4.1).
- Vendor receives **in-app notification + audible alert + browser push** on new orders. Email fallback if no active session.
- Daily summary panel: today's orders count, revenue, online vs offline split.

**Acceptance criteria**

- New order appears in dashboard within 5 seconds of creation (canonical, p95).
- Status transitions outside the legal state machine are rejected with an error.
- A staff member's role determines which actions/buttons appear (Section 5.7).

### 5.5 Offline Sales [P1]

**User stories**

- Vendor or cashier clicks "Log Offline Sale", picks items + quantities, picks payment method (cash / transfer / POS), optionally enters customer name+phone.
- Bulk entry: enter end-of-day batch in one form (table-style input).
- Offline sales tagged distinctly in analytics; deduct from inventory.

**Acceptance criteria**

- Logged offline sales are atomic with inventory deduction (DB transaction).
- A reversal action exists (creates a negative-quantity offline sale, audited).

### 5.6 Inventory [P1]

**User stories**

- Set per-item `stock_qty` and `low_stock_threshold`.
- Auto-deduct on online order or offline sale.
- Items at 0 stock auto-flip `is_available = false` (configurable per item).
- Daily reset action: bulk set stock for chosen items.
- Inventory history log: when, who (staff_id), what changed.
- Low-stock in-app alert + email digest (no WhatsApp in v1).

### 5.7 Staff Management & Roles [P1 (basic) → P2 (advanced)]

P1 ships with a single non-owner role: **Cashier**. Full role matrix and activity log are P2.

**Roles and permissions matrix (P2 target):**

| Permission | Owner | Manager | Kitchen | Cashier |
|---|:-:|:-:|:-:|:-:|
| View orders | ✅ | ✅ | ✅ | ✅ |
| Update order status | ✅ | ✅ | ✅ | — |
| Edit menu | ✅ | ✅ | — | — |
| View analytics (revenue) | ✅ | ✅ | — | — |
| Log offline sales | ✅ | ✅ | — | ✅ |
| Manage inventory | ✅ | ✅ | — | — |
| Manage staff | ✅ | — | — | — |
| Subscription / billing | ✅ | — | — | — |

(Delivery role removed for MVP since we have no delivery integration; bring back in P3.)

### 5.8 Sales Analytics [P1 (basic) → P2 (advanced)]

P1: Daily/weekly/monthly revenue, top items, online vs offline split.
P2: Peak-hours heatmap, retention metrics, AI-generated weekly report (see 5.9).

### 5.9 AI Sales Reports [P2]

A scheduled Laravel job runs every Monday at 06:00 vendor-local time and generates a plain-language summary using the **OpenAI/Anthropic batch API at 50% pricing** (canonical cost decision). Stored in `ai_reports` table; emailed to vendor.

### 5.10 Customer Order Flow & Payments [P1]

**Funds flow:** Customer payments are **pass-through to the vendor's own bank account** via Paystack subaccounts / split payments. The platform never holds vendor funds; FoodStack's commission share is automatically deducted at gateway level and settled to FoodStack's account on the same transaction. This is enforced by configuring a Paystack `Subaccount` per vendor at onboarding, with an associated `transaction_charge` percentage (the platform commission).

**User stories**

- As a vendor, I provide my bank details during onboarding; FoodStack creates a Paystack subaccount and verifies the account name via Paystack's name-resolution API.
- As a vendor, every customer payment lands in *my* bank account on next Paystack settlement, minus the platform commission (no platform-held escrow).
- As a vendor, I can update my payout bank account at any time; the change is verified and propagated to my Paystack subaccount.
- As a customer, I browse the menu, add items to cart, and check out as guest or signed-in.
- As a customer, I can pay via Paystack (card, transfer, USSD) or select "pay on delivery" / "manual bank transfer" (already implemented in `PaymentController`).
- As a customer, I receive an email confirmation (existing template system); SMS optional in P2.
- As a customer, I can view order status on a tracking page `/order/{token}` without login.

**Acceptance criteria**

- A vendor without verified bank details cannot mark themselves "Open" or accept paid orders (gated by `RequireKYCApproved` + bank verification).
- The Paystack subaccount creation is idempotent and re-runs on bank account change.
- Refunds initiated from FoodStack debit the vendor's Paystack subaccount; if the subaccount has insufficient balance, the refund is queued and the vendor is notified to resolve.
- Webhook reconciliation confirms each successful charge against the order's `payment_status` (`unpaid` → `paying` → `paid`).

### 5.11 Subscription Management [P1 (read) → P2 (full)]

- P1: vendor sees current plan and usage; admin manually upgrades.
- P2: self-service upgrades via Paystack, prorations, downgrade rules.

### 5.12 Automated Customer Follow-Ups [P2 — email-only in v1]

Email-based only since WhatsApp is deferred:

- 24h after first order: thank-you email (vendor-branded).
- 7 days after last order: re-engagement email with current daily specials.
- Rate-limited to 1 automated email per customer per 7 days.
- Vendor can disable each trigger.

---

## 6. Page Inventory

Every page in the system, organized by audience. Anything not on this list is out of v1 scope.

### 6.1 Public / Marketing Pages

| Route | Page | Notes |
|---|---|---|
| `/` | Marketing landing | Hero, features, social proof, pricing CTA |
| `/pricing` | Pricing tiers | Section 9 table |
| `/about`, `/contact` | About, Contact | Static |
| `/terms`, `/privacy`, `/refund-policy` | Legal | NDPR-compliant |
| `/signup` | Vendor signup wizard | 6 steps; spans 5.1 acceptance |
| `/login` | Unified login | Detects role; redirects |
| `/admin/login` | Admin login | Already exists in codebase |
| `/forgot-password`, `/reset-password/{token}` | Auth | Already exists |
| `/verify-email`, `/email/verify/{id}/{hash}` | Email verification | Already exists |

### 6.2 Customer Pages (Storefront — `{vendor:slug}.foodstack.africa`)

| Route | Page |
|---|---|
| `/` | Storefront home (hero + categories + chat widget) |
| `/menu` | Full menu, filterable |
| `/menu/{item}` | Item detail page (modal on mobile) |
| `/cart` | Cart |
| `/checkout` | Checkout (delivery details + payment) |
| `/order/{token}` | Public order tracking page (token-based, no login) |
| `/order/{token}/receipt.pdf` | Receipt PDF download |
| (overlay) | AI chat widget — embedded on every storefront page |

### 6.3 Customer Account Pages (Optional Login)

| Route | Page | Status |
|---|---|---|
| `/dashboard` | Customer dashboard | Already exists, repurpose |
| `/dashboard/orders` | Past orders | Already exists |
| `/dashboard/orders/{order}` | Order detail | Already exists |
| `/dashboard/saved-vendors` | Saved/favourite vendors | Rename existing `saved-events` |
| `/profile` | Profile settings | Already exists |

### 6.4 Vendor Pages (`/vendor/...`)

> **Codebase note:** rename URL prefix `/organizer/` → `/vendor/` and namespace `App\Http\Controllers\Organizer` → `App\Http\Controllers\Vendor`.

| Route | Page | Maps to existing |
|---|---|---|
| `/vendor/dashboard` | Today's overview: orders today, revenue, low stock, alerts | `Organizer\DashboardController` (rewrite) |
| `/vendor/menu` | Menu items list | NEW |
| `/vendor/menu/categories` | Manage categories | NEW |
| `/vendor/menu/categories/create`, `/{category}/edit` | Category CRUD | NEW |
| `/vendor/menu/items/create`, `/{item}/edit` | Item CRUD | NEW (replaces TicketType screens) |
| `/vendor/menu/items/{item}/duplicate` | Quick duplicate | NEW |
| `/vendor/menu/bulk-restock` | Bulk daily reset | NEW |
| `/vendor/orders` | Orders list (live) | Adapt `Organizer\DashboardController@orders` |
| `/vendor/orders/{order}` | Order detail + status | Adapt existing |
| `/vendor/orders/queue` | Kitchen view (large fonts, audible) | NEW |
| `/vendor/offline-sales` | Offline sales list | NEW |
| `/vendor/offline-sales/create` | Log offline sale | NEW |
| `/vendor/offline-sales/bulk` | End-of-day bulk entry | NEW |
| `/vendor/inventory` | Stock levels overview | NEW |
| `/vendor/inventory/history` | Inventory history log | NEW |
| `/vendor/analytics` | Charts: revenue, top items, peak hours | Extend `OrganizerEventController@analytics` |
| `/vendor/analytics/reports` | AI-generated weekly reports archive | NEW |
| `/vendor/customers` | Repeat customers, top customers | NEW |
| `/vendor/customers/{customer}` | Customer order history | NEW |
| `/vendor/staff` | Staff list | NEW (P2 full; P1 list-only) |
| `/vendor/staff/invite` | Invite staff | NEW |
| `/vendor/staff/{staff}/edit` | Edit role | NEW |
| `/vendor/storefront` | Storefront settings (branding, hours, open/closed) | NEW |
| `/vendor/storefront/preview` | Preview live storefront | NEW |
| `/vendor/chatbot/settings` | Chatbot custom messages, rules | NEW |
| `/vendor/chatbot/conversations` | Browse recent customer chats | NEW |
| `/vendor/transactions` | Payments list | Already exists |
| `/vendor/refunds` | Refunds | Already exists |
| `/vendor/payouts` | Payouts | Already exists |
| `/vendor/subscription` | Plan + usage + invoices | NEW |
| `/vendor/profile` | Profile + KYC docs | Adapt existing |
| `/vendor/notifications` | Preferences | NEW |

### 6.5 Admin Pages (`/admin/...`)

| Route | Page | Status |
|---|---|---|
| `/admin/dashboard` | Platform metrics: vendors, MRR, GMV, AI cost, anomalies | Adapt existing |
| `/admin/vendors` | All vendors list | Adapt `OrganizerController` (rename) |
| `/admin/vendors/{vendor}` | Vendor detail (impersonate, suspend, etc.) | Adapt existing |
| `/admin/vendors/pending-kyc` | KYC review queue | NEW |
| `/admin/vendors/{vendor}/kyc` | KYC document review | NEW |
| `/admin/vendors/{vendor}/impersonate` | Login as vendor (audited) | NEW |
| `/admin/orders` | All platform orders | Already exists, extend filters |
| `/admin/orders/{order}` | Order detail + intervention | Already exists |
| `/admin/transactions` | All payments | Already exists |
| `/admin/refunds` | Refunds | Already exists |
| `/admin/payouts` | Payouts | Already exists |
| `/admin/subscriptions` | All vendor subscriptions | NEW |
| `/admin/subscriptions/plans` | Manage plan tiers | NEW |
| `/admin/ai-usage` | Token consumption per vendor, monthly | NEW |
| `/admin/ai-usage/anomalies` | Vendors exceeding cost ceilings | NEW |
| `/admin/reports` | Platform analytics | Already exists, extend |
| `/admin/analytics` | Already exists | |
| `/admin/users` | All users (admins, vendors, staff, customers) | Already exists |
| `/admin/categories` | Cuisine categories (global) | Already exists, repurpose |
| `/admin/email-templates` | Email templates | Already exists |
| `/admin/email-templates/controls` | Toggle which emails fire | Already exists |
| `/admin/settings/*` | Platform settings (currencies, payment methods, branding, legal, commission, maintenance, email) | Already exist |
| `/admin/activity-logs` | Audit log | Already exists |
| `/admin/profile`, `/admin/profile/edit` | Admin profile | Already exists |

### 6.6 System / API Pages (no UI)

- `/payment/callback`, `/payment/webhook` — already exist (Paystack)
- `/api/chat/{vendor}` — POST endpoint for storefront chat widget (NEW)
- `/api/menu/{vendor}/snapshot` — cache for chatbot prompts (NEW)
- `/storage/{path}` — already exists

---

## 7. Technical Architecture (Laravel)

### 7.1 Stack (final)

| Layer | Technology | Notes |
|---|---|---|
| Framework | **Laravel 11** (existing) | Keep current version |
| Frontend | **Blade + Tailwind + Vite + Alpine.js** | Existing setup; chat widget can use Livewire 3 or Alpine + fetch |
| Real-time | **Laravel Reverb** (preferred) or Pusher | For order push; small footprint, self-hostable |
| DB | **MySQL/MariaDB** (XAMPP local, RDS in prod) | Existing |
| Cache / Queue | **Redis** | For prompt caching, queues, broadcasts |
| Queue worker | **Laravel Horizon** | For chat completions, emails, AI reports |
| File storage | **Local (dev) / S3 (prod)** | Existing flysystem config |
| Auth | **Laravel Breeze + Spatie Permission** | Existing — extend roles |
| Multi-tenancy | **Row-level via `vendor_id` + global Eloquent scopes** | NOT separate schemas; explicit and audited |
| Subdomain routing | **Laravel `Route::domain('{slug}.foodstack.africa')`** | No DNS provisioning per vendor; wildcard A/CNAME at DNS once |
| AI providers | **OpenAI + Anthropic** behind a `ChatService` interface | Default GPT-5.4 mini |
| Payments | **Paystack** (primary, integrated) — **subaccount/split** mode so funds settle directly to vendor's bank; **Flutterwave** (fallback, P2) | Existing webhook + manual flows; platform takes commission share at gateway level |
| Email | Existing Laravel mailer + Resend or SES in prod | Existing template system |
| Background jobs | Laravel scheduler + queue | Existing pattern (`SyncFailedPaymentOrders`, etc.) |

**Why we are NOT switching to Next.js / NestJS** (as `food2.md` proposed):
- The existing codebase already implements ~60% of the platform.
- Laravel can absolutely handle SaaS multi-tenancy, real-time, and AI service calls.
- A rewrite would cost 4–6 months with no incremental customer value.
- One language, one deploy, one team — better fit for a small-team African startup.

### 7.2 Multi-tenancy Model

- **Row-level tenancy**: every domain table has `vendor_id` (FK).
- A `BelongsToVendor` trait adds a global scope `where('vendor_id', currentVendorId())` to every tenant-scoped model.
- Current vendor is resolved by middleware:
  - `IdentifyVendorBySubdomain` for the storefront (`{slug}.foodstack.africa`).
  - `ResolveVendorFromUser` for `/vendor/...` routes (gets vendor from authed user → staff → vendor relationship).
- **Tenancy is logical, not schema-isolated.** All tenant queries MUST go through the scoped models; raw queries are forbidden in app code (lint rule).

### 7.3 Subdomain Routing (without DNS provisioning)

- DNS: a single wildcard record `*.foodstack.africa → Lb/IP` configured **once** at the domain level.
- Laravel: `Route::domain('{slug}.foodstack.africa')->group(...)` resolves `{slug}` from the request and the `IdentifyVendorBySubdomain` middleware loads the `Vendor` model and binds it to the container for the request lifecycle.
- New vendor signup just inserts a row with a unique `slug` — instantly routable, no Cloudflare/Route53 API calls.
- Reserved slugs (admin, api, www, app, status, blog, etc.) are blacklisted at signup.

### 7.4 Real-Time Order Updates

- New order created → fires `OrderPlaced` event → broadcasted on private channel `vendor.{vendor_id}.orders` via Laravel Reverb.
- Vendor dashboard subscribes via Echo; updates DOM live.
- Fallback poll every 15s if WebSocket fails.

### 7.5 Data Model (high-level)

New / modified tables. **`(N)` = new, `(R)` = renamed/repurposed, `(K)` = kept as-is.**

```
vendors                        (R from organizers)
  id, user_id (owner), name, slug (unique), description, logo_path,
  brand_color, bio, business_hours_json, is_open, kyc_status,
  kyc_documents_json, plan_id, country_id, currency_code, phone,
  address, lat, lng, timezone, suspended_reason, created_at, ...

menu_categories                (N)
  id, vendor_id, name, sort_order, is_active

menu_items                     (N — replaces ticket_types semantically)
  id, vendor_id, menu_category_id, name, description, price, currency_code,
  photo_path, is_available, stock_qty, low_stock_threshold,
  available_from, available_until, is_daily_special, dietary_tags_json,
  addons_json, sort_order, deleted_at

orders                         (R — keep, add fields)
  id, vendor_id, customer_id (nullable), guest_name, guest_phone, guest_email,
  delivery_address, delivery_lat, delivery_lng, source (enum 4.2),
  status (enum 4.1), payment_status (enum 4.3), subtotal, delivery_fee,
  discount, total, currency_code, public_token, notes, accepted_at,
  ready_at, completed_at, cancelled_reason, created_at

order_items                    (N — replaces tickets-per-order)
  id, order_id, menu_item_id, name_snapshot, price_snapshot, quantity,
  addons_snapshot_json, line_total

offline_sales                  (N)
  id, vendor_id, staff_id, customer_name, customer_phone,
  payment_method (cash/transfer/pos), tag (walkin/phone/staff),
  total, currency_code, items_json, created_at

inventory_logs                 (N)
  id, vendor_id, menu_item_id, change_qty, reason (order/offline/manual/restock),
  reference_type, reference_id, staff_id, created_at

staff                          (N)
  id, vendor_id, user_id, role (owner/manager/kitchen/cashier),
  is_active, invited_at, accepted_at, deactivated_at

customers                      (N — distinct from User; tenant-scoped)
  id, vendor_id, user_id (nullable), name, phone, email,
  first_order_at, last_order_at, total_spent, order_count

chat_conversations             (N)
  id, vendor_id, customer_id (nullable), session_token, started_at,
  ended_at, summary_text

chat_messages                  (N)
  id, conversation_id, role (user/assistant/system), content,
  model_used, input_tokens, output_tokens, cached_tokens, cost_estimate,
  latency_ms, created_at

ai_usage_logs                  (N)
  id, vendor_id, conversation_id (nullable), feature (chat/report),
  provider, model, input_tokens, output_tokens, cached_tokens,
  cost_usd, cost_ngn, created_at

ai_reports                     (N)
  id, vendor_id, period_start, period_end, content_md, delivered_at

subscription_plans             (N)
  id, name, code (starter/growth/pro), monthly_price_ngn,
  features_json, is_active

vendor_subscriptions           (N)
  id, vendor_id, plan_id, status (trial/active/past_due/cancelled),
  trial_ends_at, current_period_start, current_period_end,
  paystack_subscription_code, cancelled_at

payments, transactions, refunds, payouts, email_templates, email_controls,
activity_logs, currencies, payment_methods, settings  (K — already exist)
```

### 7.6 What Existing Tables Are Repurposed vs Removed

| Existing table | v1 disposition |
|---|---|
| `users` | **Keep**, add nothing (already has phone/avatar/bank_*) |
| `organizers` | **Rename → `vendors`**; add fields per 7.5 |
| `events` | **Keep but hide for food MVP**; ground for future event-catering hybrid |
| `event_lineups`, `event_images`, `event_tag` | **Keep dormant**; not used by food vendors |
| `ticket_types` | **Keep dormant** — `menu_items` is the new analog |
| `tickets` | **Keep dormant** — `order_items` is the new analog (food orders don't have per-attendee tickets) |
| `venues`, `tags` | **Keep dormant** |
| `categories` | **Repurpose** as global cuisine categories (Nigerian, Continental, etc.) for vendor discovery later |
| `payments`, `transactions`, `refunds`, `payouts` | **Keep, reuse** |
| `email_templates`, `email_controls` | **Keep, add new templates** for food order flows |
| `discount_codes`, `discount_code_usages` | **Repurpose** as menu coupons |
| `activity_logs` | **Keep, expand** to staff actions |
| `permission_tables` (Spatie) | **Keep**; add new roles `vendor_owner`, `vendor_manager`, `vendor_kitchen`, `vendor_cashier` |

We are **not dropping** any existing tables in v1 — keeping them dormant lets us pivot back or expand to event-catering hybrid in P3 with zero data loss.

---

## 8. AI Architecture & Cost Model

### 8.1 Provider Abstraction

```
App\Services\AI\ChatServiceInterface
  ├── OpenAIChatService          (default)
  ├── AnthropicChatService       (failover / preferred for some workloads)
  └── (later) GeminiChatService

Configurable via `config/ai.php`:
  default_provider = 'openai'
  default_model    = 'gpt-5.4-mini'
  failover_provider = 'anthropic'
  failover_model    = 'claude-haiku-4.5'
  escalation_model  = 'gpt-5.4'   (or 'claude-sonnet-4.6')
```

Vendor-level overrides allowed for paid plans (Pro can pin a specific model).

### 8.2 Prompt Construction (RAG-lite)

For every chat request:

1. Load the vendor's current menu snapshot from Redis (TTL 30s; rebuilt on menu mutation events).
2. Assemble a system prompt with a **stable prefix** (vendor name + instructions + menu) followed by the user message.
3. Send to the LLM **with prompt caching enabled** (OpenAI cached_input or Anthropic `cache_control` markers).
4. Stream the response back to the chat widget.
5. Log tokens + cost to `ai_usage_logs`.

The prefix order matters: keep the most stable parts (vendor name, instructions, menu) at the *start* so caching kicks in.

### 8.3 Cost Ceilings & Throttling

- **Per-vendor monthly cost ceiling** (configurable, default ₦2,000 on Starter, ₦4,000 on Growth, ₦10,000 on Pro). When breached:
  - 90% → email warning to admin and vendor.
  - 100% → chatbot enters "lite mode" (canned menu listing only, no LLM calls) for the rest of the month.
- **Per-conversation token cap**: max 8,000 input + 1,000 output tokens. After cap, bot says "Let me hand you over to the vendor directly".
- **Per-IP rate limiting** on `/api/chat/{vendor}`: 30 messages / 5 minutes (Laravel `RateLimiter`).

### 8.4 Escalation Logic

Default model `gpt-5.4-mini`. We escalate to the larger model only when:

- The bot's own response includes an explicit low-confidence marker (we instruct the model to emit `[uncertain]` when unsure).
- The conversation has > 6 turns and the customer is mid-order with multiple items + addons.
- The vendor has explicitly enabled "smart mode" on Pro plan.

### 8.5 Estimated Unit Cost (target)

| Vendor activity level | Monthly AI cost (target, with caching) |
|---|---|
| 30 chats/day | ~₦900 |
| 100 chats/day | ~₦3,000 |
| 300 chats/day | ~₦8,500 |

Margin check: at Growth (₦9,999/mo) for a 100-chats/day vendor, AI cost ≈ 30% of revenue. Rest covers hosting, support, dev, profit.

### 8.6 Evaluation & Quality

- A canonical **200-question evaluation set** per vendor archetype (home caterer, restaurant, bakery), rebuilt monthly.
- Nightly job runs the eval set against the current default model and records accuracy.
- Accuracy regressions trigger a Slack/email alert; the previous-month champion model is hot-swapped back via config.

---

## 9. Pricing & Business Model

### 9.1 Tiers (₦ amounts, Nigeria launch)

| Feature | Starter (Free) | Growth (₦9,999/mo) | Pro (₦24,999/mo) |
|---|:-:|:-:|:-:|
| Custom subdomain | ✅ | ✅ | ✅ |
| AI menu chatbot | ✅ (basic) | ✅ | ✅ (smart mode) |
| Orders/month | 50 | Unlimited | Unlimited |
| Menu items | 20 | 100 | Unlimited |
| Photos per item | 1 | 3 | 5 |
| Offline sales | ✅ | ✅ | ✅ |
| Inventory tracking | — | ✅ | ✅ |
| Staff accounts | 1 (owner) | 3 | 10 |
| Customer email follow-ups | — | ✅ | ✅ |
| AI weekly reports | — | ✅ | ✅ + on-demand |
| Advanced analytics | — | — | ✅ |
| AI cost ceiling/month | ₦2,000 | ₦4,000 | ₦10,000 |
| Priority support | — | — | ✅ |
| Annual discount | — | 2 months free | 2 months free |

### 9.2 Revenue streams

1. **Subscriptions** (primary).
2. **Order overage** for Starter: ₦50/order beyond 50/month (instead of forced upgrade).
3. **Future:** transaction fee 0.5% on Starter orders above 100/month → upgrade nudge.

### 9.3 Unit economics

| Metric | Target |
|---|---|
| Blended ARPU (paid only) | ₦12,500/mo |
| CAC | ₦7,000 |
| Payback | < 1 month (paid users) |
| AI cost / paid vendor | < ₦3,000/mo |
| Gross margin (mature) | 60–70% |

Free → paid conversion target: **25%** by Month 12.

---

## 10. Development Roadmap

### Phase 1 — MVP (Months 1–4)

> Goal: vendor signs up, builds menu, gets storefront + AI bot, accepts paid orders, logs offline sales.

- [ ] Rename: `Organizer` → `Vendor` across code (controllers, models, routes, views, role names)
- [ ] DB migrations for new tables (vendors fields, menu_categories, menu_items, order_items, offline_sales, inventory_logs, staff, customers, chat_conversations, chat_messages, ai_usage_logs, subscription_plans, vendor_subscriptions)
- [ ] Subdomain middleware + route domain group
- [ ] Vendor onboarding wizard (6 steps)
- [ ] KYC document upload + admin review queue
- [ ] Menu management (categories, items, photos, availability, daily reset)
- [ ] Public storefront pages (home, menu, item detail, cart, checkout)
- [ ] AI chat widget + `ChatService` abstraction (OpenAI default, Anthropic failover)
- [ ] Prompt caching + Redis menu snapshot cache
- [ ] Cost ceilings + lite-mode fallback
- [ ] Vendor order dashboard with Reverb live updates
- [ ] Order state machine enforcement (Section 4.1)
- [ ] Offline sales logging
- [ ] Inventory deductions (online + offline) + low-stock alerts
- [ ] Customer payment via Paystack (already integrated) + manual options
- [ ] Order confirmation email + tracking page
- [ ] Basic analytics dashboard
- [ ] Admin: vendors list, KYC queue, AI usage view, suspend/activate
- [ ] Subscription plans seeded; Starter free; manual admin upgrade for paid

### Phase 2 — Growth (Months 5–7)

- [ ] Self-service Paystack subscription billing (Growth/Pro upgrades)
- [ ] Full staff management with roles, invites, activity log
- [ ] Inventory history page
- [ ] Advanced analytics: peak hours heatmap, retention metrics
- [ ] AI weekly reports (batch API, scheduled job)
- [ ] Customer email follow-ups (24h thank you, 7-day re-engagement)
- [ ] Menu coupons (repurpose existing discount_codes)
- [ ] Vendor-customizable chatbot greetings/rules
- [ ] Customer accounts (saved addresses, order history)
- [ ] Flutterwave as payment failover

### Phase 3 — Scale (Months 8–12)

- [ ] WhatsApp Business API integration (now back on the menu)
- [ ] Multi-branch / multi-location for restaurant groups
- [ ] Custom domain support (vendor uses their own domain)
- [ ] Vendor marketplace directory (opt-in)
- [ ] Mobile apps (vendor + customer)
- [ ] Ghana/Kenya expansion (currency + locale)
- [ ] Delivery integrations (Kwik, GIG)

---

## 11. Codebase Migration Plan

This section is a checklist for the engineering team. Read it as the *diff* between today's `foodproject` and v1 of FoodStack.

### 11.1 Renames — Strategy: Parallel Introduction, Deferred Physical Rename

A wholesale `Organizer → Vendor` rename touches **100+ files** (controllers, blades, routes, migrations, seeders, notifications) and is high-risk to ship in the same patch as new feature work. We therefore split it into two phases:

**Phase 1A — Parallel introduction (DONE):**

- Add a new `App\Models\Vendor` model that **extends** `App\Models\Organizer` and uses the same `organizers` table. All new food-domain code uses `Vendor`; all existing event-ticketing code keeps using `Organizer` unchanged.
- All new food migrations reference `vendor_id` as a foreign key targeting `organizers.id` so the column name reads correctly in the food domain even though the parent table has not been renamed yet.
- New tenancy infrastructure (`BelongsToVendor` trait, `VendorScope`, `VendorContext`) is built around `Vendor`. Existing organizer code is untouched.
- New Spatie roles `vendor_owner / vendor_manager / vendor_kitchen / vendor_cashier` are added alongside the legacy `organizer` role.

**Phase 1B — Physical rename (deferred to its own focused migration patch):**

Once the food MVP is functionally stable end-to-end, ship a single rename PR that does the table below in one pass. This isolates the blast radius and makes regressions trivial to bisect.

| From | To |
|---|---|
| `App\Http\Controllers\Organizer\*` namespace | `App\Http\Controllers\Vendor\*` |
| `routes` prefix `/organizer/` | `/vendor/` |
| Route name prefix `organizer.` | `vendor.` |
| Middleware `role:organizer` | `role:vendor_owner|vendor_manager|vendor_kitchen|vendor_cashier` |
| `App\Models\Organizer` (kept as legacy alias for events) | Removed; `Vendor` extends `Model` directly |
| `organizers` table | `vendors` (migration: `Schema::rename`) |
| `organizer_id` FKs (in `events`, `payouts`, etc.) | `vendor_id` |
| Spatie role `organizer` | Mapped to `vendor_owner` (plus 3 new roles) |
| Blade views `resources/views/organizer/*` | `resources/views/vendor/*` |
| Layout `layouts/organizer.blade.php` | `layouts/vendor.blade.php` |

**Why this is safe:** `App\Models\Vendor` already drives all new food-domain reads/writes against the `organizers` table. When we rename the table, only `protected $table` on the Vendor model and the FK references in food migrations need to change — every food-domain call site stays identical.

### 11.2 New Migrations

In dependency order:

1. `2026_05_XX_rename_organizers_to_vendors.php` (rename + add new columns: slug, is_open, kyc_status, kyc_documents, plan_id, business_hours_json, suspended_reason)
2. `2026_05_XX_create_menu_categories_table.php`
3. `2026_05_XX_create_menu_items_table.php`
4. `2026_05_XX_create_order_items_table.php` + backfill from `tickets` (or skip; we keep `tickets` dormant)
5. `2026_05_XX_add_food_fields_to_orders_table.php` (delivery_address, lat/lng, source, payment_status, public_token, notes)
6. `2026_05_XX_create_offline_sales_table.php`
7. `2026_05_XX_create_inventory_logs_table.php`
8. `2026_05_XX_create_staff_table.php`
9. `2026_05_XX_create_customers_table.php` (tenant-scoped, separate from `users`)
10. `2026_05_XX_create_chat_conversations_table.php`
11. `2026_05_XX_create_chat_messages_table.php`
12. `2026_05_XX_create_ai_usage_logs_table.php`
13. `2026_05_XX_create_ai_reports_table.php`
14. `2026_05_XX_create_subscription_plans_table.php` + `vendor_subscriptions_table.php`
15. `2026_05_XX_seed_food_email_templates.php` (new template batch)
16. `2026_05_XX_seed_subscription_plans.php` (Starter, Growth, Pro)
17. `2026_05_XX_seed_vendor_roles.php` (Spatie roles)

### 11.3 New Controllers

Under `App\Http\Controllers\Vendor`:

- `MenuCategoryController`
- `MenuItemController`
- `OrderController` (food orders, distinct from existing event order code)
- `OrderQueueController` (kitchen view)
- `OfflineSaleController`
- `InventoryController`
- `AnalyticsController`
- `CustomerController`
- `StaffController`
- `StorefrontSettingsController`
- `ChatbotSettingsController`
- `ChatConversationController` (browse logs)
- `SubscriptionController`
- `NotificationPreferenceController`

Under `App\Http\Controllers\Admin`:

- `VendorController` (replaces `OrganizerController` — same routes, new name/scope)
- `KYCController`
- `SubscriptionPlanController`
- `AIUsageController`

Under `App\Http\Controllers\Storefront` (NEW namespace, public):

- `StorefrontController` (home, menu, item detail)
- `CartController` (already exists; refactor for tenant scope)
- `CheckoutController` (already exists; adapt)
- `ChatApiController` (POST `/api/chat`)
- `OrderTrackingController` (`/order/{token}`)

### 11.4 New Services / Classes

- `App\Services\AI\ChatServiceInterface`
- `App\Services\AI\OpenAIChatService`
- `App\Services\AI\AnthropicChatService`
- `App\Services\AI\PromptBuilder`
- `App\Services\AI\MenuSnapshotCache`
- `App\Services\AI\CostTracker`
- `App\Services\AI\PromptInjectionFilter`
- `App\Services\Tenancy\VendorContext`
- `App\Services\Tenancy\BelongsToVendor` (trait + global scope)
- `App\Services\Orders\OrderStateMachine`
- `App\Services\Inventory\InventoryService`
- `App\Services\Subscriptions\SubscriptionService`
- `App\Services\KYC\KycService`
- `App\Jobs\GenerateAIWeeklyReport`
- `App\Jobs\CheckLowStockJob`
- `App\Jobs\SendOrderFollowUpJob`
- `App\Jobs\RebuildMenuSnapshotJob`
- `App\Events\OrderPlaced`, `OrderStatusChanged`, `MenuUpdated`
- `App\Listeners\BroadcastOrderPlaced`, `InvalidateMenuCache`

### 11.5 New Middleware

- `IdentifyVendorBySubdomain`
- `ResolveVendorFromUser`
- `EnsureVendorIsActive`
- `EnsureVendorIsOpen` (storefront writes only)
- `EnforceAICostCeiling`
- `RequireKYCApproved` (gates first paid transaction)

### 11.6 New Mailables / Email Templates

Add a new `EmailTemplateSeederBatch9.php` containing:

- `vendor.welcome` (after signup)
- `vendor.kyc_approved` / `vendor.kyc_rejected`
- `vendor.new_order_notification`
- `vendor.low_stock_alert`
- `vendor.weekly_ai_report`
- `vendor.subscription_payment_succeeded` / `_failed`
- `customer.order_confirmation` (food)
- `customer.order_status_updated`
- `customer.order_completed_followup` (24h thank you)
- `customer.reengagement` (7-day inactive)
- `admin.kyc_pending_review`

### 11.7 What to Hide / Disable for Food MVP

In existing routes/views, gate the following behind a `feature_flag('events_module')` check (default off in v1):

- `/admin/events/*` and Blade views under `admin/events/` (event-specific actions)
- Event lineup, event images carousel, ticket scanner UI
- "Trending events", "Featured events" sections on the public homepage (replace with featured vendors when discovery directory ships in P3)

Do **not** delete the code — we may revive it for an event-catering hybrid in P3.

### 11.8 Things to Add to `.env`

```env
APP_DOMAIN=foodstack.africa

AI_DEFAULT_PROVIDER=openai
AI_DEFAULT_MODEL=gpt-5.4-mini
AI_FAILOVER_PROVIDER=anthropic
AI_FAILOVER_MODEL=claude-haiku-4.5
AI_ESCALATION_MODEL=gpt-5.4
AI_MAX_INPUT_TOKENS=8000
AI_MAX_OUTPUT_TOKENS=1000

OPENAI_API_KEY=
ANTHROPIC_API_KEY=

REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST=
REVERB_PORT=8080

REDIS_HOST=127.0.0.1
REDIS_PORT=6379

KYC_REQUIRED_BEFORE_PAID_ORDER=true
```

### 11.9 New Composer Packages

- `laravel/reverb` — broadcasting
- `laravel/horizon` — queue dashboard
- `openai-php/laravel` — OpenAI SDK (or use direct HTTP)
- `anthropic-ai/sdk` (or direct HTTP — Anthropic's official PHP SDK is still young)
- `intervention/image` — image resizing for menu photos
- `spatie/laravel-medialibrary` — optional, for cleaner image handling
- `spatie/laravel-activitylog` — optional, replaces or augments existing `ActivityLog`

### 11.10 Things You Already Have That Just Need Wiring

These need almost no new code; only re-pointing:

- **Paystack integration** (`PaymentController`, webhook, transactions, refunds, payouts) — repoint at `Order` (food) instead of `Order` (event tickets). Same model, same flow.
- **Email template system** (`EmailTemplate` + `EmailControl` + 8 seeder batches + admin UI) — add Batch 9 as above.
- **Spatie roles & permissions** — add new roles, reuse middleware.
- **Activity logs** — already wired; just emit new events.
- **Settings / branding / commission / SEO admin pages** — reuse as-is.
- **Currencies / countries / states / cities** — reuse for localization.
- **Customer dashboard scaffold** — adapt orders/saved-events views to food context.
- **PDF generation** (`resources/views/orders/pdf.blade.php`) — adapt for food order receipts.
- **Background command pattern** (`SyncFailedPaymentOrders`, `VerifyPendingPaystackPayments`, `BackfillCommissionCommand`) — clone for `RebuildMenuSnapshots`, `GenerateWeeklyReports`, etc.
- **Admin login + password reset** (separate from user login) — reuse exactly.

---

## 12. Risks, Compliance & Open Questions

### 12.1 Top Risks (revised)

| Risk | Impact | Mitigation |
|---|---|---|
| AI hallucination | Wrong orders, vendor distrust | Server-side filter unavailable items before prompt; `[uncertain]` fallback; strict guardrails; nightly eval against canonical question set |
| AI cost overrun | Margin collapse on Free/Growth tiers | Per-vendor monthly cost ceilings; lite-mode fallback at 100%; default to cheapest model + caching |
| Vendor doesn't update menu | Stale menu = wrong customer expectations | Daily 10am push email "Update today's menu"; 2-min update flow; "last updated X hrs ago" badge on storefront |
| Vendor fraud / fake stores | Platform reputation, customer harm | KYC gate before first paid transaction; admin review queue; spotcheck on first 10 orders per vendor |
| Food safety / liability | Legal exposure | Clear ToS — FoodStack is software, not vendor; allergen/dietary tag system; vendor-attested |
| Payment gateway outage | Lost orders | Paystack primary + Flutterwave hot failover (P2); manual bank transfer always available |
| Vendor's bank account is wrong / closed | Vendor's funds bounce; refund liability | At onboarding, validate account via Paystack bank name resolution API; require re-verification on bank change; lock storefront until verified |
| Refund larger than vendor's Paystack subaccount balance | Refund cannot be auto-processed | Queue refund; notify vendor; provide top-up instructions; admin can intervene |
| Multi-tenancy data leak | Catastrophic — one bug exposes all vendors' data | `BelongsToVendor` trait + global scope on every model; PR review checklist; static analysis rule banning unscoped queries; weekly audit query |
| NDPR non-compliance | Regulatory fine | Legal review pre-launch; data export endpoint for users; documented retention policy; consent UI |
| Vendor digital literacy | High churn in Month 1 | Video walkthroughs; in-app tour (introjs); WhatsApp support number for vendors (irony noted — humans, not API) |
| WebSocket reliability | Vendors miss orders | 15s polling fallback; audible alert on dashboard; email + future SMS on new order |

### 12.2 Compliance & Legal

- **NDPR (Nigeria Data Protection Regulation)**: legal review required pre-launch.
  - DPO contact published.
  - Data export endpoint for any user / vendor / customer.
  - Data retention policy: orders 7 years (tax); chat logs 90 days; KYC docs 5 years post-account-closure.
  - Consent capture in signup wizard.
- **Food safety disclaimer** in ToS: FoodStack provides software; vendor is solely responsible for food quality, allergens, hygiene.
- **KYC**: NIN or BVN proof for individual vendors; CAC document for registered businesses. Stored encrypted at rest.
- **Refund policy**: standardized, vendor-overridable within bounds (no less than statutory minimum).
- **Accessibility**: WCAG 2.1 AA target for storefronts (alt text required for menu photos; keyboard nav; color contrast).

### 12.3 Open Questions (must close before Phase 1 sprint planning)

1. **Customer auth required or guest checkout?** Recommendation: **guest by default**, optional account. (Existing codebase supports both.)
2. **Subdomain TLD:** `foodstack.africa` confirmed? Backup `.com.ng` / `.shop`?
3. **AI eval set ownership:** who builds and maintains the 200-question canonical set? Recommend: a product/AI lead + 5 pilot vendors.
4. **Initial KYC reviewers:** founders manually for first 100 vendors, then admin team.
5. **Image storage in dev:** local disk via the existing `/storage/{path}` fallback (Windows/XAMPP) — confirmed acceptable until S3.
6. **Customer payments**: ✅ **DECIDED — pass-through to vendor's own bank** via Paystack subaccount / split payments. Platform commission is taken at gateway level; FoodStack never holds vendor funds. Existing `Payout` model and `BackfillCommissionCommand` are repurposed for commission reconciliation rather than vendor disbursement.
7. **Pricing currency for Ghana/Kenya** (P3): pay in local currency or accept ₦/USD? Out of v1 scope but worth flagging now.

---

*FoodStack Africa — PRD v2.0 — May 2026 — Draft for review*
