# Foodie

A multi-restaurant platform. Every restaurant gets its own branded website, a menu, and online ordering: pickup, delivery and QR table ordering.

Foodie is built on our Shopify-style platform (`../Shopify`). It has everything that platform has, but restaurants sell **menu items** instead of products:

- **Website builder.** Themes, layouts, the visual editor, page sections, custom pages, custom CSS/JS, fonts, and hero/nav/footer styling.
- **Platform.** Custom domains with automatic SSL, the theme and layout marketplace, the designer portal, the admin panel, email templates, team accounts with 2FA, billing, and MooChedda digital-asset payments.

## What's restaurant-specific

**Guests**

- **Menu.** The storefront's product grid is the menu, ordered by menu category. Tapping a dish opens an item picker with required and optional option groups (sizes, add-ons), special instructions and quantity. The full dish page at `/:store/menu/:id` keeps the photo gallery and reviews.
- **Ordering modes.** Pickup or delivery from the restaurant's link. Dine-in from a table's QR code at `/:store/t/:qrToken`, locked to that table.
- **Payment.** Card through each restaurant's own Stripe (Payment Element: cards, Apple Pay, Google Pay), MooChedda, or pay in person (pickup and dine-in only).
- **Optional accounts.** Guest checkout is the default. A signed-in guest gets saved details, order history and one-tap reorder. A guest who signs up afterwards can claim the order they just placed.
- **Order tracking.** A live status page at `/:store/order/:token`, also linked from the emails.

**Restaurant dashboard**

- **Orders.** A live kitchen board (New → In the kitchen → Ready / on the way) that polls every 5 seconds and chimes from any dashboard screen. There's a pause switch, "Mark paid" for in-person orders, and a full history.
- **Take orders (staff app).** `/take-order` is an ordering app for phones and tablets that staff use to ring up takeout at the counter or dine-in at a table (`src/routes/staff-orders.js`, `public/js/take-order.js`). It uses the dashboard sign-in. Owners always have it; employees need the **Take orders** permission, and employees with only that permission are sent straight to the app. Orders use the same server-side pricing, required options, tax and counted stock as online checkout. They go straight to the kitchen as pay-in-person orders (optionally **Paid now**), carry the staff member's name (`placed_by`, shown on kitchen tickets), and ignore the online-ordering switches (paused, hours, pickup/dine-in toggles). Each order has a device-generated `client_ref`, so a retried submit on flaky Wi-Fi returns the same order instead of creating a second one. The draft order survives a page reload. **Orders** shows the last 12 hours, with **Mark paid** for unpaid orders. API: `GET /api/staff-orders/setup`, `POST /api/staff-orders/quote`, `POST /api/staff-orders`, `GET /api/staff-orders/recent`, `PATCH /api/staff-orders/:id/paid`.
- **Counter display (customer-facing).** A screen of type **Customer display** at `/display/<token>`, paired through `/pair` like the kitchen screens, faces the guest at the counter. In Take orders, staff choose the display, and it shows the order live as it's rung up: items, options, notes, subtotal, tax and total, all priced on the server, never by the staff device. After the order is sent, it shows "Thank you" with the order number for 20 seconds, then returns to a welcome screen with the restaurant's logo and accent color. The live channel is Server-Sent Events held in memory (`src/lib/counter-display.js`). A counter display can never read the kitchen's order list. API: staff `GET /api/staff-orders/displays`, `PUT /api/staff-orders/displays/:id { phase: ordering | done | idle }`; device `GET /api/screen/:token/live`.
- **Self-order kiosk.** A screen of type **Self-order kiosk** at `/kiosk/<token>`, paired through `/pair`, lets guests order on their own on a tablet (`src/routes/kiosk.js`, `public/js/kiosk.js`). The flow is: touch to start, choose for here or to go, browse the menu with photos and categories, pick options (required choices enforced), review the order, enter a name, then pay by card on the tablet (Stripe Payment Element, including Apple Pay / Google Pay where Stripe offers them) or at the counter. The thank-you screen shows the order number and returns to the start after 25 seconds. A guest who walks away gets "Are you still there?" after 75 seconds, and the order is cleared 15 seconds later. Kiosk orders are priced on the server and show on kitchen tickets as `· kiosk`, with a "For here" / "To go" note. Card orders reach the kitchen only after the public `verify-payment` check confirms the exact total with Stripe, the same rule as online checkout. Counter orders go straight to the kitchen as unpaid pay-in-person orders. **Pay at counter** can be switched off per kiosk (card only). The kiosk follows the dashboard's **Taking orders / Orders paused** switch, and when there is no way to pay it shows "please order at the counter". Each order has a retry key, so a double tap never creates two orders or two charges. The kiosk can never read the order list. API (device): `GET /api/screen/:token/kiosk`, `POST …/kiosk/quote`, `POST …/kiosk/orders`.
- **Order board screens.** A full-screen order board for iPads, monitors and TVs at `/kitchen/<token>` (`src/routes/screens.js`, `public/js/kitchen.js`). Pairing works like the Kiosk project: open `/pair` on the device, which shows a 6-character code (no lookalike characters, valid 10 minutes, single use, held in memory). In **Orders → Screens**, press **Pair device** and type the code, or scan the screen's QR code. **Kitchen** screens move orders forward with one tap (Start → Ready → Picked up / Served / Out for delivery → Delivered) and send the same customer emails as the dashboard. **View only** screens just show orders. Screens never cancel orders (cancelling can refund a card), and they never receive payment ids, tracking tokens or customer emails. Tickets turn amber after 10 minutes and red after 20, new orders chime, and the screen stays awake. **Disconnect** issues a new link and sends any device showing the old one back to `/pair`. Screens stop when the restaurant is suspended or its subscription lapses. Screen links and `/pair` use `APP_URL`, never a custom domain. API: managers `GET/POST /api/screens`, `PATCH/DELETE /api/screens/:id`, `GET /api/screens/:id/qr`, `POST /api/screens/:id/pair { code }`; devices `GET /api/screen/:token`, `PATCH /api/screen/:token/orders/:id/status`; public `POST /api/pair`, `GET /api/pair/:code/stream`.
- **Menu.** Grouped by category, with a sold-out ("86") switch on every dish. The editor has photos, pricing, a MooChedda price, optional daily quantity counting, description, **Options & add-ons**, and dietary tags with calories.
- **Tables & QR.** Create tables, print a sheet of QR codes, regenerate one table's code, plus a QR code for the main ordering page.
- **Ordering & Delivery.** Replaces Shipping. Covers ordering modes, pay in person, tips, prep time, tax rate, cuisine, delivery fee, minimum and free threshold, ZIP codes, delivery time, opening hours and time zone.
- **Menu Categories.** Reorder with ↑ ↓ and rename.

