Functional Weave
Code in Rust

telecoms.call-rating@1.0.1

impl/typescript.ts

3,001 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { type RoundingMode, roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type CallRate, type RatedCall } from "./telecoms_call_rating_types.ts";

const DIGITS = /^[0-9]+$/;

/**
 * Rate one call: longest-prefix match on the dialled number, duration rounded
 * up to the rate's billing increments, per-minute charge rounded once by
 * `mode`, connection fee added and the minimum charge applied.
 */
export function rateCall(dialledNumber: string, durationSeconds: number, rates: readonly CallRate[], mode: RoundingMode): RatedCall {
  const number = dialledNumber.startsWith("+") ? dialledNumber.slice(1) : dialledNumber;
  if (!DIGITS.test(number)) {
    throw new RangeError(`dialled number must be international format digits, optionally with a leading +, received "${dialledNumber}"`);
  }
  if (!Number.isInteger(durationSeconds) || durationSeconds < 0) {
    throw new RangeError(`duration must be whole seconds of 0 or more, received ${durationSeconds}`);
  }
  const seen = new Set<string>();
  let best: CallRate | null = null;
  for (const rate of rates) {
    if (!DIGITS.test(rate.prefix)) {
      throw new RangeError(`rate prefix must be digits, received "${rate.prefix}"`);
    }
    if (seen.has(rate.prefix)) {
      throw new RangeError(`duplicate prefix ${rate.prefix} in the rate deck`);
    }
    seen.add(rate.prefix);
    if (!Number.isInteger(rate.firstIncrementSeconds) || rate.firstIncrementSeconds < 1 || !Number.isInteger(rate.nextIncrementSeconds) || rate.nextIncrementSeconds < 1) {
      throw new RangeError(`billing increments must be 1 second or more, received ${rate.firstIncrementSeconds}/${rate.nextIncrementSeconds} for prefix ${rate.prefix}`);
    }
    assertSameCurrency(rates[0].perMinute, rate.perMinute);
    assertSameCurrency(rates[0].perMinute, rate.connectionFee);
    assertSameCurrency(rates[0].perMinute, rate.minimumCharge);
    if (number.startsWith(rate.prefix) && (best === null || rate.prefix.length > best.prefix.length)) {
      best = rate;
    }
  }
  if (best === null) {
    throw new RangeError(`no rate for dialled number ${number}`);
  }

  const currency = best.perMinute.currency;
  if (durationSeconds === 0) {
    const zero = money(0, currency);
    return { prefix: best.prefix, destination: best.destination, billedSeconds: 0, usageCharge: zero, connectionFee: zero, total: zero };
  }
  const first = best.firstIncrementSeconds;
  const next = best.nextIncrementSeconds;
  const billedSeconds = durationSeconds <= first ? first : first + Math.ceil((durationSeconds - first) / next) * next;
  const usage = roundDiv(best.perMinute.minor * billedSeconds, 60, mode);
  const total = Math.max(usage + best.connectionFee.minor, best.minimumCharge.minor);
  return {
    prefix: best.prefix,
    destination: best.destination,
    billedSeconds,
    usageCharge: money(usage, currency),
    connectionFee: best.connectionFee,
    total: money(total, currency),
  };
}