Appearance
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:
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.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), whilerecordRedemption()relies on the caller (SubscriptionManager) to normalize before passing the code - Subscription row is inserted before promo validation to obtain the
subscriptionIdFK — if validation fails, the row is explicitly deleted
See Also
- SubscriptionManager — sole caller of
recordRedemption() - PromoCodeRegistry DO — global validation and usage tracking