Skip to content

PRD: Promotion setup & lifecycle

ModuleCampaign (EXT-03)PRD IDPRD-PRM-001
StatusShippedOwnerCampaign squad
Date2026-03-23Versionv1.0
Packages@nx/pricing · @nx/core · apps/clientURDPRM

TL;DR

Lets a merchant set up a promotion as a first-class pricing concept - its discount method (percentage or fixed), eligibility/source/target rules, validity window, and usage limit - in a single operation, then drive it through a clear DRAFT → ACTIVATED → DEACTIVATED/EXPIRED/ARCHIVED lifecycle. Promotions are scoped per merchant, so each storefront owns and controls its own offers from draft to retirement.

1. Context & Problem

Merchants want to drive sales with targeted discounts, but the pricing package only carries fare-based primitives (Fare, FareRule). There is no promotion entity, no discount method, and no lifecycle to move a promotion from a draft into a live offer - so a merchant has no way to author a structured discount, define who/what it applies to, cap its usage, or schedule when it is valid.

This increment introduces promotions as a first-class pricing concept. The rule primitive is generalized so the same eligibility/source/target condition is shared by both fares and promotions, and a Promotion + PromotionMethod pair gives the merchant full CRUD, a validity window, a usage limit, and a status-driven lifecycle. Promotions are scoped per merchant so each storefront owns its own catalog of offers.

2. Goals & Non-Goals

Goals

  • A Promotion + PromotionMethod pair as first-class pricing entities, created with method and rules in one aggregate operation.
  • A discount method on the promotion: FIXED / PERCENTAGE, with a target type and an allocation strategy.
  • A reusable Rule primitive for eligibility, source, and target rules - shared with fares.
  • A status-driven lifecycle defaulting to DRAFT, covering DRAFT → ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED.
  • A validity window (time-base fields) and a usage limit per promotion.
  • Promotion types STANDARD and BUY_GET.
  • Per-merchant scoping (merchantId on Promotion).

Non-Goals

3. Success Metrics

MetricTarget / signal
Atomicity100% of promotion creates persist promotion + method + rules together, or none on error
Lifecycle integrityEvery promotion sits in exactly one valid status; only valid transitions are accepted
Tenancy isolationA merchant only ever sees and mutates its own promotions (merchantId scoped)
Setup coverageMerchants can configure percentage and fixed offers with eligibility/target rules without engineering help

4. Personas & Use Cases

PersonaGoal in this feature
OwnerCreate, activate, and archive any promotion for the merchant
ManagerAuthor and manage promotions - method, rules, validity, usage limit

Core scenarios: create a promotion with its method and rules in one step → set validity window and usage limit → activate it → later deactivate, let it expire, or archive it.

5. User Stories

  • As a manager, I want to create a promotion with its discount method and rules in one step, so the offer is fully defined in a single operation.
  • As a manager, I want to choose a FIXED or PERCENTAGE method with a target type and allocation, so the discount behaves the way the campaign needs.
  • As a manager, I want to define eligibility, source, and target rules, so the promotion only qualifies the right customers and products.
  • As a manager, I want to set a validity window and a usage limit, so the offer runs for a fixed period and cannot be over-redeemed.
  • As an owner, I want to move a promotion through its lifecycle (activate, deactivate, expire, archive), so I control when an offer is live.
  • As an owner, I want promotions scoped to my merchant, so my offers never leak across storefronts.

6. Functional Requirements

#RequirementURD ref
FR-1Create a promotion with method and rules in one aggregate operation (all or none on error)URD-PRM-001
FR-2Lifecycle DRAFT → ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED, defaulting to DRAFT (IGNIS Statuses)URD-PRM-002
FR-3Discount method FIXED / PERCENTAGE, with target type and allocation strategyURD-PRM-001
FR-4Define eligibility, source, and target rules via the shared Rule primitiveURD-PRM-003
FR-5Promotion types STANDARD and BUY_GETURD-PRM-001
FR-6Validity-window (time-base) fields on the promotionURD-PRM-002
FR-7Enforce a usageLimit per promotionURD-PRM-004
FR-8Per-merchant scoping (merchantId on Promotion, (merchantId, status) index)URD-PRM-001

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

7. Non-Functional Requirements

AreaRequirement
Data integrityPromotion, method, and rules are created together transactionally - partial failures don't leave a half-written promotion
Lifecycle validitystatus defaults to DRAFT; transitions map directly to IGNIS built-in Statuses (no custom enum)
Tenancy & authzAll operations scoped per merchant; gated by pricing/campaign permissions
ReuseThe Rule primitive is shared between fares and promotions - one rule model, two consumers
CascadeDeleting a promotion cascades to its method and rules
i18nUser-facing labels/statuses are bilingual ({ en, vi })

8. UX & Flows

Key screens (in apps/client): the promotion list view, plus promotion create/edit for the method, rules, validity window, and usage limit.

9. Data & Domain

EntityRole
PromotionThe promotion document - type, status, validity window, usageLimit, merchantId
PromotionMethodThe discount method - FIXED/PERCENTAGE, target type, allocation strategy
RuleShared eligibility/source/target condition primitive (reused by fares and promotions)

Conceptual only - full schema and invariants in the pricing domain model and the promotion docs.

10. Dependencies & Assumptions

Depends on

  • Rule primitive (@nx/pricing / @nx/core) - generalized from FareRule so promotions and fares share one rule model.
  • Products (Products) - target rules reference products and categories.
  • Customer (Customer) - eligibility rules reference customer segments.

Assumptions

  • A promotion has a valid method before it can activate (constraint C-01).
  • Each merchant owns its own promotions; merchantId is always present.

11. Risks & Open Questions

Risk / questionMitigation / status
Promotion/method/rule writes could diverge on partial failureCreated transactionally as one aggregate - all or none
Renaming FareRule to Rule could break fare consumersShared primitive verified against both fare and promotion usage
Promotions configured but not yet applied at checkoutAccepted - discount application is the separate APP feature (P2); compute service disabled here
Activating a promotion without a methodGuarded by constraint C-01 (method required before activation)

12. Release Plan & Launch Criteria

AspectPlan
PhaseP1 (foundation) - see URD feature catalog
RolloutAll merchants; no feature flag
MigrationNew entities (Promotion, PromotionMethod); FareRuleRule rename; merchantId added to Promotion
Launch criteriaAggregate create persists promotion + method + rules; lifecycle transitions verified; per-merchant isolation verified; client list renders
MonitoringPromotion count per merchant, create error rate, lifecycle transition errors

13. FAQ

Are a promotion's method and rules created separately? No - promotion, method, and rules are created together in one aggregate operation; on error none are persisted.

What discount methods are supported? FIXED and PERCENTAGE, each with a target type and an allocation strategy.

What is the lifecycle? A promotion starts in DRAFT and moves through ACTIVATED → DEACTIVATED / EXPIRED / ARCHIVED, mapped to IGNIS built-in Statuses.

Does activating a promotion apply discounts at checkout? Not yet - setup and lifecycle are live, but auto-apply/coupon at checkout is the separate Discount Application feature (APP, P2). The compute service is disabled here.

Can a promotion be shared across merchants? No - promotions are scoped per merchant via merchantId; a merchant only sees and mutates its own.

References

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