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
50 changes: 50 additions & 0 deletions backend/billing/domain/billing-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* BillingEngine
*
* Core billing calculation engine using the strategy pattern for pluggable pricing models.
* Delegates pricing calculations to registered strategies based on plan type.
*
* Performance: Each calculate() call must complete in <5ms.
* This is achieved by using O(1) or O(n) strategies where n is small (typically <10 tiers).
*/

import type { Amount, Plan, Subscriber, Usage } from './strategies/pricing-strategy.interface';
import { getStrategyRegistry } from './strategy-registry';

export class BillingEngine {
/**
* Calculate the charge amount for a given subscription
*
* @param usage - Usage and metering data
* @param plan - Subscription plan with type code and config
* @param subscriber - Subscriber information
* @returns Calculated amount with breakdown
* @throws Error if calculation fails or inputs are invalid
*/
calculate(usage: Usage, plan: Plan, subscriber: Subscriber): Amount {
if (!plan) {
throw new Error('Plan is required for billing calculation');
}

// Get the appropriate strategy based on plan type
const registry = getStrategyRegistry();
const strategy = registry.getStrategy(plan.typeCode);

// Delegate to the strategy for actual calculation
// Performance requirement: This must complete in <5ms total
return strategy.calculate(usage, plan, subscriber);
}

/**
* Get available pricing models
*
* @returns List of registered plan type codes
*/
getAvailablePricingModels(): string[] {
const registry = getStrategyRegistry();
return registry.getRegisteredTypes();
}
}

// Export singleton instance for dependency injection
export const billingEngine = new BillingEngine();
18 changes: 18 additions & 0 deletions backend/billing/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Billing Domain - Public API
*
* Exports the BillingEngine and strategy-related types for use throughout the application.
*/

export { BillingEngine, billingEngine } from './billing-engine';
export { StrategyRegistry, getStrategyRegistry, resetStrategyRegistry } from './strategy-registry';
export { type PricingStrategy, type Usage, type Plan, type Subscriber, type Amount } from './strategies';
export {
FlatPricingStrategy,
PerSeatPricingStrategy,
UsageBasedPricingStrategy,
TieredPricingStrategy,
FallbackPricingStrategy,
type PricingTier,
type TierBreakdownLine,
} from './strategies';
46 changes: 46 additions & 0 deletions backend/billing/domain/strategies/fallback-pricing.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* FallbackPricingStrategy
*
* A safe fallback strategy that handles unknown or unsupported plan types gracefully.
* It returns a safe default (the base price) instead of failing, allowing the system
* to continue operating even when encountering unexpected pricing models.
*
* This strategy should only be used as a last resort via the strategy registry.
* It should never be the primary strategy for a known plan type.
*
* Configuration: No additional config needed
* Performance: O(1), completes in <1ms
*/

import type { PricingStrategy, Usage, Plan, Subscriber, Amount } from './pricing-strategy.interface';

export class FallbackPricingStrategy implements PricingStrategy {
getName(): string {
return 'Fallback Pricing (Base Price)';
}

calculate(usage: Usage, plan: Plan, subscriber: Subscriber): Amount {
// Validate minimal inputs
if (!plan) {
throw new Error('Invalid plan: plan object is required');
}

if (!plan.currency) {
throw new Error('Invalid plan: currency is required');
}

// Default to base price (or 0 if not specified)
const basePrice = typeof plan.basePrice === 'number' && plan.basePrice >= 0 ? plan.basePrice : 0;
const value = Math.round(basePrice * 100) / 100; // Round to 2 decimals

return {
value,
currency: plan.currency,
breakdown: {
strategy: 'fallback',
basePrice: value,
note: 'Using fallback pricing for unknown plan type',
},
};
}
}
38 changes: 38 additions & 0 deletions backend/billing/domain/strategies/flat-pricing.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* FlatPricingStrategy
*
* Implements a fixed price model where the subscriber pays the same amount
* regardless of usage. This is the simplest pricing model.
*
* Configuration: No additional config needed
* Performance: O(1), completes in <1ms
*/

import type { PricingStrategy, Usage, Plan, Subscriber, Amount } from './pricing-strategy.interface';

export class FlatPricingStrategy implements PricingStrategy {
getName(): string {
return 'Flat Pricing';
}

calculate(usage: Usage, plan: Plan, subscriber: Subscriber): Amount {
// Validate inputs
if (!plan || typeof plan.basePrice !== 'number' || plan.basePrice < 0) {
throw new Error('Invalid plan: basePrice must be a non-negative number');
}

if (!plan.currency) {
throw new Error('Invalid plan: currency is required');
}

const value = Math.round(plan.basePrice * 100) / 100; // Round to 2 decimals

return {
value,
currency: plan.currency,
breakdown: {
basePrice: value,
},
};
}
}
14 changes: 14 additions & 0 deletions backend/billing/domain/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Pricing Strategies Barrel Export
*
* Exports all pricing strategy implementations and interfaces.
* This serves as the public API for the strategies module.
*/

