def downgrade_small_realms_behind_on_payments_as_needed() -> None: customers = Customer.objects.all().exclude(stripe_customer_id=None) for customer in customers: realm = customer.realm # For larger realms, we generally want to talk to the customer # before downgrading or cancelling invoices; so this logic only applies with 5. if get_latest_seat_count(realm) >= 5: continue if get_current_plan_by_customer(customer) is not None: # Only customers with last 2 invoices open should be downgraded. if not customer_has_last_n_invoices_open(customer, 2): continue # We've now decided to downgrade this customer and void all invoices, and the below will execute this. downgrade_now_without_creating_additional_invoices(realm) void_all_open_invoices(realm) context: Dict[str, Union[str, Realm]] = { "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}", "realm": realm, } send_email_to_billing_admins_and_realm_owners( "zerver/emails/realm_auto_downgraded", realm, from_name=FromAddress.security_email_from_name(language=realm.default_language), from_address=FromAddress.tokenized_no_reply_address(), language=realm.default_language, context=context, ) else: if customer_has_last_n_invoices_open(customer, 1): void_all_open_invoices(realm)
def initial_upgrade(request: HttpRequest) -> HttpResponse: if not settings.BILLING_ENABLED: return render(request, "404.html") user = request.user customer = get_customer_by_realm(user.realm) if customer is not None and get_current_plan_by_customer( customer) is not None: return HttpResponseRedirect(reverse('corporate.views.billing_home')) percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: percent_off = customer.default_discount seat_count = get_latest_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context = { 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'email': user.delivery_email, 'seat_count': seat_count, 'signed_seat_count': signed_seat_count, 'salt': salt, 'min_invoiced_licenses': max(seat_count, MIN_INVOICED_LICENSES), 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", 'page_params': { 'seat_count': seat_count, 'annual_price': 8000, 'monthly_price': 800, 'percent_off': float(percent_off), }, } # type: Dict[str, Any] response = render(request, 'corporate/upgrade.html', context=context) return response
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context: Dict[str, Any] = {'admin_access': False} return render(request, 'corporate/billing.html', context=context) context = { 'admin_access': True, 'has_active_plan': False, } plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now) if last_ledger_entry is not None: plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] free_trial = plan.status == CustomerPlan.FREE_TRIAL downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' context.update({ 'plan_name': plan_name, 'has_active_plan': True, 'free_trial': free_trial, 'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.), 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, 'CustomerPlan': CustomerPlan, 'onboarding': request.GET.get("onboarding") is not None, }) return render(request, 'corporate/billing.html', context=context)
def initial_upgrade( request: HttpRequest, onboarding: bool = REQ(default=False, json_validator=check_bool) ) -> HttpResponse: user = request.user assert user.is_authenticated if not settings.BILLING_ENABLED or user.is_guest: return render(request, "404.html", status=404) billing_page_url = reverse(billing_home) customer = get_customer_by_realm(user.realm) if customer is not None and ( get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending ): if onboarding: billing_page_url = f"{billing_page_url}?onboarding=true" return HttpResponseRedirect(billing_page_url) if is_sponsored_realm(user.realm): return HttpResponseRedirect(billing_page_url) percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: percent_off = customer.default_discount seat_count = get_latest_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context: Dict[str, Any] = { "realm": user.realm, "email": user.delivery_email, "seat_count": seat_count, "signed_seat_count": signed_seat_count, "salt": salt, "min_invoiced_licenses": max(seat_count, MIN_INVOICED_LICENSES), "default_invoice_days_until_due": DEFAULT_INVOICE_DAYS_UNTIL_DUE, "plan": "Zulip Standard", "free_trial_days": settings.FREE_TRIAL_DAYS, "onboarding": onboarding, "page_params": { "seat_count": seat_count, "annual_price": 8000, "monthly_price": 800, "percent_off": float(percent_off), }, "realm_org_type": user.realm.org_type, "sorted_org_types": sorted( ( [org_type_name, org_type] for (org_type_name, org_type) in Realm.ORG_TYPES.items() if not org_type.get("hidden") ), key=lambda d: d[1]["display_order"], ), } response = render(request, "corporate/upgrade.html", context=context) return response
def initial_upgrade(request: HttpRequest) -> HttpResponse: user = request.user if not settings.BILLING_ENABLED or user.is_guest: return render(request, "404.html", status=404) billing_page_url = reverse(billing_home) customer = get_customer_by_realm(user.realm) if customer is not None and ( get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending ): if request.GET.get("onboarding") is not None: billing_page_url = f"{billing_page_url}?onboarding=true" return HttpResponseRedirect(billing_page_url) if user.realm.plan_type == user.realm.STANDARD_FREE: return HttpResponseRedirect(billing_page_url) percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: percent_off = customer.default_discount seat_count = get_latest_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context: Dict[str, Any] = { "realm": user.realm, "publishable_key": STRIPE_PUBLISHABLE_KEY, "email": user.delivery_email, "seat_count": seat_count, "signed_seat_count": signed_seat_count, "salt": salt, "min_invoiced_licenses": max(seat_count, MIN_INVOICED_LICENSES), "default_invoice_days_until_due": DEFAULT_INVOICE_DAYS_UNTIL_DUE, "plan": "Zulip Standard", "free_trial_days": settings.FREE_TRIAL_DAYS, "onboarding": request.GET.get("onboarding") is not None, "page_params": { "seat_count": seat_count, "annual_price": 8000, "monthly_price": 800, "percent_off": float(percent_off), }, } response = render(request, "corporate/upgrade.html", context=context) return response
def initial_upgrade(request: HttpRequest) -> HttpResponse: user = request.user if not settings.BILLING_ENABLED or user.is_guest: return render(request, "404.html", status=404) customer = get_customer_by_realm(user.realm) if customer is not None and (get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending): billing_page_url = reverse('corporate.views.billing_home') if request.GET.get("onboarding") is not None: billing_page_url = f"{billing_page_url}?onboarding=true" return HttpResponseRedirect(billing_page_url) percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: percent_off = customer.default_discount seat_count = get_latest_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context: Dict[str, Any] = { 'realm': user.realm, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'email': user.delivery_email, 'seat_count': seat_count, 'signed_seat_count': signed_seat_count, 'salt': salt, 'min_invoiced_licenses': max(seat_count, MIN_INVOICED_LICENSES), 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", "free_trial_days": settings.FREE_TRIAL_DAYS, "onboarding": request.GET.get("onboarding") is not None, 'page_params': { 'seat_count': seat_count, 'annual_price': 8000, 'monthly_price': 800, 'percent_off': float(percent_off), }, } response = render(request, 'corporate/upgrade.html', context=context) return response
def downgrade_small_realms_behind_on_payments_as_needed() -> None: customers = Customer.objects.all() for customer in customers: realm = customer.realm # For larger realms, we generally want to talk to the customer # before downgrading; so this logic only applies with 5. if get_latest_seat_count(realm) >= 5: continue if get_current_plan_by_customer(customer) is None: continue due_invoice_count = 0 for invoice in stripe.Invoice.list( customer=customer.stripe_customer_id, limit=2): if invoice.status == "open": due_invoice_count += 1 # Customers with only 1 overdue invoice are ignored. if due_invoice_count < 2: continue # We've now decided to downgrade this customer and void all invoices, and the below will execute this. downgrade_now_without_creating_additional_invoices(realm) void_all_open_invoices(realm) context: Dict[str, str] = { "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}", "realm": realm, } send_email_to_billing_admins_and_realm_owners( "zerver/emails/realm_auto_downgraded", realm, from_name=FromAddress.security_email_from_name( language=realm.default_language), from_address=FromAddress.tokenized_no_reply_address(), language=realm.default_language, context=context, )
def process_initial_upgrade(user: UserProfile, licenses: int, automanage_licenses: bool, billing_schedule: int, stripe_token: Optional[str]) -> None: realm = user.realm customer = update_or_create_stripe_customer(user, stripe_token=stripe_token) charge_automatically = stripe_token is not None free_trial = settings.FREE_TRIAL_DAYS not in (None, 0) if get_current_plan_by_customer(customer) is not None: # Unlikely race condition from two people upgrading (clicking "Make payment") # at exactly the same time. Doesn't fully resolve the race condition, but having # a check here reduces the likelihood. billing_logger.warning( "Customer %s trying to upgrade, but has an active subscription", customer, ) raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) billing_cycle_anchor, next_invoice_date, period_end, price_per_license = compute_plan_parameters( automanage_licenses, billing_schedule, customer.default_discount, free_trial) # The main design constraint in this function is that if you upgrade with a credit card, and the # charge fails, everything should be rolled back as if nothing had happened. This is because we # expect frequent card failures on initial signup. # Hence, if we're going to charge a card, do it at the beginning, even if we later may have to # adjust the number of licenses. if charge_automatically: if not free_trial: stripe_charge = stripe.Charge.create( amount=price_per_license * licenses, currency='usd', customer=customer.stripe_customer_id, description="Upgrade to Zulip Standard, ${} x {}".format( price_per_license / 100, licenses), receipt_email=user.delivery_email, statement_descriptor='Zulip Standard') # Not setting a period start and end, but maybe we should? Unclear what will make things # most similar to the renewal case from an accounting perspective. description = "Payment (Card ending in {})".format( cast(stripe.Card, stripe_charge.source).last4) stripe.InvoiceItem.create(amount=price_per_license * licenses * -1, currency='usd', customer=customer.stripe_customer_id, description=description, discountable=False) # TODO: The correctness of this relies on user creation, deactivation, etc being # in a transaction.atomic() with the relevant RealmAuditLog entries with transaction.atomic(): # billed_licenses can greater than licenses if users are added between the start of # this function (process_initial_upgrade) and now billed_licenses = max(get_latest_seat_count(realm), licenses) plan_params = { 'automanage_licenses': automanage_licenses, 'charge_automatically': charge_automatically, 'price_per_license': price_per_license, 'discount': customer.default_discount, 'billing_cycle_anchor': billing_cycle_anchor, 'billing_schedule': billing_schedule, 'tier': CustomerPlan.STANDARD } if free_trial: plan_params['status'] = CustomerPlan.FREE_TRIAL plan = CustomerPlan.objects.create(customer=customer, next_invoice_date=next_invoice_date, **plan_params) ledger_entry = LicenseLedger.objects.create( plan=plan, is_renewal=True, event_time=billing_cycle_anchor, licenses=billed_licenses, licenses_at_next_renewal=billed_licenses) plan.invoiced_through = ledger_entry plan.save(update_fields=['invoiced_through']) RealmAuditLog.objects.create( realm=realm, acting_user=user, event_time=billing_cycle_anchor, event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED, extra_data=ujson.dumps(plan_params)) if not free_trial: stripe.InvoiceItem.create( currency='usd', customer=customer.stripe_customer_id, description='Zulip Standard', discountable=False, period={ 'start': datetime_to_timestamp(billing_cycle_anchor), 'end': datetime_to_timestamp(period_end) }, quantity=billed_licenses, unit_amount=price_per_license) if 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=customer.stripe_customer_id, days_until_due=days_until_due, statement_descriptor='Zulip Standard') stripe.Invoice.finalize_invoice(stripe_invoice) from zerver.lib.actions import do_change_plan_type do_change_plan_type(realm, Realm.STANDARD)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user assert user.is_authenticated customer = get_customer_by_realm(user.realm) context: Dict[str, Any] = { "admin_access": user.has_billing_access, "has_active_plan": False, } if user.realm.plan_type == user.realm.STANDARD_FREE: context["is_sponsored"] = True return render(request, "corporate/billing.html", context=context) if customer is None: from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) if customer.sponsorship_pending: context["sponsorship_pending"] = True return render(request, "corporate/billing.html", context=context) if not CustomerPlan.objects.filter(customer=customer).exists(): from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) if not user.has_billing_access: return render(request, "corporate/billing.html", context=context) plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( plan, now) if last_ledger_entry is not None: if new_plan is not None: # nocoverage plan = new_plan assert plan is not None # for mypy downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE switch_to_annual_at_end_of_cycle = ( plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE) licenses = last_ledger_entry.licenses licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal seat_count = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = "{dt:%B} {dt.day}, {dt.year}".format( dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically assert customer.stripe_customer_id is not None # for mypy stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = "Billed by invoice" context.update( plan_name=plan.name, has_active_plan=True, free_trial=plan.is_free_trial(), downgrade_at_end_of_cycle=downgrade_at_end_of_cycle, automanage_licenses=plan.automanage_licenses, switch_to_annual_at_end_of_cycle= switch_to_annual_at_end_of_cycle, licenses=licenses, licenses_at_next_renewal=licenses_at_next_renewal, seat_count=seat_count, renewal_date=renewal_date, renewal_amount=cents_to_dollar_string(renewal_cents), payment_method=payment_method, charge_automatically=charge_automatically, publishable_key=STRIPE_PUBLISHABLE_KEY, stripe_email=stripe_customer.email, CustomerPlan=CustomerPlan, onboarding=request.GET.get("onboarding") is not None, ) return render(request, "corporate/billing.html", context=context)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) context: Dict[str, Any] = { "admin_access": user.has_billing_access, 'has_active_plan': False, } if user.realm.plan_type == user.realm.STANDARD_FREE: context["is_sponsored"] = True return render(request, 'corporate/billing.html', context=context) if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if customer.sponsorship_pending: context["sponsorship_pending"] = True return render(request, 'corporate/billing.html', context=context) if not CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.has_billing_access: return render(request, 'corporate/billing.html', context=context) plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( plan, now) if last_ledger_entry is not None: if new_plan is not None: # nocoverage plan = new_plan assert (plan is not None) # for mypy free_trial = plan.status == CustomerPlan.FREE_TRIAL downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE switch_to_annual_at_end_of_cycle = plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' context.update({ 'plan_name': plan.name, 'has_active_plan': True, 'free_trial': free_trial, 'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle, 'automanage_licenses': plan.automanage_licenses, 'switch_to_annual_at_end_of_cycle': switch_to_annual_at_end_of_cycle, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': f'{renewal_cents / 100.:,.2f}', 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, 'CustomerPlan': CustomerPlan, 'onboarding': request.GET.get("onboarding") is not None, }) return render(request, 'corporate/billing.html', context=context)