Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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,
        )
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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)
Ejemplo n.º 10
0
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)