# NVG-BEA-006 — Detailed Charges Report — Gap Analysis + Implementation Plan

**Status: COMPLETE — started + finished 2026-07-23.** Live tracker at the bottom.
Spec: NVG-BEA-006 BRD (user-provided). Audited 2026-07-23 (agent, file:line evidence).

## Verdict: PARTIAL — both surfaces exist on the right foundation; spec polish missing

### DONE
- The immutable `charge` ledger IS the line-item source (one row per charge; reversals =
  negative rows via `reversal_of_id` — marketing + processing both reverse now).
- **Admin**: `/admin-charge/index` in the Finance hub tab already named "Detailed Charges";
  columns Charge ID/Shop/Booking/Date/Type/Rate("{rate}% + {fixed}")/Charge/VAT/Status;
  filters status+type+shop; filter-aware CSV.
- **Shop**: Earnings hub tab "Detailed Charges" (`earnings/_charges.php`); columns incl.
  Invoice/TR; filters status+type; filter-aware CSV (`actionChargesCsv`).
- Reversal linkage rendered ("reverses #{id}"), signed amounts print naturally.
- BRD rule coverage by construction: no marketing row for shop-owned, no processing row for
  pay-on-visit (rows simply don't exist).

### GAPS (both portals unless noted)
1. No **date-range filter**; no **Booking ID search** (ChargeQuery::forBooking never wired).
2. No **totals row** (per-charge_type + overall over the filtered set).
3. No **PDF export** (only window.print).
4. No **Booking Amount (SAR)** column (the underlying booking total).
5. No composed **Charge Description** column (type chip + rate are separate; no desc field —
   derive "{Type label} — {rate}% + {fixed} on {basis}" style string; no schema change).
6. Admin table missing **Transfer Request ID** column (model has it; shop side shows it).
7. Reversal rows: style negative amounts distinctly (red) per BRD readability.

## Waves
- **W1 Admin polish** (backend/controllers/AdminChargeController.php +
  backend/views/admin-charge/index.php): date-range + booking-id filters (query + CSV),
  Booking Amount column (one batched booking lookup), derived Description column,
  Transfer Request ID column, totals row (per-type + overall, single aggregate query over
  the filtered set), Mpdf PDF export action (repo pattern: PaymentController /
  agents-wallet _pdf), negative-amount styling.
- **W2 Shop polish** (frontend/controllers/EarningsController.php +
  frontend/views/earnings/_charges.php + actionChargesCsv): the same additions, kept as
  the existing "Detailed Charges" tab (nav slot already satisfies the BRD). Read-only.
- **W3 Translations + light unit tests (totals/description helpers if extracted) + smoke +
  docs.**

### Sequencing
W1 ∥ W2 (backend vs frontend, disjoint) → W3. Coordinated with the still-running BEA-011
W5 agent (messages writer — W1/W2 don't touch messages).

### Out of scope (per BRD)
Real-time charge notifications · customer-facing breakdown · shop-configurable charge
types. OQ (near-term charge types) → Muhannad.

## Live tracker
- [x] W0 audit + this plan
- [x] W1 admin polish — done 2026-07-23. `AdminChargeController` gained
  `parseFilters()`/`buildQuery()`/`computeTotals()`/`bookingsByCharges()`/
  `chargeDescription()` shared by index/CSV/PDF; date-range (`date_from`/`date_to`
  on `created_at`) + Booking ID (`ChargeQuery::forBooking`) filters; `actionExportPdf()`
  (Mpdf, `A4-L`, new `_pdf.php`). `index.php` gained Booking Amount (SAR) + Transfer
  Request ID columns, Description folded under the Type chip, a filtered-set totals
  `<tfoot>` (per-type + overall, one `GROUP BY type` aggregate query), red styling for
  negative (reversal) charge amounts, and an Export PDF button. CSV columns extended
  to match. Curl smoke: index/CSV/PDF all 200, no error markers, booking_id filter
  verified to correctly empty the table. New `Yii::t('backend', ...)` keys (not yet in
  message files — flagged for the i18n pass): `From`, `To`, `Booking ID`, `e.g. 1024`,
  `Apply`, `Clear dates/booking`, `Export PDF`, `Booking Amount (SAR)`,
  `Transfer Request`, `Overall total`, `Detailed Charges`, `Generated {date}`,
  `Date range`, `Navagoo Marketing Fee — {rate}% of booking value`,
  `Payment Processing Fee — {rate}% + {fixed} of collected`, `Navagoo Subscription Fee`,
  `{count} billed message(s)`.
- [x] W2 shop polish — done 2026-07-23. `EarningsController` gained
  `parseChargeFilters()`/`filteredChargesQuery()`/`chargesTotals()`/
  `bookingsByCharges()`/`chargeDescription()` shared by the index charges block,
  `actionChargesCsv()`, and the new `actionChargesPdf()`; date-range
  (`filterDateFrom`/`filterDateTo` on `created_at`) + Booking ID (`filterBookingId`,
  `ChargeQuery::forBooking`) filters, submitted via a second GET filter form that
  preserves `filterStatus`/`filterType`/`tab=charges`. `_charges.php` gained a
  Booking Amount (SAR) column (one batched booking lookup), derived Charge
  Description folded under the Type chip, a filtered-set totals `<tfoot>` (per-type
  + overall, one `GROUP BY type` aggregate), red styling for negative/reversal
  charge amounts, an Export PDF button next to Export CSV, and the footer's
  `{shown} of {total}` now receives the real unfiltered shop charge count (was
  silently defaulting to shown===total). New `frontend/views/earnings/_charges_pdf.php`
  (Mpdf, `A4-L`, mirrors `agents-wallet/_pdf.php`). CSV columns extended with
  Booking Amount / Description / Transfer Request ID. Bug found + fixed along the
  way: `frontend/views/earnings/index.php` explicitly whitelists which vars get
  forwarded to `_charges.php` and was dropping every new one (date/booking-id
  filters silently reset on every request) — added the 6 missing keys. Curl smoke:
  index (unfiltered + status/date/booking-id filtered), CSV, PDF all 200, guest
  hits both exports → 302, no PHP error markers in any response; totals footer and
  sticky filter values verified in the rendered HTML for the dev shop-owner
  account. New `Yii::t('frontend', ...)` keys added directly to
  `common/messages/{en,ar}/frontend.php` (bilingual, real Arabic — not just
  flagged): `Navagoo Marketing Fee — {rate}% of booking value`,
  `Payment Processing Fee — {rate}% + {fixed} of collected`,
  `Navagoo Subscription Fee`, `Booking Amount (SAR)`, `e.g. 1024`, `Apply`,
  `Clear dates/booking`, `Overall total`, `Export PDF`, `Filters`,
  `Transfer Request`, `Description` (`{count} billed message(s)`, `From`, `To`,
  `Booking ID`, `Generated {date}`, `Shop not found.`,
  `No charges match these filters.`, `reverses {id}` already existed and were
  reused as-is).
- [x] W3 translations + tests + verify — done 2026-07-23. Residual `Yii::t('backend', ...)`
  keys from W1 (`AdminChargeController.php` + `admin-charge/{index,_pdf}.php`) grepped
  against `common/messages/en/backend.php`: 11 were actually missing (not the ~17 the
  W1 tracker note flagged — 6 of those, `From`/`To`/`Booking ID`/`Apply`/
  `Transfer Request`/`Detailed Charges`, already existed pre-feature as generic backend
  strings). Added to `en/backend.php` + `ar/backend.php` (real Arabic, reusing the
  identical W2 frontend translations where the English text is byte-identical, for
  cross-portal wording consistency): `Booking Amount (SAR)`, `Clear dates/booking`,
  `Date range`, `Export PDF`, `Generated {date}`, `Navagoo Marketing Fee — {rate}% of
  booking value`, `Navagoo Subscription Fee`, `Overall total`, `Payment Processing Fee
  — {rate}% + {fixed} of collected`, `e.g. 1024`, `{count} billed message(s)`. Parity
  verified: en/ar both 3361 keys, 0 drift either direction. `php -l` clean on both
  message files + the controller + both views.
  Tests: new `common/tests/unit/controllers/{AdminChargeControllerTest,
  EarningsControllerTest}.php` (38 tests, 57 assertions, all green) — reflection-based
  (both controllers' `chargeDescription()`/`parseFilters()`/`parseChargeFilters()` are
  private; instances built via `newInstanceWithoutConstructor()` to skip
  `BackendController::init()`'s web-request/cookie dependency the console-app test
  bootstrap doesn't provide). Covers: `chargeDescription()` per charge type (marketing/
  processing/subscription/sms/whatsapp incl. meta-count pluralization and floor-to-1/
  net-from-settlement/reversal/unknown-type on both controllers, incl. the frontend's
  "Net from Settlement" vs backend's "Net from settlement" wording divergence);
  `parseFilters()`/`parseChargeFilters()` date-range validation (rejects garbage,
  accepts Y-m-d, one-sided garbage doesn't clobber the valid side) + status/type/
  booking-id parsing (admin defaults to `'all'`, shop defaults to `''`; shop strips
  non-digits from `filterBookingId`, admin does a plain int cast); `buildQuery()`/
  `filteredChargesQuery()` day-boundary conversion (00:00:00/23:59:59 inclusive
  timestamps), verified by inspecting the built `ActiveQuery::$where` array (no DB
  execution). Full suite: 388 tests / 885 assertions, 6 errors / 15 failures — byte-
  identical to the pre-existing baseline (350 tests before + 38 new; all 15
  failures/6 errors are unrelated pre-existing flakes in Aurora/CalendarFormat/
  SmsLog/TokenExpiration tests, confirmed by diffing the failing-test list).
  Smoke (backend session cookie, `Host: backend.navagoo.localhost`): `/admin-charge/index`
  200 no error markers (Arabic renders by default — CSV export in the same pass
  confirmed the new "رسوم اشتراك نافاجو" Arabic string rendering correctly end-to-end),
  `/admin-charge/export-csv` 200 (`text/csv`, new Description column populated),
  `/admin-charge/export-pdf` 200 (valid 1-page PDF). Frontend guest
  `/earnings/index` → 302 as expected. No production code changed — read-only report
  surfaces, i18n-only diff.
