Product Management
The Product Management submodule handles product catalog operations, pricing tier management, loyalty program adjustments, and platform-specific product filtering for the DashClicks Store platform.
API Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
GET | /v1/store/products | Get products with loyalty adjustments |
GET | /v1/store/products/:id | Get specific product details |
POST | /v1/store/products | Create custom product |
PUT | /v1/store/products/:id | Update product information |
DELETE | /v1/store/products/:id | Delete custom product |
GET | /v1/store/prices | Get price listings |
GET | /v1/store/prices/:id | Get specific price details |
POST | /v1/store/prices | Create new price tier |
PUT | /v1/store/prices/:id | Update price information |
DELETE | /v1/store/prices/:id | Deactivate price |
MongoDB Collections Used
Primary Collections
_store.products- Product catalog with metadata_store.prices- Pricing tiers linked to products_accounts- Account settings and loyalty program_loyalty.program.tiers- Loyalty tier configurationsstripe.keys- Connected account Stripe keys
Related Collections
_store.subscriptions- Active subscriptions using products_store.cart- Cart items referencing prices_store.orders- Order records with product metadata
Core Product Workflows
Product Creation Flow
graph TD
A[Create Product Request] --> B{Account Type}
B -->|Platform| C[Error: Platform products managed by admin]
B -->|Connected| D[Validate payments_enabled flag]
D --> E{Payments Enabled?}
E -->|No| F[Error: Stripe not connected]
E -->|Yes| G[Create in Stripe]
G --> H[Save to MongoDB]
H --> I[Set platform_type: custom]
I --> J[Return Created Product]
Product Retrieval with Loyalty Flow
graph TD
A[Get Products Request] --> B[Filter by platform_type]
B --> C[Filter by pricing_type]
C --> D{Has Loyalty Program?}
D -->|No| E[Return Standard Prices]
D -->|Yes| F[Calculate Loyalty Discount]
F --> G[Apply to unit_amount]
G --> H[Apply to setup_fee]
H --> I[Add loyalty_unit_amount field]
I --> J[Return Products with Discounts]
Service Methods & Functionality
Product Operations
getProducts(req, res, next) - Retrieve product catalog
-
Purpose: Get products with loyalty pricing adjustments and filtering
-
Query Parameters:
type(String): Product category filter'store': Physical/service products with offers and bundles'software': Software subscription products'manage': Managed service products
platform_type(String): Platform filter'dashclicks': Platform products'default': Default white-label products'custom': Custom account products
pricing_type(String): Pricing tier filter'partner': Wholesale pricing for resellers'standard': Retail pricing for end customers
page(Number): Page number (default: 1)limit(Number): Items per page (default: 20)
-
Business Logic:
-
Account Context Resolution:
const isMainAccount = req.auth.account.main;
const targetAccountId = isMainAccount ? req.auth.account._id : req.auth.account.parent_account; -
Platform Type Filtering:
const platformFilter = {
$or: [
{ platform_type: 'dashclicks' }, // Platform products
{ platform_type: 'default' }, // Default white-label
{
platform_type: 'custom', // Custom products
connected_account: targetAccountId,
},
],
}; -
Pricing Type Filtering:
- Main accounts: Show both partner and standard pricing
- Sub-accounts: Show only pricing_type specified by parent
- Filter applied at price level, not product level
-
Price Aggregation:
await Product.aggregate([
{ $match: platformFilter },
{
$lookup: {
from: '_store.prices',
localField: '_id',
foreignField: 'product',
as: 'prices',
},
},
{
$addFields: {
prices: {
$filter: {
input: '$prices',
as: 'price',
cond: {
$and: [
{ $eq: ['$$price.active', true] },
{ $eq: ['$$price.pricing_type', pricingType] },
],
},
},
},
},
},
]); -
Loyalty Program Application:
if (account.loyalty_program?.enabled) {
const tier = await LoyaltyTier.findById(account.loyalty_program.current_tier);
product.prices = product.prices.map(price => {
price.loyalty_unit_amount = Math.floor(price.unit_amount * (1 - tier.discount / 100));
price.loyalty_setup_fee = price.setup_fee
? Math.floor(price.setup_fee * (1 - tier.discount / 100))
: 0;
price.loyalty_discount_percentage = tier.discount;
return price;
});
} -
Price Sorting:
- Primary:
nicknamealphabetically - Secondary:
recurring.interval_countascending - Ensures consistent display order
- Primary:
-
Type-Specific Enrichment:
-
Store Type: Includes active offers and bundles
product.offers = await Offer.find({
product: product._id,
active: true,
start_date: { $lte: new Date() },
end_date: { $gte: new Date() },
});
product.bundles = await Bundle.find({
products: product._id,
active: true,
});
-
-
-
Returns: Products array with loyalty-adjusted pricing
[
{
_id: 'prod_xxx',
name: 'Content Services',
description: 'Professional content creation',
type: 'store',
platform_type: 'dashclicks',
active: true,
metadata: {...},
prices: [
{
_id: 'price_xxx',
nickname: 'Monthly - 5 Articles',
unit_amount: 29900,
loyalty_unit_amount: 26910, // 10% discount
loyalty_discount_percentage: 10,
setup_fee: 9900,
loyalty_setup_fee: 8910,
recurring: {
interval: 'month',
interval_count: 1
},
pricing_type: 'standard'
}
],
offers: [...],
bundles: [...]
}
]
getProduct(req, res, next) - Retrieve single product
- Purpose: Get specific product with full price details
- Parameters: Product ID
- Business Logic:
- Same filtering logic as
getProducts - Account-aware: Main accounts see all, sub-accounts filtered by parent
- Applies loyalty pricing if applicable
- Filters prices by active status and pricing_type
- Validates version compatibility if specified
- Same filtering logic as
- Returns: Single product object with populated prices
newProduct(req, res, next) - Create custom product
-
Purpose: Create product in Stripe and MongoDB for connected accounts
-
Parameters:
name(String, required): Product name (max 250 chars)description(String, optional): Product descriptiontype(String, required): 'store' | 'software' | 'manage'metadata(Object, optional): Additional metadatareference_product(ObjectId, optional): Link to platform product for white-labeling
-
Validation:
// Joi schema
{
name: Joi.string().max(250).required(),
description: Joi.string().optional(),
type: Joi.string().valid('store', 'software', 'manage').required(),
metadata: Joi.object().optional(),
reference_product: Joi.objectId().optional()
} -
Business Logic:
-
Permissions Check:
const account = await Account.findById(req.auth.account._id);
if (!account.payments_enabled) {
throw new ApiError(403, 'Stripe account not connected');
} -
Stripe Product Creation:
const stripeKeys = await StripeKey.findOne({
account_id: account.parent_account || account._id,
});
const stripe = Stripe(stripeKeys.token.access_token);
const stripeProduct = await stripe.products.create({
name: req.body.name,
description: req.body.description,
type: 'service',
metadata: req.body.metadata,
}); -
MongoDB Product Creation:
const product = await Product.create({
stripe_id: stripeProduct.id,
name: req.body.name,
description: req.body.description,
type: req.body.type,
platform_type: 'custom',
connected_account: account.parent_account || account._id,
reference_product: req.body.reference_product,
active: true,
metadata: req.body.metadata,
created_at: new Date(),
});
-
-
Returns: Created product object
updateProduct(req, res, next) - Update product
-
Purpose: Update product metadata and details
-
Parameters: Product ID + update fields
name(String, optional)description(String, optional)metadata(Object, optional)active(Boolean, optional)reference_product(ObjectId, optional)
-
Validation:
- Product must belong to account (custom products only)
- If
reference_productprovided, validates it exists and is active
-
Business Logic:
-
Ownership Validation:
const product = await Product.findOne({
_id: productId,
platform_type: 'custom',
connected_account: account.parent_account || account._id,
});
if (!product) {
throw new ApiError(404, 'Product not found or unauthorized');
} -
Reference Product Validation:
if (req.body.reference_product) {
const refProduct = await Product.findOne({
_id: req.body.reference_product,
active: true,
platform_type: { $in: ['dashclicks', 'default'] },
});
if (!refProduct) {
throw new ApiError(400, 'Invalid reference product');
}
} -
Stripe Update:
await stripe.products.update(product.stripe_id, {
name: req.body.name,
description: req.body.description,
metadata: req.body.metadata,
active: req.body.active,
}); -
MongoDB Sync:
Object.assign(product, req.body);
await product.save();
-
-
Returns: Updated product
deleteProduct(req, res, next) - Delete custom product
-
Purpose: Remove custom product and associated prices
-
Parameters: Product ID
-
Business Logic:
-
Ownership Validation: Ensures custom product belongs to account
-
Active Subscription Check:
const activeSubscriptions = await Subscription.countDocuments({
product: productId,
status: { $in: ['active', 'trialing', 'past_due'] },
});
if (activeSubscriptions > 0) {
throw new ApiError(409, 'Cannot delete product with active subscriptions');
} -
Stripe Deletion (with fallback):
try {
await stripe.products.del(product.stripe_id);
} catch (err) {
// If deletion fails, archive instead
await stripe.products.update(product.stripe_id, {
active: false,
});
} -
Price Deletion:
const prices = await Price.find({ product: productId });
for (const price of prices) {
await stripe.prices.update(price.stripe_id, {
active: false,
});
await price.remove();
} -
Product Removal:
await product.remove();
-
-
Returns: Deletion confirmation
Price Operations
newPrice(req, res, next) - Create price tier
-
Purpose: Create new price for product
-
Parameters:
product(ObjectId, required): Product IDunit_amount(Number, required): Price in cents (min 50)nickname(String, required): Display nametype(String, required): 'one-time' | 'recurring'recurring(Object, required if type='recurring'):interval(String): 'day' | 'week' | 'month' | 'year'interval_count(Number): Billing frequency (e.g., 3 for quarterly)
pricing_type(String, required): 'partner' | 'standard'setup_fee(Number, optional): One-time setup fee in centsmetadata(Object, optional): Additional data
-
Validation:
{
product: Joi.objectId().required(),
unit_amount: Joi.number().integer().min(50).required(),
nickname: Joi.string().max(100).required(),
type: Joi.string().valid('one-time', 'recurring').required(),
recurring: Joi.when('type', {
is: 'recurring',
then: Joi.object({
interval: Joi.string()
.valid('day', 'week', 'month', 'year')
.required(),
interval_count: Joi.number().integer().min(1).required()
}).required()
}),
pricing_type: Joi.string().valid('partner', 'standard').required(),
setup_fee: Joi.number().integer().min(0).optional(),
metadata: Joi.object().optional()
} -
Business Logic:
-
Product Validation:
const product = await Product.findOne({
_id: req.body.product,
platform_type: 'custom',
connected_account: account.parent_account || account._id,
});
if (!product) {
throw new ApiError(404, 'Product not found');
} -
Stripe Price Creation:
const stripePrice = await stripe.prices.create({
product: product.stripe_id,
unit_amount: req.body.unit_amount,
currency: 'usd',
nickname: req.body.nickname,
recurring: req.body.type === 'recurring' ? req.body.recurring : undefined,
metadata: req.body.metadata,
}); -
MongoDB Price Creation:
const price = await Price.create({
stripe_id: stripePrice.id,
product: product._id,
unit_amount: req.body.unit_amount,
nickname: req.body.nickname,
type: req.body.type,
recurring: req.body.recurring,
pricing_type: req.body.pricing_type,
setup_fee: req.body.setup_fee || 0,
active: true,
metadata: req.body.metadata,
additional_info: {
wholesale_unit_amount: req.body.unit_amount, // For app fee calculation
},
}); -
Product Update (link price):
await Product.updateOne({ _id: product._id }, { $addToSet: { prices: price._id } });
-
-
Returns: Created price object
updatePrice(req, res, next) - Update price
-
Purpose: Update price metadata (amount cannot change)
-
Parameters:
nickname(String, optional)active(Boolean, optional)metadata(Object, optional)
-
Note: Stripe does not allow changing
unit_amounton existing prices. To change price, create new price and archive old one. -
Business Logic:
- Updates nickname and metadata in Stripe
- Syncs active status
- Updates MongoDB record
-
Returns: Updated price
deletePrice(req, res, next) - Deactivate price
-
Purpose: Soft delete price (set active=false)
-
Parameters: Price ID
-
Business Logic:
-
Active Subscription Check:
const activeCount = await Subscription.countDocuments({
'items.price': priceId,
status: { $in: ['active', 'trialing'] },
});
if (activeCount > 0) {
throw new ApiError(409, 'Cannot delete price with active subscriptions');
} -
Stripe Deactivation:
await stripe.prices.update(price.stripe_id, {
active: false,
}); -
MongoDB Update:
price.active = false;
await price.save(); -
Product Unlink:
await Product.updateOne({ _id: price.product }, { $pull: { prices: price._id } });
-
-
Returns: Deletion confirmation
Technical Implementation Details
Platform Type Hierarchy
Three-Tier Product System:
-
DashClicks Platform Products (
platform_type: 'dashclicks'):- Core products created by platform
- Available to all accounts
- Cannot be modified by accounts
- Examples: Content Services, Website Packages, Ads Management
-
Default White-Label Products (
platform_type: 'default'):- Template products for resellers
- Customizable by connected accounts
- Serve as base for custom products
- Referenced via
reference_productfield
-
Custom Account Products (
platform_type: 'custom'):- Created by connected accounts
- Only visible to creating account and sub-accounts
- Full customization control
- Require
payments_enabled: true
Pricing Type System
Two-Tier Pricing:
{
pricing_type: 'partner', // Wholesale pricing
unit_amount: 19900,
additional_info: {
wholesale_unit_amount: 15000 // Cost basis for app fees
}
}
{
pricing_type: 'standard', // Retail pricing
unit_amount: 29900,
additional_info: {
wholesale_unit_amount: 19900 // Partner price as cost
}
}
Application Fee Calculation (for connected accounts):
// Extract wholesale amount from prices
const wholesaleTotal = prices.reduce(
(sum, price) => sum + (price.additional_info?.wholesale_unit_amount || 0),
0,
);
// Calculate percentage fees
const percentageFee =
prices.reduce((sum, price) => sum + price.unit_amount, 0) *
(parseFloat(process.env.ADDITIONAL_APP_FEE_PERCENTAGE) +
parseFloat(process.env.ADDITIONAL_APP_FEE_SUBSCRIPTION_PERCENTAGE));
// Total application fee
const application_fee_amount = wholesaleTotal + Math.floor(percentageFee);
Loyalty Program Integration
Discount Calculation:
function applyLoyaltyDiscount(price, loyaltyTier) {
if (!loyaltyTier || !loyaltyTier.discount) {
return price;
}
return {
...price,
loyalty_unit_amount: Math.floor(price.unit_amount * (1 - loyaltyTier.discount / 100)),
loyalty_setup_fee: price.setup_fee
? Math.floor(price.setup_fee * (1 - loyaltyTier.discount / 100))
: 0,
loyalty_discount_percentage: loyaltyTier.discount,
loyalty_savings: Math.floor(price.unit_amount * (loyaltyTier.discount / 100)),
};
}
Tier Progression:
// Example loyalty tiers
[
{ name: 'Bronze', discount: 5, threshold: 0 },
{ name: 'Silver', discount: 10, threshold: 5000 },
{ name: 'Gold', discount: 15, threshold: 15000 },
{ name: 'Platinum', discount: 20, threshold: 50000 },
];
Bundle and Offer Integration
Bundle Structure:
{
_id: 'bundle_xxx',
name: 'Digital Marketing Starter Pack',
description: 'Website + SEO + Content',
products: [
'product_website_id',
'product_seo_id',
'product_content_id'
],
discount_type: 'percentage', // or 'fixed_amount'
discount_value: 15, // 15% off or $15 off
active: true,
platform_type: 'dashclicks'
}
Offer Structure:
{
_id: 'offer_xxx',
name: 'Holiday Sale - 20% Off',
product: 'product_xxx',
discount_type: 'percentage',
discount_value: 20,
start_date: '2025-12-01',
end_date: '2025-12-31',
active: true,
terms: 'First 3 months only'
}
Version Management
Product Versioning:
Products can have multiple versions for API compatibility:
{
version: 'v2',
previous_versions: ['v1'],
deprecation_date: '2026-01-01',
migration_guide: 'https://...'
}
Error Handling
Common Error Codes
- 400 Bad Request: Invalid price amount, missing required fields
- 403 Forbidden: Stripe not connected, insufficient permissions
- 404 Not Found: Product or price not found
- 409 Conflict: Cannot delete product/price with active subscriptions
- 422 Unprocessable: Invalid reference product, incompatible settings
- 500 Server Error: Stripe API errors, database failures
Performance Considerations
Optimization Strategies
- Price Filtering at Database Level: Reduces data transfer
- Loyalty Calculation in Application: Avoid complex DB queries
- Product Caching: Cache platform products (updated rarely)
- Indexed Queries: Indexes on
platform_type,active,connected_account
Database Indexes
// Products collection
{
platform_type: 1,
active: 1,
type: 1
}
{
connected_account: 1,
active: 1
}
// Prices collection
{
product: 1,
active: 1,
pricing_type: 1
}
{
stripe_id: 1
}
Related Documentation
- Subscription Management - Using products in subscriptions
- Cart Management (link removed - file does not exist) - Product selection and checkout
- Store Module Overview - Architecture and configuration