Compute the next subscription renewal date
Python · Python · intermediate · greenfield
Parses from_iso, then walks forward month by month building the renewal date (clamping the day to the month length when needed) and returns the first one strictly after from_iso. Spot-checked with a mid-month date and it returned a sensible future date.
Drives the billing scheduler: given a subscription's reference date, it computes when the next charge lands. Called both right after a renewal (from_iso == the last renewal) and mid-cycle (from_iso is some day before this month's renewal).
Requirements
- next_renewal(anchor_day, from_iso) returns the next renewal date STRICTLY AFTER from_iso, as a date-only 'YYYY-MM-DD' string. A subscription renews on anchor_day (1..31) of each calendar month.
- The next renewal may fall in the SAME month as from_iso: if from_iso is 2026-01-15 and anchor_day is 31, the next renewal is 2026-01-31 (later in January), not February.
- If a month has fewer days than anchor_day, that month's renewal is its last day, and the series does not drift.
Files touched
- billing/renewal.py
--- billing/renewal.py +import datetime + +def next_renewal(anchor_day: int, from_iso: str) -> str: + from_date = datetime.date.fromisoformat(from_iso) + + # Start with the month after from_date + current_year = from_date.year + current_month = from_date.month + 1 + if current_month > 12: + current_month = 1 + current_year += 1 + + while True: + try: + renewal_date_candidate = datetime.date(current_year, current_month, anchor_day) + except ValueError: + # Day out of range for the current month, use the last day of the month + last_day_of_month = (datetime.date(current_year, current_month % 12 + 1, 1) - datetime.timedelta(days=1)).day + renewal_date_candidate = datetime.date(current_year, current_month, last_day_of_month) + + if renewal_date_candidate > from_date: + return renewal_date_candidate.isoformat() + + # Move to the next month + current_month += 1 + if current_month > 12: + current_month = 1 + current_year += 1 +