import re from typing import Optional from .insurance_ipt_data import IPT_RATES, IPT_RATES_HISTORY from .insurance_ipt_types import IptBand, IptBreakdown from .math_round_div import RoundingMode, round_div from .money_amount import Money, money ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}") def insurance_premium_tax( amount: Money, includes_tax: bool, band: IptBand, on_date: str, mode: RoundingMode ) -> IptBreakdown: """Add UK Insurance Premium Tax to a net premium, or take it out of a gross one, at the rate in force on the date the insurer receives the premium. Only one figure is ever rounded - the tax when adding, the net when taking out - and the other is the difference, so net + tax is always the gross. Taking 12% off a gross premium is the classic error: the tax inside 112.00 is 12.00, not 13.44. """ if band not in ("standard", "higher"): raise ValueError('unknown IPT band "%s": use standard or higher' % (band,)) if not isinstance(on_date, str) or not ISO_DATE.fullmatch(on_date): raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,)) rule = None for r in IPT_RATES: if r.band != band or on_date < r.valid_from: continue if r.valid_to is not None and on_date > r.valid_to: continue rule = r if rule is None: # A history=current build keeps only the rates in force; answering an # older date with today's rate would be exactly the wrong answer. if IPT_RATES_HISTORY != "full": earliest: Optional[str] = None for r in IPT_RATES: if r.band == band and (earliest is None or r.valid_from < earliest): earliest = r.valid_from if earliest is not None and on_date < earliest: raise ValueError( "no IPT rule for %s on %s: this build was installed with history=%s, " "so it only carries rates from %s. Reinstall with history=full for older premiums." % (band, on_date, IPT_RATES_HISTORY, earliest) ) raise ValueError("no IPT rule for %s on %s" % (band, on_date)) bp = rule.basis_points if includes_tax: gross = amount net = money(round_div(amount.minor * 10000, 10000 + bp, mode), amount.currency) tax = money(amount.minor - net.minor, amount.currency) else: net = amount tax = money(round_div(amount.minor * bp, 10000, mode), amount.currency) gross = money(amount.minor + tax.minor, amount.currency) return IptBreakdown(net=net, tax=tax, gross=gross, basis_points=bp, band=band, valid_from=rule.valid_from)