export type { PricingStrategy, Usage, Plan, Subscriber, Amount } from './pricing-strategy.interface';

export { FlatPricingStrategy } from './flat-pricing.strategy';
export { PerSeatPricingStrategy } from './per-seat-pricing.strategy';
export { UsageBasedPricingStrategy } from './usage-based-pricing.strategy';
export { TieredPricingStrategy, type PricingTier, type TierBreakdownLine } from './tiered-pricing.strategy';
export { FallbackPricingStrategy } from './fallback-pricing.strategy';
48 changes: 48 additions & 0 deletions backend/billing/domain/strategies/per-seat-pricing.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* PerSeatPricingStrategy
*
* Implements a per-seat (per-user) pricing model where the total cost is calculated
* by multiplying the base price per seat by the number of seats/users.
*
* Configuration: { pricePerSeat: number }
* Performance: O(1), completes in <1ms
*
* Example: $10 per seat × 5 seats = $50
*/

import type { PricingStrategy, Usage, Plan, Subscriber, Amount } from './pricing-strategy.interface';

export class PerSeatPricingStrategy implements PricingStrategy {
getName(): string {
return 'Per-Seat Pricing';
}

calculate(usage: Usage, plan: Plan, subscriber: Subscriber): Amount {
// Validate inputs
if (!plan || typeof plan.basePrice !== 'number' || plan.basePrice < 0) {
throw new Error('Invalid plan: basePrice must be a non-negative number');
}

if (!plan.currency) {
throw new Error('Invalid plan: currency is required');
}

if (!usage || typeof usage.seatCount !== 'number' || usage.seatCount < 0) {
throw new Error('Invalid usage: seatCount must be a non-negative number');
}

const seatCount = Math.floor(Math.max(0, usage.seatCount));
const pricePerSeat = plan.basePrice;
const value = Math.round(seatCount * pricePerSeat * 100) / 100; // Round to 2 decimals

return {
value,
currency: plan.currency,
breakdown: {
pricePerSeat,
seatCount,
total: value,
},
};
}
}
84 changes: 84 additions & 0 deletions backend/billing/domain/strategies/pricing-strategy.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* PricingStrategy Interface
*
* Defines the contract for pricing calculation strategies.
* Each strategy handles a specific pricing model (flat, per-seat, usage-based, tiered, etc.).
*
* This interface enables the strategy pattern for pluggable pricing calculations,
* allowing new pricing models to be added without modifying existing code.
*
* Performance requirement: Each calculate() call must complete in <5ms.
*/

export interface Usage {
/** Unique identifier for the usage record */
id: string;
/** Units consumed (for usage-based models) */
unitsConsumed: number;
/** Number of seats/users (for per-seat models) */
seatCount: number;
/** Additional properties for model-specific calculations */
[key: string]: any;
}

export interface Plan {
/** Unique identifier for the plan */
id: string;
/** Plan type code (e.g., 'flat', 'per-seat', 'usage-based', 'tiered') */
typeCode: string;
/** Base price of the plan */
basePrice: number;
/** Currency code (e.g., 'USD') */
currency: string;
/** Model-specific configuration */
config?: {
[key: string]: any;
};
}

export interface Subscriber {
/** Unique identifier for the subscriber */
id: string;
/** Subscription identifier */
subscriptionId: string;
/** Additional properties for model-specific calculations */
[key: string]: any;
}

export interface Amount {
/** Calculated amount in the subscription currency */
value: number;
/** Currency code */
currency: string;
/** Breakdown of calculation (for transparency) */
breakdown?: {
[key: string]: number;
};
}

/**
* PricingStrategy - Core interface for calculating subscription charges.
*
* Implementations should:
* 1. Be stateless (can be reused across multiple calculations)
* 2. Complete in <5ms for typical inputs
* 3. Handle edge cases (null, zero, negative values)
* 4. Return consistent, predictable results
*/
export interface PricingStrategy {
/**
* Calculate the charge amount for given inputs.
*
* @param usage - Metering data and usage details
* @param plan - Subscription plan with type-specific configuration
* @param subscriber - Subscriber information
* @returns Amount with value and currency
* @throws Error if calculation fails or inputs are invalid
*/
calculate(usage: Usage, plan: Plan, subscriber: Subscriber): Amount;

/**
* Get human-readable name of the strategy
*/
getName(): string;
}
Loading