diff --git a/DECISIONS.md b/DECISIONS.md index 8211ba6..2dc422f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -60,7 +60,7 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready - 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").~~ — superseded by §8b (PayMongo, opt-in, guarded). +- Card payment processing (UI tile only, "Coming Soon"). - Push notifications (stretch; not promised). - Multi-tenant / multi-seller (single owner). @@ -70,7 +70,9 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready > Phase-1 prohibitions in §7 / AGENTS.md §5 *only* for the items below, and only > as described. Everything else in §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. +> 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`). @@ -81,10 +83,8 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready 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. -- When `GOOGLE_MAPS_API_KEY` is present (§8c), the fee is derived from road - distance via a distance-band heuristic (0–10 / 10–25 / 25–50 / 50+ km). When - the key is absent, the buyer picks a city from a dropdown and the tier table - supplies the fee directly. Both paths land in the same `shipping_fee` column. +- 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` @@ -94,7 +94,7 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready `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 (§8d) fire on + 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: "We're preparing your order. Your Lalamove/Grab tracking link will @@ -102,49 +102,45 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready - Seller admin UI: for each `out_for_delivery` (or `confirmed`/`preparing` delivery) order, a field to paste the courier booking/tracking link. Pasting it surfaces the link to the customer's order tracker **and** triggers the - confirmation email (§8d). - -### 8b. PayMongo (opt-in, guarded) -- **Reasoning first (weakest point):** a real PayMongo flow needs live secret - keys the project does not have; without them the card tile is - "temporarily unavailable" — the same end state as the old "Coming Soon" tile - for a live demo. So PayMongo adds architecture/defensibility value (a real - PHP→third-party JSON integration, a mocked test, a webhook signature check) - but little *demo* value over the Phase-1 tile. We implement it anyway, **fully - guarded**, because the graded primitives it surfaces (server-side JSON API - calls, signature validation, a testable singleton) are worth more than the - tile's demo state — and the task explicitly asks for it with a graceful - fallback. -- **Architecture decision:** call the PayMongo REST API via Laravel's `Http` - facade through a `PayMongoService` bound as a singleton in - `AppServiceProvider` (same test seam as Cloudinary) — **not** the PayMongo - PHP SDK. Reason: raw HTTP keeps the JSON request/response handling visible in - source (rubric V), avoids a fragile composer dependency that can't be - exercised without real keys, and mirrors how the graded PHP integration is - expected to look. The singleton is swapped with a mock in tests. -- Flow: server creates a Checkout Session, returns the redirect URL; the SPA - redirects the buyer. On return, a webhook endpoint validates the - `Paymongo-Signature` header and marks the order `payment_uploaded`/`confirmed`. - **Card data never touches the server** — PayMongo hosts the card form. If any - approach required storing card numbers it would be rejected. -- `PAYMONGO_SECRET_KEY=` / `PAYMONGO_PUBLIC_KEY=` added to `.env.example` - (placeholders). Left blank in `.env`; the service short-circuits to a - "temporarily unavailable" response and the card tile renders that state. COD / - GCash / Bank verification workflows are preserved as fallback so the rubric's - verification flow still exists. - -### 8c. Maps API -- Google Maps JavaScript API + Geocoding + Distance Matrix in `apps/web`. -- Server-side: a thin `POST /api/distance` (auth) endpoint proxies a Distance - Matrix call using a server-stored `GOOGLE_MAPS_API_KEY` (never exposed to the - client). Rate-limited by auth. -- Address autocomplete (Places Autocomplete, restricted to Philippines) on the - checkout address field, **only when the key is present**. -- `GOOGLE_MAPS_API_KEY=` added to `.env.example` (placeholder). **Feature flag:** - when the key is empty, the shipping tier falls back to the manual city - dropdown (§8a tier table) so the app stays fully functional for grading. - -### 8d. Transactional email + confirmation email (§8e). + +### 8b. OCR-assisted receipt verification +- **Reasoning first (weakest point):** OCR can misread noisy GCash 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 + GCash/bank receipts. No amount/reference parsing is attempted because wallet + and bank screenshot layouts vary. 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 GCash + numbers and multiple bank accounts: one row per account/QR. This avoids a new + schema and keeps the buyer flow simple. +- 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 @@ -156,7 +152,7 @@ Statuses (ENUM): `pending`, `payment_uploaded`, `confirmed`, `preparing`, `ready - Feature test asserts the mailable renders and is sent on the status-transition event via `Mail::fake()`. -### 8e. Post-payment success screen +### 8f. Post-payment success screen - Replaces the post-checkout redirect with a dedicated `/orders/:id/success` route showing the "preparing your order, delivery link will appear here shortly" state. Same screen for same-day and pre-orders. The existing