apps/recallassess/recallassess-api/src/common/pricing/banded-pricing.util.ts
Cumulative banded pricing — stacks ordered bands (Starter → Enterprise). Slice widths match banded-pricing-packages-concept.md: 50, 50, 150, 250, then unlimited.
Properties |
| rate |
rate:
|
Type : number
|
| sortOrder |
sortOrder:
|
Type : number
|
export interface BandedPricingBand {
sortOrder: number;
rate: number;
}
/** Seat capacity per band slice (cumulative stack), aligned to concept doc. */
const BAND_SLICE_SIZES = [50, 50, 150, 250, Number.POSITIVE_INFINITY] as const;
export function bandSliceSize(bandIndex: number): number {
return BAND_SLICE_SIZES[bandIndex] ?? Number.POSITIVE_INFINITY;
}
/**
* Monthly subtotal for participant count using cumulative band rates.
* Bands must be sorted by sortOrder ascending (Starter first).
*/
export function calculateBandedMonthlySubtotal(
participantCount: number,
bands: BandedPricingBand[],
): number {
if (participantCount <= 0 || bands.length === 0) {
return 0;
}
const sorted = [...bands].sort((a, b) => a.sortOrder - b.sortOrder);
let remaining = participantCount;
let total = 0;
for (let i = 0; i < sorted.length && remaining > 0; i++) {
const slice = bandSliceSize(i);
const seatsInBand = slice === Number.POSITIVE_INFINITY ? remaining : Math.min(remaining, slice);
total += seatsInBand * sorted[i].rate;
remaining -= seatsInBand;
}
return total;
}
export interface BandedPricingLine {
bandIndex: number;
seats: number;
rate: number;
subtotal: number;
}
/** Band breakdown for invoices / previews. */
export function calculateBandedBreakdown(
participantCount: number,
bands: BandedPricingBand[],
): BandedPricingLine[] {
if (participantCount <= 0 || bands.length === 0) {
return [];
}
const sorted = [...bands].sort((a, b) => a.sortOrder - b.sortOrder);
const lines: BandedPricingLine[] = [];
let remaining = participantCount;
for (let i = 0; i < sorted.length && remaining > 0; i++) {
const slice = bandSliceSize(i);
const seatsInBand = slice === Number.POSITIVE_INFINITY ? remaining : Math.min(remaining, slice);
lines.push({
bandIndex: i,
seats: seatsInBand,
rate: sorted[i].rate,
subtotal: seatsInBand * sorted[i].rate,
});
remaining -= seatsInBand;
}
return lines;
}
/** Effective flat rate for display (total / seats). */
export function effectiveRatePerParticipant(monthlySubtotal: number, participantCount: number): number {
if (participantCount <= 0) {
return 0;
}
return monthlySubtotal / participantCount;
}