Single source of truth for the locked architecture, user flows, and scope. All implementation conforms to this document.
| Layer | Technology | Notes |
|---|---|---|
| Backend | PHP 8.2 + Laravel 12 + PDO (MySQL/MariaDB) | Sessions for auth; explicit json_encode() responses; prepared statements |
| Database | MySQL 8 / MariaDB 10.4+ | 11 tables, 3NF, FK constraints, composite indexes |
| Web frontend | Vite + React 18 + TypeScript + Tailwind CSS + shadcn/ui + TanStack Query | SPA, role-routed (customer + admin shells) |
| Mobile | Capacitor 6 wrapping the React build | Native Camera plugin for payment screenshot capture; Android APK |
| Image storage | Cloudinary | Payment proofs + menu photos; signed uploads from PHP |
| Auth | PHP sessions, password_hash, session_regenerate_id, idle timeout, role in session |
Laravel session driver + CSRF |
| Hosting | API: Render free Docker web service; DB: TiDB Cloud Starter; SPA: Vercel static app; Mobile: Capacitor APK | XAMPP for local dev; provider subdomains are the free domains |
The CMPE 364 rubric grades PHP sessions, PHP AJAX endpoints, json_encode, and PDO/MySQLi. A pure BaaS stack (e.g. Supabase) forfeits ~50 of 120 points. The backend is therefore hand-written PHP on Laravel so the graded primitives are visible in the codebase: session_start() / session_regenerate_id(), json_encode() with Content-Type: application/json, and PDO prepared statements. Modern tooling (Vite/React/Capacitor/Cloudinary) is used only where it does not cost rubric points. See docs/ARCHITECTURE.md § Rubric traceability.
E-wallet/Bank/COD are not integrated as payment APIs. The system is a verification workflow around out-of-band payments.
Admin manages a payment_methods table: E-wallet (GCash, Maya, QR Ph, GoTyme, Maribank QR/account), Bank (account name/number), COD (toggle). Card is a disabled "Coming Soon" tile, out of scope.
Customer checkout:
- Choose fulfillment mode + slot (batch capacity meter + cutoff enforced).
- Payment method screen shows only seller-enabled methods.
- E-wallet/Bank → app shows seller QR/account + copy button + instructions. Customer pays in their own app, returns, uploads screenshot (Capacitor Camera or file picker). Order:
pending→payment_uploaded. - COD → no screenshot. Order:
pending→ admin accepts →confirmed. Payment marked received on delivery. - Admin payment queue: order + screenshot + expected total side-by-side. Verify →
confirmed. Reject →cancelled+ reason; customer notified. Order enters prep only onconfirmed.
Admin configures fulfillment_options per context: pickup (+address), lalamove, grab, pop_up_pickup (+stall location).
- Delivery (Lalamove/Grab): the seller arranges the courier outside the app. The app records mode + customer address. The seller moves status to
out_for_deliveryand may paste a rider/courier note the customer sees in the tracker. No live tracking integration. - Pickup / pop-up: customer sees the address; status
ready→completed.
pending ──(payment proof uploaded)──▶ payment_uploaded ──(admin verify)──▶ confirmed
pending ──(COD, admin accept)───────────────────────────────────────────▶ confirmed
confirmed ──▶ preparing ──▶ ready | out_for_delivery ──▶ completed
any ──(admin/customer cancel or payment rejected)──▶ cancelled (reason)
Statuses (ENUM): pending, payment_uploaded, confirmed, preparing, ready, out_for_delivery, completed, cancelled. COD skips payment_uploaded.
- Auto-generated ingredient procurement list —
recipe_ingredientsJOINorder_itemsGROUP BY ingredient; surfaced as a SQL view + Laravel endpoint. - Batch capacity meter + cutoff — per-date
batchesrow plus optional per-item cutoff; capacity checked inside the order-insert transaction; CSS progress bar in the UI. - Order pattern dashboard — SQL aggregates rendered as charts.
- Smart reorder / favorites — top-N per customer; "Order Again" pre-fills the cart.
- GCash / bank payment APIs (no third-party money movement).
- Lalamove / Grab delivery APIs (no live tracking, no dispatch integration).
- Card payment processing (UI tile only, "Coming Soon").
- Push notifications (Phase 2 native hardening only; see §8k).
- Multi-tenant / multi-seller (single owner).
Added 2026-06-27 by the autonomous completion pass. This section overrides the Phase-1 prohibitions in section 7 only for the items below, and only as described. Everything else in section 7 stays prohibited. The seller still books couriers manually and pastes the booking link. No courier dispatch API is introduced, so the "no Lalamove/Grab API" rule is preserved. Payment remains buyer receipt upload plus seller verification; no payment processor API is introduced.
- Seller ships from Cavite (Molino, Bacoor — see
settings.pickup_address). - Buyer picks
fulfillment_mode = lalamove | grabat checkout (reuses the existing ENUM;pickup/pop_up_pickup⇒ shipping = 0). For pre-orders (fulfillment_datein the future) the flow is identical. - A lookup table (
shipping_zones) maps a destination city/region → a flat fee charged at the upper end of that courier's observed price range (to absorb surges). No courier API is called. Research + sources live indocs/planning/shipping-research.md; at least 15 destinations are seeded. - Buyer picks a destination city from the tier table. The tier row supplies the fee directly and no map, geocoding, distance, or courier API is required.
orders.shipping_fee(DECIMAL(10,2), default 0) +orders.courier_link(VARCHAR(255), nullable) are added.total_amount = subtotal + shipping_fee, computed inside the existingDB::transactionwithlockForUpdate(race-safe). Three-mirror sync:db/schema.sql, migration,packages/shared/src/index.ts,apps/web/src/types.ts.- Column choice:
courier_linkis added separately from the existingcourier_note.courier_notekeeps its meaning (free-text rider/booking notes the customer sees);courier_linkholds the tracking/booking URL the seller pastes post-payment. Splitting them keeps the link renderable as a clickable card in the tracker and lets the confirmation email (§8e) fire on link-paste specifically, not on every note edit. - On payment confirmation the order enters
preparingand the success screen shows that courier booking details will appear once the seller books the courier. - Seller admin UI: for each
out_for_delivery(orconfirmed/preparingdelivery) order, a field to paste courier booking/tracking details. Pasting it surfaces the details to the customer's order tracker and triggers the confirmation email (§8e).
- Reasoning first (weakest point): OCR can misread noisy e-wallet and bank screenshots, so it must not approve or reject payments automatically. It is a seller aid only: the buyer still uploads a receipt screenshot, the backend stores raw extracted text when available, and the seller manually verifies the receipt against the expected total.
orders.proof_ocr_text(TEXT, nullable) stores raw OCR output for uploaded e-wallet/bank receipts.orders.proof_ocr_data(JSON/TEXT, nullable) stores advisory parsed hints when the raw OCR text contains likely amount, reference number, transfer/order date, transfer time, sender, or recipient. Parsed hints are never used for auto-approval because wallet and bank screenshot layouts vary. The seller still confirms or rejects manually. Three-mirror sync:db/schema.sql, migration,packages/shared/src/index.ts,apps/web/src/types.ts.- OCR strategy is best-effort and non-blocking: first use Cloudinary OCR output
when the upload response provides it, then optionally fall back to local
Tesseract when
OCR_TECHNIQUE=tesseractandTESSERACT_BINis configured. If neither path works,proof_ocr_textstaysnulland the admin UI shows "OCR not available — verify manually." - Admin payment verification shows the receipt screenshot, expected total, and OCR text side-by-side. The seller still chooses Verify or Reject.
- The existing
payment_methodstable remains the model for multiple e-wallet accounts and multiple bank accounts: one row per account/QR. This avoids a new schema and keeps the buyer flow simple. payment_methods.type = e_walletreplaces the earlier narrowgcashenum value. Method labels carry the concrete wallet/network name: GCash, Maya, QR Ph, GoTyme, Maribank, etc. Existinggcashrows are migrated toe_wallet.- Admin Settings manages all rows, active and inactive: type, label, account
name, account number, QR image URL, active toggle, and sort order. QR image
upload reuses the Cloudinary data-URL pattern under a
payment-methodsfolder. - Checkout lists only active methods and shows the selected QR/account details before receipt upload. Card remains disabled as "Coming Soon."
- Maps is not required for the graded flow because shipping tiers already solve delivery fee calculation without API keys or network-dependent demos.
- If all required work is complete and time remains, a future enhancement may add address autocomplete or distance estimates behind a feature flag. It must not replace the tier-table fallback or introduce courier dispatch.
- Laravel mail configured via
MAIL_*in.env.example(defaultlogdriver for dev — writes the rendered HTML tostorage/logs/laravel.log). App\Mail\OrderConfirmationMailMailable, inline-CSS HTML template styled Shopify-style: store header, "Thank you for your order" hero, order summary table (items × qty, subtotal, shipping, total), customer info block (shipping address, payment method label, shipping method label), footer contact email.- Triggered on order confirmation (
status → confirmed) and on courier details being pasted intocourier_link. No real SMTP provider without keys. - Feature test asserts the mailable renders and is sent on the
status-transition event via
Mail::fake().
- Replaces the post-checkout redirect with a dedicated
/orders/:id/successroute showing the "preparing your order, courier details will appear here shortly" state. Same screen for same-day and pre-orders. The existing/orders/:idtracker renderscourier_linkas a clickable card when it is a URL or as plain pasted text otherwise, with an "Awaiting courier booking" placeholder until the seller pastes it.
- Available menu items must have at least one
recipe_ingredientsrow. Draft hidden menu items may omit recipes, but they cannot be made available until ingredient mapping exists. This keeps the core procurement tracker from silently missing newly added dishes. - Menu items may also store
category, comma/JSONtags,taste_profile, optional item-levelcutoff_time, andsort_order. The item cutoff is stricter than the batch cutoff; an order is rejected when any selected available item is past its configured cutoff for the fulfillment date. - Batch capacity is measured in servings, not order rows. The capacity meter
and order transaction both sum
order_items.quantityfor the matching batch context (onlineorpop_up). - Order status updates are constrained to the lifecycle in §5. E-wallet/bank
orders cannot be confirmed while still
pending; COD may move directly frompendingtoconfirmed; completed/cancelled orders cannot be revived. - Root
bun run build:mobilebuilds a production APK bundle againsthttps://prepflow-api.onrender.com/api. Local Android emulator testing usesbun run build:mobile:local, a Capacitor origin ofhttp://10.0.2.2, and a native API fallback ofhttp://10.0.2.2:8000/api. The Laravel API therefore allows credentialed CORS only for explicit local/native origins fromCORS_ALLOWED_ORIGINS; production APKs must use a real HTTPS API origin.
- Seller-facing order queues support bulk status updates for surge days. Bulk updates use the same lifecycle validation and audit-log requirements as single-order updates; invalid rows are reported back instead of silently skipped.
courier_linkaccepts the seller's pasted courier booking/tracking detail as plain text. If it is a valid URL, the customer UI renders it as a link; if it is not a URL, the customer still sees the exact pasted value. The backend does not validate it as a URL.
- The deployed demo uses a split free-tier architecture: Render runs the Laravel
API from
api/Dockerfile, TiDB Cloud Starter provides the MySQL-compatible database, Vercel serves the static Vite app, and Cloudinary remains the image store. - The API must run in Docker because OCR needs the Tesseract binary in addition
to PHP extensions and Composer dependencies. The Docker image installs
tesseract-ocr, setsOCR_TECHNIQUE=tesseract, and usesTESSERACT_BIN=/usr/bin/tesseract. - The free domains are provider subdomains:
*.onrender.comfor the API and*.vercel.appfor the web app. A custom.comor.phis not free and is intentionally out of scope for the graded demo. - Weak point: Render free web services spin down after inactivity, so the first request after idle time may be slow. This is acceptable for the graded demo and avoids Koyeb's card-gated signup; the design remains portable to any Docker host if free-tier limits become too tight.
- Because Vercel and Render are different sites, production session cookies
default to
SameSite=None; Secure. Local development staysSameSite=Laxunless.envoverrides it.
- Payment proofs are digital receipts, but the APK hardening pass now exposes both camera and gallery only for customer receipt proof flows. Camera capture remains available for seller-owned menu photos. Admin QR upload stays gallery unless deliberately changed by a future settings workflow.
- The order status enum is unchanged. Pickup and pop-up pickup orders render the
readystate as Ready for Pickup and advanceready -> completed; only delivery modes render/advance throughout_for_delivery. - Customer profile management uses the existing
usersfields (name,email,phone,address,password_hash).users.addressstores a newline-separated address book for quick checkout reuse, avoiding a larger address-table migration during this hardening pass. - Forgot-password is in scope as a real reset-token flow, not a decorative link.
The API stores hashed reset tokens in
password_reset_tokens, sends a reset link by Laravel Mail, and returns neutral responses to avoid email enumeration. Production delivery still depends on configured SMTP/domain authentication.
- Mobile-native notification support is now in scope for the APK rubric pass. The customer app schedules local notifications after order creation and receipt upload. Permission prompts run only on native platforms and failure or denial never blocks checkout.
- Push notification delivery uses Capacitor Push Notifications and Firebase
Cloud Messaging. The APK registers an FCM token after authenticated session
bootstrap; Laravel stores tokens in
notification_tokensand attempts best-effort pushes on payment-proof upload and admin status transitions. - Firebase files and credentials remain deployment values, not source files.
Android uses
apps/mobile/android/app/google-services.jsonwhen provided, and Laravel usesFCM_PROJECT_ID,FCM_CLIENT_EMAIL, andFCM_PRIVATE_KEY. Missing Firebase setup logs a no-op fallback and must not fail status updates.