## Security changes from the Shopify-style platform

- **Prices are computed only on the server** (`src/lib/pricing.js`). The original checkout accepted the subtotal and line prices from the browser, created a Stripe PaymentIntent for any amount the client asked for, and marked an order paid whenever a `payment_intent_id` was posted. In Foodie:
  - **Card orders.** Checkout creates the order as `pending_payment` with a PaymentIntent for the server total. It reaches the kitchen only after `verify-payment` confirms with Stripe that the payment succeeded for that exact amount and order id.
  - **MooChedda orders.** The server now creates the invoice from database prices. Confirmation looks up the invoice stored on the order, never one supplied by the browser.
- **Claiming a guest order** needs its private tracking token. Before, an order number was enough, and order numbers are guessable; claiming exposes the guest's address and phone.
- **Employee permissions are enforced on the API** (`requireSection` in `src/middleware/auth.js`). The original platform only hid nav items, so an employee could call any API directly.
- **Database sessions use UTC** so `NOW()` matches how mysql2 reads dates.
- **Inline `onerror=""` image handlers** are replaced with a CSP-safe listener; the storefront's CSP was silently blocking them.

## Running it

```bash
cp .env.example .env        # DB credentials, JWT_SECRET, ENCRYPTION_KEY (see comments)
npm install
npm run seed                # demo restaurant at /demo — demo@foodie.test / demo-password
npm start                   # http://localhost:8900
```

The schema and migrations apply automatically on boot (`src/schema.sql` plus `initSchema()` in `src/database.js`; the restaurant tables are at the end of the migrations list).

Set `APP_URL` to the public domain before printing table QR codes, because printed codes keep the URL they were made with. If a restaurant has a live custom domain, its QR codes use that domain instead.

## Tests

```bash
DOMAIN_TEST_DB_NAME=foodie_test DOMAIN_TEST_DB_USER=foodie DOMAIN_TEST_DB_PASSWORD=… npm test
```

The database-backed tests only run against a scratch database whose name contains `test`.

- **`test/ordering-api.test.js`** runs the restaurant flow end to end against fake Stripe and MooChedda servers (via `STRIPE_API_BASE` and `MOOCHEDDA_API_BASE`): checkout, payment verification, refunds, tenant isolation and employee permissions.
- **`test/pricing.test.js` and `test/hours.test.js`** cover pricing and opening hours.
- **The rest** are the original platform's tests.

## Internal naming

Menu items are stored in the `products` tables, and menu categories in `categories`. The storefront keeps the original CSS class names (`.sf-product-card` and so on) because marketplace themes and layouts target them. Everything a restaurant or guest sees says menu, dish, restaurant and website.

## Before launch

- **Legal pages.** `public/terms.html` and `public/privacy.html` were carried over from the original platform and rebranded to Foodie (a SheriChic Inc. product); have them reviewed for restaurant ordering before launch.
- **MooChedda.** Each dish has one all-inclusive token price, and options have no token price, so an order with a paid add-on (e.g. "Large +$2.50") must be paid by card or in person.
- **Delivery.** Delivery uses the restaurant's own drivers (ZIP list, fee, minimum). There's no third-party dispatch (DoorDash Drive, Uber Direct) yet.
- **Stripe webhooks.** Card orders are confirmed when the browser returns and again from the tracking page. If a guest closes the tab mid-payment before either runs, the order stays `pending_payment` until someone opens its tracking link. A Stripe webhook would close that gap.
