Skip to main content

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

MethodEndpointDescription
GET/v1/store/productsGet products with loyalty adjustments
GET/v1/store/products/:idGet specific product details
POST/v1/store/productsCreate custom product
PUT/v1/store/products/:idUpdate product information
DELETE/v1/store/products/:idDelete custom product
GET/v1/store/pricesGet price listings
GET/v1/store/prices/:idGet specific price details
POST/v1/store/pricesCreate new price tier
PUT/v1/store/prices/:idUpdate price information
DELETE/v1/store/prices/:idDeactivate 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 configurations
  • stripe.keys - Connected account Stripe keys
  • _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:

    1. Account Context Resolution:

      const isMainAccount = req.auth.account.main;
      const targetAccountId = isMainAccount ? req.auth.account._id : req.auth.account.parent_account;
    2. 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,
      },
      ],
      };
    3. 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
    4. 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] },
      ],
      },
      },
      },
      },
      },
      ]);
    5. 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;
      });
      }
    6. Price Sorting:

      • Primary: nickname alphabetically
      • Secondary: recurring.interval_count ascending
      • Ensures consistent display order
    7. 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
  • 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 description
    • type (String, required): 'store' | 'software' | 'manage'
    • metadata (Object, optional): Additional metadata
    • reference_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:

    1. Permissions Check:

      const account = await Account.findById(req.auth.account._id);
      if (!account.payments_enabled) {
      throw new ApiError(403, 'Stripe account not connected');
      }
    2. 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,
      });
    3. 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_product provided, validates it exists and is active
  • Business Logic:

    1. 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');
      }
    2. 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');
      }
      }
    3. 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,
      });
    4. 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:

    1. Ownership Validation: Ensures custom product belongs to account

    2. 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');
      }
    3. 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,
      });
      }
    4. 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();
      }
    5. 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 ID
    • unit_amount (Number, required): Price in cents (min 50)
    • nickname (String, required): Display name
    • type (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 cents
    • metadata (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:

    1. 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');
      }
    2. 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,
      });
    3. 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
      },
      });
    4. 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_amount on existing prices. To change price, create new price and archive old one.

  • Business Logic:

    1. Updates nickname and metadata in Stripe
    2. Syncs active status
    3. Updates MongoDB record
  • Returns: Updated price

deletePrice(req, res, next) - Deactivate price

  • Purpose: Soft delete price (set active=false)

  • Parameters: Price ID

  • Business Logic:

    1. 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');
      }
    2. Stripe Deactivation:

      await stripe.prices.update(price.stripe_id, {
      active: false,
      });
    3. MongoDB Update:

      price.active = false;
      await price.save();
    4. 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:

  1. 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
  2. Default White-Label Products (platform_type: 'default'):

    • Template products for resellers
    • Customizable by connected accounts
    • Serve as base for custom products
    • Referenced via reference_product field
  3. 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

  1. Price Filtering at Database Level: Reduces data transfer
  2. Loyalty Calculation in Application: Avoid complex DB queries
  3. Product Caching: Cache platform products (updated rarely)
  4. 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
}
💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM