Skip to content

PRD: Customer loyalty points seam (earn)

ModuleCustomer (CORE-09)PRD IDPRD-PNT-001
StatusShippedOwnerSale squad
Date2026-06-15Versionv1.0
Packages@nx/sale · @nx/coreURDPNT

TL;DR

Closes the loop between a sale and the buyer's loyalty balance. When an order completes payment and a customer is attached, the system earns that customer points - computed from the order total against a per-merchant conversion rate - records the award as an immutable ledger entry, and increments the customer's running point balance in one atomic step. The award is idempotent per order: a redelivered payment event never double-credits. The conversion rate is a spend-per-point factor (how much order value equals one point), so points are floored to whole numbers. This PRD covers earn only - point redemption (burn) is a deliberate non-goal and is not built.

1. Context & Problem

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

Without that seam a merchant cannot reward repeat business. There is no automatic point earning, no per-customer balance to read, and no auditable record of why a balance moved. A naive earn path would also risk double-crediting when a payment webhook is redelivered, or crediting against an unconfigured or zero rate. This increment adds the earn seam on top of the existing profile and payment primitives: one award, computed once, recorded once, per paid order.

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 to an order and takes payment. When the order becomes fully paid, the seam reads the merchant's conversion rate, computes the points from the order total, writes one ledger entry, and increments the customer's balance - all atomically. If the same payment event is redelivered, the seam detects 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 floor(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 API is scoped per merchant (x-merchant-id) and gated by point-transaction read permissions
PrecisionOrder total and rate use decimal precision (4 places); 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 ({ en, vi })

8. UX & Flows

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

9. Data & Domain

EntityRole in the seam
PointTransactionAn immutable ledger entry per award - type (EARN), points, applied conversion rate, links to customer / order / merchant. One per order
CustomerCarries the running pointBalance, incremented atomically with each award
ConfigurationHolds the per-merchant point conversion rate read at award time
SaleOrderThe paid order whose total drives the award

Conceptual only - full schema and invariants live in the Sale - Customer Points developer docs. Relations are soft references; integrity is enforced in the seam, 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.
  • @nx/core - the point-transaction ledger, customer balance, order, and configuration models.

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; balance column already present
Launch criteriaA fully-paid order with a customer and a configured rate earns floor(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? Points = floor(order total ÷ conversion rate). 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 points are awarded - it is a safe no-op, not an error. The same holds 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 API is read-only and merchant-scoped.

References

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