from .math_round_div import round_div from .money_amount import Money, assert_same_currency, money from .subscriptions_ltv_cac_types import LtvCac MAX_EXACT = 9007199254740991 def _check_rate(name: str, value: int) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > 10000: raise ValueError("%s must be between 0 and 10000, received %r" % (name, value)) def ltv_cac(arpa: Money, gross_margin_basis_points: int, monthly_churn_basis_points: int, cac: Money) -> LtvCac: """LTV, LTV:CAC and CAC payback, each computed as one exact division from the inputs so no figure inherits another's rounding. """ assert_same_currency(arpa, cac) if arpa.minor < 0 or cac.minor < 0: raise ValueError("ARPA and CAC must not be negative, received %d and %d" % (arpa.minor, cac.minor)) _check_rate("gross margin basis points", gross_margin_basis_points) _check_rate("monthly churn basis points", monthly_churn_basis_points) # Monthly margin per account, in minor units x 10000. margin = arpa.minor * gross_margin_basis_points churn = monthly_churn_basis_points if margin * 10000 > MAX_EXACT or cac.minor * 10000 > MAX_EXACT or churn * cac.minor > MAX_EXACT: # Python would carry on exactly; TypeScript could not, and the three must agree. raise ValueError("amounts too large for exact arithmetic (2^53 - 1)") return LtvCac( ltv=None if churn == 0 else money(round_div(margin, churn, "half-up"), arpa.currency), ltv_to_cac_basis_points=None if churn == 0 or cac.minor == 0 else round_div(margin * 10000, churn * cac.minor, "half-up"), payback_months=None if margin == 0 else round_div(cac.minor * 10000, margin, "up"), )