# NVG-BEA-001 — Customer Payment Options · Work Log

**Branch:** `dev` · **Feature:** three payment modes (Pay on Visit / Pay Deposit / Pay 100% Online)

**Scope decision (locked):** NVG-BEA-001 delivers the **payment-mode mechanics only**. The
Navagoo Marketing Fee stays *uniform* (untouched). Payment Processing Fee → **NVG-BEA-002**;
Navagoo-sourced exemption + classification engine → **NVG-BEA-005**.

**Backend-only:** the Flutter customer app is a separate repo and is **out of scope** for this log.

> Times are local (UTC+3). This log is appended **after every task**.
> ⚠️ Chat messages are not timestamped, so wall-clock from the very first prompt isn't precisely
> measurable. The estimation + planning phase preceded the first commit and has no commit timestamps.

---

## Timing anchors
| Event | Time (UTC+3) |
|---|---|
| Migrations applied | 2026-06-08 ~10:41 |
| A1 committed (`3e3796c`) | 2026-06-08 11:04 |
| A1 testing + bug fix | 2026-06-08 ~11:30 |

---

## Task log

### A1 — DB + Models ✅ DONE
- **Estimate:** 6.5h (AI-assisted) · **Actual hands-on:** ~1h
- **Commit:** `3e3796c` (2026-06-08 11:04)
- **Delivered:**
  - 3 migrations — `shop_payment_settings` table · `payment_mode`/`deposit_amount`/`balance_due`/`amount_collected` on `booking` · `settings.max_deposit_percent` (=100) + `shop.max_deposit_percent_override`
  - `common/models/ShopPaymentSettings.php` (clone of `ShopSocialSettings`) — validators + `getEffectiveMaxDeposit()` / `getEnabledModes()` / `isModeAllowed()`
  - `Booking.php` (PAYMENT_MODE_* constants + rules), `Settings.php`, `Shop.php` (max-deposit rules)
- **Verification:** schema + model smoke test **14/14 pass**.

### A1 — Testing ✅ DONE · ⏳ commit pending
- **Estimate:** part of A7 · **Actual hands-on:** ~25min
- Added `common/tests/unit/models/ShopPaymentSettingsTest.php` (8 cases) → **8/8 pass**.
- 🐞 **Bug caught & fixed:** the "deposit % required when Pay Deposit is enabled" rule was being
  **skipped on an empty value** (`skipOnEmpty` default) → a deposit config could save with no
  percentage. Fixed with `skipOnEmpty => false` in `ShopPaymentSettings::rules()`.
