def test_replace_payment_source(self, *mocks: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) self.upgrade() # Try replacing with a valid card stripe_token = stripe_create_token(card_number='5555555555554444').id response = self.client_post("/json/billing/sources/change", {'stripe_token': ujson.dumps(stripe_token)}) self.assert_json_success(response) number_of_sources = 0 for stripe_source in stripe_get_customer(Customer.objects.first().stripe_customer_id).sources: self.assertEqual(cast(stripe.Card, stripe_source).last4, '4444') number_of_sources += 1 self.assertEqual(number_of_sources, 1) audit_log_entry = RealmAuditLog.objects.order_by('-id') \ .values_list('acting_user', 'event_type').first() self.assertEqual(audit_log_entry, (user.id, RealmAuditLog.STRIPE_CARD_CHANGED)) RealmAuditLog.objects.filter(acting_user=user).delete() # Try replacing with an invalid card stripe_token = stripe_create_token(card_number='4000000000009987').id with patch("corporate.lib.stripe.billing_logger.error") as mock_billing_logger: response = self.client_post("/json/billing/sources/change", {'stripe_token': ujson.dumps(stripe_token)}) mock_billing_logger.assert_called() self.assertEqual(ujson.loads(response.content)['error_description'], 'card error') self.assert_json_error_contains(response, 'Your card was declined') number_of_sources = 0 for stripe_source in stripe_get_customer(Customer.objects.first().stripe_customer_id).sources: self.assertEqual(cast(stripe.Card, stripe_source).last4, '4444') number_of_sources += 1 self.assertEqual(number_of_sources, 1) self.assertFalse(RealmAuditLog.objects.filter(event_type=RealmAuditLog.STRIPE_CARD_CHANGED).exists())
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 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 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 CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'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) plan = get_active_plan(customer) if plan is not None: plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] now = timezone_now() last_ledger_entry = add_plan_renewal_to_license_ledger_if_needed( plan, now) licenses = last_ledger_entry.licenses licenses_used = get_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=next_renewal_date(plan, now)) renewal_cents = renewal_amount(plan, now) # TODO: this is the case where the plan doesn't automatically renew if renewal_cents is None: # nocoverage renewal_cents = 0 charge_automatically = plan.charge_automatically if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' # 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" licenses = 0 renewal_date = '' renewal_cents = 0 payment_method = '' charge_automatically = False context.update({ 'plan_name': plan_name, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.), 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context: Dict[str, Any] = {'admin_access': False} return render(request, 'corporate/billing.html', context=context) context = { 'admin_access': True, 'has_active_plan': False, } plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now) if last_ledger_entry is not None: plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] free_trial = plan.status == CustomerPlan.FREE_TRIAL downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' context.update({ 'plan_name': plan_name, 'has_active_plan': True, 'free_trial': free_trial, 'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.), 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, 'CustomerPlan': CustomerPlan, 'onboarding': request.GET.get("onboarding") is not None, }) return render(request, 'corporate/billing.html', context=context)
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 CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'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) plan = get_active_plan(customer) if plan is not None: plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] now = timezone_now() last_ledger_entry = add_plan_renewal_to_license_ledger_if_needed(plan, now) licenses = last_ledger_entry.licenses licenses_used = get_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(dt=next_renewal_date(plan, now)) renewal_cents = renewal_amount(plan, now) # TODO: this is the case where the plan doesn't automatically renew if renewal_cents is None: # nocoverage renewal_cents = 0 charge_automatically = plan.charge_automatically if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' # 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" licenses = 0 renewal_date = '' renewal_cents = 0 payment_method = '' charge_automatically = False context.update({ 'plan_name': plan_name, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.), 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) 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 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 CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'corporate/billing.html', context=context) context = {'admin_access': True} plan_name = "Zulip Free" licenses = 0 renewal_date = '' renewal_cents = 0 payment_method = '' charge_automatically = False stripe_customer = stripe_get_customer(customer.stripe_customer_id) plan = get_current_plan(customer) if plan is not None: plan_name = { CustomerPlan.STANDARD: 'Zulip Standard', CustomerPlan.PLUS: 'Zulip Plus', }[plan.tier] now = timezone_now() last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now) if last_ledger_entry is not None: licenses = last_ledger_entry.licenses licenses_used = get_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' context.update({ 'plan_name': plan_name, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.), 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def initial_upgrade(request: HttpRequest) -> HttpResponse: if not settings.BILLING_ENABLED: return render(request, "404.html") user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is not None and customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.billing_home')) percent_off = 0 if customer is not None: stripe_customer = stripe_get_customer(customer.stripe_customer_id) if stripe_customer.discount is not None: percent_off = stripe_customer.discount.coupon.percent_off seat_count = get_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context = { 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'email': user.email, 'seat_count': seat_count, 'signed_seat_count': signed_seat_count, 'salt': salt, 'min_seat_count_for_invoice': max(seat_count, MIN_INVOICED_SEAT_COUNT), 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", 'nickname_monthly': Plan.CLOUD_MONTHLY, 'nickname_annual': Plan.CLOUD_ANNUAL, 'page_params': JSONEncoderForHTML().encode({ 'seat_count': seat_count, 'nickname_annual': Plan.CLOUD_ANNUAL, 'nickname_monthly': Plan.CLOUD_MONTHLY, 'annual_price': 8000, 'monthly_price': 800, 'percent_off': percent_off, }), } # type: Dict[str, Any] response = render(request, 'corporate/upgrade.html', context=context) return response
def initial_upgrade(request: HttpRequest) -> HttpResponse: if not settings.BILLING_ENABLED: return render(request, "404.html") user = request.user error_message = "" error_description = "" # only used in tests customer = Customer.objects.filter(realm=user.realm).first() if customer is not None and customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.billing_home')) percent_off = 0 if customer is not None: stripe_customer = stripe_get_customer(customer.stripe_customer_id) if stripe_customer.discount is not None: percent_off = stripe_customer.discount.coupon.percent_off if request.method == 'POST': try: plan, seat_count = unsign_and_check_upgrade_parameters( user, request.POST['plan'], request.POST['signed_seat_count'], request.POST['salt'], request.POST['billing_modality']) if request.POST['billing_modality'] == 'send_invoice': try: invoiced_seat_count = int(request.POST['invoiced_seat_count']) except (KeyError, ValueError): invoiced_seat_count = -1 min_required_seat_count = max(seat_count, MIN_INVOICED_SEAT_COUNT) if invoiced_seat_count < min_required_seat_count: raise BillingError( 'lowball seat count', "You must invoice for at least %d users." % (min_required_seat_count,)) seat_count = invoiced_seat_count process_initial_upgrade(user, plan, seat_count, request.POST.get('stripe_token', None)) except BillingError as e: error_message = e.message error_description = e.description except Exception as e: billing_logger.exception("Uncaught exception in billing: %s" % (e,)) error_message = BillingError.CONTACT_SUPPORT error_description = "uncaught exception during upgrade" else: return HttpResponseRedirect(reverse('corporate.views.billing_home')) seat_count = get_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context = { 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'email': user.email, 'seat_count': seat_count, 'signed_seat_count': signed_seat_count, 'salt': salt, 'min_seat_count_for_invoice': max(seat_count, MIN_INVOICED_SEAT_COUNT), 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", 'nickname_monthly': Plan.CLOUD_MONTHLY, 'nickname_annual': Plan.CLOUD_ANNUAL, 'error_message': error_message, 'page_params': JSONEncoderForHTML().encode({ 'seat_count': seat_count, 'nickname_annual': Plan.CLOUD_ANNUAL, 'nickname_monthly': Plan.CLOUD_MONTHLY, 'annual_price': 8000, 'monthly_price': 800, 'percent_off': percent_off, }), } # type: Dict[str, Any] response = render(request, 'corporate/upgrade.html', context=context) response['error_description'] = error_description return response
def test_upgrade_where_first_card_fails(self, *mocks: 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. with patch("corporate.lib.stripe.billing_logger.error") as mock_billing_logger: self.upgrade(stripe_token=stripe_create_token('4000000000000341').id) mock_billing_logger.assert_called() # Check that we created a Customer object but no CustomerPlan stripe_customer_id = Customer.objects.get(realm=get_realm('zulip')).stripe_customer_id self.assertFalse(CustomerPlan.objects.exists()) # Check that we created a Customer in stripe, a failed Charge, and no Invoices or Invoice Items self.assertTrue(stripe_get_customer(stripe_customer_id)) stripe_charges = [charge for charge in stripe.Charge.list(customer=stripe_customer_id)] self.assertEqual(len(stripe_charges), 1) self.assertEqual(stripe_charges[0].failure_code, 'card_declined') # TODO: figure out what these actually are self.assertFalse(stripe.Invoice.list(customer=stripe_customer_id)) self.assertFalse(stripe.InvoiceItem.list(customer=stripe_customer_id)) # 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.assertNotEqual(realm.plan_type, Realm.STANDARD) # 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, after they added a few users with patch('corporate.lib.stripe.get_seat_count', return_value=23): with patch('corporate.views.get_seat_count', return_value=23): self.upgrade() customer = Customer.objects.get(realm=get_realm('zulip')) # It's impossible to create two Customers, but check that we didn't # change stripe_customer_id self.assertEqual(customer.stripe_customer_id, stripe_customer_id) # Check that we successfully added a CustomerPlan self.assertTrue(CustomerPlan.objects.filter(customer=customer, licenses=23).exists()) # Check the Charges and Invoices in Stripe self.assertEqual(8000 * 23, [charge for charge in stripe.Charge.list(customer=stripe_customer_id)][0].amount) stripe_invoice = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer_id)][0] self.assertEqual([8000 * 23, -8000 * 23], [item.amount for item in stripe_invoice.lines]) # 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')) # TODO: Test for REALM_PLAN_TYPE_CHANGED as the last entry self.assertEqual(audit_log_entries, [RealmAuditLog.STRIPE_CUSTOMER_CREATED, RealmAuditLog.STRIPE_CARD_CHANGED, RealmAuditLog.STRIPE_CARD_CHANGED, RealmAuditLog.CUSTOMER_PLAN_CREATED]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertEqual(realm.plan_type, Realm.STANDARD) # 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_by_invoice(self, *mocks: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) # Click "Make payment" in Stripe Checkout with patch('corporate.lib.stripe.timezone_now', return_value=self.now): self.upgrade(invoice=True) # Check that we correctly created a Customer in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) # It can take a second for Stripe to attach the source to the customer, and in # particular it may not be attached at the time stripe_get_customer is called above, # causing test flakes. # So commenting the next line out, but leaving it here so future readers know what # is supposed to happen here # self.assertEqual(stripe_customer.default_source.type, 'ach_credit_transfer') # Check Charges in Stripe self.assertFalse(stripe.Charge.list(customer=stripe_customer.id)) # Check Invoices in Stripe stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_invoices), 1) self.assertIsNotNone(stripe_invoices[0].due_date) self.assertIsNotNone(stripe_invoices[0].finalized_at) invoice_params = { 'amount_due': 8000 * 123, 'amount_paid': 0, 'attempt_count': 0, 'auto_advance': True, 'billing': 'send_invoice', 'statement_descriptor': 'Zulip Standard', 'status': 'open', 'total': 8000 * 123} for key, value in invoice_params.items(): self.assertEqual(stripe_invoices[0].get(key), value) # Check Line Items on Stripe Invoice stripe_line_items = [item for item in stripe_invoices[0].lines] self.assertEqual(len(stripe_line_items), 1) line_item_params = { 'amount': 8000 * 123, 'description': 'Zulip Standard', 'discountable': False, 'period': { 'end': datetime_to_timestamp(self.next_year), 'start': datetime_to_timestamp(self.now)}, 'plan': None, 'proration': False, 'quantity': 123} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[0].get(key), value) # Check that we correctly populated Customer and CustomerPlan in Zulip customer = Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).first() self.assertTrue(CustomerPlan.objects.filter( customer=customer, licenses=123, automanage_licenses=False, charge_automatically=False, price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, billing_schedule=CustomerPlan.ANNUAL, billed_through=self.now, next_billing_date=self.next_year, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE).exists()) # Check RealmAuditLog 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.CUSTOMER_PLAN_CREATED, self.now), # TODO: Check for REALM_PLAN_TYPE_CHANGED # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( 'extra_data', flat=True).first())['licenses'], 123) # Check that we correctly updated Realm realm = get_realm("zulip") 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(['Pay annually', 'Update card'], response) for substring in [ 'Zulip Standard', str(123), 'Your plan will renew on', 'January 2, 2013', '$9,840.00', # 9840 = 80 * 123 'Billed by invoice']: self.assert_in_response(substring, response)
def test_upgrade_by_card(self, *mocks: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['Pay annually'], response) self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout with patch('corporate.lib.stripe.timezone_now', return_value=self.now): self.upgrade() # Check that we correctly created a Customer object 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'}) # Check Charges in Stripe stripe_charges = [charge for charge in stripe.Charge.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_charges), 1) self.assertEqual(stripe_charges[0].amount, 8000 * self.seat_count) # TODO: fix Decimal self.assertEqual(stripe_charges[0].description, "Upgrade to Zulip Standard, $80.0 x {}".format(self.seat_count)) self.assertEqual(stripe_charges[0].receipt_email, user.email) self.assertEqual(stripe_charges[0].statement_descriptor, "Zulip Standard") # Check Invoices in Stripe stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_invoices), 1) self.assertIsNotNone(stripe_invoices[0].finalized_at) invoice_params = { # auto_advance is False because the invoice has been paid 'amount_due': 0, 'amount_paid': 0, 'auto_advance': False, 'billing': 'charge_automatically', 'charge': None, 'status': 'paid', 'total': 0} for key, value in invoice_params.items(): self.assertEqual(stripe_invoices[0].get(key), value) # Check Line Items on Stripe Invoice stripe_line_items = [item for item in stripe_invoices[0].lines] self.assertEqual(len(stripe_line_items), 2) line_item_params = { 'amount': 8000 * self.seat_count, 'description': 'Zulip Standard', 'discountable': False, 'period': { 'end': datetime_to_timestamp(self.next_year), 'start': datetime_to_timestamp(self.now)}, # There's no unit_amount on Line Items, probably because it doesn't show up on the # user-facing invoice. We could pull the Invoice Item instead and test unit_amount there, # but testing the amount and quantity seems sufficient. 'plan': None, 'proration': False, 'quantity': self.seat_count} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[0].get(key), value) line_item_params = { 'amount': -8000 * self.seat_count, 'description': 'Payment (Card ending in 4242)', 'discountable': False, 'plan': None, 'proration': False, 'quantity': 1} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[1].get(key), value) # Check that we correctly populated Customer and CustomerPlan in Zulip customer = Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).first() self.assertTrue(CustomerPlan.objects.filter( customer=customer, licenses=self.seat_count, automanage_licenses=True, price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, billing_schedule=CustomerPlan.ANNUAL, billed_through=self.now, next_billing_date=self.next_month, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE).exists()) # Check RealmAuditLog 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)), (RealmAuditLog.CUSTOMER_PLAN_CREATED, self.now), # TODO: Check for REALM_PLAN_TYPE_CHANGED # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( 'extra_data', flat=True).first())['licenses'], self.seat_count) # Check that we correctly updated Realm realm = get_realm("zulip") 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(['Pay annually'], response) for substring in [ 'Zulip Standard', str(self.seat_count), 'Your plan will renew on', 'January 2, 2013', '$%s.00' % (80 * self.seat_count,), 'Visa ending in 4242', 'Update card']: self.assert_in_response(substring, response)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user assert user.is_authenticated customer = get_customer_by_realm(user.realm) context: Dict[str, Any] = { "admin_access": user.has_billing_access, "has_active_plan": False, } if user.realm.plan_type == user.realm.STANDARD_FREE: context["is_sponsored"] = True return render(request, "corporate/billing.html", context=context) if customer is None: from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) if customer.sponsorship_pending: context["sponsorship_pending"] = True return render(request, "corporate/billing.html", context=context) if not CustomerPlan.objects.filter(customer=customer).exists(): from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) if not user.has_billing_access: return render(request, "corporate/billing.html", context=context) plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( plan, now) if last_ledger_entry is not None: if new_plan is not None: # nocoverage plan = new_plan assert plan is not None # for mypy downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE switch_to_annual_at_end_of_cycle = ( plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE) licenses = last_ledger_entry.licenses licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal seat_count = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = "{dt:%B} {dt.day}, {dt.year}".format( dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically assert customer.stripe_customer_id is not None # for mypy stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = "Billed by invoice" context.update( plan_name=plan.name, has_active_plan=True, free_trial=plan.is_free_trial(), downgrade_at_end_of_cycle=downgrade_at_end_of_cycle, automanage_licenses=plan.automanage_licenses, switch_to_annual_at_end_of_cycle= switch_to_annual_at_end_of_cycle, licenses=licenses, licenses_at_next_renewal=licenses_at_next_renewal, seat_count=seat_count, renewal_date=renewal_date, renewal_amount=cents_to_dollar_string(renewal_cents), payment_method=payment_method, charge_automatically=charge_automatically, publishable_key=STRIPE_PUBLISHABLE_KEY, stripe_email=stripe_customer.email, CustomerPlan=CustomerPlan, onboarding=request.GET.get("onboarding") is not None, ) return render(request, "corporate/billing.html", context=context)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) context: Dict[str, Any] = { "admin_access": user.has_billing_access, 'has_active_plan': False, } if user.realm.plan_type == user.realm.STANDARD_FREE: context["is_sponsored"] = True return render(request, 'corporate/billing.html', context=context) if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if customer.sponsorship_pending: context["sponsorship_pending"] = True return render(request, 'corporate/billing.html', context=context) if not CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.has_billing_access: return render(request, 'corporate/billing.html', context=context) plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( plan, now) if last_ledger_entry is not None: if new_plan is not None: # nocoverage plan = new_plan assert (plan is not None) # for mypy free_trial = plan.status == CustomerPlan.FREE_TRIAL downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE switch_to_annual_at_end_of_cycle = plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE licenses = last_ledger_entry.licenses licenses_used = get_latest_seat_count(user.realm) # Should do this in javascript, using the user's timezone renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=start_of_next_billing_cycle(plan, now)) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer) else: payment_method = 'Billed by invoice' context.update({ 'plan_name': plan.name, 'has_active_plan': True, 'free_trial': free_trial, 'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle, 'automanage_licenses': plan.automanage_licenses, 'switch_to_annual_at_end_of_cycle': switch_to_annual_at_end_of_cycle, 'licenses': licenses, 'licenses_used': licenses_used, 'renewal_date': renewal_date, 'renewal_amount': f'{renewal_cents / 100.:,.2f}', 'payment_method': payment_method, 'charge_automatically': charge_automatically, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, 'CustomerPlan': CustomerPlan, 'onboarding': request.GET.get("onboarding") is not None, }) return render(request, 'corporate/billing.html', context=context)
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)