Functional Weave
Code in Rust

subscriptions.churn@1.0.0

impl/python.py

2,210 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Optional

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency  ← from money.amount ^1.0.0 · built alongside by fune
from .subscriptions_churn_types import ChurnMetrics


def _rate(numerator: int, denominator: int) -> Optional[int]:
    return None if denominator == 0 else round_div(numerator * 10000, denominator, "half-up")


def _is_count(value: object) -> bool:
    return not isinstance(value, bool) and isinstance(value, int) and value >= 0


def churn_metrics(
    customers_at_start: int,
    customers_lost: int,
    mrr_at_start: Money,
    expansion_mrr: Money,
    contraction_mrr: Money,
    churned_mrr: Money,
) -> ChurnMetrics:
    """Logo churn, gross and net revenue churn, and gross and net revenue
    retention for one period, each as basis points of the starting cohort.
    """
    if not _is_count(customers_at_start) or not _is_count(customers_lost):
        raise ValueError(
            "customer counts must be whole numbers of 0 or more, received %r and %r"
            % (customers_at_start, customers_lost)
        )
    if customers_lost > customers_at_start:
        raise ValueError(
            "customers lost must not exceed customers at start, received %d of %d"
            % (customers_lost, customers_at_start)
        )
    for amount in (expansion_mrr, contraction_mrr, churned_mrr):
        assert_same_currency(mrr_at_start, amount)
    start = mrr_at_start.minor
    expansion = expansion_mrr.minor
    lost = contraction_mrr.minor + churned_mrr.minor
    if start < 0 or expansion < 0 or contraction_mrr.minor < 0 or churned_mrr.minor < 0:
        raise ValueError("MRR amounts must not be negative")
    if lost > start:
        raise ValueError(
            "contraction and churned MRR must not exceed MRR at start, received %d of %d" % (lost, start)
        )
    return ChurnMetrics(
        logo_churn_basis_points=_rate(customers_lost, customers_at_start),
        gross_revenue_churn_basis_points=_rate(lost, start),
        net_revenue_churn_basis_points=_rate(lost - expansion, start),
        gross_revenue_retention_basis_points=_rate(start - lost, start),
        net_revenue_retention_basis_points=_rate(start + expansion - lost, start),
    )