Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions backend/services/billing/__tests__/billing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { PricingStrategyFactory, PlanType } from '../strategyFactory';
import { PricingContext } from '../pricingStrategy';
import { BillingEngine } from '../billingEngine';
import { PricingAnalyticsService } from '../billingAnalytics';

describe('PricingStrategyFactory', () => {
afterEach(() => {
PricingStrategyFactory.reset();
});

it('should create flat_rate strategy for basic plans', () => {
const strategy = PricingStrategyFactory.resolveStrategy({ planType: 'basic' });
expect(strategy.name).toBe('flat_rate');
});

it('should create tiered strategy for premium plans', () => {
const strategy = PricingStrategyFactory.resolveStrategy({ planType: 'premium' });
expect(strategy.name).toBe('tiered');
});

it('should create dynamic strategy for enterprise plans', () => {
const strategy = PricingStrategyFactory.resolveStrategy({ planType: 'enterprise' });
expect(strategy.name).toBe('dynamic');
});

it('should allow strategy override', () => {
const strategy = PricingStrategyFactory.resolveStrategy({
planType: 'basic',
strategyOverride: 'usage_based',
});
expect(strategy.name).toBe('usage_based');
});

it('should return available strategies', () => {
const strategies = PricingStrategyFactory.getAvailableStrategies();
expect(strategies).toContain('flat_rate');
expect(strategies).toContain('usage_based');
expect(strategies).toContain('tiered');
expect(strategies).toContain('dynamic');
});
});

describe('Pricing Strategies', () => {
const baseContext: PricingContext = {
planId: 'plan_1',
subscriberAddress: '0xABC',
currentPrice: 10.0,
currency: 'USD',
usageData: {
sessionsPerWeek: 5,
retentionRate: 0.7,
apiCallsThisPeriod: 500,
storageUsedMB: 200,
seatsActive: 3,
},
};

describe('FlatRateStrategy', () => {
it('should calculate flat rate price', () => {
const strategy = PricingStrategyFactory.getStrategy('flat_rate');
const result = strategy.calculatePrice(baseContext);
expect(result.price).toBeGreaterThan(0);
expect(result.strategyName).toBe('flat_rate');
expect(result.breakdown).toBeDefined();
});
});

describe('UsageBasedStrategy', () => {
it('should calculate usage-based price', () => {
const strategy = PricingStrategyFactory.getStrategy('usage_based');
const result = strategy.calculatePrice(baseContext);
expect(result.price).toBeGreaterThanOrEqual(0);
expect(result.strategyName).toBe('usage_based');
});
});

describe('TieredPricingStrategy', () => {
it('should calculate tiered price', () => {
const strategy = PricingStrategyFactory.getStrategy('tiered');
const result = strategy.calculatePrice(baseContext);
expect(result.price).toBeGreaterThan(0);
expect(result.strategyName).toBe('tiered');
});
});

describe('DynamicPricingStrategy', () => {
it('should calculate dynamic price', () => {
const strategy = PricingStrategyFactory.getStrategy('dynamic');
const result = strategy.calculatePrice(baseContext);
expect(result.price).toBeGreaterThan(0);
expect(result.strategyName).toBe('dynamic');
expect(result.metadata).toHaveProperty('recommendation');
});
});
});

describe('BillingEngine', () => {
it('should calculate price for a plan type', () => {
const engine = new BillingEngine();
const result = engine.calculatePrice('basic', {
planId: 'plan_1',
subscriberAddress: '0xABC',
currentPrice: 10.0,
currency: 'USD',
});
expect(result.price).toBeGreaterThan(0);
});

it('should process a charge and record billing history', () => {
const engine = new BillingEngine();
const record = engine.processCharge('basic', {
planId: 'plan_1',
subscriberAddress: '0xABC',
currentPrice: 10.0,
currency: 'USD',
});
expect(record.amount).toBeGreaterThan(0);
expect(engine.getBillingHistory()).toHaveLength(1);
});

it('should get AB test variants', () => {
const engine = new BillingEngine();
const variants = engine.getABTestVariants('basic', 10.0);
expect(variants.length).toBeGreaterThan(0);
});
});

describe('PricingAnalyticsService', () => {
it('should track pricing events', () => {
const service = new PricingAnalyticsService();
service.trackPricingEvent(
{ price: 10.0, strategyName: 'flat_rate', breakdown: {} as any, metadata: {} },
'basic',
'0xABC'
);
const metrics = service.getRevenueMetrics();
expect(metrics.totalRevenue).toBe(10.0);
});
});
104 changes: 104 additions & 0 deletions backend/services/billing/billingAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Pricing Analytics Service
*
* Tracks pricing performance, conversion rates, and revenue metrics
* across different pricing strategies.
*/

import { PricingAnalytics, PricingResult } from './pricingStrategy';

export interface PricingEvent {
strategyName: string;
planType: string;
price: number;
converted: boolean;
timestamp: string;
subscriberAddress: string;
}

export interface RevenueMetrics {
totalRevenue: number;
averageOrderValue: number;
conversionRate: number;
revenuePerStrategy: Record<string, number>;
revenuePerPlanType: Record<string, number>;
periodStart: string;
periodEnd: string;
}

