PRD: Merchant Growth Analysis
| Module | Reports | PRD ID | PRD-MAN-001 |
| Status | Ready to dev | FEAT | MAN · BANA-1440 |
| Epic | BANA-1337 | Plane | BANA-1707 |
| 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 single cross-merchant adoption view in the Back Office: the scale of the merchant and store base (total registered, new in the period), how new merchants arrive across the period's natural time buckets (broken down by industry), and how merchants distribute by industry (household vs business) and by province (top 5 + a grouped "Other"). One calendar-based period filter (4 presets + a custom date range) + province drives every panel.
1. Context & Problem
Every merchant already has its own revenue reports (SLS, surface = client) - but those answer "how is this merchant doing", scoped to a single merchant. Our operations team has a different question as onboarding ramps: how is the platform as a whole growing - how many merchants there are, what state they are in, how fast new ones arrive, in which industries, and in which provinces.
Until now that answer was reachable only through ad-hoc data pulls. This increment makes it a first-class operator screen: an adoption dashboard that presents the cross-merchant picture in one place. It is the first of two screens under Merchant Analytics (MAN); the second, Merchant Health Analysis, covers transactions-by-store and operational health.
2. Goals & Non-Goals
Goals
- Show the merchant and store base at a glance: the total registered on the system and the new count within the period, split separately for merchants and stores.
- Show the new-merchant trend across the period's natural time buckets (day/week/month depending on period length), broken down by industry.
- Show distribution by industry, each industry split into household (HKD) vs business (DN).
- Show distribution by province - the top 5 provinces plus a grouped "Other" - each with its % share of the total.
- Drive every panel from one filter: a calendar-based period (4 presets + a custom date range) and a province.
Non-Goals
- A single merchant's own revenue / shift reports (
SLS,SHF- surface = client). - Transactions-by-store and operational health - owned by PRD-MAN-002.
- The unified operations home landing that aggregates all analytics (not yet built).
- Editing merchants - this screen is read-only; merchant CRUD lives in Commerce.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Self-serve adoption reporting | An operator answers "merchants onboarded by industry / province this period" without an ad-hoc data pull |
| Single-filter consistency | Changing the period or province re-reads all four panels together; none is left stale; reopening the same preset within the same period always yields the same date range (calendar-based, not rolling) |
| Fast trend reading | New-merchant growth is grasped across the period's natural time buckets, broken down by industry |
| Correct reading of the Total figures | Operators don't confuse "Total merchants/stores" with the count that's actually active - the Total card's tooltip clearly points to Merchant Health |
| Distribution integrity | Each industry total reconciles with its household + business split; province shares sum to 100% (including "Other") |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Operations team (NEXPANDO & VNPAY) | Watch platform adoption - counts, status mix, and where growth comes from |
| Leadership / partner | Report onboarding progress by industry and region |
Core scenario: an operator opens Merchant Growth Analysis, picks This month (default) and (optionally) a province; the snapshot shows total registered merchants/stores and the new count for the period (each with a clear date label), the growth panel shows new merchants across the period's natural time buckets broken down by industry, and the two distribution panels show which industries (household vs business) and which provinces drive the base.
5. User Stories
- As an operator, I want the total registered merchants/stores and the new count for the period on one row, so I see platform size and onboarding pace at a glance.
- As an operator, I want the new-merchant trend broken down by industry, so I see not just how fast we grow but in which sectors.
- As an operator, I want merchants split by industry into household vs business, so I understand the customer mix.
- As an operator, I want the top 5 provinces (plus a grouped "Other") with their % share, so I see geographic concentration.
- As an operator, I want one period + province filter to drive every panel, so the whole screen tells one consistent story.
6. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
| FR-1 | Snapshot - 4 stat cards: total/new for merchants and stores | 🚧 needs rebuild | URD-MAN-003 |
| FR-2 | New-merchant trend - vertical stacked columns, natural period buckets, stacked by industry | 🚧 bucketing logic needs rebuild | URD-MAN-003 |
| FR-3 | Distribution by industry - horizontal stacked bars, household vs business split, sorted descending | ✅ built | URD-MAN-001 |
| FR-4 | Distribution by province - horizontal bars, top 5 + grouped "Other", with % share | ✅ built | URD-MAN-001 |
| FR-5 | Shared filter - 4 calendar-based period presets + single province, resets on leaving | 🚧 period computation needs review | URD-MAN-006 |
| FR-6 | Ops scope & empty states - cross-merchant, permission-gated, empty states | ✅ built | URD-MAN-005 |
| FR-7 | Custom date range (from - to) alongside the presets | 🚧 not built | URD-MAN-006 |
FR-1 - Merchant & store scale snapshot (🚧 needs rebuild)
Four stat cards on one row: Total merchants · New merchants · Total stores · New stores. These four cards fully replace the previous three status cards (Enabled / Suspended / Expired) - merchant lifecycle status no longer shows on this panel; see Merchant Health Analysis (PRD-MAN-002) to see which merchants are actually active.
Calculation, within the province filter scope (the period only applies to the two "new" cards):
Card Formula Metric type Total merchants COUNT(Organizer) WHERE deleted_at IS NULLSnapshot - as of now New merchants COUNT(Organizer) WHERE created_at BETWEEN [selected period]Flow - within the period Total stores COUNT(Store) WHERE deleted_at IS NULLSnapshot - as of now New stores COUNT(Store) WHERE created_at BETWEEN [selected period], computed independently of New merchants (a new store may belong to a pre-existing merchant)Flow - within the period A date context label renders right under each value: the snapshot cards (Total merchants, Total stores) show "as of DD/MM/YYYY" (today's date); the flow cards (New merchants, New stores) show "within DD/MM/YYYY - DD/MM/YYYY" (the exact boundaries of the selected period).
A mandatory tooltip/help text on the two Total cards (Total merchants, Total stores): "Total merchants/stores = the total count registered on the system, regardless of whether they are still active or have gone inactive. To see which merchants are actually active, see Merchant Health." Reason for the tooltip: the Total figures include merchants/stores that have gone inactive but are not yet deleted (
deleted_at IS NULL), which is easy to misread without an explanation.Each card has a distinct background colour (Total = blue, New = green).
A missing value renders as a dash "-"; while loading, a placeholder shows.
FR-2 - New-merchant trend (🚧 bucketing logic needs rebuild)
Chart type: vertical stacked columns (unchanged).
The horizontal axis no longer evenly divides into 6 fixed buckets - it divides into the natural time buckets of the selected period:
Period Bucketing rule This month / Last month By week This quarter By month (exactly 3 columns, one per month in the quarter) This year By month, from January through the current month (no future months) Custom range < 31 days By day Custom range 31-90 days By week Custom range > 90 days By month The number of columns is therefore no longer fixed - it varies with the period length per the rule above (e.g. This quarter always yields exactly 3 columns; This month typically yields 4-5 columns depending on how the weeks fall).
Vertical axis: the number of new merchants in each bucket, by the date each merchant joined the platform (
created_at).Each column is stacked by industry / business type with a colour legend; the bucket's total renders on top of the column.
Hovering a column shows a tooltip with the bucket's actual date range and the per-industry new-merchant counts for that bucket.
FR-3 - Distribution by industry (✅ built)
- Chart type: horizontal stacked bars, one row per industry, sorted descending by total merchant count.
- Each row splits into two segments: household (HKD) and business (DN); the HKD and DN counts render right under the industry name; the industry total renders to the right of the bar.
- Each industry total always equals HKD + DN. Merchants with no industry set fall under a dash "-" label.
- Tooltip and legend clearly distinguish HKD vs DN.
FR-4 - Distribution by province (✅ built)
- Chart type: horizontal bars, showing exactly the 5 provinces with the most merchants (sorted descending) plus one "Other" row grouping all remaining provinces.
- Each row: the province name (in the viewer's language) + merchant count + % share rounded to a whole number of the total.
- The "Other" row renders in a lighter colour to distinguish it from the top 5.
- The shares of all rows (including "Other") sum to 100%.
FR-5 - Shared filter (🚧 period computation needs review)
- The period filter uses calendar-based logic: every period has fixed boundaries anchored to the calendar. No more rolling logic (e.g. no "last 30 days" counted back from the moment the screen opens) - this guarantees that reopening the same preset within the same period always returns the same date range, and that comparisons across periods never drift with the time of day the screen was opened.
- 4 fixed presets: This month (default) · Last month · This quarter · This year, plus an optional custom date range (see FR-7). Presets for a running period (This month / This quarter / This year) have a calendar-fixed start date (the 1st of the month/quarter/year); the end boundary is today because future data doesn't exist yet - that is a data-availability limit, not a rolling window.
- Single-select province, defaulting to "All provinces"; province names follow the viewer's language.
- Default on entry: This month + all provinces.
- Changing the preset/date range or province → all four panels re-read together.
- Leaving the screen → the filter resets to the default; every visit starts clean.
FR-6 - Ops scope & empty states (✅ built)
- The screen is cross-merchant, lives in the Back Office "Reports & Analytics" section, and is permission-gated to the operations team; a merchant's own users can never reach it.
- A panel with no data shows its own empty state, not an error; partially missing data never breaks the screen.
FR-7 - Custom date range (🚧 not built)
- Alongside the 4 presets, the operator can pick any date range (from - to, both ends inclusive); all four panels react exactly as with a preset, including the bucketing rule in FR-2 (based on the selected range's length: <31 days / 31-90 days / >90 days).
- The custom range also uses calendar-based logic (no counting back from the current time) and resets on leaving, as in FR-5.
Acceptance criteria
- Pick the "Last month" preset: the snapshot and all 3 charts re-read; no panel keeps the previous period's figures.
- The snapshot shows all 4 cards: Total merchants · New merchants · Total stores · New stores; the Total cards always carry a "as of DD/MM/YYYY" label, the New cards always carry a "within DD/MM/YYYY-DD/MM/YYYY" label; a card with no data shows "-" while the others render normally.
- Hovering/tapping the tooltip icon on the Total merchants or Total stores card → shows the correct explanation that the total does not distinguish active from inactive, with a link over to Merchant Health.
- The growth chart buckets correctly per the rule table in FR-2 (e.g. This quarter always yields exactly 3 columns by month; This month yields columns by week); the column totals sum to the period's total new merchants; each column's tooltip shows the correct sub-range of dates.
- Reopening the same preset on two different days within the same period (e.g. opening "This month" on day 5, then on day 20) → the period's start date is unchanged; there is no "last 30 days" rolling-window behaviour.
- In the industry panel, for every row: HKD count + DN count = the total shown to the right; rows are ordered descending by total.
- In the province panel: at most 6 rows (5 provinces + "Other"); the row shares sum to 100%.
- Selecting a province in the filter → all four panels count only merchants in that province.
- Leaving and returning → the filter is back to "This month + all provinces".
- Signing in as a merchant's own user → the screen cannot be opened.
- A period with no new merchants → the growth panel shows an empty state, not an error.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Read-only | The screen never changes data - it only summarises the merchant base |
| Tenancy & authz | Cross-merchant scope, permission-gated to operator roles; never reachable by a merchant's own users |
| Consistency | All panels reflect the same filter at the same time |
| i18n | Labels are bilingual (EN/VI); province names follow the viewer's language |
| Empty & loading | No data → an empty state per panel; 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 lives in the Back Office "Reports & Analytics" section. The filter is remembered while the operator works and reset on leaving, so each visit starts clean.
9. Data & Domain
| Concept | Role in this screen |
|---|---|
| Merchant | The unit being counted - carries an industry / business type and a province; lifecycle status (enabled / suspended / expired) no longer shows on this screen, see Merchant Health Analysis |
| Industry / business type | Groups merchants for the growth breakdown and the industry distribution; also distinguishes household vs business |
| Province | Groups merchants for the geographic distribution |
| Period | The calendar-based date range (per preset or custom range) over which new-merchant growth is measured; divided into natural buckets (day/week/month depending on length) on the growth chart - see FR-2 |
Conceptual only - the underlying figures are cross-merchant aggregates over the merchant base and its onboarding dates; the data model lives in the developer docs.
10. Dependencies & Assumptions
Depends on
- Commerce - merchants, their status, and their industry / business type (Commerce).
- Onboarding dates - when each merchant joined, to compute the growth trend.
- Province reference data - to label and group merchants geographically.
Assumptions
- The operator is authenticated with a cross-merchant operations role.
- Merchants/stores have reliable
created_at/deleted_atfields, and merchants carry an industry / business type, so the snapshot and distribution panels are meaningful.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| The currently-built period computation may still be rolling under the hood | Audit the period-computation layer before treating FR-5 as fully calendar-based |
| A very long custom range still produces many monthly columns on the growth chart | Accepted - the column count scales linearly with period length, with no hard cap |
| "Industry" depends on each merchant's business type being set | Unset industries fall back to a dash; ensure onboarding captures business type |
| Province view shows only the top 5 + "Other" | By design; a full per-province breakdown can come later if needed |
| No free date range yet (FR-7) | Tracked separately; does not block the existing panels |
| The 4 Total/New cards fully replace the previous 3 status cards - operators used to the old version need time to adjust | The Total cards' tooltip and the link to Merchant Health help reorient them |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 - available to operators in the Back Office |
| Rollout | All operator users; no feature flag |
| Migration | None - it summarises the existing merchant base |
| Launch criteria | The filter (4 calendar-based presets + province) drives all four panels with no rolling behaviour; the snapshot shows all 4 Total merchants/New merchants/Total stores/New stores cards with snapshot/flow labels and the Total tooltip; the growth chart buckets naturally per FR-2, stacked by industry, with totals on top; the industry panel splits HKD vs DN with reconciling totals; the province panel shows the top 5 + "Other" with shares summing to 100%; no data shows an empty state. The custom date range (FR-7) does not block launch - tracked separately |
| Monitoring | How often the report is opened; empty-result rate |
13. FAQ
Is this a single merchant's revenue? No - it is cross-merchant adoption for operators. A merchant's own revenue is the client-side SLS reports.
Why don't Total merchants/Total stores match the active count? Because the Total figures include merchants/stores that have gone inactive but are not yet deleted (deleted_at IS NULL) - see the card tooltip, or open Merchant Health Analysis to see which merchants are actually active.
Where do transactions-by-store and merchant health live? In the sibling screen, Merchant Health Analysis (PRD-MAN-002).
Why does the province panel show only 5 provinces? To keep the picture focused on the biggest contributors - every remaining province is grouped into one "Other" row with its combined share, so the total still sums to 100%.
Can I pick an arbitrary date range? Not yet - there are 4 period presets today; the custom date range is FR-7, not built.
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-002 - Merchant Health Analysis
- Module: Reports - URD · Roadmap: Phase 2 - By Month
- Developer: @nx/sale - Sales Report