Skip to content

PRD: Customer loyalty points seam (earn)

ModuleCustomerPRD IDPRD-PNT-001
StatusReady to devFEATPNT
EpicPlaneBANA-1537
Date2026-06-15Versionv1.0
Packages@nx/saleURDPNT
SurfaceBackend
OwnerPhát Nguyễn

TL;DR

Pay for an order with a customer attached, and that customer earns points - in one atomic step that writes an immutable ledger entry and bumps their running point balance. Points are the order total divided by a per-merchant conversion rate (a spend-per-point factor), floored to a whole number, and the award is idempotent per order so a redelivered payment never double-credits. Earn only - redemption (burn) is a deliberate non-goal and is not built.

1. Context & Problem

The Customer module already keeps profiles (CUS) and attaches a customer to an order at checkout; the Sale module already drives an order to fully paid. The seam between them was missing - nothing turned a completed payment into loyalty value for the buyer.

So a merchant could not reward repeat business: no automatic earning, no per-customer balance to read, no auditable record of why a balance moved. A sloppy earn path would also risk double-crediting on a redelivered payment, or crediting against an unset or zero rate. This increment adds the earn seam on top of the existing profile and payment building blocks: one award per paid order - computed once, recorded once.

2. Goals & Non-Goals

Goals

  • Earn points automatically when an order completes payment and a customer is attached (URD-PNT-001).
  • Compute the award from the order total and a per-merchant conversion rate (spend-per-point), floored to whole points (URD-PNT-002).
  • Track a running point balance per customer, incremented atomically (URD-PNT-003).
  • Idempotent per order - award at most once even if the payment event is redelivered (URD-PNT-004).
  • Award nothing when no customer is attached, the rate is unset, or the rate / computed points are not positive (URD-PNT-005).
  • Record every award as an immutable ledger entry linking customer, order, merchant, point count, and the rate applied (URD-PNT-006).
  • Expose a merchant-scoped, read-only ledger so staff can browse a customer's point history (URD-PNT-007).

Non-Goals

  • Point redemption (burn) - spending points on an order. Not built; the ledger records only earn entries and the balance only ever increases. Tracked as an excluded scope item (URD - Non-Goals).
  • Loyalty tiers (Bronze / Silver / Gold) and tier-based earn multipliers.
  • Manual point adjustments or external write access to the ledger - entries are written only by the earn seam.
  • Expiry of earned points, and point statements / notifications to the customer.

3. Success Metrics

MetricTarget / signal
Earn coverageEvery fully-paid order with an attached customer and a configured rate produces exactly one award
No double-creditA redelivered payment event never adds a second award for the same order
Balance integrityA customer's balance equals the sum of their ledger entries' points
Audit completenessNo balance increment exists without a matching immutable ledger entry
Safe defaultsOrders with no customer, no rate, or a non-positive rate award nothing - never an error

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerConfigure the conversion rate; read a customer's balance and point history
CashierAttach a customer at checkout so the sale earns them points on payment
CustomerAccrue a point balance from purchases, auditable entry by entry

Core scenario: a cashier attaches a customer and takes payment. Once the order is fully paid, the seam reads the merchant's conversion rate, computes points from the order total, writes one ledger entry, and increments the balance - all atomically. A redelivered payment finds the order already earned and does nothing.

5. User Stories

  • As an owner, I want a customer to earn points automatically when their order is paid, so loyalty needs no manual step.
  • As an owner, I set a single per-merchant conversion rate, so earning is consistent across every sale.
  • As an owner, I want each award recorded as an immutable entry, so I can always explain a balance.
  • As an owner, I want a redelivered payment event to never double-credit, so balances stay trustworthy.
  • As a cashier, I attach the customer at checkout and do nothing else, so points just follow the sale.
  • As an owner, I browse a customer's point history in one read-only list, so I can audit how their balance grew.

6. Functional Requirements

#RequirementURD ref
FR-1When an order completes payment and a customer is attached, earn that customer pointsURD-PNT-001
FR-2Compute the award as ⌊order total ÷ per-merchant conversion rate⌋ - the rate is spend-per-pointURD-PNT-002
FR-3Maintain a running point balance per customer, incremented by each awardURD-PNT-003
FR-4Award at most once per order; a redelivered payment event is a no-op (idempotent by order)URD-PNT-004
FR-5Award nothing when no customer is attached, the rate is unset, or rate / computed points ≤ 0URD-PNT-005
FR-6Record every award as an immutable ledger entry: type, points, applied rate, links to customer / order / merchantURD-PNT-006
FR-7The ledger entry and the balance increment are written in one atomic transactionURD-PNT-003 · URD-PNT-006
FR-8Provide a merchant-scoped, read-only ledger to browse a customer's point historyURD-PNT-007

