Skip to content

PRD: Promotions, methods & segment rules

ModulePricing (CORE-14)PRD IDPRD-PROMO-001
StatusShippedOwnerPricing squad
Date2026-06-15Versionv1.0
Packages@nx/pricing · @nx/coreURDPROMO

TL;DR

Lets a merchant configure a discount campaign as a single, self-contained object: a promotion carries exactly one method (fixed amount or percentage; aimed at the line items, the whole order, or shipping; spread per-item, across items, or once) and a scheme that is either a straight discount (STANDARD) or a buy-X-get-Y deal (BUY_GET). Three rule sets segment the campaign - eligibility rules decide who/when qualifies, source rules decide what must be bought, target rules decide what gets discounted. The whole graph - promotion, method, and all three rule sets - is created and edited in one atomic aggregate with denormalized rule counts kept accurate. Applying the discount during checkout is a deliberate later increment; this PRD ships the configuration surface.

1. Context & Problem

Pricing already selects fares (FARE) and computes taxes (TAX), but a merchant had no way to express a discount campaign - "10% off drinks before 5pm", "buy 2 coffees get 1 free", "50,000₫ off orders over 300,000₫". A campaign is not a fare: it spans more than one line, it can target the order or shipping rather than an item, and it must say not just how much to discount but who qualifies, what triggers it, and what it lands on.

Without a first-class promotion object an owner cannot build these offers at all, and the data needed to apply them at checkout does not exist. This increment closes the configuration gap: it gives the owner a promotion with one method, a standard or buy-get scheme, and three distinct rule sets, all authored and edited as one atomic unit - so a campaign is always saved complete or not at all, never half-built.

2. Goals & Non-Goals

Goals

  • A promotion object with a scheme (STANDARD or BUY_GET), a code or automatic trigger, an effective window, a usage limit, and a lifecycle status.
  • Exactly one method per promotion declaring the mechanic: FIXED / PERCENTAGE, a target (ITEMS / ORDER / SHIPPING), an allocation (EACH / ACROSS / ONCE), and a discounted-quantity cap.
  • Buy-get support: a method that declares a source minimum quantity (must buy) and a target quantity (gets discount).
  • Three segment rule sets - eligibility (on the promotion), source and target (on the method) - reusing the shared rule model (attribute, operator, value, AND logic).
  • One atomic aggregate create and update across promotion + method + all rule sets, with denormalized rule counts kept accurate and a shape-based rule merge on update.
  • Cascade delete that removes the method and every attached rule with the promotion.

