from .dates_days_between import days_between from .math_round_div import RoundingMode, round_div from .money_add import add_money from .money_amount import Money, assert_same_currency, money from .subscriptions_proration_types import PlanChangeProration def prorate_change( old_price: Money, new_price: Money, period_start: str, period_end: str, change_date: str, mode: RoundingMode, ) -> PlanChangeProration: """Stripe-style proration for a mid-cycle plan change, by whole days: a credit for the unused remainder of the old plan and a charge for the same remainder on the new plan, each rounded once from the exact fraction. """ total_days = days_between(period_start, period_end) if total_days <= 0: raise ValueError( "period end must be after period start, received %s to %s" % (period_start, period_end) ) elapsed = days_between(period_start, change_date) if elapsed < 0 or elapsed > total_days: raise ValueError( "change date must be within the billing period, received %s for %s to %s" % (change_date, period_start, period_end) ) assert_same_currency(old_price, new_price) remaining_days = total_days - elapsed credit = money(-round_div(old_price.minor * remaining_days, total_days, mode), old_price.currency) charge = money(round_div(new_price.minor * remaining_days, total_days, mode), new_price.currency) return PlanChangeProration( total_days=total_days, remaining_days=remaining_days, credit=credit, charge=charge, net=add_money(charge, credit), )