- ⚠️ **Env note:** Codeception is **not installed** locally (missing from `composer.json`; the
  existing `common/tests` files don't run locally either). Verified via a temporary harness that
  runs the real test class. Optional follow-up: re-add Codeception so the suite runs in CI/local.
- **Uncommitted:** `common/models/ShopPaymentSettings.php` (fix) + the new test file.

---

## ⚠️ Incident — transient "unknown property" during A1 rollout (resolved)
- **Symptom:** customer-app booking creation + shop-portal working-hours save threw
  `Getting unknown property: ...::payment_mode` / `::max_deposit_percent_override`.
- **Root cause:** deploy-ordering race. The model edits (Booking/Shop `rules()` that *read* the new
  columns) went live via opcache (`revalidate_freq=2`) a few seconds **before** the migration added
  the columns. Requests in that window read a schema without the columns → unknown property.
- **Self-resolved:** env is `dev` → `DummyCache` (no schema cache) → fresh schema each request; once
  the migration completed, the next requests saw the columns.
- **Confirmed fixed:** booted the api web app exactly as `api/web/index.php` →
  `BookingResource->hasAttribute('payment_mode')` = true, reading the property = OK.
- **Wrong turn (logged honestly):** initial hypothesis was a stale schema cache → flushed schema /
  cleared per-app FileCache dirs. Those were harmless no-ops — dev uses `DummyCache`. The real fix
  was simply the migration finishing.
- **Process lesson:** run migrations **before** activating code that references new columns
  (in this manual flow the model edits hit opcache ~seconds ahead of the migration).

---

## A3 — Shop payment settings UI ✅ DONE
- **Estimate:** 5h (AI) · **Actual hands-on:** ~1h (incl. tests)
- New `frontend/controllers/PaymentSettingsController.php` + `frontend/views/payment-settings/index.php`
  (3 mode toggles + deposit % + warning banner), a "Payment Methods" menu item, and ar/en translations.
- Reads/writes `ShopPaymentSettings` (A1); validation lives on the model.
- **Tests:** `common/tests/functional/PaymentSettingsTest.php` — 4/4 (save round-trip, online-only
  default, all-disabled rejected, and the A1↔A3 effective-max + banner integration). Also verified
  end-to-end via curl (render + i18n + save→persist).

---

## A4 — Admin Commercial Config ✅ DONE
- **Estimate:** 4h (AI) · **Actual:** ~30 min
- Added the global **Maximum Deposit Percentage** field to the admin Settings form
  (`backend/views/settings/_form.php`) and the per-shop **Maximum Deposit % Override** to the admin
  Shop form (`backend/views/shop/_form.php`), plus ar/en labels. Columns + validation came from A1
  (`settings.max_deposit_percent`, `shop.max_deposit_percent_override`).
- **Tests:** +2 (global + override validate 1..100) → suite now 6/6.

---

## A2 — API checkout flow (3 modes) ✅ DONE
- **Estimate:** 7.5h (AI) · **Actual hands-on:** ~2h (incl. tests + live curl verification)
- **Delivered (`api/`):**
  - `BookingController::actionPay` now **branches by `payment_mode`**: `online` pays the full
    `total_amount`; `deposit` charges a **server-computed** deposit (`round(total × deposit%, 2)`)
    and records `balance_due`. The chosen mode is gated against the shop's `ShopPaymentSettings`.
  - New `actionPaymentOptions` (read-only: enabled modes + deposit/balance/total for the app's
    checkout step) and `actionConfirmOnVisit` (Pay-on-Visit → booking `Scheduled`, no Paymob /
    no Payment row, full amount owed at the shop).
  - Private `assertPaymentModeAllowed()` (shop with no settings = online-only) + `computeDeposit()`.
  - `BookingResource` exposes `payment_mode`, `deposit_amount`, `balance_due`, `amount_collected`,
    and a mode-aware `payment_message`.
  - Routes `payment-options` + `confirm-on-visit` registered in `_CustomerUrls.php` (the api uses
    an explicit `yii\rest\UrlRule` allow-list — actions are **not** auto-routed).
- 🐞 **Bug caught (IDE, pre-commit):** `PAYMENT_MODE_*` lived only on the `Booking` **subclass**,
  but `BookingController` imports `common\models\base\Booking` → *Undefined class constant* →
  runtime fatal on **every** `actionPay` (online included). Fixed by moving the constants to the
  **base** class (where `STATUS_*` / `BOOKING_METHOD_*` already live); the subclass inherits them.
- **Verification:**
  - **8/8** unit tests (added `testApiDepositMathAndModeGating` + `testConfirmationMessagesByMode`).
  - **3 live authenticated curls** against `POST /booking/payment-options`: no-token → **401**;
    valid token + own booking → **200** with correct math (total 1.00 → deposit 0.30 / balance 0.70,
    all 3 modes); valid token + another customer's booking → **404** (ownership scoping).
  - `confirm-on-visit` (mutates booking + sends notifications) and `pay` (real Paymob invoice) are
    covered by unit tests, **not** live-curled, to avoid side effects / external charges.
- **Note:** the 7.5h estimate bundled a webhook; no new webhook was needed — the Paymob IPN already
  exists in `PaymobPaymentHelper` and is reused.

---

## Estimate vs Actual

Hands-on dev time only (not wall-clock; **excludes** the upfront exploration done during the
estimation phase, which is real time that made the build fast).

| Task | Est. (AI) | Est. (Raw) | Actual hands-on | Speed-up |
|---|---:|---:|---:|---:|
| A1 — DB + Models | 6.5h | 10.5h | ~0.75h (~45 min) | ~8.6× |
| A1 — Testing (part of A7) | ~1.5h | ~2h | ~0.4h (~25 min) | ~4× |
| A3 — Shop payment settings UI (+ tests) | 5h | ~8h | ~1h | ~5–6× |
| A4 — Admin config (global + per-shop override) | 4h | ~6h | ~0.5h | ~8× |
| A2 — API checkout flow (3 modes) + endpoints | 7.5h | ~12h | ~2h | ~3.75× |
| **Total so far** | **~24.5h** | **~38.5h** | **~4.7h** | **~5×** |

**Why the gap (A1):**
- Exploration/grounding already done in the estimation phase (not counted in the 1.2h).
- A1 is the most "template" task — direct clone of `ShopSocialSettings` + simple `addColumn`.
- AI writes the code; the estimate carries buffer + assumes a cold start.

**⚠️ Do not extrapolate linearly** — 8.6× is template-task best-case. Tasks with real logic
compress less: expected A2 ~2–3×, A3/A4 ~3–4×, A6 ~1.5–2×.

**Conservative projection:** remaining backend (A2–A7 ≈ 25h AI est) → likely **~8–12h** actual under
these conditions, not the ~3h a flat 8.6× would imply.

**Note:** the estimate isn't "wrong" — it's a planning number with contingency; actual reflects
best-case conditions. And A1 testing paid off (caught a real validation bug).

---

## Remaining tasks (NVG-BEA-001 backend)
| Task | Estimate (AI) | Status |
|---|---:|---|
| A2 — API checkout flow (3 modes) + enabled-modes endpoint | 7.5h | ✅ done (~2h actual · 8 tests · 3 live curls) |
| A3 — Shop settings UI (toggles + deposit% + warning banner) | 5h | ✅ done (~1h actual · 4 tests) |
| A4 — Admin config (global max + per-shop override) | 4h | ✅ done (~0.5h actual · 2 tests) |
| A5 — Pay-on-Visit fee accrual | 1.5h | ⬜ (deferred hook → 005) |
| A6 — Cancellation (pay-on-visit only; deposit refund pending policy) | 2.5h | ⬜ |
| A7 — Backend tests (remaining) | 4.5h | ⬜ (partly started in A1 testing) |

**Backend build total (reviewed):** ~31.5h · **done so far:** A1 + A2 + A3 + A4 (~23h equivalent).
