Skip to content

PromoCodeRedemptionManager

Per-user audit trail for promo code redemptions, stored in the user's local SQLite database.

Design Overview

PromoCodeRedemptionManager is a local record-keeper — it stores redemption history after validation succeeds elsewhere. It owns no validation logic and makes no cross-DO calls. The actual promo code validation, usage limits, and global deduplication live in the PromoCodeRegistry DO.

Redemption Flow

Promo codes are only redeemable during SubscriptionManager.subscribe() for "regular" subscriptions:

User subscribes with promoCode


SubscriptionManager inserts subscription row (needed for redemption FK)


Calls PromoCodeRegistry.validateAndRecordUsage()  ← atomic global validation

       ├─ FAIL → deletes subscription row, returns error

       ▼ SUCCESS
Records locally via PromoCodeRedemptionManager.recordRedemption()


Records payment via PaymentHistoryManager.recordPayment()
  (includes discountAmount, originalAmount)

Two-Phase Commit Pattern

Redemption uses a validate-then-record pattern across two DOs:

  1. Phase 1 — Remote atomic validation: PromoCodeRegistry.validateAndRecordUsage() validates the code (expiry, usage limits, plan eligibility, user type) and atomically increments global usage. Since PromoCodeRegistry is a single global DO, all concurrent redemptions are serialized — no two users can race on a limited-use code.

  2. Phase 2 — Local audit write: recordRedemption() inserts the audit record in the user's SQLite. This runs in the same DO request with no network boundary, so there's no partial-failure window between validation and recording. The insert includes a defensive throw if the row can't be read back.

Deliberate trade-off: If Phase 1 succeeds but the surrounding subscription logic fails afterward, the global usage is already recorded with no rollback. This prevents double-use exploits at the cost of potentially "wasting" a use on a failed subscription attempt.

Rollback on Validation Failure

When PromoCodeRegistry rejects a code, SubscriptionManager deletes the just-created subscription row before returning the error. This keeps local state clean without needing distributed transaction coordination.

Code Normalization

All promo codes are stored and queried as toUpperCase().trim(), ensuring case-insensitive matching throughout the system.

Local Deduplication

hasUsedPromoCode() provides a per-user check for whether a code has been redeemed before. This complements PromoCodeRegistry's global usage tracking — the global DO enforces system-wide limits, while this method enables user-scoped queries without a cross-DO call.

Edge Cases

  • recordRedemption() throws if the inserted row cannot be read back (defensive integrity check)
  • Normalization (toUpperCase().trim()) is applied in query methods (getRedemptionsByCode, hasUsedPromoCode), while recordRedemption() relies on the caller (SubscriptionManager) to normalize before passing the code
  • Subscription row is inserted before promo validation to obtain the subscriptionId FK — if validation fails, the row is explicitly deleted

See Also