import re from typing import Optional from .dates_add_days import epoch_day_from_iso from .money_amount import money from .money_sum import sum_money from .telecoms_roaming_charge_data import ROAMING_TARIFFS, ROAMING_TARIFFS_HISTORY, RoamingRate from .telecoms_roaming_charge_types import RoamingCharge, RoamingUsage COUNTRY = re.compile(r"^[A-Z]{2}$") def _is_count(value: object) -> bool: return not isinstance(value, bool) and isinstance(value, int) and value >= 0 def roaming_charge(tariff: str, country: str, on_date: str, usage: RoamingUsage) -> RoamingCharge: """Price roaming usage in one country on one date: find the tariff's zone for the country in force on that date, then multiply each kind of usage by its price. Exact integer arithmetic throughout. """ if not isinstance(country, str) or not COUNTRY.match(country): raise ValueError('country must be an ISO 3166-1 alpha-2 code in capitals, received "%s"' % (country,)) epoch_day_from_iso(on_date) counts = (usage.days, usage.data_mb, usage.call_out_minutes, usage.call_in_minutes, usage.sms_out) if not all(_is_count(n) for n in counts): raise ValueError("usage must be whole numbers of 0 or more") def covers(rule: RoamingRate) -> bool: return rule.tariff == tariff and country in rule.countries.split(" ") best: Optional[RoamingRate] = None for rule in ROAMING_TARIFFS: if not covers(rule): continue if on_date < rule.valid_from: continue if rule.valid_to is not None and on_date > rule.valid_to: continue if best is None or rule.valid_from > best.valid_from: best = rule if best is None: # A history=current build has dropped the rows that ended; answering an # older date with today's prices would be wrong, so say what happened. if ROAMING_TARIFFS_HISTORY != "full": earliest = None for rule in ROAMING_TARIFFS: if covers(rule) and (earliest is None or rule.valid_from < earliest): earliest = rule.valid_from if earliest is not None and on_date < earliest: raise ValueError( "no roaming zone for %s in tariff %s on %s: this build was installed with history=%s, " "so it only carries rules from %s. Reinstall with history=full to price older usage." % (country, tariff, on_date, ROAMING_TARIFFS_HISTORY, earliest) ) raise ValueError("no roaming zone for %s in tariff %s on %s" % (country, tariff, on_date)) c = best.currency daily_fees = money(usage.days * best.daily_fee_minor, c) data = money(usage.data_mb * best.data_per_mb_minor, c) calls = money( usage.call_out_minutes * best.call_out_per_minute_minor + usage.call_in_minutes * best.call_in_per_minute_minor, c, ) sms = money(usage.sms_out * best.sms_out_minor, c) return RoamingCharge( tariff=tariff, zone=best.zone, daily_fees=daily_fees, data=data, calls=calls, sms=sms, total=sum_money([daily_fees, data, calls, sms], c), )