Skip to content

PRD: Merchant Growth Analysis

ModuleReportsPRD IDPRD-MAN-001
StatusReady to devFEATMAN · BANA-1440
EpicBANA-1337PlaneBANA-1707
Date2026-07-07Versionv1.2
Packages@nx/saleURDMAN
SurfaceBO · Ops
OwnerPhá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

MetricTarget / signal
Self-serve adoption reportingAn operator answers "merchants onboarded by industry / province this period" without an ad-hoc data pull
Single-filter consistencyChanging 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 readingNew-merchant growth is grasped across the period's natural time buckets, broken down by industry
Correct reading of the Total figuresOperators don't confuse "Total merchants/stores" with the count that's actually active - the Total card's tooltip clearly points to Merchant Health
Distribution integrityEach industry total reconciles with its household + business split; province shares sum to 100% (including "Other")

4. Personas & Use Cases

PersonaGoal in this feature
Operations team (NEXPANDO & VNPAY)Watch platform adoption - counts, status mix, and where growth comes from
Leadership / partnerReport 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

#RequirementStatusURD ref
FR-1Snapshot - 4 stat cards: total/new for merchants and stores🚧 needs rebuildURD-MAN-003
FR-2New-merchant trend - vertical stacked columns, natural period buckets, stacked by industry🚧 bucketing logic needs rebuildURD-MAN-003
FR-3Distribution by industry - horizontal stacked bars, household vs business split, sorted descending✅ builtURD-MAN-001
FR-4Distribution by province - horizontal bars, top 5 + grouped "Other", with % share✅ builtURD-MAN-001
FR-5Shared filter - 4 calendar-based period presets + single province, resets on leaving🚧 period computation needs reviewURD-MAN-006
FR-6Ops scope & empty states - cross-merchant, permission-gated, empty states✅ builtURD-MAN-005
FR-7Custom date range (from - to) alongside the presets🚧 not builtURD-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):

    CardFormulaMetric type
    Total merchantsCOUNT(Organizer) WHERE deleted_at IS NULLSnapshot - as of now
    New merchantsCOUNT(Organizer) WHERE created_at BETWEEN [selected period]Flow - within the period
    Total storesCOUNT(Store) WHERE deleted_at IS NULLSnapshot - as of now
    New storesCOUNT(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:

    PeriodBucketing rule
    This month / Last monthBy week
    This quarterBy month (exactly 3 columns, one per month in the quarter)
    This yearBy month, from January through the current month (no future months)
    Custom range < 31 daysBy day
    Custom range 31-90 daysBy week
    Custom range > 90 daysBy 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

AreaRequirement
Read-onlyThe screen never changes data - it only summarises the merchant base
Tenancy & authzCross-merchant scope, permission-gated to operator roles; never reachable by a merchant's own users
ConsistencyAll panels reflect the same filter at the same time
i18nLabels are bilingual (EN/VI); province names follow the viewer's language
Empty & loadingNo 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

ConceptRole in this screen
MerchantThe 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 typeGroups merchants for the growth breakdown and the industry distribution; also distinguishes household vs business
ProvinceGroups merchants for the geographic distribution
PeriodThe 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_at fields, and merchants carry an industry / business type, so the snapshot and distribution panels are meaningful.

11. Risks & Open Questions

Risk / questionMitigation / status
The currently-built period computation may still be rolling under the hoodAudit 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 chartAccepted - the column count scales linearly with period length, with no hard cap
"Industry" depends on each merchant's business type being setUnset 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 adjustThe Total cards' tooltip and the link to Merchant Health help reorient them

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 - available to operators in the Back Office
RolloutAll operator users; no feature flag
MigrationNone - it summarises the existing merchant base
Launch criteriaThe 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
MonitoringHow 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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.