Exemplo n.º 1
0
def change_plan_status(
    request: HttpRequest,
    user: UserProfile,
    status: int = REQ("status", validator=check_int)
) -> HttpResponse:
    assert (status in [
        CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
        CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE, CustomerPlan.ENDED
    ])

    plan = get_current_plan_by_realm(user.realm)
    assert (plan is not None)  # for mypy

    if status == CustomerPlan.ACTIVE:
        assert (plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
        do_change_plan_status(plan, status)
    elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
        assert (plan.status == CustomerPlan.ACTIVE)
        downgrade_at_the_end_of_billing_cycle(user.realm)
    elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
        assert (plan.billing_schedule == CustomerPlan.MONTHLY)
        assert (plan.status == CustomerPlan.ACTIVE)
        assert (plan.fixed_price is None)
        do_change_plan_status(plan, status)
    elif status == CustomerPlan.ENDED:
        assert (plan.status == CustomerPlan.FREE_TRIAL)
        downgrade_now_without_creating_additional_invoices(user.realm)
    return json_success()
Exemplo n.º 2
0
def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None:
    Customer.objects.update_or_create(realm=realm, defaults={"default_discount": discount})
    plan = get_current_plan_by_realm(realm)
    if plan is not None:
        plan.price_per_license = get_price_per_license(plan.tier, plan.billing_schedule, discount)
        plan.discount = discount
        plan.save(update_fields=["price_per_license", "discount"])
Exemplo n.º 3
0
def attach_discount_to_realm(realm: Realm, discount: Decimal, *,
                             acting_user: Optional[UserProfile]) -> None:
    customer = get_customer_by_realm(realm)
    old_discount: Optional[Decimal] = None
    if customer is not None:
        old_discount = customer.default_discount
        customer.default_discount = discount
        customer.save(update_fields=["default_discount"])
    else:
        Customer.objects.create(realm=realm, default_discount=discount)
    plan = get_current_plan_by_realm(realm)
    if plan is not None:
        plan.price_per_license = get_price_per_license(plan.tier,
                                                       plan.billing_schedule,
                                                       discount)
        plan.discount = discount
        plan.save(update_fields=["price_per_license", "discount"])
    RealmAuditLog.objects.create(
        realm=realm,
        acting_user=acting_user,
        event_type=RealmAuditLog.REALM_DISCOUNT_CHANGED,
        event_time=timezone_now(),
        extra_data={
            "old_discount": old_discount,
            "new_discount": discount
        },
    )
Exemplo n.º 4
0
def update_license_ledger_if_needed(realm: Realm, event_time: datetime) -> None:
    plan = get_current_plan_by_realm(realm)
    if plan is None:
        return
    if not plan.automanage_licenses:
        return
    update_license_ledger_for_automanaged_plan(realm, plan, event_time)
Exemplo n.º 5
0
def switch_realm_from_standard_to_plus_plan(realm: Realm) -> None:
    standard_plan = get_current_plan_by_realm(realm)

    if (
        not standard_plan
        or standard_plan.status != CustomerPlan.ACTIVE
        or standard_plan.tier != CustomerPlan.STANDARD
    ):
        raise BillingError("Organization does not have an active Standard plan")

    if not standard_plan.customer.stripe_customer_id:
        raise BillingError("Organization missing Stripe customer.")

    plan_switch_time = timezone_now()

    standard_plan.status = CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS
    standard_plan.next_invoice_date = plan_switch_time
    standard_plan.save(update_fields=["status", "next_invoice_date"])

    standard_plan_next_renewal_date = start_of_next_billing_cycle(standard_plan, plan_switch_time)

    standard_plan_last_renewal_ledger = (
        LicenseLedger.objects.filter(is_renewal=True, plan=standard_plan).order_by("id").last()
    )
    assert standard_plan_last_renewal_ledger is not None
    assert standard_plan.price_per_license is not None
    standard_plan_last_renewal_amount = (
        standard_plan_last_renewal_ledger.licenses * standard_plan.price_per_license
    )
    standard_plan_last_renewal_date = standard_plan_last_renewal_ledger.event_time
    unused_proration_fraction = 1 - (plan_switch_time - standard_plan_last_renewal_date) / (
        standard_plan_next_renewal_date - standard_plan_last_renewal_date
    )
    amount_to_credit_back_to_realm = math.ceil(
        standard_plan_last_renewal_amount * unused_proration_fraction
    )
    stripe.Customer.create_balance_transaction(
        standard_plan.customer.stripe_customer_id,
        amount=-1 * amount_to_credit_back_to_realm,
        currency="usd",
        description="Credit from early termination of Standard plan",
    )
    invoice_plan(standard_plan, plan_switch_time)
    plus_plan = get_current_plan_by_realm(realm)
    assert plus_plan is not None  # for mypy
    invoice_plan(plus_plan, plan_switch_time)
Exemplo n.º 6
0
def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None:
    plan = get_current_plan_by_realm(realm)
    if plan is None:
        return

    process_downgrade(plan)
    plan.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last()
    plan.next_invoice_date = next_invoice_date(plan)
    plan.save(update_fields=["invoiced_through", "next_invoice_date"])
Exemplo n.º 7
0
def check_spare_licenses_available_for_adding_new_users(
        realm: Realm, number_of_users_to_add: int) -> None:
    plan = get_current_plan_by_realm(realm)
    if (plan is None or plan.automanage_licenses
            or plan.customer.exempt_from_from_license_number_check):
        return

    if plan.licenses() < get_latest_seat_count(realm) + number_of_users_to_add:
        raise LicenseLimitError()
Exemplo n.º 8
0
def change_plan_status(
    request: HttpRequest,
    user: UserProfile,
    status: int = REQ("status", validator=check_int)
) -> HttpResponse:
    plan = get_current_plan_by_realm(user.realm)
    assert (plan is not None)  # for mypy
    do_change_plan_status(plan, status)
    return json_success()
Exemplo n.º 9
0
def ensure_realm_does_not_have_active_plan(realm: Customer) -> None:
    if get_current_plan_by_realm(realm) 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(
            "Upgrade of %s failed because of existing active plan.",
            realm.string_id,
        )
        raise UpgradeWithExistingPlanError()
Exemplo n.º 10
0
def downgrade_for_realm_deactivation(realm: Realm) -> None:
    plan = get_current_plan_by_realm(realm)
    if plan is None:
        return

    process_downgrade(plan)
    plan.invoiced_through = LicenseLedger.objects.filter(
        plan=plan).order_by('id').last()
    plan.next_invoice_date = next_invoice_date(plan)
    plan.save(update_fields=["invoiced_through", "next_invoice_date"])
Exemplo n.º 11
0
def change_plan_at_end_of_cycle(
    request: HttpRequest,
    user: UserProfile,
    status: int = REQ("status", validator=check_int)
) -> HttpResponse:
    assert (status
            in [CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE])
    plan = get_current_plan_by_realm(user.realm)
    assert (plan is not None)  # for mypy
    do_change_plan_status(plan, status)
    return json_success()
Exemplo n.º 12
0
def update_billing_method_of_current_plan(
    realm: Realm, charge_automatically: bool, *, acting_user: Optional[UserProfile]
) -> None:
    plan = get_current_plan_by_realm(realm)
    if plan is not None:
        plan.charge_automatically = charge_automatically
        plan.save(update_fields=["charge_automatically"])
        RealmAuditLog.objects.create(
            realm=realm,
            acting_user=acting_user,
            event_type=RealmAuditLog.REALM_BILLING_METHOD_CHANGED,
            event_time=timezone_now(),
            extra_data={
                "charge_automatically": charge_automatically,
            },
        )
Exemplo n.º 13
0
def change_plan_status(request: HttpRequest, user: UserProfile,
                       status: int=REQ("status", validator=check_int)) -> HttpResponse:
    assert(status in [CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, CustomerPlan.ENDED])

    plan = get_current_plan_by_realm(user.realm)
    assert(plan is not None)  # for mypy

    if status == CustomerPlan.ACTIVE:
        assert(plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
        do_change_plan_status(plan, status)
    elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
        assert(plan.status == CustomerPlan.ACTIVE)
        do_change_plan_status(plan, status)
    elif status == CustomerPlan.ENDED:
        assert(plan.status == CustomerPlan.FREE_TRIAL)
        downgrade_now(user.realm)
    return json_success()
Exemplo n.º 14
0
def generate_licenses_low_warning_message_if_required(realm: Realm) -> Optional[str]:
    plan = get_current_plan_by_realm(realm)
    if plan is None or plan.automanage_licenses:
        return None

    licenses_remaining = plan.licenses() - get_latest_seat_count(realm)
    if licenses_remaining > 3:
        return None

    format_kwargs = {
        "billing_page_link": "/billing/#settings",
        "deactivate_user_help_page_link": "/help/deactivate-or-reactivate-a-user",
    }

    if licenses_remaining <= 0:
        return _(
            "Your organization has no Zulip licenses remaining and can no longer accept new users. "
            "Please [increase the number of licenses]({billing_page_link}) or "
            "[deactivate inactive users]({deactivate_user_help_page_link}) to allow new users to join."
        ).format(**format_kwargs)

    return {
        1: _(
            "Your organization has only one Zulip license remaining. You can "
            "[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
            "to allow more than one user to join."
        ),
        2: _(
            "Your organization has only two Zulip licenses remaining. You can "
            "[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
            "to allow more than two users to join."
        ),
        3: _(
            "Your organization has only three Zulip licenses remaining. You can "
            "[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
            "to allow more than three users to join."
        ),
    }[licenses_remaining].format(**format_kwargs)
Exemplo n.º 15
0
def support(
    request: HttpRequest,
    realm_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
    plan_type: Optional[int] = REQ(default=None,
                                   converter=to_non_negative_int),
    discount: Optional[Decimal] = REQ(default=None, converter=to_decimal),
    new_subdomain: Optional[str] = REQ(default=None),
    status: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_STATUS_VALUES)),
    billing_method: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_BILLING_METHODS)),
    sponsorship_pending: Optional[bool] = REQ(default=None,
                                              json_validator=check_bool),
    approve_sponsorship: Optional[bool] = REQ(default=None,
                                              json_validator=check_bool),
    downgrade_method: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_DOWNGRADE_METHODS)),
    scrub_realm: Optional[bool] = REQ(default=None, json_validator=check_bool),
    query: Optional[str] = REQ("q", default=None),
    org_type: Optional[int] = REQ(default=None, converter=to_non_negative_int),
) -> HttpResponse:
    context: Dict[str, Any] = {}

    if "success_message" in request.session:
        context["success_message"] = request.session["success_message"]
        del request.session["success_message"]

    if settings.BILLING_ENABLED and request.method == "POST":
        # We check that request.POST only has two keys in it: The
        # realm_id and a field to change.
        keys = set(request.POST.keys())
        if "csrfmiddlewaretoken" in keys:
            keys.remove("csrfmiddlewaretoken")
        if len(keys) != 2:
            raise JsonableError(_("Invalid parameters"))

        realm = Realm.objects.get(id=realm_id)

        acting_user = request.user
        assert isinstance(acting_user, UserProfile)
        if plan_type is not None:
            current_plan_type = realm.plan_type
            do_change_plan_type(realm, plan_type, acting_user=acting_user)
            msg = f"Plan type of {realm.string_id} changed from {get_plan_name(current_plan_type)} to {get_plan_name(plan_type)} "
            context["success_message"] = msg
        elif org_type is not None:
            current_realm_type = realm.org_type
            do_change_realm_org_type(realm, org_type, acting_user=acting_user)
            msg = f"Org type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
            context["success_message"] = msg
        elif discount is not None:
            current_discount = get_discount_for_realm(realm) or 0
            attach_discount_to_realm(realm, discount, acting_user=acting_user)
            context[
                "success_message"] = f"Discount of {realm.string_id} changed to {discount}% from {current_discount}%."
        elif new_subdomain is not None:
            old_subdomain = realm.string_id
            try:
                check_subdomain_available(new_subdomain)
            except ValidationError as error:
                context["error_message"] = error.message
            else:
                do_change_realm_subdomain(realm,
                                          new_subdomain,
                                          acting_user=acting_user)
                request.session[
                    "success_message"] = f"Subdomain changed from {old_subdomain} to {new_subdomain}"
                return HttpResponseRedirect(
                    reverse("support") + "?" + urlencode({"q": new_subdomain}))
        elif status is not None:
            if status == "active":
                do_send_realm_reactivation_email(realm,
                                                 acting_user=acting_user)
                context[
                    "success_message"] = f"Realm reactivation email sent to admins of {realm.string_id}."
            elif status == "deactivated":
                do_deactivate_realm(realm, acting_user=acting_user)
                context["success_message"] = f"{realm.string_id} deactivated."
        elif billing_method is not None:
            if billing_method == "send_invoice":
                update_billing_method_of_current_plan(
                    realm, charge_automatically=False, acting_user=acting_user)
                context[
                    "success_message"] = f"Billing method of {realm.string_id} updated to pay by invoice."
            elif billing_method == "charge_automatically":
                update_billing_method_of_current_plan(
                    realm, charge_automatically=True, acting_user=acting_user)
                context[
                    "success_message"] = f"Billing method of {realm.string_id} updated to charge automatically."
        elif sponsorship_pending is not None:
            if sponsorship_pending:
                update_sponsorship_status(realm, True, acting_user=acting_user)
                context[
                    "success_message"] = f"{realm.string_id} marked as pending sponsorship."
            else:
                update_sponsorship_status(realm,
                                          False,
                                          acting_user=acting_user)
                context[
                    "success_message"] = f"{realm.string_id} is no longer pending sponsorship."
        elif approve_sponsorship:
            do_approve_sponsorship(realm, acting_user=acting_user)
            context[
                "success_message"] = f"Sponsorship approved for {realm.string_id}"
        elif downgrade_method is not None:
            if downgrade_method == "downgrade_at_billing_cycle_end":
                downgrade_at_the_end_of_billing_cycle(realm)
                context[
                    "success_message"] = f"{realm.string_id} marked for downgrade at the end of billing cycle"
            elif downgrade_method == "downgrade_now_without_additional_licenses":
                downgrade_now_without_creating_additional_invoices(realm)
                context[
                    "success_message"] = f"{realm.string_id} downgraded without creating additional invoices"
            elif downgrade_method == "downgrade_now_void_open_invoices":
                downgrade_now_without_creating_additional_invoices(realm)
                voided_invoices_count = void_all_open_invoices(realm)
                context[
                    "success_message"] = f"{realm.string_id} downgraded and voided {voided_invoices_count} open invoices"
        elif scrub_realm:
            do_scrub_realm(realm, acting_user=acting_user)
            context["success_message"] = f"{realm.string_id} scrubbed."

    if query:
        key_words = get_invitee_emails_set(query)

        users = set(UserProfile.objects.filter(delivery_email__in=key_words))
        realms = set(Realm.objects.filter(string_id__in=key_words))

        for key_word in key_words:
            try:
                URLValidator()(key_word)
                parse_result = urllib.parse.urlparse(key_word)
                hostname = parse_result.hostname
                assert hostname is not None
                if parse_result.port:
                    hostname = f"{hostname}:{parse_result.port}"
                subdomain = get_subdomain_from_hostname(hostname)
                try:
                    realms.add(get_realm(subdomain))
                except Realm.DoesNotExist:
                    pass
            except ValidationError:
                users.update(
                    UserProfile.objects.filter(full_name__iexact=key_word))

        for realm in realms:
            realm.customer = get_customer_by_realm(realm)

            current_plan = get_current_plan_by_realm(realm)
            if current_plan is not None:
                new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(
                    current_plan, timezone_now())
                if last_ledger_entry is not None:
                    if new_plan is not None:
                        realm.current_plan = new_plan
                    else:
                        realm.current_plan = current_plan
                    realm.current_plan.licenses = last_ledger_entry.licenses
                    realm.current_plan.licenses_used = get_latest_seat_count(
                        realm)

        # full_names can have , in them
        users.update(UserProfile.objects.filter(full_name__iexact=query))

        context["users"] = users
        context["realms"] = realms

        confirmations: List[Dict[str, Any]] = []

        preregistration_users = PreregistrationUser.objects.filter(
            email__in=key_words)
        confirmations += get_confirmations(
            [
                Confirmation.USER_REGISTRATION, Confirmation.INVITATION,
                Confirmation.REALM_CREATION
            ],
            preregistration_users,
            hostname=request.get_host(),
        )

        multiuse_invites = MultiuseInvite.objects.filter(realm__in=realms)
        confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE],
                                           multiuse_invites)

        confirmations += get_confirmations([Confirmation.REALM_REACTIVATION],
                                           [realm.id for realm in realms])

        context["confirmations"] = confirmations

    def get_realm_owner_emails_as_string(realm: Realm) -> str:
        return ", ".join(realm.get_human_owner_users().order_by(
            "delivery_email").values_list("delivery_email", flat=True))

    def get_realm_admin_emails_as_string(realm: Realm) -> str:
        return ", ".join(
            realm.get_human_admin_users(include_realm_owners=False).order_by(
                "delivery_email").values_list("delivery_email", flat=True))

    context[
        "get_realm_owner_emails_as_string"] = get_realm_owner_emails_as_string
    context[
        "get_realm_admin_emails_as_string"] = get_realm_admin_emails_as_string
    context["get_discount_for_realm"] = get_discount_for_realm
    context["get_org_type_display_name"] = get_org_type_display_name
    context["realm_icon_url"] = realm_icon_url
    context["Confirmation"] = Confirmation
    context["sorted_realm_types"] = sorted(Realm.ORG_TYPES.values(),
                                           key=lambda d: d["display_order"])

    return render(request, "analytics/support.html", context=context)