Full requirement text and acceptance criteria live in the Customer URD - PNT. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
IdempotencyAt most one award per order; the seam checks for an existing entry before crediting
AtomicityLedger entry + balance increment commit together; a partial failure rolls back fully
Data integrityNo balance increment without a matching immutable ledger entry; entries are append-only
Tenancy & authzThe ledger read is confined to the user's own merchant and gated by point-history read permissions
PrecisionOrder total and rate use 4-decimal precision; points are floored to whole numbers
ResilienceMissing customer, missing rate, or a non-positive rate / point result is a safe no-op, not an error
i18nUser-facing labels are bilingual (English / Vietnamese)

8. UX & Flows

The earn path has no UI - it runs on payment completion. The only owner-facing surfaces are the per-merchant conversion-rate setting and the read-only point ledger per customer.

9. Data & Domain

ConceptRole in the seam
Point ledger entryAn immutable earn record per award - points, applied conversion rate, links to customer / order / merchant. One per order
CustomerCarries the running point balance, incremented atomically with each award
Merchant configurationHolds the per-merchant point conversion rate read at award time
Sale orderThe paid order whose total drives the award

Conceptual only - full data model and invariants live in the Sale - Customer Points developer docs. Links are logical references; integrity is enforced by the earn logic, not by database constraints.

10. Dependencies & Assumptions

Depends on

  • Customer profiles (CUS) - the award credits the customer attached to the order.
  • Order payment completion (Orders) - the seam fires when an order becomes fully paid.
  • Per-merchant configuration - the conversion rate is read from the merchant's point-conversion setting; a sensible default applies when the setting exists without an explicit value.
  • Shared core data - the point ledger, customer balance, order, and configuration.

Assumptions

  • A customer is attached to the order at checkout; otherwise the seam awards nothing.
  • The merchant has configured a positive conversion rate; an unset or non-positive rate yields no award.
  • The payment seam may redeliver its completion event, so the earn path must be idempotent.

11. Risks & Open Questions

Risk / questionMitigation / status
Redelivered payment event double-creditsAward is idempotent by order - an existing entry short-circuits the seam
Ledger entry and balance diverge on partial failureBoth are written in one atomic transaction; partial failure rolls back fully
Merchant has not configured a rateSafe no-op - no award, no error; logged for visibility
Rate is zero or negativeSafe no-op - guarded before any write
Small orders earn zero pointsExpected - points are floored; a sub-one-point total awards nothing
Customers expect to spend pointsOut of scope - redemption (burn) is a deliberate non-goal in this increment

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - PNT in the URD feature catalog
RolloutAll merchants; no feature flag. Earning is active once a merchant sets a conversion rate
MigrationNone - the seam runs on the existing payment-completion path; the balance is already present
Launch criteriaA fully-paid order with a customer and a configured rate earns ⌊total ÷ rate⌋ points; the award writes one immutable ledger entry and increments the balance atomically; a redelivered event awards nothing; no customer / no rate / non-positive rate awards nothing
MonitoringAwards per merchant, balance-vs-ledger consistency checks, no-op reasons (no customer, no rate, non-positive), idempotent-skip count

13. FAQ

How are points computed? The order total divided by the conversion rate, floored to a whole number. The rate is a spend-per-point factor - how much order value equals one point - so a rate of 1000 earns one point per 1000 of order total.

What happens if a payment event is redelivered? Nothing is credited twice. The seam checks whether the order already earned and, if so, does nothing.

What if the merchant hasn't set a rate? No award - a safe no-op, not an error. Same when no customer is attached or the rate is not positive.

Can a customer spend their points? Not yet. This increment ships earn only; redemption (burn) is a deliberate non-goal - the ledger records only earn entries and the balance only increases.

Is every balance change auditable? Yes - each award writes an immutable ledger entry (points, applied rate, customer / order / merchant) in the same transaction as the balance increment, and the ledger is browsable read-only per merchant.

Can staff edit the ledger? No - entries are written only by the earn seam. The ledger is read-only and confined to the user's own merchant.

References

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