@happyvertical/smrt-affiliates

Revenue sharing with multi-type partners (publisher/salesperson/referrer), multi-tier commission attribution, and payout batch processing.

v0.20.44Revenue ShareCommissionsPayouts

Overview

smrt-affiliates provides affiliate partner and commission tracking for revenue sharing networks. Partners can hold multiple roles, commissions are immutable records tied to ad events, and payouts aggregate earnings into batch disbursements.

Installation

bash
npm install @happyvertical/smrt-affiliates

Quick Start

typescript
import {
  Partner, PartnerCollection,
  Commission, CommissionCollection,
  Payout, PayoutCollection,
  PartnerType, CommissionType, CommissionStatus, PayoutStatus
} from '@happyvertical/smrt-affiliates';

// Register a publisher partner (earns display commissions)
const partners = new PartnerCollection(db);
const publisher = await partners.create({
  profileId: 'profile-uuid',
  propertyId: 'property-uuid',
  partnerTypes: JSON.stringify([PartnerType.PUBLISHER]),
  displayCommissionRate: 0.50,
  status: 'active',
});

// Attach a salesperson to the publisher
// parentCommissionShare: 20% of sales commission goes to parent
const salesperson = await partners.create({
  profileId: 'sales-profile-uuid',
  parentPartnerId: publisher.id,
  partnerTypes: JSON.stringify([PartnerType.SALESPERSON]),
  salesCommissionRate: 0.10,
  parentCommissionShare: 0.20,
  status: 'active',
});
// Effective sales rate: 0.10 * (1 - 0.20) = 0.08
salesperson.getEffectiveSalesRate(); // 0.08

// Record a commission (all monetary values in integer cents)
const commissions = new CommissionCollection(db);
await commissions.create({
  eventId: 'adevent-uuid',
  partnerId: publisher.id,
  commissionType: CommissionType.DISPLAY,
  grossRevenue: 1000,       // $10.00
  commissionRate: 0.50,
  commissionAmount: Commission.calculateAmount(1000, 0.50), // 500 cents
  currency: 'CAD',
  status: CommissionStatus.PENDING,
});

// Create a payout batch
const payouts = new PayoutCollection(db);
const payout = await payouts.create({
  partnerId: publisher.id,
  periodStart: new Date('2024-01-01'),
  periodEnd: new Date('2024-01-31'),
  displayEarnings: 25000,   // $250.00
  referralEarnings: 500,    // $5.00
  salesEarnings: 0,
  parentEarnings: 0,
  totalAmount: 25500,       // $255.00
  currency: 'CAD',
  status: PayoutStatus.PENDING,
});

// Payout lifecycle
payout.approve();
payout.markProcessing();
payout.complete('transfer-ref-123');
await payout.save();

Core Models

Partner

typescript
class Partner extends SmrtObject {
  profileId: string          // FK to smrt-profiles
  propertyId?: string        // FK to smrt-properties
  parentPartnerId?: string   // Parent publisher hierarchy
  referredById?: string      // Referral attribution
  partnerTypes: string       // JSON array of PartnerType
  displayCommissionRate: number
  salesCommissionRate: number
  referralCommissionRate: number
  parentCommissionShare: number  // Share passed to parent
  status: 'pending' | 'active' | 'suspended'

  getPartnerTypes(): PartnerType[]
  getEffectiveSalesRate(): number
}

Commission (Immutable)

typescript
class Commission extends SmrtObject {
  eventId: string            // FK to smrt-ads AdEvent
  partnerId: string
  commissionType: 'display' | 'referral' | 'sales' | 'parent'
  grossRevenue: number       // Integer cents
  commissionRate: number
  commissionAmount: number   // Integer cents
  currency: string
  status: 'pending' | 'included' | 'paid'

  static calculateAmount(grossRevenue: number, rate: number): number
  getAmountInDollars(): number
}

Payout

typescript
class Payout extends SmrtObject {
  partnerId: string
  periodStart: Date
  periodEnd: Date
  displayEarnings: number    // Integer cents
  referralEarnings: number
  salesEarnings: number
  parentEarnings: number
  totalAmount: number        // Integer cents
  currency: string
  status: 'pending' | 'approved' | 'processing' | 'completed' | 'failed'

  approve(): void
  markProcessing(): void
  complete(reference: string): void
  getTotalInDollars(): number
}

Commission Types

typescript
// Each ad event can generate up to 4 commissions:
//
// DISPLAY   -> Publisher (site owner earns share of impression revenue)
// REFERRAL  -> Referrer (partner who referred the publisher)
// SALES     -> Salesperson (partner who brought in the advertiser)
// PARENT    -> Parent publisher (share of salesperson's commission)
//
// Parent commission share example:
// Salesperson.parentCommissionShare = 0.20 (20%)
// Effective sales rate = salesRate * (1 - parentCommissionShare)

// Commission rate is copied at event time (immutable record)
// It does NOT update if Partner.commissionRate changes later

Best Practices

DOs

  • Use getPartnerTypes() to parse the JSON string array
  • Store all monetary values as integer cents (divide by 100 for display)
  • Use Commission.calculateAmount() for consistent rounding
  • Copy commission rates at event time for immutable attribution
  • Use payout lifecycle methods for status transitions

DON'Ts

  • Don't modify or delete Commission records (immutable by design)
  • Don't reference Partner.commissionRate after event time (use the copied rate)
  • Don't display cent values directly (use getTotalInDollars() helpers)
  • Don't add tenant scoping (intentionally cross-tenant for network visibility)
  • Don't assume payout maps to ledger entries (integration is external)

Related Modules