export class PricingAnalyticsService {
private events: PricingEvent[] = [];

trackPricingEvent(
result: PricingResult,
planType: string,
subscriberAddress: string,
converted: boolean = true
): void {
this.events.push({
strategyName: result.strategyName,
planType,
price: result.price,
converted,
timestamp: new Date().toISOString(),
subscriberAddress,
});
}

getRevenueMetrics(periodDays: number = 30): RevenueMetrics {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - periodDays);

const periodEvents = this.events.filter(
(e) => new Date(e.timestamp) >= cutoff
);

const convertedEvents = periodEvents.filter((e) => e.converted);

const totalRevenue = convertedEvents.reduce((sum, e) => sum + e.price, 0);
const averageOrderValue =
convertedEvents.length > 0 ? totalRevenue / convertedEvents.length : 0;
const conversionRate =
periodEvents.length > 0 ? convertedEvents.length / periodEvents.length : 0;

const revenuePerStrategy: Record<string, number> = {};
for (const event of convertedEvents) {
revenuePerStrategy[event.strategyName] =
(revenuePerStrategy[event.strategyName] || 0) + event.price;
}

const revenuePerPlanType: Record<string, number> = {};
for (const event of convertedEvents) {
revenuePerPlanType[event.planType] =
(revenuePerPlanType[event.planType] || 0) + event.price;
}

return {
totalRevenue: Math.round(totalRevenue * 100) / 100,
averageOrderValue: Math.round(averageOrderValue * 100) / 100,
conversionRate: Math.round(conversionRate * 10000) / 100,
revenuePerStrategy,
revenuePerPlanType,
periodStart: cutoff.toISOString(),
periodEnd: new Date().toISOString(),
};
}

getStrategyPerformance(): Record<string, { events: number; revenue: number; avgPrice: number }> {
const performance: Record<string, { events: number; revenue: number; avgPrice: number }> = {};

for (const event of this.events) {
if (!performance[event.strategyName]) {
performance[event.strategyName] = { events: 0, revenue: 0, avgPrice: 0 };
}
const perf = performance[event.strategyName];
perf.events++;
if (event.converted) {
perf.revenue += event.price;
}
perf.avgPrice = perf.events > 0 ? perf.revenue / perf.events : 0;
}

return performance;
}
}
132 changes: 132 additions & 0 deletions backend/services/billing/billingEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Billing Engine (Refactored with Strategy Pattern)
*
* Orchestrates billing operations using pluggable pricing strategies.
* Supports strategy selection based on plan type, runtime overrides,
* and comprehensive pricing analytics.
*/

import {
PricingStrategy,
PricingContext,
PricingResult,
PricingAnalytics,
} from './pricingStrategy';
import { PricingStrategyFactory, PlanType, StrategyConfig } from './strategyFactory';

export interface BillingEngineConfig {
defaultPlanType: PlanType;
enableAnalytics: boolean;
enableABTesting: boolean;
}

export interface BillingRecord {
subscriptionId: string;
subscriberAddress: string;
planId: string;
planType: PlanType;
amount: number;
currency: string;
strategyUsed: string;
timestamp: string;
metadata: Record<string, unknown>;
}

export class BillingEngine {
private config: BillingEngineConfig;
private billingHistory: BillingRecord[] = [];

constructor(config?: Partial<BillingEngineConfig>) {
this.config = {
defaultPlanType: 'basic',
enableAnalytics: true,
enableABTesting: false,
...config,
};
}

/**
* Calculate the price for a subscription using the appropriate strategy.
*/
calculatePrice(
planType: PlanType,
context: PricingContext,
strategyOverride?: string
): PricingResult {
const strategyConfig: StrategyConfig = {
planType,
strategyOverride,
};

return PricingStrategyFactory.calculatePrice(strategyConfig, context);
}

/**
* Process a billing charge for a subscription.
*/
processCharge(
planType: PlanType,
context: PricingContext,
strategyOverride?: string
): BillingRecord {
const result = this.calculatePrice(planType, context, strategyOverride);

const record: BillingRecord = {
subscriptionId: context.planId,
subscriberAddress: context.subscriberAddress,
planId: context.planId,
planType,
amount: result.price,
currency: context.currency,
strategyUsed: result.strategyName,
timestamp: new Date().toISOString(),
metadata: result.metadata,
};

this.billingHistory.push(record);
return record;
}

/**
* Get A/B test variants for a plan type.
*/
getABTestVariants(planType: PlanType, basePrice: number) {
const strategy = PricingStrategyFactory.resolveStrategy({ planType });
return strategy.getABTestVariants(basePrice);
}

/**
* Get pricing analytics for a specific strategy or all strategies.
*/
getAnalytics(strategyName?: string): PricingAnalytics[] {
if (strategyName) {
const strategy = PricingStrategyFactory.getStrategy(strategyName);
return [strategy.getAnalytics()];
}
return PricingStrategyFactory.getAllAnalytics();
}

/**
* Get billing history for a subscriber.
*/
getBillingHistory(subscriberAddress?: string): BillingRecord[] {
if (subscriberAddress) {
return this.billingHistory.filter((r) => r.subscriberAddress === subscriberAddress);
}
return [...this.billingHistory];
}

/**
* Get available pricing strategies.
*/
getAvailableStrategies(): string[] {
return PricingStrategyFactory.getAvailableStrategies();
}

/**
* Get the recommended strategy for a given plan type.
*/
getRecommendedStrategy(planType: PlanType): string {
return PricingStrategyFactory.resolveStrategy({ planType }).name;
}
}
Loading
Loading