Care Package Module Backend Documentation
Care Package Module - Backend Documentation
1. Backend Scope and Boundaries
Care package backend owns:
- Care package configuration management (admin CRUD + features + pricing tiers + eligibility rules + notification schedule)
- Customer eligibility check service
- Care-package cart operations (attach/detach care package per cart item)
- Subscription lifecycle (creation on order success, activation, expiry, reminder notifications, cancellation)
- Redemption request management (customer raise + admin status transitions + usage tracking)
- Customer subscription account views (list, detail with feature slots and remaining days)
- Admin subscription dashboard (list, detail, suspend, unsuspend, manual activate)
Care package does not:
- Initiate payment directly (payment initiation is owned by order endpoints)
- Manage physical product inventory
- Track shipping (care packages are non-physical; no
shippingDistrictrequired)
2. Module Composition (Aggregate + Leaf)
CarePackageModule composes:
CarePackageCoreModule— exports shared DB injection; no HTTP surfaceCarePackageAdminModule— admin CRUD for packages, features, pricing, eligibility, notification scheduleCarePackageCustomerModule— customer eligibility listCarePackageCartModule— attach/detach care package per cart itemCarePackageRedemptionAdminModule— admin redemption request managementCarePackageRedemptionCustomerModule— customer redemption request operationsCarePackageSubscriptionsAdminModule— admin subscription list/detail/actions; importsBullModule.registerQueue({ name: QueueName.CARE_PACKAGE })for manual activateCarePackageSubscriptionCustomerModule— customer subscription list/detailCarePackageWorkersModule— BullMQ processor + scheduler; registered onCarePackageModule
Ownership model:
CarePackageCoreModuleprovides database injection viaDATABASEtoken- Admin/customer leaf modules own their HTTP surfaces
CarePackageWorkersModuleowns the BullMQ processor and scheduler
Route registration:
- Admin modules registered at root NestJS level (via
CarePackageModule→app.module.ts) - Customer/mobile modules mounted under
MobileModulerouting with/api/mobile/prefix
3. Data Model (Drizzle / PostgreSQL)
Source of truth: packages/db/src/schema/care-package/
Migrations: packages/db/src/migrations/0002_CarePackage.sql, packages/db/src/migrations/0003_care_package_seo.sql
Tables
care_packages (carePackages)
id,name,slug,seoId(FK →seo.id, nullable, SET NULL on delete),description,sortOrder,isActive,isDraft,termsAndConditions,createdAt,updatedAtslug: auto-generated fromnameat create/update; globally unique; URL-safe lowercase with hyphens. Example:"Basic Care" → "basic-care". Duplicate names append-2,-3suffix.seoId: optional link to a sharedseorecord for meta title, description, OG tags, Twitter cards, JSON-LD.isActive = false+isDraft = trueby default. Must be explicitly activated before customers can see it.- Indexes:
care_package_slug_idx,care_package_seo_id_idx.
care_package_slug_history (carePackageSlugHistory)
id,carePackageId(FK →care_packages.id, CASCADE DELETE),oldSlug(varchar 255, NOT NULL),createdAt- Append-only audit table of previous slugs for each package.
- Unique index on
old_slug(globally unique across all packages) — prevents any package from taking a historically-used slug. - When a care package name changes, the previous slug is inserted to this table with
onConflictDoNothing(). - When a slug is reclaimed (package slug cycles back to a previously-used value), the stale history entry is deleted to prevent ghost entries.
- Customer API uses this table to resolve historical slugs to the current active package — enables SEO-preserving URL redirects.
care_package_features (carePackageFeatures)
id,carePackageId(FK →care_packages.id),featureType(enum),isEnabled,isUnlimited,usageCount(nullable),notes,featureMetadata(jsonb),createdAt,updatedAt- Unique:
(carePackageId, featureType)— one row per feature per package. featureTypeenum:pick_drop | repair | service | priority_support | diagnostics | on_site | parts_coverage | warranty_extension
care_package_pricing_tiers (carePackagePricingTiers)
id,carePackageId(FK),durationMonths,basePrice(integer, in paisa),discountAmount(integer, in paisa, default 0),isActive,sortOrder,createdAt,updatedAt- Unique:
(carePackageId, durationMonths)— one tier per duration per package. - Effective price =
basePrice - discountAmount.
care_package_eligibility_rules (carePackageEligibilityRules)
id,carePackageId(FK),dimension(enum:category | subcategory | brand | brand_series | product | price_range),mode(enum:include | exclude),targetId(integer, nullable),priceMin(nullable, integer, in paisa),priceMax(nullable, integer, in paisa),createdAt- Unique:
(carePackageId, dimension, mode, targetId)WHEREtargetId IS NOT NULL. - CHECK:
dimension = 'price_range' OR targetId IS NOT NULL(target required for non-price dimensions).
care_package_notification_schedule (carePackageNotificationSchedule)
id,carePackageId(nullable FK — NULL = global default),daysBeforeExpiry(integer),isActive(boolean, default true),createdAt,updatedAt- Unique:
(carePackageId, daysBeforeExpiry)WHEREcarePackageId IS NOT NULL— one schedule entry per (package, days) pair. - Unique:
(daysBeforeExpiry)WHEREcarePackageId IS NULL— one global entry per interval. - 3 global rows seeded with
daysBeforeExpiry = 30, 7, 0andcarePackageId = NULL.
cart_care_package_items (cartCarePackageItems)
id,cartId(FK →cart.id, CASCADE DELETE),cartItemId(FK →cart_item.id, CASCADE DELETE),carePackageId(FK →care_packages.id, RESTRICT),pricingTierId(FK →care_package_pricing_tiers.id, RESTRICT),unitPrice(integer, in paisa — snapshot at add time),createdAt,updatedAt- Unique:
(cartItemId)— enforces 1 care package per cart item.
care_package_subscriptions (carePackageSubscriptions)
id,customerId(text FK → customer),orderId(FK →orders.id),orderItemId(FK →order_items.id),carePackageId(FK, nullable — SET NULL on delete),pricingTierId(FK, nullable — SET NULL on delete)carePackageSnapshot(jsonb, not null) — immutable snapshot of package at purchase timepricingSnapshot(jsonb, not null) — immutable snapshot of pricing tier at purchase time; must containdurationMonthslinkedProductId(FK →products.id, nullable),linkedProductSnapshot(jsonb, nullable)status(enum:pending_activation | active | expired | cancelled | suspended)durationMonths(integer, not null) — copied from pricingTier at creationstartDate(date, nullable),expiryDate(date, nullable) — set on activationpurchasedAt(timestamptz),activatedAt(timestamptz, nullable),cancelledAt(timestamptz, nullable),suspendedAt(timestamptz, nullable),suspensionReason(text, nullable)termsAcceptedAt(timestamptz, nullable),termsAcceptedVersion(varchar(50), nullable)createdAt,updatedAt- Indexes: composite
(customerId, status); per-column FK indexes.
care_package_feature_usage (carePackageFeatureUsage)
id,subscriptionId(FK →care_package_subscriptions.id),featureType(enum),usedCount(integer, default 0),createdAt,updatedAt- Unique:
(subscriptionId, featureType).
care_package_redemption_requests (carePackageRedemptionRequests)
id,subscriptionId(FK),customerId(text FK),featureType(enum),status(enum:pending | accepted | in_progress | completed | rejected | cancelled),description(text),attachmentUrls(jsonb array, nullable),requestMetadata(jsonb, nullable),adminNotes(text, nullable),resolvedAt(timestamptz, nullable),createdAt,updatedAt
Enums
Source: packages/db/src/schema/care-package/enums.ts
carePackageSubscriptionStatusEnum:pending_activation | active | expired | cancelled | suspendedcarePackageFeatureTypeEnum:pick_drop | repair | service | priority_support | diagnostics | on_site | parts_coverage | warranty_extensioncarePackageRedemptionStatusEnum:pending | accepted | in_progress | completed | rejected | cancelledcarePackageDimensionEnum:category | subcategory | brand | brand_series | product | price_rangecarePackageRuleModeEnum:include | exclude
Product Schema Addition
Source: packages/db/src/schema/product/product.ts
productKindEnumextended withcare_packagevalue.- A product with
productKind = "care_package"is used as the linked product for a care package.
Order Item Schema Addition
Source: packages/db/src/schema/order/order-items.ts
carePackageSubscriptionIdcolumn (FK →care_package_subscriptions.id, nullable) — links an order item to its resulting subscription.
4. BullMQ Runtime
Queue name (from packages/jobs/src/index.ts):
QueueName.CARE_PACKAGE = "care_package"
Queue Registration
The CARE_PACKAGE queue is registered in:
apps/api/src/services/bullmq/bull.module.ts—@Global()JobsModule.registerQueueAsync()(global registration)apps/api/src/modules/care-package/workers/care-package-workers.module.ts—BullModule.registerQueue({ name: QueueName.CARE_PACKAGE })(for processor token)apps/api/src/modules/care-package/admin/care-package-subscriptions-admin.module.ts—BullModule.registerQueue({ name: QueueName.CARE_PACKAGE })(for@InjectQueuein admin service)apps/api/src/modules/order/processing/order.module.ts— localBullModule.registerQueue({ name: QueueName.CARE_PACKAGE })(for order processor to enqueue activate jobs)
Jobs
| Job enum | Job name (string) | Payload interface | Trigger |
|---|---|---|---|
CarePackageJob.ACTIVATE_SUBSCRIPTION | care_package.activate_subscription | ActivateCarePackageSubscriptionPayload | OrderProcessor.processPaymentSuccess() — one job per subscription in the order |
CarePackageJob.SEND_EXPIRY_REMINDER | care_package.send_expiry_reminder | SendCarePackageExpiryReminderPayload | CarePackageProcessor.processActivation() — delayed; also CarePackageSubscriptionsAdminService.manualActivate() |
CarePackageJob.EXPIRE_SUBSCRIPTIONS | care_package.expire_subscriptions | ExpireCarePackageSubscriptionsPayload | CarePackageWorkersScheduler — daily cron sweep |
CarePackageJob.CANCEL_SUBSCRIPTION | care_package.cancel_subscription | CancelCarePackageSubscriptionPayload | Future hook; cancellation currently handled synchronously via order cancel flow |
Payload Interfaces
interface ActivateCarePackageSubscriptionPayload {
subscriptionId: number;
orderId: number;
correlationId?: string;
}
interface SendCarePackageExpiryReminderPayload {
subscriptionId: number;
daysBeforeExpiry: number;
correlationId?: string;
}
interface ExpireCarePackageSubscriptionsPayload {
correlationId?: string;
}
interface CancelCarePackageSubscriptionPayload {
subscriptionId: number;
reason: string;
correlationId?: string;
}Processor
File: apps/api/src/modules/care-package/workers/care-package.processor.ts
- Class:
CarePackageProcessor extends WorkerHost - Decorator:
@Processor(QueueName.CARE_PACKAGE) - Routes by
job.nameto:processActivation,processExpiryReminder,processExpireSubscriptions,processCancellation
processActivation idempotency
- Guard:
if (subscription.status !== "pending_activation") return— replays are safe - Validates
pricingSnapshot.durationMonthsis a positive integer - Sets
startDate = today,expiryDate = today + durationMonths(viaDate.setMonth) - After activation, schedules
SEND_EXPIRY_REMINDERjobs for 30d and 7d before expiry with deterministic job IDs:care-package-reminder-{subscriptionId}-{daysBeforeExpiry}d - Reminder jobs use
delay = reminderTimeMs - Date.now(); skips ifdelay <= 0 - Duplicate job errors (BullMQ
jobIdalready exists) are silently swallowed viaisDuplicateQueueJobError()helper
processExpireSubscriptions bulk sweep
- Drizzle
UPDATEwithWHERE status = "active" AND expiryDate <= today - Runs as a single bulk update; not per-subscription
- Deterministic daily job ID:
care-package-expire-sweep-{YYYY-MM-DD}
processExpiryReminder
- Skips if subscription is not
active(deactivated since scheduling) - Enqueues
NotificationJob.SEND_EMAILto theQueueName.NOTIFICATIONSqueue with subject + body usingcarePackageSnapshot.name
processCancellation idempotency
- Guard:
if (status === "cancelled" || status === "expired") return
Scheduler
File: apps/api/src/modules/care-package/workers/care-package-workers.scheduler.ts
- Class:
CarePackageWorkersScheduler implements OnModuleInit - Uses
ConfigService+SchedulerRegistry+CronJob - Registers cron job
care_package_expiry_sweep_cronwith expression fromCARE_PACKAGE_EXPIRY_SWEEP_CRONenv var - Enqueues
EXPIRE_SUBSCRIPTIONSjob with deterministic dailyjobId; duplicate detection handled inline (not viaisDuplicateQueueJobError)
5. Runtime Invariants
Snapshot Immutability
carePackageSnapshot and pricingSnapshot on care_package_subscriptions are written at order creation time and never modified. They capture the exact package definition and pricing terms the customer purchased. This ensures subscription behavior is stable even if admin changes the care package config after purchase.
Activation Idempotency
processActivation checks subscription.status !== "pending_activation" before making any writes. Duplicate BullMQ delivery or manual re-enqueue will be safely skipped.
Ownership Check at DB Query Level
CarePackageSubscriptionCustomerService.findMineById() uses a single DB query with AND eq(carePackageSubscriptions.customerId, customerId). Returns generic CARE_PACKAGE_SUBSCRIPTION_NOT_FOUND for both not-found and wrong-owner cases — no ownership disclosure.
Unsuspend Status Restoration
When unsuspend is called, new status is determined at query time:
expiryDatepresent ANDexpiryDate <= today→"expired"- Otherwise →
"active"(includes null expiryDate edge case for admin-overridden subscriptions) suspendedAtandsuspensionReasonare cleared tonullon unsuspend.
manualActivate Snapshot Validation
Before activating, admin service validates pricingSnapshot.durationMonths is a positive integer. Throws CARE_PACKAGE_SUBSCRIPTION_ACTIVATE_FAILED if snapshot is invalid.
6. Caching Strategy
Eligibility Endpoint
GET /api/mobile/care-packages/available:
- Cache key:
CacheKeyUtil.build("care_pkg_eligibility", [productId, categoryId, brandId, ...])— stable segments viaCacheKeyUtil.segment() - TTL:
CARE_PACKAGE_CACHE_TTL_SECONDS(default120) - Invalidated via
invalidatePattern("care_pkg_eligibility*")when admin updates care package configuration or eligibility rules
Subscription Endpoints
No caching. Subscription data is user-scoped and write-intensive (status changes after activation, suspension, expiry sweep). Caching would risk serving stale status to customers.
Admin Reads
No caching. Admin views are infrequent and need current data for support/operations.
7. Error and Resilience Contracts
Full error code table:
| HTTP | errorCode | Condition |
|---|---|---|
| 404 | CARE_PACKAGE_NOT_FOUND | Package ID not in DB |
| 400 | CARE_PACKAGE_INACTIVE | Package not active |
| 409 | CARE_PACKAGE_DELETE_HAS_ACTIVE_SUBSCRIPTIONS | Delete blocked by active subs |
| 404 | CARE_PACKAGE_PRICING_TIER_NOT_FOUND | Tier ID not found |
| 400 | CARE_PACKAGE_PRICING_TIER_INACTIVE | Tier not active |
| 400 | CARE_PACKAGE_ELIGIBILITY_RULE_INVALID_TARGET | Rule target invalid |
| 400 | CARE_PACKAGE_ELIGIBILITY_RULE_PRICE_RANGE_INVALID | Price range min > max |
| 409 | CARE_PACKAGE_NOTIFICATION_SCHEDULE_DUPLICATE | Duplicate days-before-expiry |
| 404 | CARE_PACKAGE_CART_ITEM_NOT_FOUND | Cart item not found |
| 409 | CARE_PACKAGE_CART_ITEM_ALREADY_HAS_PACKAGE | Cart item already has care package |
| 400 | CARE_PACKAGE_NOT_ELIGIBLE_FOR_PRODUCT | Selected package eligibility rules do not match product |
| 400 | CARE_PACKAGE_CART_PACKAGE_NO_LONGER_AVAILABLE | Package deactivated after add |
| 400 | CARE_PACKAGE_CART_PRICING_TIER_NO_LONGER_ACTIVE | Tier deactivated after add |
| 404 | CARE_PACKAGE_SUBSCRIPTION_NOT_FOUND | Subscription not found or wrong owner (no disclosure) |
| 400 | CARE_PACKAGE_SUBSCRIPTION_NOT_ACTIVE | Subscription not active (for redemption raise) |
| 400 | CARE_PACKAGE_SUBSCRIPTION_ALREADY_ACTIVE | Admin manual activate on non-pending_activation |
| 400 | CARE_PACKAGE_SUBSCRIPTION_ACTIVATE_FAILED | Snapshot missing durationMonths |
| 400 | CARE_PACKAGE_SUBSCRIPTION_SUSPEND_INVALID_STATUS | Suspend requires active |
| 400 | CARE_PACKAGE_SUBSCRIPTION_UNSUSPEND_INVALID_STATUS | Unsuspend requires suspended |
| 404 | CARE_PACKAGE_REDEMPTION_NOT_FOUND | Redemption request not found |
| 403 | CARE_PACKAGE_REDEMPTION_NOT_OWNED | Redemption does not belong to customer |
| 400 | CARE_PACKAGE_REDEMPTION_FEATURE_NOT_ENABLED | Feature not enabled in package |
| 400 | CARE_PACKAGE_REDEMPTION_QUOTA_EXCEEDED | Feature quota exhausted |
| 400 | CARE_PACKAGE_REDEMPTION_INVALID_STATUS_TRANSITION | Admin invalid status transition |
| 400 | CARE_PACKAGE_REDEMPTION_CANCEL_NOT_PENDING | Cancel only allowed on pending |
BullMQ Resilience
- Duplicate job ID errors (
jobIdalready exists) are handled byisDuplicateQueueJobError()— suppressed silently to allow safe replay/retry - Expiry sweep uses daily deterministic
jobId(care-package-expire-sweep-{YYYY-MM-DD}) — idempotent per day - Reminder jobs use per-subscription deterministic IDs — safe to re-run activate
8. Performance Notes
CarePackageSubscriptionsAdminService.findOne()usesPromise.all([subscriptionQuery, featureUsageQuery, recentRedemptionsQuery])for parallel DB reads.- All list endpoints use
PaginationUtil.normalize()+getDrizzleParams(); count queries only when pagination is enabled. - Eligibility endpoint is cached to avoid repeated DB reads for the same customer+product combination.
- Expiry sweep is a single bulk
UPDATE— not per-subscription loops.
9. Backend Diagram
10. Release/QA Checklist
- Care package schema migration
0002_CarePackage.sqlruns without errors. -
care_package_notification_scheduleseeded with 3 global rows (30, 7, 0 days). - Eligibility endpoint filters packages by
productKindrules; returns only active packages. - 1-per-cart-item constraint enforced via unique index on
(cartItemId). - Checkout creates subscription rows with
status = "pending_activation"and snapshots. -
activate_subscriptionjob is idempotent (replay skips if notpending_activation). - Activation calculates
expiryDate = today + durationMonthscorrectly. - Reminder jobs use deterministic IDs; duplicate job errors suppressed.
- Expiry sweep bulk-updates
activesubscriptions pastexpiryDate. -
suspend/unsuspend/manualActivateenforce correct status guards. -
unsuspendrestores toactiveorexpiredbased onexpiryDate. - Feature usage incremented in transaction on redemption
acceptedtransition. - Quota enforcement at raise-time:
usedCount >= usageCountblocks raise for finite features. -
BullModule.registerQueue({ name: QueueName.CARE_PACKAGE })present in workers module AND admin subscriptions module. -
CARE_PACKAGE_EXPIRY_SWEEP_CRONenv var present and valid cron expression.
11. File Map
packages/db/src/schema/care-package/
enums.ts
care-package.ts
care-package-features.ts
care-package-pricing-tiers.ts
care-package-eligibility-rules.ts
care-package-notification-schedule.ts
cart-care-package-items.ts
care-package-subscriptions.ts
care-package-feature-usage.ts
care-package-redemption-requests.ts
index.ts
packages/jobs/src/index.ts (QueueName.CARE_PACKAGE, CarePackageJob, payload interfaces)
apps/api/src/modules/care-package/
care-package-core.module.ts
care-package.module.ts
admin/
care-package-admin.module.ts
care-package-admin.controller.ts
care-package-admin.service.ts
care-package-eligibility.service.ts
care-package-subscriptions-admin.module.ts
care-package-subscriptions-admin.controller.ts
care-package-subscriptions-admin.service.ts
care-package-redemption-admin.module.ts
care-package-redemption-admin.controller.ts
care-package-redemption-admin.service.ts
dto/
customer/
care-package-customer.module.ts
care-package-customer.controller.ts
care-package-customer.service.ts
care-package-subscription-customer.module.ts
care-package-subscription-customer.controller.ts
care-package-subscription-customer.service.ts
care-package-redemption-customer.module.ts
care-package-redemption-customer.controller.ts
care-package-redemption-customer.service.ts
dto/
cart/
care-package-cart.module.ts
care-package-cart.controller.ts
care-package-cart.service.ts
dto/
workers/
care-package-workers.module.ts
care-package.processor.ts
care-package-workers.scheduler.ts12. Environment Variables
| Variable | Default | Description |
|---|---|---|
CARE_PACKAGE_EXPIRY_SWEEP_CRON | (required) | Cron expression for daily expiry sweep job. Recommended: 0 1 * * * (1 AM daily). No default — app fails to start if missing. |
CARE_PACKAGE_CACHE_TTL_SECONDS | 120 | Cache TTL in seconds for GET /api/mobile/care-packages/available eligibility reads |