JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 218
  • Score
    100M100P100Q88702F
  • License MIT

Enterprise revenue management system with subscriptions, purchases, proration, payment processing, escrow, and multi-party splits

Package Exports

  • @classytic/revenue
  • @classytic/revenue/enums
  • @classytic/revenue/schemas
  • @classytic/revenue/utils

Readme

@classytic/revenue

Enterprise revenue management with subscriptions and payment processing

Thin, focused, production-ready library with smart defaults. Built for SaaS, marketplaces, and subscription businesses.

Features

  • Subscriptions: Create, renew, pause, cancel with lifecycle management
  • Payment Processing: Multi-gateway support (Stripe, SSLCommerz, manual, etc.)
  • Transaction Management: Income/expense tracking with verification and refunds
  • Escrow & Hold/Release: Platform-as-intermediary payment flow (NEW in v0.1.0)
  • Multi-Party Splits: Distribute revenue to platform, affiliates, partners (NEW)
  • Affiliate Commissions: Built-in support for referral/affiliate programs (NEW)
  • Commission Tracking: Automatic platform commission calculation with gateway fee deduction
  • Provider Pattern: Pluggable payment providers (like LangChain/Vercel AI SDK)
  • Framework Agnostic: Works with Express, Fastify, Next.js, or standalone
  • TypeScript Ready: Full type definitions included

Installation

npm install @classytic/revenue
npm install @classytic/revenue-manual  # For manual payments

Quick Start

Single-Tenant (Simple SaaS)

import { createRevenue } from '@classytic/revenue';
import { ManualProvider } from '@classytic/revenue-manual';

const revenue = createRevenue({
  models: { Transaction },
  providers: { manual: new ManualProvider() },
});

// Create subscription (no organizationId needed)
const { transaction } = await revenue.monetization.create({
  data: { customerId: user._id },
  planKey: 'monthly',
  monetizationType: 'subscription',
  amount: 2999,  // $29.99
  gateway: 'manual',
  paymentData: { method: 'card' },
});

// Verify → Refund
await revenue.payments.verify(transaction.gateway.paymentIntentId);

Multi-Tenant (Marketplace/Platform)

// Same API, just pass organizationId
const { transaction } = await revenue.monetization.create({
  data: {
    organizationId: vendor._id,  // ← Multi-tenant
    customerId: customer._id,
    referenceId: order._id,
    referenceModel: 'Order',
  },
  planKey: 'one_time',
  monetizationType: 'purchase',  // One-time purchase
  amount: 1500,
  gateway: 'manual',
  paymentData: { method: 'bkash' },
});

Works for both! Same API, different use cases.

Transaction Model Setup

The library requires a Transaction model with specific fields and provides reusable schemas:

import mongoose from 'mongoose';
import {
  TRANSACTION_TYPE_VALUES,
  TRANSACTION_STATUS_VALUES,
} from '@classytic/revenue/enums';
import {
  gatewaySchema,
  paymentDetailsSchema,
} from '@classytic/revenue/schemas';

const transactionSchema = new mongoose.Schema({
  // ============ REQUIRED BY LIBRARY ============
  amount: { type: Number, required: true, min: 0 },
  type: { type: String, enum: TRANSACTION_TYPE_VALUES, required: true },  // 'income' | 'expense'
  method: { type: String, required: true },  // 'manual' | 'bkash' | 'card' | etc.
  status: { type: String, enum: TRANSACTION_STATUS_VALUES, required: true },
  category: { type: String, required: true },  // Your custom categories
  
  // ============ MULTI-TENANT (optional) ============
  organizationId: { type: String, index: true },  // For multi-tenant platforms

  // ============ LIBRARY SCHEMAS (nested) ============
  gateway: gatewaySchema,              // Payment gateway details
  paymentDetails: paymentDetailsSchema, // Payment info (wallet, bank, etc.)

  // ============ POLYMORPHIC REFERENCE (recommended) ============
  // Links transaction to any entity (Order, Subscription, Enrollment, etc.)
  referenceId: {
    type: mongoose.Schema.Types.ObjectId,
    refPath: 'referenceModel',
  },
  referenceModel: {
    type: String,
    enum: ['Subscription', 'Order', 'Enrollment', 'Membership'], // Your models
  },

  // ============ YOUR CUSTOM FIELDS ============
  customerId: String,
  currency: { type: String, default: 'BDT' },
  verifiedAt: Date,
  verifiedBy: mongoose.Schema.Types.ObjectId,
  refundedAmount: Number,
  idempotencyKey: { type: String, unique: true, sparse: true },
  metadata: mongoose.Schema.Types.Mixed,
}, { timestamps: true });

export default mongoose.model('Transaction', transactionSchema);

Available Schemas

