from .dates_days_between import days_between from .energy_meter_advance import meter_advance from .energy_water_bill_types import MeterRead, WaterBill, WaterTariff from .math_round_div import RoundingMode, round_div from .money_amount import money MAX_SAFE = 9007199254740991 def _check_rate(what: str, value: object) -> None: if not isinstance(value, int) or isinstance(value, bool) or value < 0: raise ValueError("%s must be a whole number of 0 or more, received %s" % (what, value)) def _exact(product: int) -> int: if product > MAX_SAFE: raise ValueError("water charge too large to calculate exactly") return product def water_bill( opening_read: MeterRead, closing_read: MeterRead, meter_digits: int, tariff: WaterTariff, surface_water_drains: bool, currency: str, mode: RoundingMode, ) -> WaterBill: """A metered water and sewerage bill: volumetric water, sewerage on the share returned to the sewer, and standing and surface water charges apportioned by days / 365, each line rounded once. """ cubic_metres = meter_advance(opening_read.value, closing_read.value, meter_digits) days = days_between(opening_read.date, closing_read.date) if days < 0: raise ValueError( "the closing read must not be dated before the opening read, received %s and %s" % (opening_read.date, closing_read.date) ) _check_rate("waterVolumeRate", tariff.water_volume_rate) _check_rate("sewerageVolumeRate", tariff.sewerage_volume_rate) _check_rate("waterStandingCharge", tariff.water_standing_charge) _check_rate("sewerageStandingCharge", tariff.sewerage_standing_charge) _check_rate("surfaceWaterCharge", tariff.surface_water_charge) share = tariff.return_to_sewer if not isinstance(share, int) or isinstance(share, bool) or share < 0 or share > 10000: raise ValueError("returnToSewer must be basis points from 0 to 10000, received %s" % (share,)) water = round_div(_exact(cubic_metres * tariff.water_volume_rate), 1000, mode) sewerage = round_div(_exact(cubic_metres * tariff.sewerage_volume_rate * share), 10_000_000, mode) water_standing = round_div(_exact(tariff.water_standing_charge * days), 365, mode) sewerage_standing = round_div(_exact(tariff.sewerage_standing_charge * days), 365, mode) surface = round_div(_exact(tariff.surface_water_charge * days), 365, mode) if surface_water_drains else 0 return WaterBill( cubic_metres=cubic_metres, days=days, water_volume_charge=money(water, currency), sewerage_volume_charge=money(sewerage, currency), water_standing_charge=money(water_standing, currency), sewerage_standing_charge=money(sewerage_standing, currency), surface_water_charge=money(surface, currency), total=money(water + sewerage + water_standing + sewerage_standing + surface, currency), )