def start_of_next_billing_cycle(plan: CustomerPlan, event_time: datetime) -> datetime: if plan.is_free_trial(): assert plan.next_invoice_date is not None # for mypy return plan.next_invoice_date months_per_period = { CustomerPlan.ANNUAL: 12, CustomerPlan.MONTHLY: 1, }[plan.billing_schedule] periods = 1 dt = plan.billing_cycle_anchor while dt <= event_time: dt = add_months(plan.billing_cycle_anchor, months_per_period * periods) periods += 1 return dt
def make_end_of_cycle_updates_if_needed( plan: CustomerPlan, event_time: datetime ) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]: last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first() last_ledger_renewal = ( LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first() ) assert last_ledger_renewal is not None last_renewal = last_ledger_renewal.event_time if plan.is_free_trial() or plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS: assert plan.next_invoice_date is not None next_billing_cycle = plan.next_invoice_date else: next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal) if next_billing_cycle <= event_time and last_ledger_entry is not None: licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal assert licenses_at_next_renewal is not None if plan.status == CustomerPlan.ACTIVE: return None, LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=next_billing_cycle, licenses=licenses_at_next_renewal, licenses_at_next_renewal=licenses_at_next_renewal, ) if plan.is_free_trial(): plan.invoiced_through = last_ledger_entry plan.billing_cycle_anchor = next_billing_cycle.replace(microsecond=0) plan.status = CustomerPlan.ACTIVE plan.save(update_fields=["invoiced_through", "billing_cycle_anchor", "status"]) return None, LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=next_billing_cycle, licenses=licenses_at_next_renewal, licenses_at_next_renewal=licenses_at_next_renewal, ) if plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: if plan.fixed_price is not None: # nocoverage raise NotImplementedError("Can't switch fixed priced monthly plan to annual.") plan.status = CustomerPlan.ENDED plan.save(update_fields=["status"]) discount = plan.customer.default_discount or plan.discount _, _, _, price_per_license = compute_plan_parameters( tier=plan.tier, automanage_licenses=plan.automanage_licenses, billing_schedule=CustomerPlan.ANNUAL, discount=plan.discount, ) new_plan = CustomerPlan.objects.create( customer=plan.customer, billing_schedule=CustomerPlan.ANNUAL, automanage_licenses=plan.automanage_licenses, charge_automatically=plan.charge_automatically, price_per_license=price_per_license, discount=discount, billing_cycle_anchor=next_billing_cycle, tier=plan.tier, status=CustomerPlan.ACTIVE, next_invoice_date=next_billing_cycle, invoiced_through=None, invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT, ) new_plan_ledger_entry = LicenseLedger.objects.create( plan=new_plan, is_renewal=True, event_time=next_billing_cycle, licenses=licenses_at_next_renewal, licenses_at_next_renewal=licenses_at_next_renewal, ) realm = new_plan.customer.realm assert realm is not None RealmAuditLog.objects.create( realm=realm, event_time=event_time, event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN, extra_data=orjson.dumps( { "monthly_plan_id": plan.id, "annual_plan_id": new_plan.id, } ).decode(), ) return new_plan, new_plan_ledger_entry if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS: standard_plan = plan standard_plan.end_date = next_billing_cycle standard_plan.status = CustomerPlan.ENDED standard_plan.save(update_fields=["status", "end_date"]) (_, _, _, plus_plan_price_per_license) = compute_plan_parameters( CustomerPlan.PLUS, standard_plan.automanage_licenses, standard_plan.billing_schedule, standard_plan.customer.default_discount, ) plus_plan_billing_cycle_anchor = standard_plan.end_date.replace(microsecond=0) plus_plan = CustomerPlan.objects.create( customer=standard_plan.customer, status=CustomerPlan.ACTIVE, automanage_licenses=standard_plan.automanage_licenses, charge_automatically=standard_plan.charge_automatically, price_per_license=plus_plan_price_per_license, discount=standard_plan.customer.default_discount, billing_schedule=standard_plan.billing_schedule, tier=CustomerPlan.PLUS, billing_cycle_anchor=plus_plan_billing_cycle_anchor, invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT, next_invoice_date=plus_plan_billing_cycle_anchor, ) standard_plan_last_ledger = ( LicenseLedger.objects.filter(plan=standard_plan).order_by("id").last() ) licenses_for_plus_plan = standard_plan_last_ledger.licenses_at_next_renewal plus_plan_ledger_entry = LicenseLedger.objects.create( plan=plus_plan, is_renewal=True, event_time=plus_plan_billing_cycle_anchor, licenses=licenses_for_plus_plan, licenses_at_next_renewal=licenses_for_plus_plan, ) return plus_plan, plus_plan_ledger_entry if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: process_downgrade(plan) return None, None return None, last_ledger_entry