Schema Purpose Key Fields
gatewaySchema Payment gateway integration type, paymentIntentId, sessionId
paymentDetailsSchema Payment method info walletNumber, trxId, bankName
commissionSchema Commission tracking (marketplace) rate, grossAmount, gatewayFeeAmount, netAmount
currentPaymentSchema Latest payment (for Order/Subscription models) transactionId, status, verifiedAt
subscriptionInfoSchema Subscription details (for Order models) planKey, startDate, endDate

Usage: Import and use as nested objects (NOT spread):

import { gatewaySchema } from '@classytic/revenue/schemas';

const schema = new mongoose.Schema({
  gateway: gatewaySchema,  // ✅ Correct - nested
  // ...gatewaySchema,     // ❌ Wrong - don't spread
});

Core API

Subscriptions

// Create subscription
const { subscription, transaction, paymentIntent } = 
  await revenue.monetization.create({
    data: { organizationId, customerId },
    planKey: 'monthly',
    amount: 1500,
    currency: 'BDT',
    gateway: 'manual',
    paymentData: { method: 'bkash', walletNumber: '01712345678' },
  });

// Verify and activate
await revenue.payments.verify(transaction.gateway.paymentIntentId);
await revenue.monetization.activate(subscription._id);

// Renew subscription
await revenue.monetization.renew(subscription._id, {
  gateway: 'manual',
  paymentData: { method: 'nagad' },
});

// Pause/Resume
await revenue.monetization.pause(subscription._id, { reason: 'Customer request' });
await revenue.monetization.resume(subscription._id, { extendPeriod: true });

// Cancel
await revenue.monetization.cancel(subscription._id, { immediate: true });

Payments

// Verify payment (admin approval for manual)
const { transaction } = await revenue.payments.verify(paymentIntentId, {
  verifiedBy: adminUserId,
});

// Get payment status
const { status } = await revenue.payments.getStatus(paymentIntentId);

// Refund (creates separate EXPENSE transaction)
const { transaction, refundTransaction } = await revenue.payments.refund(
  transactionId,
  500,  // Amount or null for full refund
  { reason: 'Customer requested' }
);

// Handle webhook (for automated providers like Stripe)
const { event, transaction } = await revenue.payments.handleWebhook(
  'stripe',
  webhookPayload,
  headers
);

Transactions

// Get transaction by ID
const transaction = await revenue.transactions.get(transactionId);

// List with filters
const { transactions, total } = await revenue.transactions.list(
  { type: 'income', status: 'verified' },
  { limit: 50, sort: { createdAt: -1 } }
);

// Calculate net revenue
const income = await revenue.transactions.list({ type: 'income' });
const expense = await revenue.transactions.list({ type: 'expense' });
const netRevenue = income.total - expense.total;

Transaction Types (Income vs Expense)

The library uses double-entry accounting:

  • INCOME ('income'): Money coming in - payments, subscriptions
  • EXPENSE ('expense'): Money going out - refunds, payouts
const revenue = createRevenue({
  models: { Transaction },
  config: {
    transactionTypeMapping: {
      subscription: 'income',
      purchase: 'income',
      refund: 'expense',  // Refunds create separate expense transactions
    },
  },
});

Refund Pattern:

  • Refund creates NEW transaction with type: 'expense'
  • Original transaction status becomes 'refunded' or 'partially_refunded'
  • Both linked via metadata for audit trail
  • Calculate net: SUM(income) - SUM(expense)

Custom Categories

Map logical entities to transaction categories:

const revenue = createRevenue({
  models: { Transaction },
  config: {
    categoryMappings: {
      Order: 'order_subscription',
      PlatformSubscription: 'platform_subscription',
      Membership: 'gym_membership',
      Enrollment: 'course_enrollment',
    },
  },
});

// Usage
await revenue.monetization.create({
  entity: 'Order',  // Maps to 'order_subscription' category
  monetizationType: 'subscription',
  // ...
});

Note: entity is a logical identifier (not a database model name) for organizing your business logic.

Commission Tracking (Marketplace)

Automatically calculate platform commission with gateway fee deduction:

const revenue = createRevenue({
  models: { Transaction },
  config: {
    // Commission rates by category
    commissionRates: {
      'product_order': 0.10,     // 10% platform commission
      'course_enrollment': 0.10, // 10% on courses
      'gym_membership': 0,       // No commission
    },
    
    // Gateway fees (deducted from commission)
    gatewayFeeRates: {
      'stripe': 0.029,    // 2.9% Stripe fee
      'bkash': 0.018,     // 1.8% bKash fee
      'manual': 0,        // No fee
    },
  },
});

// Commission calculated automatically
const { transaction } = await revenue.monetization.create({
  amount: 10000, // $100
  entity: 'ProductOrder',  // → 10% commission
  gateway: 'stripe',       // → 2.9% fee
});

