PRD: Merchant Health Analysis
| Module | Reports | PRD ID | PRD-MAN-002 |
| Status | Ready to dev | FEAT | MAN · BANA-1440 |
| Epic | BANA-1337 | Plane | BANA-1736 |
| Date | 2026-07-07 | Version | v1.2 |
| Packages | @nx/sale | URD | MAN |
| Surface | BO · Ops | ||
| Owner | Phát Nguyễn | ||
TL;DR
Gives our operations team (NEXPANDO & VNPAY) a cross-merchant health view in the Back Office: how many merchants are Active vs Inactive (with a comparison to the immediately preceding period), the platform's total sales value (GMV) and average order value (AOV), how GMV moves over the period's natural time buckets, and a per-merchant table with each merchant's revenue, orders, and activity - filterable down to one specific merchant via a searchable dropdown. One calendar-based period + province + merchant filter drives every panel. It is the companion to Merchant Growth Analysis; the growth screen answers "how many and from where", this one answers "how well are they doing".
1. Context & Problem
Merchant Growth Analysis (PRD-MAN-001) shows the size and shape of the merchant base - counts, industries, provinces. It does not show whether those merchants are transacting: who is active, who has gone quiet, how much value flows through the platform, and which merchants drive it.
Our operations team needs that to steer the rollout - to spot merchants slipping into inactivity, to report platform GMV, and to see the per-merchant transaction picture without an ad-hoc data pull. This increment is the second screen under Merchant Analytics (MAN); it delivers operational health and the transactions-by-merchant view.
2. Goals & Non-Goals
Goals
- Show health KPIs at a glance: Active merchants, Inactive merchants, total GMV, average order value (AOV) - each with a comparison to the immediately preceding period and a "within period" context label.
- Show the Active vs Inactive split of the merchant base.
- Show the GMV trend over the period's natural time buckets (day/week/month depending on period length).
- Show a per-merchant table - each merchant's revenue, orders, and activity - with pagination, sort, and a mandatory filter down to one specific merchant via a searchable dropdown (matching on merchant code or name).
- Drive every panel from one filter: a calendar-based period (4 presets + a custom date range), a province, and (optionally) one specific merchant.
Non-Goals
- Merchant adoption counts, industry / province distribution - owned by PRD-MAN-001.
- A single merchant's own revenue / shift reports (
SLS,SHF- surface = client). - A composite "health score" formula - this screen shows activity and value directly; a scored model, if wanted, is a separate increment (see Open Questions).
- The unified operations home landing (not yet built).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Inactivity visibility | An operator can tell, for a period, how many merchants are Inactive and how that moved (%) vs the prior period |
| GMV reporting | Platform GMV and AOV for a period are read from the screen, not an ad-hoc query |
| Fast per-merchant lookup | Support/Sales types a merchant's code or name into the search dropdown and immediately gets its revenue/orders/activity, without paging through the table |
| Single-filter consistency | Changing period, province, or merchant re-reads every panel together; reopening the same preset within the same period always yields the same date range (calendar-based, not rolling) |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Operations team (NEXPANDO & VNPAY) | Watch merchant activity and value; find quiet or high-value merchants |
| Leadership / partner | Report platform GMV and the active/inactive trend |
Core scenario: an operator opens Merchant Health Analysis, picks a period and (optionally) a province; the 4 KPI cards show Active/Inactive counts, GMV and AOV with up/down arrows vs the prior period; the donut shows the Active vs Inactive split; the GMV chart shows value over the period's natural time buckets; and whenever they need to look at one specific merchant (e.g. support handling a ticket), the operator types the merchant's code or name into the search dropdown - every panel narrows down to that merchant.
5. User Stories
- As an operator, I want Active vs Inactive counts plus GMV and AOV on one row, with % change vs the prior period, so I judge platform health at a glance.
- As an operator, I want the Active/Inactive split visualised as a donut with percentages, so I see how much of the base is dormant.
- As an operator, I want the GMV trend over the period's natural time buckets, so I see whether value is rising or falling without the chart getting noisy on long periods.
- As an operator, I want a per-merchant table of revenue and orders, so I can inspect any single merchant.
- As a Support/Sales user, I want to type a code or name to find one specific merchant via a dropdown, so I can look up that merchant's situation instantly without paging through the table.
- As an operator, I want one period + province + merchant filter to drive every panel, so the screen tells one consistent story.
6. Functional Requirements
Locked definition: a merchant carries the label "Active" in a period = it has at least 1 completed transaction within the selected period; otherwise it is "Inactive". The label displays exactly as "Active" / "Inactive" per the mockup - it is not localized to another phrase. This one definition applies uniformly to the KPIs (FR-1), the donut (FR-2), and the table's Status column (FR-4). Note: this is a measure of transaction activity within the period, distinct from the merchant's license/lifecycle status (see the Total card tooltip in Merchant Growth Analysis) - a merchant with a valid license can still be Inactive if it had no transactions in the period.
| # | Requirement | Status | URD ref |
|---|---|---|---|
| FR-1 | Health KPIs - 4 cards (Active · Inactive · GMV · AOV) with prior-period comparison + "within period" label | ✅ built, 🚧 date label to add | URD-MAN-004 |
| FR-2 | Active vs Inactive split - 2-slice donut with % in the legend | ✅ built, 🚧 relabel | URD-MAN-004 |
| FR-3 | GMV trend - area chart, bucketed naturally by period length | 🚧 bucketing logic needs rebuild | URD-MAN-004 |
| FR-4 | Per-merchant table - 7 columns, server-side pagination, Active/Inactive badge | ✅ built, 🚧 relabel | URD-MAN-002 |
| FR-5 | Merchant search - searchable dropdown in the shared filter, by code/name, mandatory | 🚧 not built | URD-MAN-002 |
| FR-6 | Shared filter - 4 calendar-based period presets + province + merchant, resets on leaving | 🚧 period computation needs review | URD-MAN-006 |
| FR-7 | Ops scope & empty states - cross-merchant, permission-gated, empty states | ✅ built | URD-MAN-005 |
FR-1 - Health KPIs (✅ built, 🚧 date label to add)
Four cards on one row, each with the metric name, value, prior-period comparison, and a short description line:
| KPI | Calculation | Format |
|---|---|---|
| Active merchants | Merchants with ≥ 1 completed transaction in the selected period | Integer, green text |
| Inactive merchants | Merchants in the base with no transaction in the period | Integer, red text; the card has a warning (amber) background so it stands out |
| GMV | Total sales value of all completed transactions in the period, cross-merchant | Currency |
| AOV | GMV ÷ total order count in the period | Currency |
- Prior-period comparison on all 4 cards: the comparison period is the one immediately preceding the selected period, same length; each card shows an up/down arrow + % change (up = green, down = red) with a "vs previous period" caption.
- All 4 cards are flow metrics - each always carries a date context label "within DD/MM/YYYY - DD/MM/YYYY" right under the prior-period comparison, to distinguish them from the snapshot cards in Merchant Growth Analysis.
- A missing value renders as a dash "-"; while loading, skeleton placeholders show.
FR-2 - Active vs Inactive split (✅ built, 🚧 relabel)
- Chart type: 2-slice donut - Active (green) and Inactive (red).
- The legend under the chart lists both labels "Active" / "Inactive" with their % share (1 decimal place); the two shares sum to 100%.
- Uses the exact same two Active/Inactive figures as FR-1 - the donut and the KPIs never diverge.
FR-3 - GMV trend (🚧 bucketing logic needs rebuild)
Chart type: area chart (unchanged).
The horizontal axis no longer fixes 1 point per day - it divides into the natural time buckets of the selected period, using the same bucketing rule as the growth panel (PRD-MAN-001 FR-2):
Period Bucketing rule This month / Last month By week This quarter By month (exactly 3 points) This year By month, from January through the current month Custom range < 31 days By day Custom range 31-90 days By week Custom range > 90 days By month Vertical axis: each bucket's GMV.
Hovering a point shows a tooltip with the bucket's actual date range + GMV in currency format.
This bucketing rule fully replaces the previous approach (fixed 1 point/day, up to ~365 points for "This year") - see Risks.
FR-4 - Per-merchant table (✅ built, 🚧 relabel)
The "transactions by merchant" table, one row per merchant, with 7 columns:
| Column | Content |
|---|---|
| Organizer | The organizer that owns the merchant, in the viewer's language |
| Merchant | The merchant's name, in the viewer's language |
| Business type | The merchant's business type (badge) |
| Orders | The merchant's completed transaction count in the period |
| GMV | The merchant's total sales value in the period |
| Last transaction | Date-time of the most recent transaction |
| Status | Active / Inactive - per the locked definition at the top of §6 (badge) |
- Server-side pagination: 25 / 50 / 75 / 100 rows per page, default 50; the total row count is shown.
- Server-side sort per column; no per-column filters.
- The table follows the same period + province + merchant filter as the other panels.
FR-5 - Merchant search (🚧 not built, mandatory before launch)
- A merchant filter in the form of a searchable dropdown, placed in the shared filter bar next to period + province (not a separate search box on the table) - type to match on merchant code or merchant name.
- Selecting one merchant → every panel (all 4 KPI cards, donut, GMV chart, table) narrows to that exact merchant; left empty = all merchants within the selected period + province scope.
- Clearing the selection → every panel returns to the full scope.
- This is a mandatory filter, not a nice-to-have: Support/Sales needs to look up a specific merchant's situation immediately (e.g. on a support ticket or account follow-up) without paging through the table.
FR-6 - Shared filter (🚧 period computation needs review)
- Shares the same calendar-based logic as PRD-MAN-001: every period has fixed calendar boundaries, no rolling logic. 4 fixed presets (This month - default · Last month · This quarter · This year; running presets have a calendar-fixed start date, ending today due to the data-availability limit) plus a custom date range (shared requirement FR-7 🚧 over in PRD-MAN-001).
- Single-select province, defaulting to "All provinces".
- Merchant selection via the searchable dropdown (FR-5) - the filter's third dimension, defaulting to empty (all merchants).
- Default on entry: This month + all provinces + no merchant selected; leaving the screen resets the filter to the default.
FR-7 - Ops scope & empty states (✅ built)
- The screen is cross-merchant, lives in the Back Office "Reports & Analytics" section beside the growth screen, and is permission-gated to the operations team; a merchant's own users can never reach it.
- No data → per-panel empty states, not errors; while loading → placeholders.
Acceptance criteria
- For a period with transactions: all 4 KPI cards show values; Active count + Inactive count = total merchants within the filter scope.
- AOV × total order count ≈ GMV (within currency rounding).
- Every KPI card shows an arrow + % vs the immediately preceding period of the same length, with a "within DD/MM/YYYY-DD/MM/YYYY" label; the "Inactive merchants" card has a warning background clearly distinct from the other 3.
- The two donut shares match the KPI Active/Inactive figures exactly and sum to 100%; the labels display exactly "Active"/"Inactive".
- The GMV chart buckets correctly per the rule table in FR-3 (e.g. This quarter yields exactly 3 points by month, This year yields points by month rather than by day); the sum of every bucket's GMV equals the KPI GMV.
- Reopening the same preset on two different days within the same period → the period's start date is unchanged; there is no "last 30 days" rolling-window behaviour.
- A merchant with ≥ 1 completed transaction in the period → its table row shows Status "Active"; with none → "Inactive".
- The table has all 7 columns; changing rows-per-page from 50 to 25 → the table reloads from page 1 with the same total row count; sorting a column reorders server-side.
- Changing the period, province, or merchant → KPIs, donut, GMV chart, and table re-read together.
- Leaving and returning → the filter is back to "This month + all provinces + no merchant selected".
- Signing in as a merchant's own user → the screen cannot be opened.
- A period with no transactions → the panels show empty states, not errors.
- (Once FR-5 is built) Typing a merchant's code or name into the search dropdown and selecting it → all 4 KPI cards, the donut, the GMV chart, and the table narrow to that exact merchant; clearing the selection → every panel returns to the full scope.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Read-only | The screen never changes data - it only summarises activity and value |
| Tenancy & authz | Cross-merchant scope, permission-gated to operator roles; never reachable by a merchant's own users |
| Consistency | KPIs, donut, GMV chart, and table all reflect the same filter (period + province + merchant) and the same "Active"/"Inactive" definition |
| Scale | The per-merchant table pages (and searches, once built) server-side so it stays responsive as merchants grow |
| i18n | Labels and money formatting are bilingual (EN/VI); organizer/merchant names follow the viewer's language; the "Active"/"Inactive" labels stay in English in both languages per the mockup |
| Empty & loading | No data → an empty state; while loading → a placeholder; partial data never breaks the screen |
8. UX & Flows
How an operator uses the screen:
What the one filter drives:
The screen belongs in the Back Office "Reports & Analytics" section, beside Merchant Growth Analysis. It is live in the menu.
9. Data & Domain
| Concept | Role in this screen |
|---|---|
| Merchant | The unit being measured - has an activity state (Active / Inactive) and its transactions |
| Activity | Active = ≥ 1 completed transaction within the selected period; otherwise Inactive - the single definition, shared by KPIs, donut, and table; distinct from the license/lifecycle status in Merchant Growth Analysis |
| GMV | Gross merchandise value - total sales value across merchants for the period |
| AOV | GMV ÷ total order count for the period |
| Comparison period | The period immediately preceding the selected one, same length - the source of the KPI arrows and % changes |
| Merchant row | A single merchant's revenue, order count, last transaction, and activity in the period (transactions by merchant) |
| Merchant filter | Searchable dropdown by merchant code/name (FR-5); when selected, narrows every panel to that one merchant |
Conceptual only - the figures are cross-merchant aggregates over completed transactions in the period; the data model lives in the developer docs.
10. Dependencies & Assumptions
Depends on
- Merchant Growth Analysis (PRD-MAN-001) - shares the same filter and the
MANanalytics surface. - Sales & orders - completed transactions are the source of GMV, AOV, activity, and the per-merchant table.
- Commerce - the merchant base, its owning organizers, and business types (Commerce).
Assumptions
- The operator is authenticated with a cross-merchant operations role.
- Merchants belong to an organizer and carry a business type, so the table's Organizer / Business type columns are meaningful.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Is operational health a single score or just the raw activity/value figures? | This PRD surfaces the figures directly; a composite health-score model would be a separate increment |
| The merchant search filter (FR-5) is not built, but is now a mandatory requirement for Support/Sales | Blocks launch until the merchant search dropdown works across every panel - track build progress closely |
| The currently-built period computation may still be rolling under the hood | Audit the period-computation layer before treating FR-6 as fully calendar-based |
| Large merchant counts in the table | Pagination (and search, once built) is server-side to stay responsive |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - live in the Back Office |
| Rollout | All operator users; no feature flag |
| Migration | None - it summarises existing transactions |
| Launch criteria | The filter (calendar-based period + province + merchant) drives every panel with no rolling behaviour; the 4 KPI cards show Active / Inactive / GMV / AOV with % vs the prior period and a "within period" label; the 2-slice donut matches the KPIs with labels exactly "Active"/"Inactive"; the GMV chart buckets naturally per FR-3; the 7-column table (Active/Inactive badge) pages server-side (25/50/75/100, default 50); the screen is reachable from the BO menu; no data shows an empty state. The merchant search filter by code/name (FR-5, dropdown in the shared filter bar) is mandatory and blocks launch - essential for Support/Sales |
| Monitoring | How often the report is opened; per-merchant table query latency |
13. FAQ
How is this different from Merchant Growth Analysis? Growth answers "how many merchants and from where"; Health answers "how active and how much value", down to a per-merchant table.
What does "Active" mean? A merchant with at least 1 completed transaction in the selected period. No transaction in the period = "Inactive". One definition for the KPIs, the donut, and the table - distinct from the license/lifecycle status shown in Merchant Growth Analysis.
Is there a single health score? Not yet - the screen shows activity and value directly. A scored model can come later if operations want one.
Can I find a specific merchant? Yes - type a code or name into the search dropdown in the shared filter bar (FR-5); selecting a merchant narrows every panel (not just the table) to that exact merchant. This is a mandatory requirement, currently not built - the table can still be paged and sorted while it's being built.
Is this a single merchant's revenue report? No - it is cross-merchant. A merchant's own revenue is the client-side SLS reports.
Can a merchant's own user open this? No - it is cross-merchant and gated to the operations team.
References
- URD: Reports -
MAN(Merchant Analytics) ·SLS - Sibling: PRD-MAN-001 - Merchant Growth Analysis
- Module: Reports - URD · Roadmap: Phase 2 - By Month
- Developer: @nx/sale - Sales Report