Happy House - Ecommerce Docs
Developer ResourcesPromotion

Promotion Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Promotion module.

Promotion Features and Flows

Use this page for the promotion domain: what it does for customers and admins, and how each flow behaves from start to finish.

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
APIapps/api/src/modules/promotion/customer/promotion-customer.controller.ts, admin/promotion-admin.controller.tsRoutes, permissions, rate limits
Backendshared/promotion-evaluation.util.ts, promotion-evaluation.service.ts, promotion-redemption.service.ts, promotion-reconciliation.service.tsDerived lifecycle, selection order, gate, release, reconciliation
Schemapackages/db/src/schema/promotion/{promotion,promotion-redemption,enums}.tsRules as columns, basis points, CHECKs
Error registryapps/api/src/common/types/error-codes.ts (// PROMOTION)PROMOTION_* codes

2. Feature Summary

FieldValue
Modulepromotion
SubmoduleN/A
Primary user valueThe customer gets the best available discount on their basket, with a clear reason when a coupon does not apply; the operator schedules and manages campaigns without a single "activate" job
ActorsCustomer (signed in), admin, checkout (future)
Main entry pointsPOST /api/mobile/promotions/evaluate, /api/admin/promotions (6 routes)
Main outputsEvaluation with applied/rejected promotions, reasons, savings; admin CRUD with a derived lifecycle badge
Related docsAPI, Backend

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
CustomerEvaluate a coupon + automatic promotions against their live cart, with an optional districtBrowse the promotion catalogue, consume usage limits by loading a pageJWTCUSTOMER_COUPON_ATTEMPT 20/min account-keyed — a coupon code is a bearer value
AdminList (derived-status filtered), detail, create, update (version required), retire, restoreSet a statusstate is the only editable fieldAdmin JWT + Promotions_*pagination=false accepted (the table is small)
Checkout (future)Reserve / confirm / release redemptionsInternalMust own the release edge incl. order cancellation

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
EvaluateCustomerCustomerPOST /api/mobile/promotions/evaluatelive cart, promotions, districtAPI §4
Admin listAdminAdminGET /api/admin/promotionspromotions (derived status)API
Admin detailAdminAdminGET /:promotionIdone promotionAPI
CreateAdminAdminPOST /api/admin/promotionspromotion rowAPI
UpdateAdminAdminPATCH /:promotionIdrow + versionpromotion rowAPI
RetireAdminAdminDELETE /:promotionIdrowdeleted_at + coupon releasedAPI
RestoreAdminAdminPOST /:promotionId/restorerowdeleted_at clearedAPI
Reserve/confirm/releaseInternalCheckout (future)service callspromotion + ledgerledger rows, usage_countbackend §6

5. User-Facing Flows

5.1 Evaluate a coupon

Summary

A customer enters a code at checkout. The backend normalises it (case-insensitive, whitespace-tolerant), evaluates it against the live cart, and returns applied + rejected promotions with reasons. Evaluation is a preview — nothing is redeemed, and no usage limit is consumed by loading a page.

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Ineligible couponAny rejection200, not 4xxeligibility.valid: false + reason
No coupon sentcouponCode omittedAutomatic promotions onlyeligibility.valid = "any automatic applied"
No district + no default addressFree shipping appliesValue nullshippingDiscount: null
Unknown districtExplicit uuid names nothing404SHIPPING_DISTRICT_NOT_FOUND
Rejected draft/disabled/not_startedLifecycle rejectionBlank name/type/discountTypeDisclosure prevented
Expired couponWindow passedNot blankedNamed — support-answerable
Coupon + automatic bothBoth applyBest selection per typepromotions.applied

5.2 The derived lifecycle

state (stored)clockstatus (derived)
draftdraft
disableddisabled
publishednow < startsAtscheduled
publishedinside the windowactive
publishednow ≥ endsAtexpired

There is no "activate" button and no "expire" button. Publishing with a future start date IS scheduling. The admin form edits state; status is a read-only badge — a form that tries to PATCH a status is rejected.

5.3 Selection order

Eligible promotions sort by (priority DESC, computed discount DESC, id ASC). With every promotion at the default priority of 0, the customer gets best-value by default; a merchant raises priority to override. The id tiebreaker makes the order total — without it two equal candidates are ordered by whatever the planner returned. At most one promotion per type: one cart discount and one free shipping, never two of either.

6. Admin Flows

6.1 Create a promotion

Permission Promotions_CREATE, ADMIN_WRITE 10/min. Field rules the database enforces anyway (a 409 is a worse experience than a disabled submit button): promotionTypediscountType must agree (free_shipping on one requires it on the other); fixed_amount requires discountAmount and forbids discountPercentageBps; percentage requires discountPercentageBps (1–10000) and forbids discountAmount; free_shipping forbids both; maxDiscountAmount is percentage-only; omit couponCode for automatic (there is no isAutomatic flag); endsAt strictly after startsAt; usageLimitPerCustomerusageLimit.

6.2 Update — version required

PATCH requires version. A concurrent edit makes it stale → 409 PROMOTION_VERSION_CONFLICT with the current version in the message. Re-fetch and re-apply.

6.3 Retire and restore

DELETE is a soft delete. Redemption history is kept — a hard delete of anything redeemed is refused by the database — and the coupon code is released for reuse, so next year's campaign can reuse this year's code. Restore can fail with 409 PROMOTION_COUPON_CODE_TAKEN if another promotion claimed the code while this one was retired.

7. Lifecycle and State Transitions

Covered in §5.2 — the only stored transitions are draft → published, published → disabled, disabled → published, published → draft (editable). Everything else is derived from the clock.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Evaluate
Create/updatepromotion rowautomatic-set cache (30s)
Retire/restoredeleted_atautomatic-set cache
Reserve/confirm/release (future)ledger rows, usage_countautomatic-set cachesweep (lapsed holds)

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Stale versionConcurrent operator409 + current versionRe-fetch, re-applyPROMOTION_VERSION_CONFLICT
Coupon code takenRestore after reuse409Pick another codePROMOTION_COUPON_CODE_TAKEN
Contradictory configBad field combo409Field rulesPROMOTION_INVALID_CONFIGURATION
Coupon rate limit20/min exceeded429Retry after fixing the cartCUSTOMER_COUPON_ATTEMPT
Lapsed holdsAbandoned checkoutsTotal limit exhaustedSweep jobreconciliation
Counter driftRetried releaseDownward driftLedger-gated release + reconciliationredemption service

11. Diagrams Required Per Module

  • Actor capability diagram — §3/§4.
  • Sequence diagram per major flow — §5.1.
  • State machine diagram — §5.2.
  • Data side-effect diagram — §9.
  • Error branch diagram — §10.

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
EvaluateNormalisationCustomerMixed-case codeUpper-cased lookupservice
EvaluateBlank rejected draftCustomerGuessed codeBlank name/typeDisclosure prevented
EvaluatefailedRules all failuresCustomerMultiple missesEvery rule reported
EvaluatefailedRules empty on lifecycleCustomerExpired couponNo thresholds shown
EvaluateusageRemaining only for named couponCustomerAutomatic appliesNot broadcast
EvaluateSelection changes with addressCustomerDistrict addedFree shipping can outrank
EvaluateValid lines onlyCustomerInvalid line in cartThresholds use valid subtotal
Admin listDerived status filterAdmin?status=activeSQL filter vs clock
Admin listpagination=false acceptedAdminList allSmall table
Admin listDate-only startsToAdmin2026-08-05Whole day
RetireCoupon releasedAdminDeleteReusable code
RestoreCode takenAdminRestore409

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
Lifecycle derivedNo flip-job stalenessNever pays out after endevaluation utilstatus computedspec
Gate carries its own predicatesEvaluation is outside the lockPulled campaign never paysgate SQLreserve UPDATEint spec
Release gated on ledgerRetry-safeNo double-decrementredemption serviceledger transitionint spec
Two limits differ on expiryLapsed holdsTotal limit needs sweepper-customer count vs usage_countsweepint spec
Basis points7.5% expressible750 = 7.5%schemapercentageBpsCHECKprobe
Two nullable value columnsNo polymorphic unit confusionType-driven columnsschema CHECKprobe
coupon_code IS NULL = automaticNo disagreeing flagsSingle sourceschemaprobe
Selection (priority, value, id)Best value by defaultTotal orderevaluation utilspec
Ineligible = 200Business outcomeBranch on eligibilityservicespec
Blank rejected draftsNo disclosureGeneric messageservicespec

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Derived lifecycleNever stale payoutNo cron flipsStored statusComputed per readCheap
No isAutomatic flagNo disagreeing factsOne sourceFlagDocumented
Basis pointsPrecisionInteger mathWhole percent7.5% needs 750Documented
Redemption internal-onlyNo page-load consumptionFrozen contractHTTP endpointsCheckout must call itProminent obligation
Coupon uncachedNo 30s exploit windowCache only automatic setCache bothSlower lookupsAccepted
Soft delete releases codeReuse next yearKeep historyHard delete409 on restoreDocumented

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
EvaluateUnknown codenot_found200 ineligible"not valid"
EvaluateNot startednot_started200 + blank entry"not started yet"
EvaluateSupersededBetter same-type wonrejected with reasonUsually hidden
EvaluateExclusive conflictNon-combinablerejectedUsually hidden
EvaluateNo districtFree shippingvalue null"choose an address"
EvaluateInvalid lineArchived productExcluded from subtotal
ReserveAlready at limitUsage exhaustedrefusedPROMOTION_USAGE_LIMIT_REACHED (future)
ReservePulled between eval + reserveOperator editGate refusespredicates
ReleaseRetryAt-least-onceSingle decrementledger gate
ReconcileConcurrent reserveDuring sweepCounter not discardedone statement
RestoreCode takenReused409PROMOTION_COUPON_CODE_TAKEN

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Evaluatecart, promotions, districtautomatic set (read)summary, discount, eligibility, promotions, metadata
Create/updatepromotionclear automaticpromotion response
Retire/restorepromotiondeleted_atclear automaticmessage/response
Reserve (future)promotionledger + usage_countclearredemption

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish.
  • The doc explains what the backend does that the actor does not see (derived lifecycle, gate predicates, ledger).
  • The doc covers every minor flow and branch.
  • The doc includes user, admin and system flows.
  • The doc explains business logic, tradeoffs, and rationale.
  • The doc maps every flow to API routes and backend side effects.
  • The doc includes diagrams appropriate to each flow type.
  • The doc covers edge cases and failure recovery.

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed.
  • Every actor has allowed and forbidden behavior.
  • Every major and minor flow includes steps, branches, and diagrams.
  • Every lifecycle has a transition table and state diagram.
  • Every flow links to the API and backend docs.
  • TDD dependencies are called out where they shape behavior (no TDD pages published yet).

See Also