console.log(transaction.commission);
// {
//   rate: 0.10,
//   grossAmount: 1000,      // $10 (10% of $100)
//   gatewayFeeAmount: 290,  // $2.90 (2.9% of $100)
//   netAmount: 710,         // $7.10 (platform keeps)
//   status: 'pending'
// }

// Query pending commissions
const pending = await Transaction.find({ 'commission.status': 'pending' });

Refund handling: Commission automatically reversed proportionally when refunds are processed.

See: examples/commission-tracking.js for complete guide.

Subscription Utilities

Universal helpers for period calculation, proration, and action eligibility:

import {
  // Period calculation
  calculatePeriodRange,
  calculateProratedAmount,
  addDuration,
  
  // Action eligibility
  canRenewSubscription,
  canPauseSubscription,
  isSubscriptionActive,
} from '@classytic/revenue/utils';

// Calculate period
const { startDate, endDate } = calculatePeriodRange({
  duration: 30,
  unit: 'days',
});

// Calculate prorated refund
const refund = calculateProratedAmount({
  amountPaid: 1500,
  startDate: sub.startDate,
  endDate: sub.endDate,
  asOfDate: new Date(),
});

// Check eligibility
if (canRenewSubscription(membership)) {
  await revenue.monetization.renew(membership.subscriptionId);
}

Escrow & Multi-Party Splits (v0.1.0+)

NEW: Platform-as-intermediary payment flow for marketplaces, group buy, and affiliate systems.

Basic Escrow Flow

// 1. Customer makes purchase
const { transaction } = await revenue.monetization.create({
  amount: 1000,
  gateway: 'stripe',
  // ...
});

// 2. Verify payment
await revenue.payments.verify(transaction._id);

// 3. Hold in escrow
await revenue.escrow.hold(transaction._id);

// 4. Split to multiple recipients
await revenue.escrow.split(transaction._id, [
  { type: 'platform_commission', recipientId: 'platform', rate: 0.10 },
  { type: 'affiliate_commission', recipientId: 'affiliate-123', rate: 0.05 },
]);
// Auto-releases remainder to organization (85%)

// Or manually release
await revenue.escrow.release(transaction._id, {
  recipientId: 'org-123',
  recipientType: 'organization',
});

Affiliate Commission (Simplified API)

import { calculateCommissionWithSplits } from '@classytic/revenue';

const commission = calculateCommissionWithSplits(
  5000,    // amount
  0.10,    // platform rate
  0.029,   // gateway fee
  {
    affiliateRate: 0.05,
    affiliateId: 'affiliate-123',
  }
);

// Returns:
// {
//   grossAmount: 500,  // Platform: 10%
//   netAmount: 355,    // After gateway fee (2.9%)
//   affiliate: {
//     grossAmount: 250,  // Affiliate: 5%
//     netAmount: 250,
//   },
//   splits: [...]
// }

Multi-Party Splits

import { calculateSplits } from '@classytic/revenue';

const splits = calculateSplits(10000, [
  { type: 'platform_commission', recipientId: 'platform', rate: 0.10 },
  { type: 'affiliate_commission', recipientId: 'level1', rate: 0.05 },
  { type: 'affiliate_commission', recipientId: 'level2', rate: 0.02 },
  { type: 'partner_commission', recipientId: 'partner', rate: 0.03 },
], 0.029);  // Gateway fee

// Returns splits array with calculated amounts
// Organization receives: 8000 (80%)

Schemas for Escrow

Add to your transaction model when using escrow:

import { holdSchema, splitsSchema } from '@classytic/revenue';

TransactionSchema.add(holdSchema);    // Adds hold/release tracking
TransactionSchema.add(splitsSchema);  // Adds multi-party splits

Use Cases:

  • E-commerce marketplaces (hold until delivery confirmed)
  • Course platforms with affiliates
  • Group buy / crowdfunding
  • Multi-level marketing
  • SaaS reseller programs

See: ESCROW_FEATURES.md and examples/escrow-flow.js

Polymorphic References

Link transactions to any entity (Order, Subscription, Enrollment):

// Create transaction linked to Order
const { transaction } = await revenue.monetization.create({
  data: {
    organizationId,
    customerId,
    referenceId: order._id,      // ⭐ Direct field (not metadata)
    referenceModel: 'Order',     // ⭐ Model name
  },
  amount: 1500,
  // ...
});

// Query all transactions for an order
const orderTransactions = await Transaction.find({
  referenceModel: 'Order',
  referenceId: order._id,
});

// Use Mongoose populate
const transactions = await Transaction.find({ ... })
  .populate('referenceId');  // Populates based on referenceModel

