import re from typing import Optional, Sequence from .math_round_div import RoundingMode, round_div from .money_amount import assert_same_currency, money from .telecoms_call_rating_types import CallRate, RatedCall DIGITS = re.compile(r"[0-9]+") def _is_increment(value: object) -> bool: return not isinstance(value, bool) and isinstance(value, int) and value >= 1 def rate_call(dialled_number: str, duration_seconds: int, rates: Sequence[CallRate], mode: RoundingMode) -> RatedCall: """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. """ number = dialled_number[1:] if dialled_number.startswith("+") else dialled_number if not DIGITS.fullmatch(number): raise ValueError( 'dialled number must be international format digits, optionally with a leading +, received "%s"' % (dialled_number,) ) if isinstance(duration_seconds, bool) or not isinstance(duration_seconds, int) or duration_seconds < 0: raise ValueError("duration must be whole seconds of 0 or more, received %r" % (duration_seconds,)) seen = set() best: Optional[CallRate] = None for rate in rates: if not DIGITS.fullmatch(rate.prefix): raise ValueError('rate prefix must be digits, received "%s"' % (rate.prefix,)) if rate.prefix in seen: raise ValueError("duplicate prefix %s in the rate deck" % (rate.prefix,)) seen.add(rate.prefix) if not _is_increment(rate.first_increment_seconds) or not _is_increment(rate.next_increment_seconds): raise ValueError( "billing increments must be 1 second or more, received %r/%r for prefix %s" % (rate.first_increment_seconds, rate.next_increment_seconds, rate.prefix) ) assert_same_currency(rates[0].per_minute, rate.per_minute) assert_same_currency(rates[0].per_minute, rate.connection_fee) assert_same_currency(rates[0].per_minute, rate.minimum_charge) if number.startswith(rate.prefix) and (best is None or len(rate.prefix) > len(best.prefix)): best = rate if best is None: raise ValueError("no rate for dialled number %s" % (number,)) currency = best.per_minute.currency if duration_seconds == 0: zero = money(0, currency) return RatedCall( prefix=best.prefix, destination=best.destination, billed_seconds=0, usage_charge=zero, connection_fee=zero, total=zero, ) first = best.first_increment_seconds step = best.next_increment_seconds if duration_seconds <= first: billed_seconds = first else: billed_seconds = first + -(-(duration_seconds - first) // step) * step usage = round_div(best.per_minute.minor * billed_seconds, 60, mode) total = max(usage + best.connection_fee.minor, best.minimum_charge.minor) return RatedCall( prefix=best.prefix, destination=best.destination, billed_seconds=billed_seconds, usage_charge=money(usage, currency), connection_fee=best.connection_fee, total=money(total, currency), )