Care Package Redemption Requests
Redemption request state machine, feature quota enforcement, usage tracking, and admin status transitions.
Care Package Redemption Requests
1. Redemption Request State Machine
2. VALID_STATUS_TRANSITIONS
Defined in care-package-redemption-admin.service.ts:
const VALID_STATUS_TRANSITIONS: Record<CarePackageRedemptionStatus, CarePackageRedemptionStatus[]> = {
pending: ["accepted", "rejected", "in_progress"],
in_progress: ["completed", "rejected"],
accepted: ["in_progress", "completed"],
completed: [],
rejected: [],
cancelled: [],
};Table form:
| From | Allowed transitions |
|---|---|
pending | accepted, rejected, in_progress |
in_progress | completed, rejected |
accepted | in_progress, completed |
completed | none (terminal) |
rejected | none (terminal) |
cancelled | none (terminal; customer-only from pending) |
3. Feature Quota Enforcement (Raise-Time)
CarePackageRedemptionCustomerService.raiseRequest() checks at request creation time:
- Subscription must be
active. - Feature must exist in
carePackageSnapshot.features[]withisEnabled = true. - If
feature.isUnlimited = true: skip quota check. - If
feature.isUnlimited = false:- Load
carePackageFeatureUsagerow for(subscriptionId, featureType). - If
usage.usedCount >= feature.usageCount: throwCARE_PACKAGE_REDEMPTION_QUOTA_EXCEEDED. - If no usage row exists:
usedCountis treated as0(row created on first accepted transition).
- Load
4. Feature Usage Tracking
Table: care_package_feature_usage
subscriptionId(FK),featureType(enum) — unique pairusedCount— incremented by 1 on eachacceptedadmin transition (not on raise)
Increment path — admin accepted transition in CarePackageRedemptionAdminService.updateStatus():
DB transaction:
UPDATE care_package_redemption_requests SET status = 'accepted' WHERE id = :id
UPDATE care_package_feature_usage
SET used_count = used_count + 1, updated_at = now()
WHERE subscription_id = :subscriptionId AND feature_type = :featureTypeThe usage row must exist (created at subscription creation or first use). The transaction ensures atomicity: if usage increment fails, request status is not updated.
5. Terminal Status Handling
When status becomes completed or rejected:
resolvedAtis set tonow()on the redemption request row.- No feature usage adjustment for
rejected(usage was already incremented onaccepted; no rollback).
When status advances without accept (pending → in_progress):
- Feature usage is NOT incremented at
in_progress— only ataccepted.
6. Customer Cancel
CarePackageRedemptionCustomerService.cancel():
- Guard:
if (request.status !== "pending")→ throwCARE_PACKAGE_REDEMPTION_CANCEL_NOT_PENDING - Sets:
status = "cancelled",resolvedAt = now(),updatedAt = now() - Does not adjust feature usage (none was allocated on
pending).
7. Ownership Verification
Customer findOwnedRequest():
- First checks request exists via
subscriptionId— if not found:CARE_PACKAGE_REDEMPTION_NOT_FOUND. - Then checks
request.customerId !== customerId— if mismatch:CARE_PACKAGE_REDEMPTION_NOT_OWNED.
Note: Unlike subscription ownership (which uses a single WHERE clause), redemption uses a two-step check because the request has no direct customerId + id compound index — only subscriptionId is indexed.
8. QA Checklist
- Raise validates active subscription, enabled feature, quota.
-
isUnlimited = truefeatures bypass quota. - Admin
acceptedtransition incrementsusedCountin a single DB transaction. -
completedandrejectedtransitions setresolvedAt. - Customer cancel only works on
pendingstatus. -
VALID_STATUS_TRANSITIONSblocks all invalid transitions withCARE_PACKAGE_REDEMPTION_INVALID_STATUS_TRANSITION.