๐ Subscription Management
๐ Overviewโ
The Subscription controller manages the complete lifecycle of recurring subscriptions in the DashClicks platform. It handles creation of new subscriptions, plan changes (upgrades/downgrades), cancellations, reactivations, payment retries, and complex software downgrade workflows with resource cleanup.
File Path: internal/api/v1/store/Controllers/subscription.js
Key Responsibilities:
- Create subscriptions with Stripe integration
- Handle subscription upgrades and downgrades
- Manage cancellations (immediate and end-of-cycle)
- Process payment retries for past_due subscriptions
- Execute software downgrades with resource cleanup queues
- Track first-time subscriber status
- Generate orders and trigger fulfillment workflows
๐๏ธ Collections Usedโ
๐ Full Schema: See Database Collections Documentation
_store.subscriptionsโ
- Operations: Create, Read, Update
- Model:
shared/models/store-subscription.js - Usage Context: Primary collection for all subscription operations, mirrors Stripe subscription data with enriched metadata
_store.ordersโ
- Operations: Create, Read
- Model:
shared/models/store-order.js - Usage Context: Generated when subscriptions are created, links to fulfillment workflows
_store.pricesโ
- Operations: Read
- Model:
shared/models/store-price.js - Usage Context: Validate pricing tiers and retrieve metadata for subscriptions
_store.productsโ
- Operations: Read
- Model:
shared/models/store-product.js - Usage Context: Validate product types and enforce business rules
_store.invoicesโ
- Operations: Read
- Model:
shared/models/store-invoice.js - Usage Context: Retrieve pending invoices for payment retry operations
_store.subscription-feedbackโ
- Operations: Create, Update, Delete
- Model:
shared/models/store-subscription-feedback.js - Usage Context: Track cancellation reasons and schedule software downgrades
_store.promo.codesโ
- Operations: Read
- Model:
shared/models/store-promo-code.js - Usage Context: Apply promotional codes during subscription creation
_accountsโ
- Operations: Read, Update
- Model:
shared/models/account.js - Usage Context: Track first-time subscriber status, loyalty programs, downgrade states
queuesโ
- Operations: Create
- Model:
shared/models/queues.js - Usage Context: Queue software downgrade jobs for background processing
projects.tasksโ
- Operations: Create, Update
- Model:
shared/models/projects-tasks.js - Usage Context: Create fulfillment tasks for subscription issues
projects.pulseโ
- Operations: Create
- Model:
shared/models/projects-pulse.js - Usage Context: Trigger onboarding and cancellation workflows
๐ Data Flowโ
flowchart TD
A[POST /subscriptions] --> B{Account Type}
B -->|Main Account| C[Use Platform Stripe]
B -->|Sub Account| D[Use Parent Connected Account]
C --> E[Build Subscription Options]
D --> E
E --> F{Has Promo Code?}
F -->|Yes| G[Add Discount]
F -->|No| H[Add Loyalty Discount]
G --> I[Create Stripe Subscription]
H --> I
I --> J[Save to MongoDB]
J --> K{First Order?}
K -->|Yes| L[Mark purchased_fulfillment_on]
K -->|No| M[Continue]
L --> N[Generate Order]
M --> N
N --> O{Is First Order?}
O -->|Yes| P[Reassign Manager]
O -->|No| Q[Use Existing Manager]
P --> R[Create Onboarding Pulse]
Q --> R
R --> S[Return Subscription]
style I fill:#635bff
style J fill:#4caf50
๐ง Business Logic & Functionsโ
newSubscription(req, res, next)โ
Purpose: Creates a new subscription in Stripe and mirrors it to MongoDB with order generation and fulfillment triggers.
Parameters:
req.body.price(String) - Required Stripe price IDreq.body.account(String, optional) - Target account ID if buying for sub-accountreq.body.customer(String, optional) - Target Stripe customer IDreq.body.promotion_code(String, optional) - Promo code to applyreq.body.digital_wallet(Boolean, optional) - Use digital wallet paymentreq.body.payment_method_id(String, optional) - Payment method for digital walletreq.body.metadata(Object, optional) - Additional metadata for subscriptionreq.body.quantity(Number, optional) - Subscription quantityreq.body.additional_options(Object, optional) - Extra Stripe subscription options
Returns: Promise<Object>
success(Boolean) - Operation success statusdata(Object) - Created subscription with order_id
Business Logic Flow:
-
Stripe Account Resolution
- Main accounts use platform Stripe key
- Sub-accounts lookup parent's Stripe key from
stripe-keycollection - Throws error if parent account key doesn't exist
-
Customer & Metadata Setup
- Default to requester's Stripe customer
- If buying for someone else, verify account access and retrieve/create customer
- Build metadata including account_id, account_name, and UTM parameters
-
Digital Wallet Handling
- If digital wallet requested, configure for off-session payments
- Attach payment method to customer
- Set as default payment method
-
Discount Application
- Apply loyalty program coupon if account has non-Standard tier
- Add promotional code discount if provided
- Track promo code creator for sales rep attribution
-
Price Validation & Application Fee Calculation (for connected accounts)
- Retrieve price from database with connected account validation
- Calculate wholesale markup based on
additional_info.wholesale_unit_amount - Add platform fee percentages:
ADDITIONAL_APP_FEE_PERCENTAGE+ADDITIONAL_APP_FEE_SUBSCRIPTION_PERCENTAGE - Add fixed
ADDITIONAL_APP_FEE_CENTS - Convert to percentage:
(total_fee / unit_amount) * 100 - Apply as
application_fee_percentto Stripe subscription
-
Stripe Subscription Creation
- Create subscription with Stripe API
- For connected accounts, include application_fee_percent
- For platform, include quantity if specified
-
First-Time Subscriber Tracking
- Check if this is first subscription for this account
- Set
became_subscriber_onif first subscription - Set
became_customer_onif first invoice - Track
purchased_fulfillment_onfor managed services (first managed subscription)
-
Database Persistence
- Save subscription to MongoDB with Stripe data
- Convert metadata.account_id to ObjectId
- Link to charge_id if provided
-
Order Generation
- Call
newOrder()utility to create order record - Link order to subscription
- For managed subscriptions (facebook_ads, google_ads, content, seo, social_posting, etc.), create activity log
- For first orders, reassign account manager via
reassignUserWorkloadUtil() - Create onboarding pulse for first managed subscription
- Call
-
Error Handling
- SMS notifications sent on order generation failure
- Errors logged but don't block subscription creation
Key Business Rules:
- Only main accounts can buy with
customerparameter - Only non-main accounts can buy with
accountparameter - Application fees only apply to connected account subscriptions
- Loyalty coupons automatically applied unless explicitly disabled
- First managed subscription triggers manager reassignment
- All managed subscriptions create activity logs on successful creation
Error Handling:
OPERATION_NOT_PERMITTED: Invalid account/customer combinationACCOUNT_ACCESS_DENIED: Account not found or access deniedERROR_CREATING_STRIPE_CUSTOMER: Stripe customer creation failedSTRIPE_CUSTOMER_NOT_FOUND: Customer doesn't existPRICE_NOT_FOUND: Price ID doesn't exist or invalid
Example Usage:
// Create subscription with promo code
POST /v1/store/subscriptions
{
"price": "price_1234567890",
"promotion_code": "promo_SUMMER2024",
"metadata": {
"action_type": "facebook_ads",
"utm_source": "dashboard"
}
}
Side Effects:
- โ ๏ธ Creates Stripe subscription
- โ ๏ธ Inserts MongoDB document in _store.subscriptions
- โ ๏ธ Updates account first-time flags
- โ ๏ธ Creates order record
- โ ๏ธ Triggers fulfillment queues
- โ ๏ธ May reassign account manager
- โ ๏ธ Creates onboarding pulse
getSubscriptions(req, res, next)โ
Purpose: Retrieves subscriptions with filtering, pagination, and website reactivation status enrichment.
Parameters:
req.query.page(Number) - Page number (1-indexed)req.query.limit(Number) - Items per pagereq.query.filter(String, base64) - Encoded filter objectreq.query.billing(Boolean) - Include billing detailsreq.query.filters(String) - Additional filter criteriareq.query.account(String) - Filter by account IDreq.query.status(String, CSV) - Filter by status (active, trialing, past_due, canceled, etc.)
Returns: Promise<Array>
- Array of subscription objects with populated price, product, and subscriber data
Business Logic Flow:
-
Build Query Options
- Exclude software subscriptions (
metadata.software != true) - Filter by account if provided
- Handle status filtering (if 'active' requested, include 'trialing')
- Apply custom filters if provided
- Exclude software subscriptions (
-
Account-Specific Filtering
- For main accounts buying for themselves: platform products only
- For sub-accounts: use parent's connected account products
- For main accounts with payments enabled: connected account products
-
Execute Query
- Populate price, product, and subscriber (account + business) relationships
- Sort by created date descending
-
Website Reactivation Status Check
- For canceled subscriptions with
action_type: 'site'or'instasite' - Check if website/instasite is NOT_PUBLISHED_YET or UNPUBLISHED
- Set
metadata.can_reactivate: trueif unpublished - Handles Mongoose 6.x nested metadata merging correctly
- For canceled subscriptions with
-
Phone Number Cost Calculation
- For phone_number subscriptions, fetch actual invoice amount_due
- Override calculated cost with actual invoice cost
-
Cost Calculation
- Calculate discount amount from coupon (amount_off or percent_off)
- Compute actual_cost: { subtotal, discount_amount, amount_due }
- Handle trialing status as 'active' for platform subscriptions
Key Business Rules:
- Software subscriptions excluded from standard lists
- Trialing subscriptions displayed as 'active' for platform products
- Website/Instasite reactivation only available if not published
- Phone number costs pulled from actual invoices, not price metadata
Example Usage:
// Get active subscriptions for account
GET /v1/store/subscriptions?account=60a7b8c9d0e1f2&status=active,past_due&limit=20&page=1
deleteSubscription(req, res, next)โ
Purpose: Cancels subscription(s) either immediately or at period end, with support for bundle cancellation and feedback collection.
Parameters:
req.params.id(String) - Subscription ID to cancelreq.query.end_of_cycle(Boolean) - If true, cancel at period end; if false, cancel immediatelyreq.body.reason(String[]) - Cancellation reasonsreq.body.feedback(String) - Detailed feedback (min 20 characters)
Returns: Promise<Array>
- Array of canceled subscription objects
Business Logic Flow:
-
Subscription Lookup
- Find subscription by ID
- Verify requester has permission (customer or account match)
- Error if subscription not found or access denied
-
Bundle Handling
- If subscription has
metadata.bundle_id, find all subscriptions in bundle - Cancel all bundle items together to maintain integrity
- If subscription has
-
Cancellation Type
- End of Cycle (
end_of_cycle=true):- Set
cancel_at_period_end: truein Stripe - For managed subscriptions, create cancellation pulse
- Create activity log for pending cancellation
- Pulse triggers pre-cancellation workflow
- Set
- Immediate:
- Delete subscription in Stripe (cancels immediately)
- For managed subscriptions, cancellation handled via webhook
- End of Cycle (
-
Database Update
- Mirror Stripe response to MongoDB
- Update all subscriptions in bundle if applicable
-
Feedback Collection
- Save cancellation feedback to
_store.subscription-feedback - Link to subscription, account, user, and price
- Include reason array and detailed feedback text
- Save cancellation feedback to
Key Business Rules:
- Bundle subscriptions must be canceled together
- End-of-cycle cancellations create warning pulses for managed services
- Immediate cancellations process instantly
- Feedback required for cancellation tracking
Error Handling:
RESOURCE_NOT_FOUND: Subscription doesn't existRESOURCE_ACCESS_DENIED: User doesn't have permission- Errors logged on pulse creation failure
Example Usage:
// Cancel at end of period
DELETE /v1/store/subscriptions/sub_123?end_of_cycle=true
{
"reason": ["too_expensive", "switching_to_competitor"],
"feedback": "Found a better price elsewhere with similar features."
}
retryPayment(req, res, next)โ
Purpose: Retries payment for past_due subscriptions with rate limiting (3 attempts per 24 hours).
Parameters:
req.params.id(String) - Subscription IDreq.body.card_id(String) - Payment method ID (pm_xxx or source ID)
Returns: Promise<Object>
- Updated subscription object with task completion status
Business Logic Flow:
-
Subscription & Invoice Lookup
- Find past_due subscription
- Find open invoice for subscription
- Error if no pending invoice found
-
Rate Limiting Check
- Count payment attempts in last 24 hours
- Max 3 attempts per 24-hour window
- Return 429 Too Many Requests if limit exceeded
-
Payment Retry
- Call Stripe
invoices.pay()with payment method - Supports both payment methods (pm_xxx) and legacy sources
- Update subscription status to 'active' if payment succeeds
- Error if payment fails
- Call Stripe
-
Task Completion
- Find existing past_due task for subscription
- Mark task as 'completed' if found
- Prevents duplicate notifications
-
Subscription Aggregation
- Fetch updated subscription with partner, buyer, and product details
- Use aggregation pipeline for enriched response
Key Business Rules:
- Maximum 3 retry attempts per 24 hours per subscription
- Only works for past_due subscriptions with open invoices
- Automatically marks related tasks as completed
- Supports both modern payment methods and legacy sources
Error Handling:
TOO_MANY_REQUESTS: Exceeded retry limitNO_PENDING_INVOICE: No open invoice to paySUBSCRIPTION_NOT_FOUND: Subscription doesn't exist or isn't past_duePAYMENT_FAILED: Stripe payment error
Example Usage:
// Retry payment with new card
POST /v1/store/subscriptions/sub_123/retry
{
"card_id": "pm_1234567890abcdef"
}
changeSubscription(req, res, next)โ
Purpose: Handles software subscription downgrades with resource cleanup queuing and delayed execution.
Parameters:
req.body.subscription_id(String) - Subscription to downgradereq.body.product_type(String) - Must be 'software'req.body.new_plan(Object) - New plan detailsnew_plan.price(String) - New price IDnew_plan.business(String) - Business contact IDnew_plan.display_name(String) - Plan tier name
req.body.software_app(Object) - Resources to deleteplatform.team_member_seats- Users to remove with assignmentsdeals.sales_pipelines- Pipeline IDs to deleteforms.form- Form IDs to deleteinbound.campaigns- Campaign IDs to deleteanalytics.seo_keywords- Keywords to removetemplates.custom_template- Template IDs to delete
req.body.reason(Array) - Downgrade reasonsreq.body.feedback(String) - Detailed feedback (min 20 characters)
Returns: Promise<Object>
- Updated subscription object
Business Logic Flow:
-
Validation
- Only supports 'software' product type
- Validate business exists and belongs to requester
- Validate new price exists and is software plan
- Ensure new price is different from current price
-
Downgrade vs Free Plan
- Downgrade to Paid Plan:
- Set
account.downgrade.pending: true - Set
account.downgrade.planto new tier - Schedule subscription change for period end
- Set
- Downgrade to Free Plan:
- Set
cancel_at_period_end: true - Schedule cancellation for period end
- Set
- Downgrade to Paid Plan:
-
Disable Downgrade Prompt
- Set
account.downgrade.prompt: false - Prevents showing downgrade UI again
- Allows re-entry if downgrade is later canceled
- Set
-
Resource Deletion Mapping
- Map user IDs to ObjectId with assignment tracking
- Map pipeline, form, campaign, template IDs to ObjectId
- Store SEO keyword objects as-is
- Save to feedback document for queue processing
-
Stripe Subscription Update
- For downgrades: update items with new price at period end
- For free: set cancel_at_period_end flag
- Use connected account if applicable
-
Database Persistence
- Save updated subscription to MongoDB
- Create/update subscription feedback with deletion data
- Queue downgrade job in
queuescollection
-
Queue Processing (executed by Queue Manager at period end)
- Delete specified users, pipelines, forms, campaigns
- Remove SEO keywords
- Delete custom templates
- Apply subscription change in Stripe
- Mark feedback as triggered
Key Business Rules:
- Only software subscriptions can be downgraded
- Resources marked for deletion are queued, not deleted immediately
- Downgrade executes at end of billing period
- Free plan downgrades result in cancellation
- Users cannot downgrade to same plan
- Downgrade disables prompt UI until canceled
Error Handling:
BUSINESS_NOT_FOUND: Business contact doesn't existPRICE_NOT_FOUND: Plan doesn't exist or isn't softwareSUBSCRIPTION_NOT_FOUND: Subscription doesn't exist or not activeRESOURCE_ACCESS_DENIED: User lacks permissionSAME_PLAN_ERROR: Attempting to downgrade to current planINVALID_PLAN_FOR_SUBSCRIPTION: Plan not valid for this product
Example Usage:
// Downgrade software plan
POST /v1/store/subscriptions/change
{
"subscription_id": "sub_123",
"product_type": "software",
"new_plan": {
"price": "price_789",
"business": "contact_456",
"display_name": "Pro"
},
"software_app": {
"platform": {
"team_member_seats": [
{
"user": "user_123",
"assigned_to": {
"contact": "contact_456",
"deals": "deal_789"
}
}
]
},
"deals": {
"sales_pipelines": ["pipeline_1", "pipeline_2"]
},
"forms": {
"form": ["form_1"]
},
"analytics": {
"seo_keywords": [
{"keyword": "keyword1", "country": "US", "near": "City"}
]
}
},
"reason": ["too_expensive"],
"feedback": "Need to reduce costs temporarily."
}
Side Effects:
- โ ๏ธ Updates Stripe subscription
- โ ๏ธ Modifies account downgrade state
- โ ๏ธ Creates feedback document
- โ ๏ธ Queues background job
- โ ๏ธ Deletes resources at period end (via queue)
cancelDeleteSubscription(req, res, next)โ
Purpose: Undoes an end-of-cycle cancellation (reactivates subscription).
Parameters:
req.params.id(String) - Subscription IDreq.query.seller_account_id(String, optional) - For admin accessreq.body.reason(Array, unused) - Reason for undoreq.body.feedback(String, unused) - Feedback
Returns: Promise<Array>
- Array of reactivated subscription objects
Business Logic Flow:
-
Permission Check
- If seller_account_id provided, verify admin/account manager role
- Lookup and validate seller account
-
Subscription Lookup
- Find subscription marked for cancellation
- Verify
cancel_at_period_end: trueand status is 'active' or 'trialing' - Error if not in pending cancellation state
-
Bundle Handling
- If subscription has bundle_id, find all bundle subscriptions
- Reactivate all bundle items together
-
Stripe Update
- Set
cancel_at_period_end: falsefor each subscription - Removes scheduled cancellation
- Set
-
Database Sync
- Update MongoDB with Stripe response
- Clears cancellation timestamp
Key Business Rules:
- Only works on subscriptions with
cancel_at_period_end: true - Bundle subscriptions reactivated together
- Admin/account manager can reactivate for any account
- Regular users can only reactivate their own subscriptions
Error Handling:
FORBIDDEN: User lacks required roleACCOUNT_NOT_FOUND: Seller account doesn't existRESOURCE_NOT_FOUND: Subscription doesn't existRESOURCE_ACCESS_DENIED: User lacks permissionNOT_CANCELLED: Subscription not pending cancellation
Example Usage:
// Undo cancellation
PUT /v1/store/subscriptions/sub_123/undo-cancellation
{
"reason": ["changed_mind"],
"feedback": "Decided to keep the service."
}
cancelDowngrade(req, res, next)โ
Purpose: Cancels a scheduled software downgrade and restores current plan.
Parameters:
req.params.id(String) - Subscription IDreq.body.price(String) - Current price ID (for validation)
Returns: Promise<Object>
- Updated subscription object
Business Logic Flow:
-
Subscription Lookup
- Find subscription by ID
- Validate price matches current plan
-
Price Validation
- Ensure provided price matches subscription's current price
- Prevents accidental cancellation of wrong downgrade
-
Stripe Update
- Set
cancel_at_period_end: false - Update items to keep current price
- Clears scheduled downgrade
- Set
-
Cleanup
- Delete subscription feedback document
- Delete queued downgrade job
- Remove
account.downgrade.planfield - Re-enables downgrade prompt
-
Database Sync
- Update MongoDB subscription with Stripe data
Key Business Rules:
- Price must match current subscription price
- Deletes feedback and queued job
- Re-enables downgrade UI prompt
- Restores subscription to active state
Error Handling:
SUBSCRIPTION_NOT_FOUND: Subscription doesn't existPRICE_NOT_FOUND: Plan doesn't existPRICE_MISMATCH: Provided price doesn't match current plan
Example Usage:
// Cancel scheduled downgrade
PUT /v1/store/subscriptions/sub_123/cancel-downgrade
{
"price": "price_current_plan"
}
stopNotification(req, res, next)โ
Purpose: Disables downgrade notification prompts for account.
Parameters:
- None (uses req.auth.account_id)
Returns: Promise<Object>
- Success response
Business Logic Flow:
-
Find Active Software Subscription
- Query for active/trialing software subscriptions
- Check both DashClicks and parent account pricing
-
Check for Past Subscriptions
- If no active, look for recently canceled (within 1 month)
- Include past_due and unpaid subscriptions
-
Update Subscription
- Set
show_notification: falseon found subscription - Prevents future downgrade prompts
- Set
Key Business Rules:
- Only affects software subscriptions
- Hides notification UI prompts
- Persists across sessions
Example Usage:
// Stop showing downgrade notifications
PUT / a / store / subscriptions / notification / stop;
๐ Integration Pointsโ
Internal Dependenciesโ
../../utilities/store-generateSubscriptionLookupOptions,newOrder../../utilities-generatePagination,sendSMS../../utilities/project-createActivity../../utilities/credits-tierOverride../../utilities/assigntoaccountsormanager-reassignUserWorkloadUtil
External Servicesโ
Stripe APIโ
- Operations: Create, update, delete subscriptions
- Error Handling: Network retries via
setMaxNetworkRetries(5) - Connected Accounts: Supports both platform and connected account subscriptions
Queue Managerโ
- Purpose: Process software downgrades at billing period end
- Trigger: Subscription feedback with deletion data queued
Notification Serviceโ
- Purpose: SMS alerts for critical errors
- Use Cases: Order generation failures, application fee issues
๐งช Edge Cases & Special Handlingโ
First-Time Subscriber Logicโ
Condition: Account has no prior active subscriptions
Handling: Set became_subscriber_on timestamp, track lifecycle milestone
Example: Used for analytics and customer success tracking
Application Fee Calculationโ
Condition: Connected account subscription with wholesale pricing Handling: Complex fee calculation including wholesale markup + platform percentages + fixed fee Example: Wholesale: $50, Platform%: 2%, Sub%: 1%, Fixed: $0.30 โ Fee: $1.80 (3.6%)
Bundle Cancellationโ
Condition: Subscription is part of a bundle
Handling: Find all bundle items via metadata.bundle_id, cancel together
Example: Website + Listings bundle canceled as atomic unit
Website Reactivationโ
Condition: Site/Instasite subscription canceled but site never published
Handling: Set metadata.can_reactivate: true to allow resubscription
Example: Customer can reactivate unpublished website subscription
Software Downgrade Queueโ
Condition: User downgrades software plan Handling: Changes scheduled for period end, resources deleted via queue Example: Pro โ Free: queues deletion of extra users, pipelines, forms
โ ๏ธ Important Notesโ
- ๐จ Subscription Creation: Always creates order and may trigger manager reassignment
- ๐จ Application Fees: Only apply to connected account subscriptions, calculated dynamically
- ๐จ Bundle Integrity: All bundle items must be canceled together
- ๐จ Payment Retry Limits: 3 attempts per 24 hours to prevent abuse
- ๐จ Software Downgrades: Execute at period end, not immediately
- ๐จ Loyalty Coupons: Auto-applied unless explicitly disabled via header
- ๐จ Webhook Dependencies: Many operations depend on webhook handlers for completion
๐ Related Documentationโ
- Cart Management - Subscription creation via checkout
- Order Management (link removed - file does not exist) - Fulfillment tracking for subscriptions
- Webhook Handling (link removed - file does not exist) - Subscription event processing
- Product Management (link removed - file does not exist) - Available subscription products
๐ Change Logโ
Last Updated: 2025-01-08
Major Updates:
- Mongoose 6.x nested metadata merging fix for website reactivation
- Enhanced first-time subscriber tracking
- Software downgrade queue system
- Payment retry rate limiting