Example #1
0
    def test_change_plan_type(self) -> None:
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, None)

        do_change_plan_type(realm, Realm.STANDARD)
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.STANDARD)
        self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)

        do_change_plan_type(realm, Realm.LIMITED)
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.LIMITED)
        self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, Realm.MESSAGE_VISIBILITY_LIMITED)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_LIMITED)

        do_change_plan_type(realm, Realm.STANDARD_FREE)
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.STANDARD_FREE)
        self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)

        do_change_plan_type(realm, Realm.LIMITED)

        do_change_plan_type(realm, Realm.SELF_HOSTED)
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, None)
Example #2
0
    def test_message_retention_days(self) -> None:
        self.login('iago')
        realm = get_realm('zulip')

        req = dict(message_retention_days=ujson.dumps(0))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': 0")

        req = dict(message_retention_days=ujson.dumps(-10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -10")

        req = dict(
            message_retention_days=ujson.dumps(Realm.RETAIN_MESSAGE_FOREVER))
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        req = dict(message_retention_days=ujson.dumps(10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        do_change_plan_type(realm, Realm.LIMITED)
        req = dict(message_retention_days=ujson.dumps(10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Feature unavailable on your current plan.")
Example #3
0
File: stripe.py Project: kou/zulip
def process_downgrade(user: UserProfile) -> None:
    stripe_customer = stripe_get_customer(
        Customer.objects.filter(realm=user.realm).first().stripe_customer_id)
    subscription_balance = preview_invoice_total_for_downgrade(stripe_customer)
    # If subscription_balance > 0, they owe us money. This is likely due to
    # people they added in the last day, so we can just forgive it.
    # Stripe automatically forgives it when we delete the subscription, so nothing we need to do there.
    if subscription_balance < 0:
        stripe_customer.account_balance = stripe_customer.account_balance + subscription_balance
    stripe_subscription = extract_current_subscription(stripe_customer)
    # Wish these two could be transaction.atomic
    stripe_subscription = stripe_subscription.delete()
    stripe.Customer.save(stripe_customer)
    with transaction.atomic():
        user.realm.has_seat_based_plan = False
        user.realm.save(update_fields=['has_seat_based_plan'])
        RealmAuditLog.objects.create(
            realm=user.realm,
            acting_user=user,
            event_type=RealmAuditLog.STRIPE_PLAN_CHANGED,
            event_time=timestamp_to_datetime(stripe_subscription.canceled_at),
            extra_data=ujson.dumps({'plan': None, 'quantity': stripe_subscription.quantity}))
    # Doing this last, since it results in user-visible confirmation (via
    # product changes) that the downgrade succeeded.
    # Keeping it out of the transaction.atomic block because it will
    # eventually have a lot of stuff going on.
    do_change_plan_type(user, Realm.LIMITED)
Example #4
0
def approve_sponsorship(realm: Realm, *,
                        acting_user: Optional[UserProfile]) -> None:
    from zerver.lib.actions import do_change_plan_type, internal_send_private_message

    do_change_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)
Example #5
0
def process_downgrade(user: UserProfile) -> None:
    stripe_customer = stripe_get_customer(
        Customer.objects.filter(realm=user.realm).first().stripe_customer_id)
    subscription_balance = preview_invoice_total_for_downgrade(stripe_customer)
    # If subscription_balance > 0, they owe us money. This is likely due to
    # people they added in the last day, so we can just forgive it.
    # Stripe automatically forgives it when we delete the subscription, so nothing we need to do there.
    if subscription_balance < 0:
        stripe_customer.account_balance = stripe_customer.account_balance + subscription_balance
    stripe_subscription = extract_current_subscription(stripe_customer)
    # Wish these two could be transaction.atomic
    stripe_subscription = stripe_subscription.delete()
    stripe.Customer.save(stripe_customer)
    with transaction.atomic():
        user.realm.has_seat_based_plan = False
        user.realm.save(update_fields=['has_seat_based_plan'])
        RealmAuditLog.objects.create(
            realm=user.realm,
            acting_user=user,
            event_type=RealmAuditLog.STRIPE_PLAN_CHANGED,
            event_time=timestamp_to_datetime(stripe_subscription.canceled_at),
            extra_data=ujson.dumps({
                'plan': None,
                'quantity': stripe_subscription.quantity
            }))
    # Doing this last, since it results in user-visible confirmation (via
    # product changes) that the downgrade succeeded.
    # Keeping it out of the transaction.atomic block because it will
    # eventually have a lot of stuff going on.
    do_change_plan_type(user, Realm.LIMITED)
Example #6
0
    def test_message_retention_days(self) -> None:
        self.login('iago')
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)

        req = dict(message_retention_days=ujson.dumps(0))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': 0")

        req = dict(message_retention_days=ujson.dumps(-10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -10")

        req = dict(
            message_retention_days=ujson.dumps(Realm.RETAIN_MESSAGE_FOREVER))
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        req = dict(message_retention_days=ujson.dumps(10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        do_change_plan_type(realm, Realm.LIMITED)
        req = dict(message_retention_days=ujson.dumps(10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(
            result, "Available on Zulip Standard. Upgrade to access.")

        do_change_plan_type(realm, Realm.STANDARD)
        req = dict(message_retention_days=ujson.dumps(10))
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)
Example #7
0
def process_downgrade(plan: CustomerPlan) -> None:
    from zerver.lib.actions import do_change_plan_type

    do_change_plan_type(plan.customer.realm,
                        Realm.PLAN_TYPE_LIMITED,
                        acting_user=None)
    plan.status = CustomerPlan.ENDED
    plan.save(update_fields=["status"])
Example #8
0
def support(request: HttpRequest) -> HttpResponse:
    context = {}  # type: Dict[str, Any]
    if settings.BILLING_ENABLED and request.method == "POST":
        realm_id = request.POST.get("realm_id", None)
        realm = Realm.objects.get(id=realm_id)

        new_plan_type = request.POST.get("plan_type", None)
        if new_plan_type is not None:
            new_plan_type = int(new_plan_type)
            current_plan_type = realm.plan_type
            do_change_plan_type(realm, new_plan_type)
            msg = "Plan type of {} changed to {} from {} ".format(realm.name, get_plan_name(new_plan_type),
                                                                  get_plan_name(current_plan_type))
            context["plan_type_msg"] = msg

        new_discount = request.POST.get("discount", None)
        if new_discount is not None:
            new_discount = Decimal(new_discount)
            current_discount = get_discount_for_realm(realm)
            attach_discount_to_realm(realm, new_discount)
            msg = "Discount of {} changed to {} from {} ".format(realm.name, new_discount, current_discount)
            context["discount_msg"] = msg

    query = request.GET.get("q", None)
    if query:
        key_words = get_invitee_emails_set(query)

        users = UserProfile.objects.filter(email__in=key_words)
        if users:
            for user in users:
                user.realm.realm_icon_url = realm_icon_url(user.realm)
                user.realm.admins = UserProfile.objects.filter(realm=user.realm, is_realm_admin=True)
                user.realm.default_discount = get_discount_for_realm(user.realm)
            context["users"] = users

        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
                if parse_result.port:
                    hostname = "{}:{}".format(hostname, parse_result.port)
                subdomain = get_subdomain_from_hostname(hostname)
                realm = get_realm(subdomain)
                if realm is not None:
                    realms.add(realm)
            except ValidationError:
                pass

        if realms:
            for realm in realms:
                realm.realm_icon_url = realm_icon_url(realm)
                realm.admins = UserProfile.objects.filter(realm=realm, is_realm_admin=True)
                realm.default_discount = get_discount_for_realm(realm)
            context["realms"] = realms
    return render(request, 'analytics/support.html', context=context)
Example #9
0
    def test_change_plan_type(self) -> None:
        realm = get_realm("zulip")
        iago = self.example_user("iago")
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites,
                         settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, None)

        do_change_plan_type(realm, Realm.STANDARD, acting_user=iago)
        realm = get_realm("zulip")
        realm_audit_log = RealmAuditLog.objects.filter(
            event_type=RealmAuditLog.REALM_PLAN_TYPE_CHANGED).last()
        assert realm_audit_log is not None
        expected_extra_data = {
            "old_value": Realm.SELF_HOSTED,
            "new_value": Realm.STANDARD
        }
        self.assertEqual(realm_audit_log.extra_data, str(expected_extra_data))
        self.assertEqual(realm_audit_log.acting_user, iago)
        self.assertEqual(realm.plan_type, Realm.STANDARD)
        self.assertEqual(realm.max_invites,
                         Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)

        do_change_plan_type(realm, Realm.LIMITED, acting_user=iago)
        realm = get_realm("zulip")
        self.assertEqual(realm.plan_type, Realm.LIMITED)
        self.assertEqual(realm.max_invites,
                         settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit,
                         Realm.MESSAGE_VISIBILITY_LIMITED)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_LIMITED)

        do_change_plan_type(realm, Realm.STANDARD_FREE, acting_user=iago)
        realm = get_realm("zulip")
        self.assertEqual(realm.plan_type, Realm.STANDARD_FREE)
        self.assertEqual(realm.max_invites,
                         Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)

        do_change_plan_type(realm, Realm.LIMITED, acting_user=iago)

        do_change_plan_type(realm, Realm.SELF_HOSTED, acting_user=iago)
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites,
                         settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
        self.assertEqual(realm.upload_quota_gb, None)
Example #10
0
def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int, stripe_token: str) -> None:
    customer = Customer.objects.filter(realm=user.realm).first()
    if customer is None:
        stripe_customer = do_create_customer_with_payment_source(user, stripe_token)
    else:
        stripe_customer = do_replace_payment_source(user, stripe_token)
    do_subscribe_customer_to_plan(
        stripe_customer=stripe_customer,
        stripe_plan_id=plan.stripe_plan_id,
        seat_count=seat_count,
        # TODO: billing address details are passed to us in the request;
        # use that to calculate taxes.
        tax_percent=0)
    do_change_plan_type(user, Realm.PREMIUM)
Example #11
0
    def test_promote_sponsoring_zulip_in_realm(self) -> None:
        realm = get_realm("zulip")

        do_change_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_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_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
        promote_zulip = promote_sponsoring_zulip_in_realm(realm)
        self.assertFalse(promote_zulip)

        do_change_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=None)
        promote_zulip = promote_sponsoring_zulip_in_realm(realm)
        self.assertFalse(promote_zulip)
Example #12
0
    def test_message_retention_days(self) -> None:
        self.login("iago")
        realm = get_realm("zulip")
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)

        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(result, "Must be an organization owner")

        self.login("desdemona")

        req = dict(message_retention_days=orjson.dumps(0).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': 0")

        req = dict(message_retention_days=orjson.dumps(-10).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -10")

        req = dict(message_retention_days=orjson.dumps("invalid").decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(
            result, "Bad value for 'message_retention_days': invalid")

        req = dict(message_retention_days=orjson.dumps(-1).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -1")

        req = dict(message_retention_days=orjson.dumps("unlimited").decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_success(result)

        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_success(result)

        do_change_plan_type(realm, Realm.LIMITED, acting_user=None)
        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_error(
            result, "Available on Zulip Standard. Upgrade to access.")

        do_change_plan_type(realm, Realm.STANDARD, acting_user=None)
        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch("/json/realm", req)
        self.assert_json_success(result)
Example #13
0
    def test_message_retention_days(self) -> None:
        self.login('iago')
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)

        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result, "Must be an organization owner")

        self.login('desdemona')

        req = dict(message_retention_days=orjson.dumps(0).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': 0")

        req = dict(message_retention_days=orjson.dumps(-10).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -10")

        req = dict(message_retention_days=orjson.dumps('invalid').decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(
            result, "Bad value for 'message_retention_days': invalid")

        req = dict(message_retention_days=orjson.dumps(-1).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(result,
                               "Bad value for 'message_retention_days': -1")

        req = dict(message_retention_days=orjson.dumps('forever').decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)

        do_change_plan_type(realm, Realm.LIMITED)
        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_error(
            result, "Available on Zulip Standard. Upgrade to access.")

        do_change_plan_type(realm, Realm.STANDARD)
        req = dict(message_retention_days=orjson.dumps(10).decode())
        result = self.client_patch('/json/realm', req)
        self.assert_json_success(result)
Example #14
0
File: stripe.py Project: kou/zulip
def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int, stripe_token: str) -> None:
    customer = Customer.objects.filter(realm=user.realm).first()
    if customer is None:
        stripe_customer = do_create_customer(user, stripe_token=stripe_token)
    else:
        stripe_customer = do_replace_payment_source(user, stripe_token)
    do_subscribe_customer_to_plan(
        user=user,
        stripe_customer=stripe_customer,
        stripe_plan_id=plan.stripe_plan_id,
        seat_count=seat_count,
        # TODO: billing address details are passed to us in the request;
        # use that to calculate taxes.
        tax_percent=0)
    do_change_plan_type(user, Realm.STANDARD)
Example #15
0
def approve_sponsorship(realm: Realm) -> None:
    from zerver.lib.actions import do_change_plan_type, internal_send_private_message
    do_change_plan_type(realm, Realm.STANDARD_FREE)
    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"])
    notification_bot = get_system_bot(settings.NOTIFICATION_BOT)
    for billing_admin in realm.get_human_billing_admin_users():
        with override_language(billing_admin.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(billing_admin.realm, notification_bot, billing_admin, message)
Example #16
0
def process_initial_upgrade(user: UserProfile, plan: Plan, seat_count: int,
                            stripe_token: Optional[str]) -> None:
    customer = Customer.objects.filter(realm=user.realm).first()
    if customer is None:
        stripe_customer = do_create_customer(user, stripe_token=stripe_token)
    # elif instead of if since we want to avoid doing two round trips to
    # stripe if we can
    elif stripe_token is not None:
        stripe_customer = do_replace_payment_source(user, stripe_token)
    do_subscribe_customer_to_plan(
        user=user,
        stripe_customer=stripe_customer,
        stripe_plan_id=plan.stripe_plan_id,
        seat_count=seat_count,
        # TODO: billing address details are passed to us in the request;
        # use that to calculate taxes.
        tax_percent=0,
        charge_automatically=(stripe_token is not None))
    do_change_plan_type(user, Realm.STANDARD)
Example #17
0
    def test_change_plan_type(self) -> None:
        user = self.example_user('iago')
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites,
                         settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)

        do_change_plan_type(user, Realm.STANDARD)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites,
                         Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)

        do_change_plan_type(user, Realm.LIMITED)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites,
                         settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit,
                         Realm.MESSAGE_VISIBILITY_LIMITED)

        do_change_plan_type(user, Realm.STANDARD_FREE)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites,
                         Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
Example #18
0
 def test_change_plan_type(self) -> None:
     user = self.example_user('iago')
     self.assertEqual(get_realm('zulip').max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
     do_change_plan_type(user, Realm.STANDARD)
     self.assertEqual(get_realm('zulip').max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
     do_change_plan_type(user, Realm.LIMITED)
     self.assertEqual(get_realm('zulip').max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
     do_change_plan_type(user, Realm.STANDARD_FREE)
     self.assertEqual(get_realm('zulip').max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
Example #19
0
    def test_show_plans(self) -> None:
        realm = get_realm("zulip")

        # Don't show plans to guest users
        self.login("polonius")
        do_change_plan_type(realm, Realm.LIMITED, acting_user=None)
        self.assertNotInHomePage("Plans")

        # Show plans link to all other users if plan_type is LIMITED
        self.login("hamlet")
        self.assertInHomePage("Plans")

        # Show plans link to no one, including admins, if SELF_HOSTED or STANDARD
        do_change_plan_type(realm, Realm.SELF_HOSTED, acting_user=None)
        self.assertNotInHomePage("Plans")

        do_change_plan_type(realm, Realm.STANDARD, acting_user=None)
        self.assertNotInHomePage("Plans")
Example #20
0
    def test_show_support_zulip(self) -> None:
        realm = get_realm("zulip")

        self.login("hamlet")

        self.assertInHomePage("Support Zulip")

        do_change_plan_type(realm, Realm.STANDARD_FREE, acting_user=None)
        self.assertInHomePage("Support Zulip")

        with self.settings(PROMOTE_SPONSORING_ZULIP=False):
            self.assertNotInHomePage("Support Zulip")

        do_change_plan_type(realm, Realm.LIMITED, acting_user=None)
        self.assertNotInHomePage("Support Zulip")

        do_change_plan_type(realm, Realm.STANDARD, acting_user=None)
        self.assertNotInHomePage("Support Zulip")
Example #21
0
    def test_change_plan_type(self) -> None:
        realm = get_realm('zulip')
        self.assertEqual(realm.plan_type, Realm.SELF_HOSTED)
        self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)

        do_change_plan_type(realm, Realm.STANDARD)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)

        do_change_plan_type(realm, Realm.LIMITED)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, Realm.MESSAGE_VISIBILITY_LIMITED)

        do_change_plan_type(realm, Realm.STANDARD_FREE)
        realm = get_realm('zulip')
        self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
        self.assertEqual(realm.message_visibility_limit, None)
Example #22
0
    def test_show_plans(self) -> None:
        realm = get_realm("zulip")

        # Don't show plans to guest users
        self.login("polonius")
        do_change_plan_type(realm, Realm.LIMITED, acting_user=None)
        result_html = self._get_home_page().content.decode("utf-8")
        self.assertNotIn("Plans", result_html)

        # Show plans link to all other users if plan_type is LIMITED
        self.login("hamlet")
        result_html = self._get_home_page().content.decode("utf-8")
        self.assertIn("Plans", result_html)

        # Show plans link to no one, including admins, if SELF_HOSTED or STANDARD
        do_change_plan_type(realm, Realm.SELF_HOSTED, acting_user=None)
        result_html = self._get_home_page().content.decode("utf-8")
        self.assertNotIn("Plans", result_html)

        do_change_plan_type(realm, Realm.STANDARD, acting_user=None)
        result_html = self._get_home_page().content.decode("utf-8")
        self.assertNotIn("Plans", result_html)
Example #23
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)
    if CustomerPlan.objects.filter(customer=customer, status=CustomerPlan.ACTIVE).exists():
        # 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 {} trying to upgrade, but has an active subscription".format(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)
    # 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.
    charge_automatically = stripe_token is not None
    if charge_automatically:
        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.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.
        stripe.InvoiceItem.create(
            amount=price_per_license * licenses * -1,
            currency='usd',
            customer=customer.stripe_customer_id,
            description="Payment (Card ending in {})".format(cast(stripe.Card, stripe_charge.source).last4),
            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_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}
        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))
    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)
Example #24
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)
Example #25
0
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
    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 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 Standard",
        )
        stripe.Invoice.finalize_invoice(stripe_invoice)

    from zerver.lib.actions import do_change_plan_type

    do_change_plan_type(realm, Realm.PLAN_TYPE_STANDARD, acting_user=user)
Example #26
0
    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_plan_type(user.realm, Realm.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_plan_type(user.realm, Realm.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)

        # member, with CustomerPlan and realm plan_type STANDARD -> neither billing link or plans
        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_plan_type(user.realm, Realm.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)
Example #27
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)
Example #28
0
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'])
Example #29
0
def do_import_realm(import_dir: Path, subdomain: str) -> Realm:
    logging.info("Importing realm dump %s" % (import_dir,))
    if not os.path.exists(import_dir):
        raise Exception("Missing import directory!")

    realm_data_filename = os.path.join(import_dir, "realm.json")
    if not os.path.exists(realm_data_filename):
        raise Exception("Missing realm.json file!")

    logging.info("Importing realm data from %s" % (realm_data_filename,))
    with open(realm_data_filename) as f:
        data = ujson.load(f)

    sort_by_date = data.get('sort_by_date', False)

    bulk_import_client(data, Client, 'zerver_client')

    # We don't import the Stream model yet, since it depends on Realm,
    # which isn't imported yet.  But we need the Stream model IDs for
    # notifications_stream.
    update_model_ids(Stream, data, 'stream')
    re_map_foreign_keys(data, 'zerver_realm', 'notifications_stream', related_table="stream")
    re_map_foreign_keys(data, 'zerver_realm', 'signup_notifications_stream', related_table="stream")

    fix_datetime_fields(data, 'zerver_realm')
    # Fix realm subdomain information
    data['zerver_realm'][0]['string_id'] = subdomain
    data['zerver_realm'][0]['name'] = subdomain
    fix_realm_authentication_bitfield(data, 'zerver_realm', 'authentication_methods')
    update_model_ids(Realm, data, 'realm')

    realm = Realm(**data['zerver_realm'][0])

    if realm.notifications_stream_id is not None:
        notifications_stream_id = int(realm.notifications_stream_id)  # type: Optional[int]
    else:
        notifications_stream_id = None
    realm.notifications_stream_id = None
    if realm.signup_notifications_stream_id is not None:
        signup_notifications_stream_id = int(realm.signup_notifications_stream_id)  # type: Optional[int]
    else:
        signup_notifications_stream_id = None
    realm.signup_notifications_stream_id = None
    realm.save()

    # Email tokens will automatically be randomly generated when the
    # Stream objects are created by Django.
    fix_datetime_fields(data, 'zerver_stream')
    re_map_foreign_keys(data, 'zerver_stream', 'realm', related_table="realm")
    bulk_import_model(data, Stream)

    realm.notifications_stream_id = notifications_stream_id
    realm.signup_notifications_stream_id = signup_notifications_stream_id
    realm.save()

    # Remap the user IDs for notification_bot and friends to their
    # appropriate IDs on this server
    for item in data['zerver_userprofile_crossrealm']:
        logging.info("Adding to ID map: %s %s" % (item['id'], get_system_bot(item['email']).id))
        new_user_id = get_system_bot(item['email']).id
        update_id_map(table='user_profile', old_id=item['id'], new_id=new_user_id)
        new_recipient_id = Recipient.objects.get(type=Recipient.PERSONAL, type_id=new_user_id).id
        update_id_map(table='recipient', old_id=item['recipient_id'], new_id=new_recipient_id)

    # Merge in zerver_userprofile_mirrordummy
    data['zerver_userprofile'] = data['zerver_userprofile'] + data['zerver_userprofile_mirrordummy']
    del data['zerver_userprofile_mirrordummy']
    data['zerver_userprofile'].sort(key=lambda r: r['id'])

    # To remap foreign key for UserProfile.last_active_message_id
    update_message_foreign_keys(import_dir=import_dir, sort_by_date=sort_by_date)

    fix_datetime_fields(data, 'zerver_userprofile')
    update_model_ids(UserProfile, data, 'user_profile')
    re_map_foreign_keys(data, 'zerver_userprofile', 'realm', related_table="realm")
    re_map_foreign_keys(data, 'zerver_userprofile', 'bot_owner', related_table="user_profile")
    re_map_foreign_keys(data, 'zerver_userprofile', 'default_sending_stream',
                        related_table="stream")
    re_map_foreign_keys(data, 'zerver_userprofile', 'default_events_register_stream',
                        related_table="stream")
    re_map_foreign_keys(data, 'zerver_userprofile', 'last_active_message_id',
                        related_table="message", id_field=True)
    for user_profile_dict in data['zerver_userprofile']:
        user_profile_dict['password'] = None
        user_profile_dict['api_key'] = generate_api_key()
        # Since Zulip doesn't use these permissions, drop them
        del user_profile_dict['user_permissions']
        del user_profile_dict['groups']

    user_profiles = [UserProfile(**item) for item in data['zerver_userprofile']]
    for user_profile in user_profiles:
        user_profile.set_unusable_password()
    UserProfile.objects.bulk_create(user_profiles)

    re_map_foreign_keys(data, 'zerver_defaultstream', 'stream', related_table="stream")
    re_map_foreign_keys(data, 'zerver_realmemoji', 'author', related_table="user_profile")
    for (table, model, related_table) in realm_tables:
        re_map_foreign_keys(data, table, 'realm', related_table="realm")
        update_model_ids(model, data, related_table)
        bulk_import_model(data, model)

    if 'zerver_huddle' in data:
        update_model_ids(Huddle, data, 'huddle')
        # We don't import Huddle yet, since we don't have the data to
        # compute huddle hashes until we've imported some of the
        # tables below.
        # TODO: double-check this.

    re_map_foreign_keys(data, 'zerver_recipient', 'type_id', related_table="stream",
                        recipient_field=True, id_field=True)
    re_map_foreign_keys(data, 'zerver_recipient', 'type_id', related_table="user_profile",
                        recipient_field=True, id_field=True)
    re_map_foreign_keys(data, 'zerver_recipient', 'type_id', related_table="huddle",
                        recipient_field=True, id_field=True)
    update_model_ids(Recipient, data, 'recipient')
    bulk_import_model(data, Recipient)

    re_map_foreign_keys(data, 'zerver_subscription', 'user_profile', related_table="user_profile")
    get_huddles_from_subscription(data, 'zerver_subscription')
    re_map_foreign_keys(data, 'zerver_subscription', 'recipient', related_table="recipient")
    update_model_ids(Subscription, data, 'subscription')
    bulk_import_model(data, Subscription)

    if 'zerver_realmauditlog' in data:
        fix_datetime_fields(data, 'zerver_realmauditlog')
        re_map_foreign_keys(data, 'zerver_realmauditlog', 'realm', related_table="realm")
        re_map_foreign_keys(data, 'zerver_realmauditlog', 'modified_user',
                            related_table='user_profile')
        re_map_foreign_keys(data, 'zerver_realmauditlog', 'acting_user',
                            related_table='user_profile')
        re_map_foreign_keys(data, 'zerver_realmauditlog', 'modified_stream',
                            related_table="stream")
        update_model_ids(RealmAuditLog, data, related_table="realmauditlog")
        bulk_import_model(data, RealmAuditLog)
    else:
        logging.info('about to call create_subscription_events')
        create_subscription_events(
            data=data,
            realm_id=realm.id,
        )
        logging.info('done with create_subscription_events')

    if 'zerver_huddle' in data:
        process_huddle_hash(data, 'zerver_huddle')
        bulk_import_model(data, Huddle)

    if 'zerver_userhotspot' in data:
        fix_datetime_fields(data, 'zerver_userhotspot')
        re_map_foreign_keys(data, 'zerver_userhotspot', 'user', related_table='user_profile')
        update_model_ids(UserHotspot, data, 'userhotspot')
        bulk_import_model(data, UserHotspot)

    if 'zerver_mutedtopic' in data:
        re_map_foreign_keys(data, 'zerver_mutedtopic', 'user_profile', related_table='user_profile')
        re_map_foreign_keys(data, 'zerver_mutedtopic', 'stream', related_table='stream')
        re_map_foreign_keys(data, 'zerver_mutedtopic', 'recipient', related_table='recipient')
        update_model_ids(MutedTopic, data, 'mutedtopic')
        bulk_import_model(data, MutedTopic)

    if 'zerver_service' in data:
        re_map_foreign_keys(data, 'zerver_service', 'user_profile', related_table='user_profile')
        fix_service_tokens(data, 'zerver_service')
        update_model_ids(Service, data, 'service')
        bulk_import_model(data, Service)

    if 'zerver_usergroup' in data:
        re_map_foreign_keys(data, 'zerver_usergroup', 'realm', related_table='realm')
        re_map_foreign_keys_many_to_many(data, 'zerver_usergroup',
                                         'members', related_table='user_profile')
        update_model_ids(UserGroup, data, 'usergroup')
        bulk_import_model(data, UserGroup)

        re_map_foreign_keys(data, 'zerver_usergroupmembership',
                            'user_group', related_table='usergroup')
        re_map_foreign_keys(data, 'zerver_usergroupmembership',
                            'user_profile', related_table='user_profile')
        update_model_ids(UserGroupMembership, data, 'usergroupmembership')
        bulk_import_model(data, UserGroupMembership)

    if 'zerver_botstoragedata' in data:
        re_map_foreign_keys(data, 'zerver_botstoragedata', 'bot_profile', related_table='user_profile')
        update_model_ids(BotStorageData, data, 'botstoragedata')
        bulk_import_model(data, BotStorageData)

    if 'zerver_botconfigdata' in data:
        re_map_foreign_keys(data, 'zerver_botconfigdata', 'bot_profile', related_table='user_profile')
        update_model_ids(BotConfigData, data, 'botconfigdata')
        bulk_import_model(data, BotConfigData)

    fix_datetime_fields(data, 'zerver_userpresence')
    re_map_foreign_keys(data, 'zerver_userpresence', 'user_profile', related_table="user_profile")
    re_map_foreign_keys(data, 'zerver_userpresence', 'client', related_table='client')
    update_model_ids(UserPresence, data, 'user_presence')
    bulk_import_model(data, UserPresence)

    fix_datetime_fields(data, 'zerver_useractivity')
    re_map_foreign_keys(data, 'zerver_useractivity', 'user_profile', related_table="user_profile")
    re_map_foreign_keys(data, 'zerver_useractivity', 'client', related_table='client')
    update_model_ids(UserActivity, data, 'useractivity')
    bulk_import_model(data, UserActivity)

    fix_datetime_fields(data, 'zerver_useractivityinterval')
    re_map_foreign_keys(data, 'zerver_useractivityinterval', 'user_profile', related_table="user_profile")
    update_model_ids(UserActivityInterval, data, 'useractivityinterval')
    bulk_import_model(data, UserActivityInterval)

    re_map_foreign_keys(data, 'zerver_customprofilefield', 'realm', related_table="realm")
    update_model_ids(CustomProfileField, data, related_table="customprofilefield")
    bulk_import_model(data, CustomProfileField)

    re_map_foreign_keys(data, 'zerver_customprofilefieldvalue', 'user_profile',
                        related_table="user_profile")
    re_map_foreign_keys(data, 'zerver_customprofilefieldvalue', 'field',
                        related_table="customprofilefield")
    fix_customprofilefield(data)
    update_model_ids(CustomProfileFieldValue, data, related_table="customprofilefieldvalue")
    bulk_import_model(data, CustomProfileFieldValue)

    # Import uploaded files and avatars
    import_uploads(os.path.join(import_dir, "avatars"), processing_avatars=True)
    import_uploads(os.path.join(import_dir, "uploads"))

    # We need to have this check as the emoji files are only present in the data
    # importer from slack
    # For Zulip export, this doesn't exist
    if os.path.exists(os.path.join(import_dir, "emoji")):
        import_uploads(os.path.join(import_dir, "emoji"), processing_emojis=True)

    sender_map = {
        user['id']: user
        for user in data['zerver_userprofile']
    }

    # Import zerver_message and zerver_usermessage
    import_message_data(realm=realm, sender_map=sender_map, import_dir=import_dir)

    re_map_foreign_keys(data, 'zerver_reaction', 'message', related_table="message")
    re_map_foreign_keys(data, 'zerver_reaction', 'user_profile', related_table="user_profile")
    re_map_foreign_keys(data, 'zerver_reaction', 'emoji_code', related_table="realmemoji", id_field=True,
                        reaction_field=True)
    update_model_ids(Reaction, data, 'reaction')
    bulk_import_model(data, Reaction)

    # Do attachments AFTER message data is loaded.
    # TODO: de-dup how we read these json files.
    fn = os.path.join(import_dir, "attachment.json")
    if not os.path.exists(fn):
        raise Exception("Missing attachment.json file!")

    logging.info("Importing attachment data from %s" % (fn,))
    with open(fn) as f:
        data = ujson.load(f)

    import_attachments(data)

    if settings.BILLING_ENABLED:
        do_change_plan_type(realm, Realm.LIMITED)
    return realm