def test_extract_current_subscription(self) -> None: self.assertIsNone(extract_current_subscription(mock_create_customer())) subscription = extract_current_subscription( mock_customer_with_subscription()) self.assertEqual(subscription["id"][:4], "sub_") self.assertIsNone( extract_current_subscription( mock_customer_with_canceled_subscription()))
def test_upgrade_where_subscription_save_fails_at_first( self, mock5: Mock, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) # From https://stripe.com/docs/testing#cards: Attaching this card to # a Customer object succeeds, but attempts to charge the customer fail. self.client_post("/upgrade/", {'stripeToken': stripe_create_token('4000000000000341').id, 'signed_seat_count': self.signed_seat_count, 'salt': self.salt, 'plan': Plan.CLOUD_ANNUAL}) # Check that we created a Customer object with has_billing_relationship False customer = Customer.objects.get(realm=get_realm('zulip')) self.assertFalse(customer.has_billing_relationship) original_stripe_customer_id = customer.stripe_customer_id # Check that we created a customer in stripe, with no subscription stripe_customer = stripe_get_customer(customer.stripe_customer_id) self.assertFalse(extract_current_subscription(stripe_customer)) # Check that we correctly populated RealmAuditLog audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', flat=True).order_by('id')) self.assertEqual(audit_log_entries, [RealmAuditLog.STRIPE_CUSTOMER_CREATED, RealmAuditLog.STRIPE_CARD_CHANGED]) # Check that we did not update Realm realm = get_realm("zulip") self.assertFalse(realm.has_seat_based_plan) # Check that we still get redirected to /upgrade response = self.client_get("/billing/") self.assertEqual(response.status_code, 302) self.assertEqual('/upgrade/', response.url) # Try again, with a valid card self.client_post("/upgrade/", {'stripeToken': stripe_create_token().id, 'signed_seat_count': self.signed_seat_count, 'salt': self.salt, 'plan': Plan.CLOUD_ANNUAL}) customer = Customer.objects.get(realm=get_realm('zulip')) # Impossible to create two Customers, but check that we didn't # change stripe_customer_id and that we updated has_billing_relationship self.assertEqual(customer.stripe_customer_id, original_stripe_customer_id) self.assertTrue(customer.has_billing_relationship) # Check that we successfully added a subscription stripe_customer = stripe_get_customer(customer.stripe_customer_id) self.assertTrue(extract_current_subscription(stripe_customer)) # Check that we correctly populated RealmAuditLog audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', flat=True).order_by('id')) self.assertEqual(audit_log_entries, [RealmAuditLog.STRIPE_CUSTOMER_CREATED, RealmAuditLog.STRIPE_CARD_CHANGED, RealmAuditLog.STRIPE_CARD_CHANGED, RealmAuditLog.STRIPE_PLAN_CHANGED, RealmAuditLog.REALM_PLAN_TYPE_CHANGED]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url)
def test_upgrade_with_outdated_seat_count( self, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: self.login(self.example_email("hamlet")) new_seat_count = 123 # Change the seat count while the user is going through the upgrade flow response = self.client_get("/upgrade/") with patch('corporate.lib.stripe.get_seat_count', return_value=new_seat_count): self.client_post("/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that the subscription call used the old quantity, not new_seat_count stripe_customer = stripe_get_customer( Customer.objects.get(realm=get_realm('zulip')).stripe_customer_id) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.quantity, self.quantity) # Check that we have the STRIPE_PLAN_QUANTITY_RESET entry, and that we # correctly handled the requires_billing_update field audit_log_entries = list(RealmAuditLog.objects.order_by('-id') .values_list('event_type', 'event_time', 'requires_billing_update')[:5])[::-1] self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created), False), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created), False), # TODO: Ideally this test would force stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created), False), (RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, timestamp_to_datetime(stripe_subscription.created), True), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra(), False), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET).values_list('extra_data', flat=True).first()), {'quantity': new_seat_count})
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'corporate/billing.html', context=context) context = {'admin_access': True} stripe_customer = stripe_get_customer(customer.stripe_customer_id) if stripe_customer.account_balance > 0: # nocoverage, waiting for mock_stripe to mature context.update({ 'account_charges': '{:,.2f}'.format(stripe_customer.account_balance / 100.) }) if stripe_customer.account_balance < 0: # nocoverage context.update({ 'account_credits': '{:,.2f}'.format(-stripe_customer.account_balance / 100.) }) subscription = extract_current_subscription(stripe_customer) if subscription: plan_name = PLAN_NAMES[Plan.objects.get( stripe_plan_id=subscription.plan.id).nickname] seat_count = subscription.quantity # Need user's timezone to do this properly renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=timestamp_to_datetime(subscription.current_period_end)) renewal_amount = stripe_get_upcoming_invoice( customer.stripe_customer_id).total # Can only get here by subscribing and then downgrading. We don't support downgrading # yet, but keeping this code here since we will soon. else: # nocoverage plan_name = "Zulip Free" seat_count = 0 renewal_date = '' renewal_amount = 0 payment_method = None if stripe_customer.default_source is not None: payment_method = "Card ending in %(last4)s" % { 'last4': stripe_customer.default_source.last4 } context.update({ 'plan_name': plan_name, 'seat_count': seat_count, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.), 'payment_method': payment_method, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'corporate/billing.html', context=context) context = {'admin_access': True} stripe_customer = stripe_get_customer(customer.stripe_customer_id) subscription = extract_current_subscription(stripe_customer) prorated_charges = stripe_customer.account_balance if subscription: plan_name = PLAN_NAMES[Plan.objects.get(stripe_plan_id=subscription.plan.id).nickname] seat_count = subscription.quantity # Need user's timezone to do this properly renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=timestamp_to_datetime(subscription.current_period_end)) upcoming_invoice = stripe_get_upcoming_invoice(customer.stripe_customer_id) renewal_amount = subscription.plan.amount * subscription.quantity prorated_charges += upcoming_invoice.total - renewal_amount # Can only get here by subscribing and then downgrading. We don't support downgrading # yet, but keeping this code here since we will soon. else: # nocoverage plan_name = "Zulip Free" seat_count = 0 renewal_date = '' renewal_amount = 0 prorated_credits = 0 if prorated_charges < 0: # nocoverage prorated_credits = -prorated_charges prorated_charges = 0 payment_method = None if stripe_customer.default_source is not None: payment_method = "Card ending in %(last4)s" % {'last4': stripe_customer.default_source.last4} context.update({ 'plan_name': plan_name, 'seat_count': seat_count, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.), 'payment_method': payment_method, 'prorated_charges': '{:,.2f}'.format(prorated_charges / 100.), 'prorated_credits': '{:,.2f}'.format(prorated_credits / 100.), 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def test_initial_upgrade(self, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.PREMIUM) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout self.client_post("/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that we correctly created Customer and Subscription objects in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) self.assertEqual(stripe_customer.default_source.id[:5], 'card_') self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") self.assertEqual(stripe_customer.discount, None) self.assertEqual(stripe_customer.email, user.email) self.assertEqual(dict(stripe_customer.metadata), {'realm_id': str(user.realm.id), 'realm_str': 'zulip'}) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.billing, 'charge_automatically') self.assertEqual(stripe_subscription.days_until_due, None) self.assertEqual(stripe_subscription.plan.id, Plan.objects.get(nickname=Plan.CLOUD_ANNUAL).stripe_plan_id) self.assertEqual(stripe_subscription.quantity, self.quantity) self.assertEqual(stripe_subscription.status, 'active') self.assertEqual(stripe_subscription.tax_percent, 0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual(1, Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).count()) audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), # TODO: Add a test where stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.PREMIUM) self.assertEqual(realm.max_invites, Realm.MAX_INVITES_PREMIUM) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url)
def payment_method_string(stripe_customer: stripe.Customer) -> str: subscription = extract_current_subscription(stripe_customer) if subscription is not None and subscription.billing == "send_invoice": return _("Billed by invoice") stripe_source = stripe_customer.default_source # In case of e.g. an expired card if stripe_source is None: # nocoverage return _("No payment method on file") if stripe_source.object == "card": return _("Card ending in %(last4)s" % {'last4': cast(stripe.Card, stripe_source).last4}) # You can get here if e.g. you sign up to pay by invoice, and then # immediately downgrade. In that case, stripe_source.object == 'source', # and stripe_source.type = 'ach_credit_transfer'. # Using a catch-all error message here since there might be one-off stuff we # do for a particular customer that would land them here. E.g. by default we # don't support ACH for automatic payments, but in theory we could add it for # a customer via the Stripe dashboard. return _("Unknown payment method. Please contact %s." % (settings.ZULIP_ADMINISTRATOR,)) # nocoverage
def test_extract_current_subscription(self) -> None: self.assertIsNone(extract_current_subscription(mock_create_customer())) subscription = extract_current_subscription(mock_customer_with_subscription()) self.assertEqual(subscription["id"][:4], "sub_") self.assertIsNone(extract_current_subscription(mock_customer_with_canceled_subscription()))
def test_initial_upgrade(self, mock5: Mock, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout self.client_post("/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that we correctly created Customer and Subscription objects in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) self.assertEqual(stripe_customer.default_source.id[:5], 'card_') self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") self.assertEqual(stripe_customer.discount, None) self.assertEqual(stripe_customer.email, user.email) self.assertEqual(dict(stripe_customer.metadata), {'realm_id': str(user.realm.id), 'realm_str': 'zulip'}) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.billing, 'charge_automatically') self.assertEqual(stripe_subscription.days_until_due, None) self.assertEqual(stripe_subscription.plan.id, Plan.objects.get(nickname=Plan.CLOUD_ANNUAL).stripe_plan_id) self.assertEqual(stripe_subscription.quantity, self.quantity) self.assertEqual(stripe_subscription.status, 'active') self.assertEqual(stripe_subscription.tax_percent, 0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual(1, Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).count()) audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), # TODO: Add a test where stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.STANDARD) self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url) # Check /billing has the correct information response = self.client_get("/billing/") self.assert_not_in_success_response(['We can also bill by invoice'], response) for substring in ['Your plan will renew on', '$%s.00' % (80 * self.quantity,), 'Card ending in 4242']: self.assert_in_response(substring, response)