def process_downgrade(plan: CustomerPlan) -> None: from zerver.lib.actions import do_change_realm_plan_type assert plan.customer.realm is not None do_change_realm_plan_type(plan.customer.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) plan.status = CustomerPlan.ENDED plan.save(update_fields=["status"])
def update_license_ledger_for_manual_plan( plan: CustomerPlan, event_time: datetime, licenses: Optional[int] = None, licenses_at_next_renewal: Optional[int] = None, ) -> None: if licenses is not None: assert plan.customer.realm is not None assert get_latest_seat_count(plan.customer.realm) <= licenses assert licenses > plan.licenses() LicenseLedger.objects.create(plan=plan, event_time=event_time, licenses=licenses, licenses_at_next_renewal=licenses) elif licenses_at_next_renewal is not None: assert plan.customer.realm is not None assert get_latest_seat_count( plan.customer.realm) <= licenses_at_next_renewal LicenseLedger.objects.create( plan=plan, event_time=event_time, licenses=plan.licenses(), licenses_at_next_renewal=licenses_at_next_renewal, ) else: raise AssertionError("Pass licenses or licenses_at_next_renewal")
def do_change_plan_status(plan: CustomerPlan, status: int) -> None: plan.status = status plan.save(update_fields=['status']) billing_logger.info( 'Change plan status: Customer.id: %s, CustomerPlan.id: %s, status: %s', plan.customer.id, plan.id, status, )
def make_end_of_cycle_updates_if_needed(plan: CustomerPlan, event_time: datetime) -> Optional[LicenseLedger]: last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by('-id').first() last_renewal = LicenseLedger.objects.filter(plan=plan, is_renewal=True) \ .order_by('-id').first().event_time next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal) if next_billing_cycle <= event_time: if plan.status == CustomerPlan.ACTIVE: return LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=next_billing_cycle, licenses=last_ledger_entry.licenses_at_next_renewal, licenses_at_next_renewal=last_ledger_entry.licenses_at_next_renewal) if plan.status == CustomerPlan.FREE_TRIAL: plan.invoiced_through = last_ledger_entry assert(plan.next_invoice_date is not None) plan.billing_cycle_anchor = plan.next_invoice_date.replace(microsecond=0) plan.status = CustomerPlan.ACTIVE plan.save(update_fields=["invoiced_through", "billing_cycle_anchor", "status"]) return LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=next_billing_cycle, licenses=last_ledger_entry.licenses_at_next_renewal, licenses_at_next_renewal=last_ledger_entry.licenses_at_next_renewal) if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: process_downgrade(plan) return None return last_ledger_entry
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 process_downgrade(plan: CustomerPlan) -> None: from zerver.lib.actions import do_change_plan_type do_change_plan_type(plan.customer.realm, Realm.LIMITED) plan.status = CustomerPlan.ENDED plan.save(update_fields=['status'])
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: if plan.invoicing_status == CustomerPlan.STARTED: raise NotImplementedError( 'Plan with invoicing_status==STARTED needs manual resolution.') make_end_of_cycle_updates_if_needed(plan, event_time) assert (plan.invoiced_through is not None) licenses_base = plan.invoiced_through.licenses invoice_item_created = False for ledger_entry in LicenseLedger.objects.filter( plan=plan, id__gt=plan.invoiced_through.id, event_time__lte=event_time).order_by('id'): price_args: Dict[str, int] = {} if ledger_entry.is_renewal: if plan.fixed_price is not None: price_args = {'amount': plan.fixed_price} else: assert (plan.price_per_license is not None) # needed for mypy price_args = { 'unit_amount': plan.price_per_license, 'quantity': ledger_entry.licenses } description = "Zulip Standard - renewal" elif ledger_entry.licenses != licenses_base: assert (plan.price_per_license) last_renewal = LicenseLedger.objects.filter( plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \ .order_by('-id').first().event_time period_end = start_of_next_billing_cycle(plan, ledger_entry.event_time) proration_fraction = (period_end - ledger_entry.event_time) / ( period_end - last_renewal) price_args = { 'unit_amount': int(plan.price_per_license * proration_fraction + .5), 'quantity': ledger_entry.licenses - licenses_base } description = "Additional license ({} - {})".format( ledger_entry.event_time.strftime('%b %-d, %Y'), period_end.strftime('%b %-d, %Y')) if price_args: plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.STARTED plan.save(update_fields=['invoicing_status', 'invoiced_through']) idempotency_key: Optional[str] = 'ledger_entry:{}'.format( ledger_entry.id) if settings.TEST_SUITE: idempotency_key = None stripe.InvoiceItem.create( currency='usd', customer=plan.customer.stripe_customer_id, description=description, discountable=False, period={ 'start': datetime_to_timestamp(ledger_entry.event_time), 'end': datetime_to_timestamp( start_of_next_billing_cycle(plan, ledger_entry.event_time)) }, idempotency_key=idempotency_key, **price_args) invoice_item_created = True plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.DONE plan.save(update_fields=['invoicing_status', 'invoiced_through']) licenses_base = ledger_entry.licenses if invoice_item_created: if plan.charge_automatically: billing_method = 'charge_automatically' days_until_due = None else: billing_method = 'send_invoice' days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE stripe_invoice = stripe.Invoice.create( auto_advance=True, billing=billing_method, customer=plan.customer.stripe_customer_id, days_until_due=days_until_due, statement_descriptor='Zulip Standard') stripe.Invoice.finalize_invoice(stripe_invoice) plan.next_invoice_date = next_invoice_date(plan) plan.save(update_fields=['next_invoice_date'])
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: if plan.invoicing_status == CustomerPlan.STARTED: raise NotImplementedError('Plan with invoicing_status==STARTED needs manual resolution.') add_plan_renewal_to_license_ledger_if_needed(plan, event_time) assert(plan.invoiced_through is not None) licenses_base = plan.invoiced_through.licenses invoice_item_created = False for ledger_entry in LicenseLedger.objects.filter(plan=plan, id__gt=plan.invoiced_through.id, event_time__lte=event_time).order_by('id'): price_args = {} # type: Dict[str, int] if ledger_entry.is_renewal: if plan.fixed_price is not None: price_args = {'amount': plan.fixed_price} else: assert(plan.price_per_license is not None) # needed for mypy price_args = {'unit_amount': plan.price_per_license, 'quantity': ledger_entry.licenses} description = "Zulip Standard - renewal" elif ledger_entry.licenses != licenses_base: assert(plan.price_per_license) last_renewal = LicenseLedger.objects.filter( plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time) \ .order_by('-id').first().event_time period_end = next_renewal_date(plan, ledger_entry.event_time) proration_fraction = (period_end - ledger_entry.event_time) / (period_end - last_renewal) price_args = {'unit_amount': int(plan.price_per_license * proration_fraction + .5), 'quantity': ledger_entry.licenses - licenses_base} description = "Additional license ({} - {})".format( ledger_entry.event_time.strftime('%b %-d, %Y'), period_end.strftime('%b %-d, %Y')) if price_args: plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.STARTED plan.save(update_fields=['invoicing_status', 'invoiced_through']) idempotency_key = 'ledger_entry:{}'.format(ledger_entry.id) # type: Optional[str] if settings.TEST_SUITE: idempotency_key = None stripe.InvoiceItem.create( currency='usd', customer=plan.customer.stripe_customer_id, description=description, discountable=False, period = {'start': datetime_to_timestamp(ledger_entry.event_time), 'end': datetime_to_timestamp(next_renewal_date(plan, ledger_entry.event_time))}, idempotency_key=idempotency_key, **price_args) invoice_item_created = True plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.DONE plan.save(update_fields=['invoicing_status', 'invoiced_through']) licenses_base = ledger_entry.licenses if invoice_item_created: if plan.charge_automatically: billing_method = 'charge_automatically' days_until_due = None else: billing_method = 'send_invoice' days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE stripe_invoice = stripe.Invoice.create( auto_advance=True, billing=billing_method, customer=plan.customer.stripe_customer_id, days_until_due=days_until_due, statement_descriptor='Zulip Standard') stripe.Invoice.finalize_invoice(stripe_invoice) plan.next_invoice_date = next_invoice_date(plan) plan.save(update_fields=['next_invoice_date'])
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_renewal = LicenseLedger.objects.filter(plan=plan, is_renewal=True) \ .order_by('-id').first().event_time next_billing_cycle = start_of_next_billing_cycle(plan, last_renewal) if next_billing_cycle <= event_time: if plan.status == CustomerPlan.ACTIVE: return None, LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=next_billing_cycle, licenses=last_ledger_entry.licenses_at_next_renewal, licenses_at_next_renewal=last_ledger_entry. licenses_at_next_renewal) if plan.status == CustomerPlan.FREE_TRIAL: plan.invoiced_through = last_ledger_entry assert (plan.next_invoice_date is not None) plan.billing_cycle_anchor = plan.next_invoice_date.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=last_ledger_entry.licenses_at_next_renewal, licenses_at_next_renewal=last_ledger_entry. 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( 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=last_ledger_entry.licenses_at_next_renewal, licenses_at_next_renewal=last_ledger_entry. licenses_at_next_renewal) RealmAuditLog.objects.create( realm=new_plan.customer.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.DOWNGRADE_AT_END_OF_CYCLE: process_downgrade(plan) return None, None return None, last_ledger_entry
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: if plan.invoicing_status == CustomerPlan.STARTED: raise NotImplementedError("Plan with invoicing_status==STARTED needs manual resolution.") if not plan.customer.stripe_customer_id: assert plan.customer.realm is not None raise BillingError( f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer." ) make_end_of_cycle_updates_if_needed(plan, event_time) if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT: invoiced_through_id = -1 licenses_base = None else: assert plan.invoiced_through is not None licenses_base = plan.invoiced_through.licenses invoiced_through_id = plan.invoiced_through.id invoice_item_created = False for ledger_entry in LicenseLedger.objects.filter( plan=plan, id__gt=invoiced_through_id, event_time__lte=event_time ).order_by("id"): price_args: Dict[str, int] = {} if ledger_entry.is_renewal: if plan.fixed_price is not None: price_args = {"amount": plan.fixed_price} else: assert plan.price_per_license is not None # needed for mypy price_args = { "unit_amount": plan.price_per_license, "quantity": ledger_entry.licenses, } description = f"{plan.name} - renewal" elif licenses_base is not None and ledger_entry.licenses != licenses_base: assert plan.price_per_license last_ledger_entry_renewal = ( LicenseLedger.objects.filter( plan=plan, is_renewal=True, event_time__lte=ledger_entry.event_time ) .order_by("-id") .first() ) assert last_ledger_entry_renewal is not None last_renewal = last_ledger_entry_renewal.event_time billing_period_end = start_of_next_billing_cycle(plan, ledger_entry.event_time) plan_renewal_or_end_date = get_plan_renewal_or_end_date(plan, ledger_entry.event_time) proration_fraction = (plan_renewal_or_end_date - ledger_entry.event_time) / ( billing_period_end - last_renewal ) price_args = { "unit_amount": int(plan.price_per_license * proration_fraction + 0.5), "quantity": ledger_entry.licenses - licenses_base, } description = "Additional license ({} - {})".format( ledger_entry.event_time.strftime("%b %-d, %Y"), plan_renewal_or_end_date.strftime("%b %-d, %Y"), ) if price_args: plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.STARTED plan.save(update_fields=["invoicing_status", "invoiced_through"]) stripe.InvoiceItem.create( currency="usd", customer=plan.customer.stripe_customer_id, description=description, discountable=False, period={ "start": datetime_to_timestamp(ledger_entry.event_time), "end": datetime_to_timestamp( get_plan_renewal_or_end_date(plan, ledger_entry.event_time) ), }, idempotency_key=get_idempotency_key(ledger_entry), **price_args, ) invoice_item_created = True plan.invoiced_through = ledger_entry plan.invoicing_status = CustomerPlan.DONE plan.save(update_fields=["invoicing_status", "invoiced_through"]) licenses_base = ledger_entry.licenses if invoice_item_created: if plan.charge_automatically: collection_method = "charge_automatically" days_until_due = None else: collection_method = "send_invoice" days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE stripe_invoice = stripe.Invoice.create( auto_advance=True, collection_method=collection_method, customer=plan.customer.stripe_customer_id, days_until_due=days_until_due, statement_descriptor=plan.name, ) stripe.Invoice.finalize_invoice(stripe_invoice) plan.next_invoice_date = next_invoice_date(plan) plan.save(update_fields=["next_invoice_date"])
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