Why top-level? Enables proper Mongoose queries and population. Storing in metadata prevents querying and indexing.

Hooks

const revenue = createRevenue({
  models: { Transaction },
  hooks: {
    // Monetization lifecycle (specific)
    'purchase.created': async ({ transaction, isFree }) => {
      console.log('One-time purchase:', transaction._id);
    },
    'subscription.created': async ({ transaction, isFree }) => {
      console.log('Recurring subscription:', transaction._id);
    },
    'free.created': async ({ transaction }) => {
      console.log('Free access granted:', transaction._id);
    },
    
    // Generic event (fires for all types)
    'monetization.created': async ({ transaction, monetizationType }) => {
      console.log(`${monetizationType} created:`, transaction._id);
    },

    // Payment lifecycle
    'payment.verified': async ({ transaction }) => {
      // Send confirmation email
    },
    'payment.failed': async ({ transaction, error, provider }) => {
      // Alert admin or send customer notification
      console.error('Payment failed:', error);
    },
    'payment.refunded': async ({ refundTransaction }) => {
      // Process refund notification
    },
    
    // Subscription management (requires Subscription model)
    'subscription.activated': async ({ subscription }) => {
      // Subscription activated after payment
    },
    'subscription.renewed': async ({ subscription, renewalCount }) => {
      // Subscription renewed
    },
    'subscription.paused': async ({ subscription }) => {
      // Subscription paused
    },
    'subscription.resumed': async ({ subscription }) => {
      // Subscription resumed
    },
    'subscription.cancelled': async ({ subscription }) => {
      // Subscription cancelled
    },
  },
});

Available hooks:

Monetization Events (specific):

  • purchase.created - One-time purchase
  • subscription.created - Recurring subscription
  • free.created - Free access granted
  • monetization.created - Generic event (fires for all types)

Payment Events:

  • payment.verified - Payment confirmed
  • payment.failed - Payment verification failed
  • payment.refunded - Refund processed
  • payment.webhook.{type} - Webhook events from providers

Subscription Management Events (requires Subscription model):

  • subscription.activated, subscription.renewed
  • subscription.paused, subscription.resumed, subscription.cancelled

Provider Patterns

Ready-to-use patterns for popular payment gateways (copy to your project):

Available Patterns

Pattern Use Case Location
stripe-checkout Single-tenant Stripe provider-patterns/stripe-checkout/
stripe-connect-standard Multi-tenant marketplace provider-patterns/stripe-connect-standard/
stripe-platform-manual Platform collects, manual payout provider-patterns/stripe-platform-manual/
sslcommerz Bangladesh payment gateway provider-patterns/sslcommerz/

See: provider-patterns/INDEX.md for complete guide.

Building Custom Providers

Create providers for any payment gateway:

import { PaymentProvider, PaymentIntent, PaymentResult } from '@classytic/revenue';

export class StripeProvider extends PaymentProvider {
  constructor(config) {
    super(config);
    this.name = 'stripe';
    this.stripe = new Stripe(config.apiKey);
  }

  async createIntent(params) {
    const intent = await this.stripe.paymentIntents.create({
      amount: params.amount,
      currency: params.currency,
    });

    return new PaymentIntent({
      id: intent.id,
      provider: 'stripe',
      status: intent.status,
      amount: intent.amount,
      currency: intent.currency,
      clientSecret: intent.client_secret,
      raw: intent,
    });
  }

  async verifyPayment(intentId) {
    const intent = await this.stripe.paymentIntents.retrieve(intentId);
    return new PaymentResult({
      id: intent.id,
      provider: 'stripe',
      status: intent.status === 'succeeded' ? 'succeeded' : 'failed',
      paidAt: new Date(),
      raw: intent,
    });
  }

  // Implement: getStatus(), refund(), handleWebhook()
}

See: docs/guides/PROVIDER_GUIDE.md for complete guide.

TypeScript

Full TypeScript support included:

import { createRevenue, Revenue, PaymentService } from '@classytic/revenue';
import { TRANSACTION_TYPE, TRANSACTION_STATUS } from '@classytic/revenue/enums';

const revenue: Revenue = createRevenue({
  models: { Transaction },
});

// All services are fully typed
const payment = await revenue.payments.verify(id);
const subscription = await revenue.monetization.create({ ... });

Examples

Error Handling

import { 
  TransactionNotFoundError,
  ProviderNotFoundError,
  AlreadyVerifiedError,
  RefundError,
} from '@classytic/revenue';

try {
  await revenue.payments.verify(id);
} catch (error) {
  if (error instanceof AlreadyVerifiedError) {
    console.log('Already verified');
  } else if (error instanceof TransactionNotFoundError) {
    console.log('Transaction not found');
  }
}

Documentation

Support

License

MIT © Classytic