PRD: Points Earning
| Module | Loyalty | PRD ID | PRD-PTS-001 |
| Status | Ready to dev | FEAT | PTS |
| Epic | — | Plane | BANA-1509 |
| Date | 2026-03-23 | Version | v0.1 |
| Packages | @nx/sale | URD | PTS |
| Surface | Backend | ||
| Owner | Phát Nguyễn | ||
TL;DR
Every paid, customer-linked order earns loyalty points automatically: the balance grows the instant payment completes, at a per-merchant conversion rate. Each order is awarded exactly once as an auditable ledger entry - no manual entry, a verifiable point trail.
1. Context & Problem
The Loyalty module turns purchases into points so customers come back. Until now, paid orders left no loyalty trace - no ledger, no balance, no link from an order to the points it should have earned. Customers got nothing for their spend, and merchants had no foundation for redemption, tiers, or rewards.
Points earning is the first slice (feature PTS, phase P1): a paid, customer-linked order adds points to that customer's balance automatically, once, at a per-merchant conversion rate. It builds on customer identity (which holds the balance) and the order/payment flow (which signals completion), and is the prerequisite for every later loyalty capability.
2. Goals & Non-Goals
Goals
- Award points automatically on a paid, customer-linked order - no manual entry.
- Make each award idempotent per order so a duplicate or redelivered payment signal never double-awards.
- Read the rate from per-merchant configuration: no config entry means no earning at all; a built-in default applies only when the entry exists but carries no value.
- Record each award as a ledger entry and increment the customer's balance in one atomic step.
- Offer a read-only way to list and look up ledger entries, scoped to the user's own merchant.
Non-Goals
- Redemption, tiers, and the rewards catalogue - feature
RDM(phase P2, URD-RDM). - Cross-merchant / franchise-wide point pooling (URD Non-Goal).
- Marketing messages on earn - owned by Marketing.
- Points for anonymous sales - only identified customers accrue (constraint C-01).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Coverage | 100% of paid, customer-linked orders award points (where a config entry exists and the rate is positive) |
| Idempotency | Zero double-awards - at most one ledger entry per order |
| Accuracy | Balance equals the sum of the customer's earning entries |
| Latency | Award completes inside the payment-confirmation path without delaying order completion |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Customer | Earn points automatically and watch the balance grow |
| Owner | Set the rate and trust that every paid order accrues correctly |
| Manager | Review member point activity and balances |
Core flow: a paid, customer-linked order → read the merchant's rate → compute points from the order total → record one ledger entry and increment the balance, once per order.
5. User Stories
- As a customer, I want points added automatically when my order is paid, so I'm rewarded without lifting a finger.
- As an owner, I want the rate configured per merchant, so I control how spend converts to points.
- As an owner, I want duplicate payment signals to never double-award, so the ledger stays trustworthy.
- As a manager, I want to list a customer's ledger entries, so I can see how their balance was earned.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Award points when a customer-linked order completes payment; the same logic runs on both payment paths - standard order payment and table-check payment | URD-PTS-001 |
| FR-2 | Idempotent per order - short-circuit if the order was already awarded | URD-PTS-002 |
| FR-3 | Rate read from per-merchant configuration: no config entry means no points; the built-in default applies only when the entry exists but carries no value | URD-PTS-003 |
| FR-4 | Points = order total ÷ rate, rounded down to whole points; a non-positive rate or a zero result is skipped (logged, no award) | URD-PTS-001 |
| FR-5 | In one atomic step: record a ledger entry (carrying the points, the rate, and the originating order) and increment the balance; roll back together on error | URD-PTS-001..002 |
| FR-6 | Read-only listing and look-up of ledger entries, scoped to the user's own merchant | URD-PTS-001 |
Full requirement text and acceptance criteria live in the Loyalty URD; this PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Ledger entry and balance increment are written together atomically - no balance change without a matching entry |
| Idempotency | At most one entry per order, enforced by checking whether the order was already awarded |
| Tenancy & authz | All operations scoped to the user's own merchant; the read-only listing requires the loyalty viewing permission |
| Precision | Money and point math use fixed decimal precision; points are floored to whole units |
| Performance | Award runs inside the payment-confirmation path without delaying order completion |
| i18n | User-facing labels and statuses are bilingual (English / Vietnamese) |
8. UX & Flows
Points earning has no dedicated UI - it runs behind the scenes on payment completion. The balance surfaces in the customer profile; ledger entries are available through the read-only listing.
9. Data & Domain
| Concept | Role |
|---|---|
| Point ledger entry | Append-only earning record - the points, the rate, and the originating order, scoped to the merchant |
| Customer point balance | The running balance, incremented atomically with each award |
| Per-merchant conversion rate | The configured rate for the merchant; a default applies when the entry is empty |
Conceptual only - full schema and invariants in the developer customer-points docs and the sale domain model.
10. Dependencies & Assumptions
Depends on
- Customer identity (Customer) - holds the balance the award increments.
- Orders / payment (Orders) - order completion on the payment path is the trigger.
- Per-merchant configuration - supplies the rate; required (no config entry → no earning).
Assumptions
- The order is linked to an identified customer (anonymous sales earn nothing, C-01).
- The merchant has a config entry (earning is skipped entirely without one); the built-in default applies only when that entry is empty.
- Payment-completion signals fire reliably for each paid order.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Duplicate / redelivered payment signals could double-award | Idempotent per order - check whether the order was already awarded before awarding (C-02) |
| Award and balance increment could diverge on partial failure | Both written atomically; rolled back together on error |
| A misconfigured (non-positive) rate could award garbage | Non-positive rate or zero points is skipped and logged, never awarded |
| Refund / cancellation after earn | Open: define the clawback / reversal path for earned points |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - see URD feature catalog |
| Rollout | All merchants, no feature flag - the per-merchant config entry is the de facto opt-in (no entry → no earning) |
| Migration | None (new ledger; rate from configuration) |
| Launch criteria | Paid customer-linked order awards once; balance matches the ledger; duplicate signal doesn't double-award; rate read per merchant |
| Monitoring | Award volume per merchant, double-award detection (orders with more than one entry), balance-vs-ledger consistency checks |
13. FAQ
When exactly are points awarded? When a customer-linked order completes payment - the same logic runs whether confirmation arrives via the standard order-payment or the table-check path.
Can the same order award points twice? No - earning is idempotent per order. If an entry already exists for the order, the award is skipped.
Where does the rate come from? From the per-merchant conversion-rate setting. A config entry must exist for earning to run at all; the built-in default applies only when that entry exists but carries no value.
How are points calculated? Order total ÷ rate, rounded down to whole points. A non-positive rate or a zero result is skipped (logged, no award).
Do anonymous sales earn points? No - only identified customers accrue (constraint C-01).
Can I redeem points yet? Not in this increment. Redemption, tiers, and the rewards catalogue are the separate RDM feature (phase P2, URD-RDM).
References
- URD: Loyalty - Points Earning
- Related PRD: Redemption & Tiers (
RDM, planned) - see URD-RDM - Module: Loyalty - URD
- Developer: @nx/sale customer-points