Non-Goals

  • Applying promotion discounts during checkout pricing - the calculator is not yet wired into either simulation pipeline (URD-PROMO-006, still a Won't this increment).
  • Fare selection and tax computation - owned by FARE and TAX.
  • Multi-currency conversion of discount amounts (a currency and exchange rate are carried, not converted here).
  • Order persistence, invoice issuance, stock or payment side effects.

3. Success Metrics

MetricTarget / signal
AtomicityA failed sub-step (missing rule field, absent method) leaves nothing persisted
Aggregate completenessEvery saved promotion has exactly one method; rule sets persist together with it
Count accuracyEligibility / source / target rule counts always match the rules actually stored
Edit integrityAn aggregate update creates, updates, and deletes rules in one transaction by entry shape
Code integrityNo two live promotions in a merchant share a code

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerBuild a discount campaign - pick the mechanic, the target, and the rules that segment it
Marketing staffAuthor time-boxed offers (happy-hour, weekday, channel-specific) and retire them via status
System (future checkout)Read a complete, valid promotion graph to apply at the point of sale (later increment)

Core scenario: an owner creates a "Buy 2 coffees, get 1 free" promotion - scheme BUY_GET, one PERCENTAGE method valued 100 targeting ITEMS with source-min 2 and target 1, one source rule (the qualifying drink) and one target rule (the free drink), plus an eligibility rule limiting it to the dine-in channel. The promotion, its method, and all three rules are saved in one atomic call; the rule counts read back correctly. Later the owner edits the aggregate to drop the channel rule and raise the usage limit - all in one transaction.

5. User Stories

  • As an owner, I create a promotion, its method, and all its rules in one request, so a campaign is never saved half-built.
  • As an owner, I choose a fixed amount or a percentage and aim it at the items, the order, or shipping, so the offer matches what I am promoting.
  • As an owner, I build a buy-X-get-Y deal by setting the source quantity to buy and the target quantity to discount, so classic combo offers are possible.
  • As marketing staff, I segment an offer with eligibility, source, and target rules, so it fires only for the right basket on the right channel.
  • As marketing staff, I retire an offer by moving its status to deactivated or expired, so it stops without being deleted.
  • As an owner, I edit an existing promotion's rules - adding, changing, and removing them - in one save, so maintenance is a single atomic step.

6. Functional Requirements

#RequirementURD ref
FR-1A promotion, its single method, and its rule sets are created together in one atomic aggregateURD-PROMO-001
FR-2A promotion has at most one methodURD-PROMO-003
FR-3The method is FIXED or PERCENTAGE, carrying the discount valueURD-PROMO-008
FR-4The method targets ITEMS, ORDER, or SHIPPINGURD-PROMO-009
FR-5The method declares an allocation (EACH / ACROSS / ONCE) and can cap the discounted quantityURD-PROMO-010..011
FR-6A promotion is STANDARD or BUY_GET; a buy-get method declares source-min and target quantitiesURD-PROMO-007 · URD-PROMO-012
FR-7Three rule sets segment the campaign: eligibility (promotion), source and target (method), reusing the shared rule modelURD-PROMO-013 · URD-PROMO-004
FR-8Eligibility / source / target rule counts are denormalized and kept accurate as rules changeURD-PROMO-014
FR-9A promotion carries an optional unique code (no code ⇒ automatic), a stacking flag, and a tax-inclusive flagURD-PROMO-015..016
FR-10A promotion carries an effective window, a usage limit, and a running usage countURD-PROMO-017
FR-11A promotion moves through a lifecycle status, defaulting to DRAFTURD-PROMO-018
FR-12An aggregate update merges rules by entry shape (create / update / delete) in one transactionURD-PROMO-019 · URD-PROMO-002
FR-13Deleting a promotion cascades to its method and every attached rule atomicallyURD-PROMO-020
FR-14Promotions, methods, and rules are merchant-isolated and soft-deletedURD-PROMO-005

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

7. Non-Functional Requirements

AreaRequirement
AtomicityThe whole campaign graph - promotion, method, eligibility / source / target rules - is one all-or-nothing transaction
Tenancy & authzAll operations scoped per merchant (x-merchant-id), authenticated (JWT or basic) and gated by promotion / method / rule permissions
ValidationA rule created during an aggregate write must carry attribute, operator, and data type, or the whole write is refused
ConsistencyDenormalized rule counts are updated inside the same transaction as the rules they count
PrecisionDiscount value, max quantity, and buy-get quantities use decimal precision (4 places)
i18nPromotion name and description are bilingual ({ en, vi })

8. UX & Flows

The configuration surface lets the owner pick the scheme, set the code (or leave it automatic), define the effective window and usage limit, choose the method's mechanic / target / allocation, enter buy-get quantities, and attach the three rule sets. Editing reuses the same aggregate; status moves a campaign through its lifecycle without deletion.

9. Data & Domain

EntityRole in this feature
PromotionThe campaign - scheme, code/automatic, stacking, tax-inclusive, effective window, usage limit/count, status; holds the eligibility rules
PromotionMethodThe single mechanic - fixed/percentage value, target type, allocation, discounted-quantity cap, buy-get source-min and target quantities
RuleA polymorphic condition (attribute, operator, value, priority); attached to the promotion (eligibility) or the method (source / target, distinguished by context)

Conceptual only - full schema and invariants live in the pricing domain model. A method's source and target rules are the same entity distinguished by a stored context tag; integrity is enforced in the aggregate service, not by database constraints.

10. Dependencies & Assumptions

Depends on

  • Shared rule model (URD-PROMO-004, also used by FARE) - eligibility, source, and target rules reuse the same attribute / operator / value shape with AND logic.
  • Commerce / Merchant - every promotion, method, and rule is merchant-scoped.
  • @nx/core - promotion, method, and rule models, schemes, and status constants.

Assumptions

  • A promotion has exactly one method; multi-method campaigns are out of scope.
  • Source and target rules are meaningful only for buy-get-style mechanics; standard promotions typically use eligibility rules alone.
  • The checkout-time calculator that consumes this configuration is a separate, later increment.

11. Risks & Open Questions

Risk / questionMitigation / status
Partial campaign on failure (promotion without a method, or with orphan rules)Whole graph is one atomic transaction - any failure rolls back fully
Rule counts drift from the rules actually storedCounts are denormalized and updated inside the same transaction as the rules
Ambiguous edit intent (add vs change vs remove a rule)Aggregate update is shape-based: no id creates, id+fields updates, id only deletes
Two live promotions reuse a codeCode is unique per merchant among non-deleted promotions
Owner expects the discount to apply at checkoutOut of scope this increment - the calculator is not yet wired (URD-PROMO-006)

12. Release Plan & Launch Criteria

AspectPlan
PhaseP3 - PROMO in the URD feature catalog
RolloutAll merchants; no feature flag
MigrationNone - new promotion / method / rule tables; no change to existing fare/tax data
Launch criteriaAggregate create persists promotion + one method + all rule sets atomically; counts read back correctly; aggregate update merges rules by shape; delete cascades; codes are unique per merchant
MonitoringAggregate create/update error rate by reason (missing rule field, absent method, code conflict)

13. FAQ

Does creating a promotion apply a discount at checkout? No - this increment ships the configuration surface only. The calculator that applies promotions during pricing is a separate, later increment (URD-PROMO-006).

How many methods can a promotion have? Exactly one. The mechanic (fixed/percentage, target, allocation, buy-get quantities) all lives on that single method.

What is the difference between the three rule sets? Eligibility rules (on the promotion) decide who/when qualifies; source rules (on the method) decide what must be bought; target rules (on the method) decide what gets discounted. Source and target are the same rule entity distinguished by a stored context tag.

How do I edit a campaign's rules? Send an aggregate update - a rule with no id is created, a rule with id and fields is updated, a rule with only an id is deleted, all in one transaction.

What happens to the method and rules when I delete a promotion? They are cascade-deleted with it in one transaction - no orphan method or rules are left behind.

What does a promotion with no code mean? It is automatic (auto-apply) only. A code, when present, is unique per merchant among live promotions.

References

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