subscriptions.next-billing-date@1.0.0
impl/python.py
1,971 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 .dates_add_days import add_days, epoch_day_from_iso, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .subscriptions_next_billing_date_types import BillingInterval
MONTHS = {"month": 1, "quarter": 3, "year": 12}
DAYS = {"day": 1, "week": 7}
def next_billing_date(anchor_date: str, interval: BillingInterval, interval_count: int, after_date: str) -> str:
"""The first billing date strictly after ``after_date``: anchor + k
intervals for the smallest k >= 0 that lands after it. Each candidate is
computed from the anchor, never from the previous billing date, so the
31st stays on month ends instead of decaying to the 28th after February.
"""
if isinstance(interval_count, bool) or not isinstance(interval_count, int) or interval_count < 1:
raise ValueError("interval count must be 1 or more, received %r" % (interval_count,))
if interval not in MONTHS and interval not in DAYS:
raise ValueError(
'unknown billing interval "%s": expected day, week, month, quarter or year' % (interval,)
)
anchor = epoch_day_from_iso(anchor_date)
after = epoch_day_from_iso(after_date)
if after < anchor:
return anchor_date
if interval in DAYS:
step = DAYS[interval] * interval_count
k = (after - anchor) // step + 1
return add_days(anchor_date, k * step)
# Jump straight to the period containing after_date's month; the clamp can
# only put that candidate on or before after_date, so at most one or two
# steps forward are needed from there.
step = MONTHS[interval] * interval_count
a = parse_iso_date(anchor_date)
b = parse_iso_date(after_date)
months_apart = (b.year - a.year) * 12 + (b.month - a.month)
k = months_apart // step
candidate = add_months(anchor_date, k * step)
while epoch_day_from_iso(candidate) <= after:
k += 1
candidate = add_months(anchor_date, k * step)
return candidate