import re from .money_amount import Money, money from .property_deposit_cap_data import DEPOSIT_CAPS, DEPOSIT_CAPS_HISTORY, DepositCapRule from .property_deposit_cap_types import DepositCap, RentPeriod _ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}") # Keeps annual rent x 6 exact as a JavaScript number; the same limit here. _MAX_MINOR = 1_000_000_000_000 def _find_rule(jurisdiction: str, on_date: str) -> DepositCapRule: best = None earliest = None for rule in DEPOSIT_CAPS: if rule.jurisdiction != jurisdiction: continue if earliest is None or rule.valid_from < earliest: earliest = rule.valid_from 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 not None: return best # A build installed with a pruned history must not answer an old date with # today's rule, so it says what happened instead. if DEPOSIT_CAPS_HISTORY != "full" and earliest is not None and on_date < earliest: raise ValueError( "no deposit cap rule for %s on %s: this build was installed with history=%s, so it only carries rules from %s" % (jurisdiction, on_date, DEPOSIT_CAPS_HISTORY, earliest) ) raise ValueError("no deposit cap rule for %s on %s" % (jurisdiction, on_date)) def deposit_cap(rent: Money, rent_period: RentPeriod, jurisdiction: str, on_date: str) -> DepositCap: """The maximum tenancy deposit: five weeks' rent below the annual-rent threshold, six at or above it. The threshold is tested on the exact annual rent, and the cap is annual rent x weeks / 52 rounded down once, so it can never sit a fraction of a penny over the statutory limit.""" 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,)) if rent.currency != "GBP": raise ValueError("rent must be in GBP, received %s" % (rent.currency,)) if rent.minor < 0: raise ValueError("rent must not be negative, received %r" % (rent.minor,)) if rent_period == "weekly": annual = rent.minor * 52 elif rent_period == "monthly": annual = rent.minor * 12 elif rent_period == "annual": annual = rent.minor else: raise ValueError('unknown rent period "%s"' % (rent_period,)) if annual > _MAX_MINOR: raise ValueError("annual rent must be at most %d minor units" % (_MAX_MINOR,)) rule = _find_rule(jurisdiction, on_date) weeks = rule.weeks_below if annual < rule.annual_rent_threshold_minor else rule.weeks_at_or_above return DepositCap( annual_rent=money(annual, "GBP"), weekly_rent=money(annual // 52, "GBP"), weeks=weeks, maximum=money(annual * weeks // 52, "GBP"), )