Skip to content

Latest commit

 

History

History
254 lines (222 loc) · 16.3 KB

File metadata and controls

254 lines (222 loc) · 16.3 KB

PrepFlow — Engineering Decisions

Single source of truth for the locked architecture, user flows, and scope. All implementation conforms to this document.

1. Stack

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

2. Why this stack (rubric alignment)

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.

3. Payment flow (no GCash API)

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:

  1. Choose fulfillment mode + slot (batch capacity meter + cutoff enforced).
  2. Payment method screen shows only seller-enabled methods.
  3. 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: pendingpayment_uploaded.
  4. COD → no screenshot. Order: pending → admin accepts → confirmed. Payment marked received on delivery.
  5. Admin payment queue: order + screenshot + expected total side-by-side. Verifyconfirmed. Rejectcancelled + reason; customer notified. Order enters prep only on confirmed.

4. Fulfillment flow (seller-preferred couriers, no API)

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_delivery and may paste a rider/courier note the customer sees in the tracker. No live tracking integration.
  • Pickup / pop-up: customer sees the address; status readycompleted.

5. Order lifecycle

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.

6. Core innovations (kept from proposal)

  1. Auto-generated ingredient procurement listrecipe_ingredients JOIN order_items GROUP BY ingredient; surfaced as a SQL view + Laravel endpoint.
  2. Batch capacity meter + cutoff — per-date batches row plus optional per-item cutoff; capacity checked inside the order-insert transaction; CSS progress bar in the UI.
  3. Order pattern dashboard — SQL aggregates rendered as charts.
  4. Smart reorder / favorites — top-N per customer; "Order Again" pre-fills the cart.

7. Out of scope (explicit)

  • 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).

8. Phase 2 scope (unlocked)

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.

8a. Shipping tier system (centerpiece)

  • Seller ships from Cavite (Molino, Bacoor — see settings.pickup_address).
  • Buyer picks fulfillment_mode = lalamove | grab at checkout (reuses the existing ENUM; pickup/pop_up_pickup ⇒ shipping = 0). For pre-orders (fulfillment_date in 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 in docs/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 existing DB::transaction with lockForUpdate (race-safe). Three-mirror sync: db/schema.sql, migration, packages/shared/src/index.ts, apps/web/src/types.ts.
  • Column choice: courier_link is added separately from the existing courier_note. courier_note keeps its meaning (free-text rider/booking notes the customer sees); courier_link holds 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 preparing and the success screen shows that courier booking details will appear once the seller books the courier.
  • Seller admin UI: for each out_for_delivery (or confirmed/preparing delivery) 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).

8b. OCR-assisted receipt verification

  • 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=tesseract and TESSERACT_BIN is configured. If neither path works, proof_ocr_text stays null and 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.

8c. Multiple QR/account payment methods

  • The existing payment_methods table 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_wallet replaces the earlier narrow gcash enum value. Method labels carry the concrete wallet/network name: GCash, Maya, QR Ph, GoTyme, Maribank, etc. Existing gcash rows are migrated to e_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-methods folder.
  • Checkout lists only active methods and shows the selected QR/account details before receipt upload. Card remains disabled as "Coming Soon."

8d. Maps API (deprioritized, optional)

  • 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.

8e. Transactional email

  • Laravel mail configured via MAIL_* in .env.example (default log driver for dev — writes the rendered HTML to storage/logs/laravel.log).
  • App\Mail\OrderConfirmationMail Mailable, 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 into courier_link. No real SMTP provider without keys.
  • Feature test asserts the mailable renders and is sent on the status-transition event via Mail::fake().

8f. Post-payment success screen

  • Replaces the post-checkout redirect with a dedicated /orders/:id/success route showing the "preparing your order, courier details will appear here shortly" state. Same screen for same-day and pre-orders. The existing /orders/:id tracker renders courier_link as 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.

8g. Completion hardening decisions

  • Available menu items must have at least one recipe_ingredients row. 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/JSON tags, taste_profile, optional item-level cutoff_time, and sort_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.quantity for the matching batch context (online or pop_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 from pending to confirmed; completed/cancelled orders cannot be revived.
  • Root bun run build:mobile builds a production APK bundle against https://prepflow-api.onrender.com/api. Local Android emulator testing uses bun run build:mobile:local, a Capacitor origin of http://10.0.2.2, and a native API fallback of http://10.0.2.2:8000/api. The Laravel API therefore allows credentialed CORS only for explicit local/native origins from CORS_ALLOWED_ORIGINS; production APKs must use a real HTTPS API origin.

8h. Admin surge handling and courier paste behavior

  • 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_link accepts 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.

8i. Free-tier deployment target

  • 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, sets OCR_TECHNIQUE=tesseract, and uses TESSERACT_BIN=/usr/bin/tesseract.
  • The free domains are provider subdomains: *.onrender.com for the API and *.vercel.app for the web app. A custom .com or .ph is 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 stays SameSite=Lax unless .env overrides it.

8j. Customer account completion and pickup-aware UX

  • 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 ready state as Ready for Pickup and advance ready -> completed; only delivery modes render/advance through out_for_delivery.
  • Customer profile management uses the existing users fields (name, email, phone, address, password_hash). users.address stores 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.

8k. Native notification hardening

  • 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_tokens and 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.json when provided, and Laravel uses FCM_PROJECT_ID, FCM_CLIENT_EMAIL, and FCM_PRIVATE_KEY. Missing Firebase setup logs a no-op fallback and must not fail status updates.