def approve_sponsorship(realm: Realm, *, acting_user: Optional[UserProfile]) -> None: from zerver.lib.actions import do_change_realm_plan_type, internal_send_private_message do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD_FREE, acting_user=acting_user) customer = get_customer_by_realm(realm) if customer is not None and customer.sponsorship_pending: customer.sponsorship_pending = False customer.save(update_fields=["sponsorship_pending"]) RealmAuditLog.objects.create( realm=realm, acting_user=acting_user, event_type=RealmAuditLog.REALM_SPONSORSHIP_APPROVED, event_time=timezone_now(), ) notification_bot = get_system_bot(settings.NOTIFICATION_BOT, realm.id) for user in realm.get_human_billing_admin_and_realm_owner_users(): with override_language(user.default_language): # Using variable to make life easier for translators if these details change. plan_name = "Zulip Cloud Standard" emoji = ":tada:" message = _( f"Your organization's request for sponsored hosting has been approved! {emoji}.\n" f"You have been upgraded to {plan_name}, free of charge." ) internal_send_private_message(notification_bot, user, message)
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 test_promote_sponsoring_zulip_in_realm(self) -> None: realm = get_realm("zulip") do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD_FREE, acting_user=None) promote_zulip = promote_sponsoring_zulip_in_realm(realm) self.assertTrue(promote_zulip) with self.settings(PROMOTE_SPONSORING_ZULIP=False): promote_zulip = promote_sponsoring_zulip_in_realm(realm) self.assertFalse(promote_zulip) do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD_FREE, acting_user=None) promote_zulip = promote_sponsoring_zulip_in_realm(realm) self.assertTrue(promote_zulip) do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) promote_zulip = promote_sponsoring_zulip_in_realm(realm) self.assertFalse(promote_zulip) do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=None) promote_zulip = promote_sponsoring_zulip_in_realm(realm) self.assertFalse(promote_zulip)
def process_initial_upgrade( user: UserProfile, licenses: int, automanage_licenses: bool, billing_schedule: int, charge_automatically: bool, free_trial: bool, ) -> None: realm = user.realm customer = update_or_create_stripe_customer(user) assert customer.stripe_customer_id is not None # for mypy assert customer.realm is not None ensure_realm_does_not_have_active_plan(customer.realm) ( billing_cycle_anchor, next_invoice_date, period_end, price_per_license, ) = compute_plan_parameters( CustomerPlan.STANDARD, automanage_licenses, billing_schedule, customer.default_discount, free_trial, ) # 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=orjson.dumps(plan_params, default=decimal_to_float).decode(), ) if not free_trial: stripe.InvoiceItem.create( currency="usd", customer=customer.stripe_customer_id, description="Zulip Cloud 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: 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=customer.stripe_customer_id, days_until_due=days_until_due, statement_descriptor="Zulip Cloud Standard", ) stripe.Invoice.finalize_invoice(stripe_invoice) from zerver.lib.actions import do_change_realm_plan_type do_change_realm_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=user)
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_realm_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)
def test_get_billing_info(self) -> None: user = self.example_user("desdemona") user.role = UserProfile.ROLE_REALM_OWNER user.save(update_fields=["role"]) # realm owner, but no CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # realm owner, with inactive CustomerPlan and realm plan_type SELF_HOSTED -> show only billing link customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id") CustomerPlan.objects.create( customer=customer, billing_cycle_anchor=timezone_now(), billing_schedule=CustomerPlan.ANNUAL, next_invoice_date=timezone_now(), tier=CustomerPlan.STANDARD, status=CustomerPlan.ENDED, ) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertTrue(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # realm owner, with inactive CustomerPlan and realm plan_type LIMITED -> show billing link and plans do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertTrue(billing_info.show_billing) self.assertTrue(billing_info.show_plans) # Always false without CORPORATE_ENABLED with self.settings(CORPORATE_ENABLED=False): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # Always false without a UserProfile with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(None) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # realm admin, with CustomerPlan and realm plan_type LIMITED -> show only billing plans user.role = UserProfile.ROLE_REALM_ADMINISTRATOR user.save(update_fields=["role"]) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertTrue(billing_info.show_plans) # billing admin, with CustomerPlan and realm plan_type STANDARD -> show only billing link user.role = UserProfile.ROLE_MEMBER user.is_billing_admin = True do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None) user.save(update_fields=["role", "is_billing_admin"]) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertTrue(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # billing admin, with CustomerPlan and realm plan_type PLUS -> show only billing link do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_PLUS, acting_user=None) user.save(update_fields=["role", "is_billing_admin"]) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertTrue(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # member, with CustomerPlan and realm plan_type STANDARD -> neither billing link or plans do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None) user.is_billing_admin = False user.save(update_fields=["is_billing_admin"]) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # guest, with CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans user.role = UserProfile.ROLE_GUEST user.save(update_fields=["role"]) do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_SELF_HOSTED, acting_user=None) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # billing admin, but no CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans user.role = UserProfile.ROLE_MEMBER user.is_billing_admin = True user.save(update_fields=["role", "is_billing_admin"]) CustomerPlan.objects.all().delete() with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # billing admin, with sponsorship pending and relam plan_type SELF_HOSTED -> show only billing link customer.sponsorship_pending = True customer.save(update_fields=["sponsorship_pending"]) with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertTrue(billing_info.show_billing) self.assertFalse(billing_info.show_plans) # billing admin, no customer object and relam plan_type SELF_HOSTED -> neither billing link or plans customer.delete() with self.settings(CORPORATE_ENABLED=True): billing_info = get_billing_info(user) self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_plans)