Exemplo n.º 16
0
def is_realm_on_free_trial(realm: Realm) -> bool:
    plan = get_current_plan_by_realm(realm)
    return plan is not None and plan.is_free_trial()
Exemplo n.º 17
0
def update_billing_method_of_current_plan(realm: Realm, charge_automatically: bool) -> None:
    plan = get_current_plan_by_realm(realm)
    if plan is not None:
        plan.charge_automatically = charge_automatically
        plan.save(update_fields=["charge_automatically"])
Exemplo n.º 18
0
def update_plan(
    request: HttpRequest,
    user: UserProfile,
    status: Optional[int] = REQ(
        "status",
        json_validator=check_int_in([
            CustomerPlan.ACTIVE,
            CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
            CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE,
            CustomerPlan.ENDED,
        ]),
        default=None,
    ),
    licenses: Optional[int] = REQ("licenses",
                                  json_validator=check_int,
                                  default=None),
    licenses_at_next_renewal: Optional[int] = REQ("licenses_at_next_renewal",
                                                  json_validator=check_int,
                                                  default=None),
) -> HttpResponse:
    plan = get_current_plan_by_realm(user.realm)
    assert plan is not None  # for mypy

    new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(
        plan, timezone_now())
    if new_plan is not None:
        raise JsonableError(
            _("Unable to update the plan. The plan has been expired and replaced with a new plan."
              ))

    if last_ledger_entry is None:
        raise JsonableError(
            _("Unable to update the plan. The plan has ended."))

    if status is not None:
        if status == CustomerPlan.ACTIVE:
            assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
            do_change_plan_status(plan, status)
        elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
            assert plan.status == CustomerPlan.ACTIVE
            downgrade_at_the_end_of_billing_cycle(user.realm)
        elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
            assert plan.billing_schedule == CustomerPlan.MONTHLY
            assert plan.status == CustomerPlan.ACTIVE
            assert plan.fixed_price is None
            do_change_plan_status(plan, status)
        elif status == CustomerPlan.ENDED:
            assert plan.is_free_trial()
            downgrade_now_without_creating_additional_invoices(user.realm)
        return json_success()

    if licenses is not None:
        if plan.automanage_licenses:
            raise JsonableError(
                _("Unable to update licenses manually. Your plan is on automatic license management."
                  ))
        if last_ledger_entry.licenses == licenses:
            raise JsonableError(
                _("Your plan is already on {licenses} licenses in the current billing period."
                  ).format(licenses=licenses))
        if last_ledger_entry.licenses > licenses:
            raise JsonableError(
                _("You cannot decrease the licenses in the current billing period."
                  ).format(licenses=licenses))
        validate_licenses(plan.charge_automatically, licenses,
                          get_latest_seat_count(user.realm))
        update_license_ledger_for_manual_plan(plan,
                                              timezone_now(),
                                              licenses=licenses)
        return json_success()

    if licenses_at_next_renewal is not None:
        if plan.automanage_licenses:
            raise JsonableError(
                _("Unable to update licenses manually. Your plan is on automatic license management."
                  ))
        if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal:
            raise JsonableError(
                _("Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses."
                  ).format(licenses_at_next_renewal=licenses_at_next_renewal))
        validate_licenses(
            plan.charge_automatically,
            licenses_at_next_renewal,
            get_latest_seat_count(user.realm),
        )
        update_license_ledger_for_manual_plan(
            plan,
            timezone_now(),
            licenses_at_next_renewal=licenses_at_next_renewal)
        return json_success()

    raise JsonableError(_("Nothing to change."))
Exemplo n.º 19
0
def downgrade_at_the_end_of_billing_cycle(realm: Realm) -> None:
    plan = get_current_plan_by_realm(realm)
    assert plan is not None
    do_change_plan_status(plan, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)