Shop It Docs
Developer Resourcescare-package

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 shippingDistrict required)

2. Module Composition (Aggregate + Leaf)

CarePackageModule composes:

  • CarePackageCoreModule — exports shared DB injection; no HTTP surface
  • CarePackageAdminModule — admin CRUD for packages, features, pricing, eligibility, notification schedule
  • CarePackageCustomerModule — customer eligibility list
  • CarePackageCartModule — attach/detach care package per cart item
  • CarePackageRedemptionAdminModule — admin redemption request management
  • CarePackageRedemptionCustomerModule — customer redemption request operations
  • CarePackageSubscriptionsAdminModule — admin subscription list/detail/actions; imports BullModule.registerQueue({ name: QueueName.CARE_PACKAGE }) for manual activate
  • CarePackageSubscriptionCustomerModule — customer subscription list/detail
  • CarePackageWorkersModule — BullMQ processor + scheduler; registered on CarePackageModule

Ownership model:

  • CarePackageCoreModule provides database injection via DATABASE token
  • Admin/customer leaf modules own their HTTP surfaces
  • CarePackageWorkersModule owns the BullMQ processor and scheduler

Route registration:

  • Admin modules registered at root NestJS level (via CarePackageModuleapp.module.ts)
  • Customer/mobile modules mounted under MobileModule routing 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, updatedAt
  • slug: auto-generated from name at create/update; globally unique; URL-safe lowercase with hyphens. Example: "Basic Care" → "basic-care". Duplicate names append -2, -3 suffix.
  • seoId: optional link to a shared seo record for meta title, description, OG tags, Twitter cards, JSON-LD.
  • isActive = false + isDraft = true by 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.
  • featureType enum: 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) WHERE targetId 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) WHERE carePackageId IS NOT NULL — one schedule entry per (package, days) pair.
  • Unique: (daysBeforeExpiry) WHERE carePackageId IS NULL — one global entry per interval.
  • 3 global rows seeded with daysBeforeExpiry = 30, 7, 0 and carePackageId = 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 time
  • pricingSnapshot (jsonb, not null) — immutable snapshot of pricing tier at purchase time; must contain durationMonths
  • linkedProductId (FK → products.id, nullable), linkedProductSnapshot (jsonb, nullable)
  • status (enum: pending_activation | active | expired | cancelled | suspended)
  • durationMonths (integer, not null) — copied from pricingTier at creation
  • startDate (date, nullable), expiryDate (date, nullable) — set on activation
  • purchasedAt (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 | suspended
  • carePackageFeatureTypeEnum: pick_drop | repair | service | priority_support | diagnostics | on_site | parts_coverage | warranty_extension
  • carePackageRedemptionStatusEnum: pending | accepted | in_progress | completed | rejected | cancelled
  • carePackageDimensionEnum: category | subcategory | brand | brand_series | product | price_range
  • carePackageRuleModeEnum: include | exclude

Product Schema Addition

Source: packages/db/src/schema/product/product.ts

  • productKindEnum extended with care_package value.
  • 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

  • carePackageSubscriptionId column (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.tsBullModule.registerQueue({ name: QueueName.CARE_PACKAGE }) (for processor token)
  • apps/api/src/modules/care-package/admin/care-package-subscriptions-admin.module.tsBullModule.registerQueue({ name: QueueName.CARE_PACKAGE }) (for @InjectQueue in admin service)
  • apps/api/src/modules/order/processing/order.module.ts — local BullModule.registerQueue({ name: QueueName.CARE_PACKAGE }) (for order processor to enqueue activate jobs)

Jobs

Job enumJob name (string)Payload interfaceTrigger
CarePackageJob.ACTIVATE_SUBSCRIPTIONcare_package.activate_subscriptionActivateCarePackageSubscriptionPayloadOrderProcessor.processPaymentSuccess() — one job per subscription in the order
CarePackageJob.SEND_EXPIRY_REMINDERcare_package.send_expiry_reminderSendCarePackageExpiryReminderPayloadCarePackageProcessor.processActivation() — delayed; also CarePackageSubscriptionsAdminService.manualActivate()
CarePackageJob.EXPIRE_SUBSCRIPTIONScare_package.expire_subscriptionsExpireCarePackageSubscriptionsPayloadCarePackageWorkersScheduler — daily cron sweep
CarePackageJob.CANCEL_SUBSCRIPTIONcare_package.cancel_subscriptionCancelCarePackageSubscriptionPayloadFuture 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.name to: processActivation, processExpiryReminder, processExpireSubscriptions, processCancellation

processActivation idempotency

  • Guard: if (subscription.status !== "pending_activation") return — replays are safe
  • Validates pricingSnapshot.durationMonths is a positive integer
  • Sets startDate = today, expiryDate = today + durationMonths (via Date.setMonth)
  • After activation, schedules SEND_EXPIRY_REMINDER jobs for 30d and 7d before expiry with deterministic job IDs: care-package-reminder-{subscriptionId}-{daysBeforeExpiry}d
  • Reminder jobs use delay = reminderTimeMs - Date.now(); skips if delay <= 0
  • Duplicate job errors (BullMQ jobId already exists) are silently swallowed via isDuplicateQueueJobError() helper

processExpireSubscriptions bulk sweep

  • Drizzle UPDATE with WHERE 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_EMAIL to the QueueName.NOTIFICATIONS queue with subject + body using carePackageSnapshot.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_cron with expression from CARE_PACKAGE_EXPIRY_SWEEP_CRON env var
  • Enqueues EXPIRE_SUBSCRIPTIONS job with deterministic daily jobId; duplicate detection handled inline (not via isDuplicateQueueJobError)

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:

  • expiryDate present AND expiryDate <= today"expired"
  • Otherwise → "active" (includes null expiryDate edge case for admin-overridden subscriptions)
  • suspendedAt and suspensionReason are cleared to null on 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 via CacheKeyUtil.segment()
  • TTL: CARE_PACKAGE_CACHE_TTL_SECONDS (default 120)
  • 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:

HTTPerrorCodeCondition
404CARE_PACKAGE_NOT_FOUNDPackage ID not in DB
400CARE_PACKAGE_INACTIVEPackage not active
409CARE_PACKAGE_DELETE_HAS_ACTIVE_SUBSCRIPTIONSDelete blocked by active subs
404CARE_PACKAGE_PRICING_TIER_NOT_FOUNDTier ID not found
400CARE_PACKAGE_PRICING_TIER_INACTIVETier not active
400CARE_PACKAGE_ELIGIBILITY_RULE_INVALID_TARGETRule target invalid
400CARE_PACKAGE_ELIGIBILITY_RULE_PRICE_RANGE_INVALIDPrice range min > max
409CARE_PACKAGE_NOTIFICATION_SCHEDULE_DUPLICATEDuplicate days-before-expiry
404CARE_PACKAGE_CART_ITEM_NOT_FOUNDCart item not found
409CARE_PACKAGE_CART_ITEM_ALREADY_HAS_PACKAGECart item already has care package
400CARE_PACKAGE_NOT_ELIGIBLE_FOR_PRODUCTSelected package eligibility rules do not match product
400CARE_PACKAGE_CART_PACKAGE_NO_LONGER_AVAILABLEPackage deactivated after add
400CARE_PACKAGE_CART_PRICING_TIER_NO_LONGER_ACTIVETier deactivated after add
404CARE_PACKAGE_SUBSCRIPTION_NOT_FOUNDSubscription not found or wrong owner (no disclosure)
400CARE_PACKAGE_SUBSCRIPTION_NOT_ACTIVESubscription not active (for redemption raise)
400CARE_PACKAGE_SUBSCRIPTION_ALREADY_ACTIVEAdmin manual activate on non-pending_activation
400CARE_PACKAGE_SUBSCRIPTION_ACTIVATE_FAILEDSnapshot missing durationMonths
400CARE_PACKAGE_SUBSCRIPTION_SUSPEND_INVALID_STATUSSuspend requires active
400CARE_PACKAGE_SUBSCRIPTION_UNSUSPEND_INVALID_STATUSUnsuspend requires suspended
404CARE_PACKAGE_REDEMPTION_NOT_FOUNDRedemption request not found
403CARE_PACKAGE_REDEMPTION_NOT_OWNEDRedemption does not belong to customer
400CARE_PACKAGE_REDEMPTION_FEATURE_NOT_ENABLEDFeature not enabled in package
400CARE_PACKAGE_REDEMPTION_QUOTA_EXCEEDEDFeature quota exhausted
400CARE_PACKAGE_REDEMPTION_INVALID_STATUS_TRANSITIONAdmin invalid status transition
400CARE_PACKAGE_REDEMPTION_CANCEL_NOT_PENDINGCancel only allowed on pending

BullMQ Resilience

  • Duplicate job ID errors (jobId already exists) are handled by isDuplicateQueueJobError() — 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() uses Promise.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.sql runs without errors.
  • care_package_notification_schedule seeded with 3 global rows (30, 7, 0 days).
  • Eligibility endpoint filters packages by productKind rules; 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_subscription job is idempotent (replay skips if not pending_activation).
  • Activation calculates expiryDate = today + durationMonths correctly.
  • Reminder jobs use deterministic IDs; duplicate job errors suppressed.
  • Expiry sweep bulk-updates active subscriptions past expiryDate.
  • suspend/unsuspend/manualActivate enforce correct status guards.
  • unsuspend restores to active or expired based on expiryDate.
  • Feature usage incremented in transaction on redemption accepted transition.
  • Quota enforcement at raise-time: usedCount >= usageCount blocks raise for finite features.
  • BullModule.registerQueue({ name: QueueName.CARE_PACKAGE }) present in workers module AND admin subscriptions module.
  • CARE_PACKAGE_EXPIRY_SWEEP_CRON env 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.ts

12. Environment Variables

VariableDefaultDescription
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_SECONDS120Cache TTL in seconds for GET /api/mobile/care-packages/available eligibility reads