diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 5c045e67e3..7474269ca3 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -163,7 +163,7 @@ def stripe_get_customer(stripe_customer_id: str) -> stripe.Customer: return stripe.Customer.retrieve(stripe_customer_id, expand=["default_source"]) @catch_stripe_errors -def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: +def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: realm = user.realm # We could do a better job of handling race conditions here, but if two # people from a realm try to upgrade at exactly the same time, the main @@ -183,7 +183,8 @@ def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None) -> C RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, event_time=event_time) - customer = Customer.objects.create(realm=realm, stripe_customer_id=stripe_customer.id) + customer, created = Customer.objects.update_or_create(realm=realm, defaults={ + 'stripe_customer_id': stripe_customer.id}) user.is_billing_admin = True user.save(update_fields=["is_billing_admin"]) return customer @@ -221,8 +222,8 @@ def add_plan_renewal_to_license_ledger_if_needed(plan: CustomerPlan, event_time: def update_or_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: realm = user.realm customer = Customer.objects.filter(realm=realm).first() - if customer is None: - return do_create_customer(user, stripe_token=stripe_token) + if customer is None or customer.stripe_customer_id is None: + return do_create_stripe_customer(user, stripe_token=stripe_token) if stripe_token is not None: do_replace_payment_source(user, stripe_token) return customer @@ -442,12 +443,8 @@ def invoice_plans_as_needed(event_time: datetime) -> None: for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): invoice_plan(plan, event_time) -def attach_discount_to_realm(user: UserProfile, discount: Decimal) -> None: - customer = Customer.objects.filter(realm=user.realm).first() - if customer is None: - customer = do_create_customer(user) - customer.default_discount = discount - customer.save() +def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None: + Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount}) def process_downgrade(user: UserProfile) -> None: # nocoverage pass diff --git a/corporate/migrations/0006_nullable_stripe_customer_id.py b/corporate/migrations/0006_nullable_stripe_customer_id.py new file mode 100644 index 0000000000..7b0348f1f6 --- /dev/null +++ b/corporate/migrations/0006_nullable_stripe_customer_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-29 01:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('corporate', '0005_customerplan_invoicing'), + ] + + operations = [ + migrations.AlterField( + model_name='customer', + name='stripe_customer_id', + field=models.CharField(max_length=255, null=True, unique=True), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 745414014f..0b399bced2 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -9,7 +9,7 @@ from zerver.models import Realm, RealmAuditLog class Customer(models.Model): realm = models.OneToOneField(Realm, on_delete=CASCADE) # type: Realm - stripe_customer_id = models.CharField(max_length=255, unique=True) # type: str + stripe_customer_id = models.CharField(max_length=255, null=True, unique=True) # type: str # Deprecated .. delete once everyone is migrated to new billing system has_billing_relationship = models.BooleanField(default=False) # type: bool # A percentage, like 85. diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json new file mode 100644 index 0000000000..2461b7f405 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.2.json new file mode 100644 index 0000000000..233e594bb6 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json new file mode 100644 index 0000000000..57497dcf4e Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json new file mode 100644 index 0000000000..a5b0b78e86 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json index ea949ad052..efd9676112 100644 Binary files a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.retrieve.1.json new file mode 100644 index 0000000000..74cd489a96 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.retrieve.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json new file mode 100644 index 0000000000..026bfd7a7f Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.1.json new file mode 100644 index 0000000000..265105506b Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json new file mode 100644 index 0000000000..d41dfb5c02 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000..cf96248b7c Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000..9771931b70 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json new file mode 100644 index 0000000000..79ad7b91fe Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json new file mode 100644 index 0000000000..ab7c8480b1 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json new file mode 100644 index 0000000000..604c1d0167 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json new file mode 100644 index 0000000000..1768a0f9bb Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.3.json new file mode 100644 index 0000000000..15ae400a25 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.3.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json new file mode 100644 index 0000000000..2e5300ced6 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json new file mode 100644 index 0000000000..60d47536e7 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json differ diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json new file mode 100644 index 0000000000..bc7b308cf4 Binary files /dev/null and b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json differ diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index f4d6542bab..cdc67db9d4 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -26,7 +26,7 @@ from zerver.models import Realm, UserProfile, get_realm, RealmAuditLog from corporate.lib.stripe import catch_stripe_errors, attach_discount_to_realm, \ get_seat_count, sign_string, unsign_string, \ BillingError, StripeCardError, StripeConnectionError, stripe_get_customer, \ - DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_LICENSES, do_create_customer, \ + DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_LICENSES, do_create_stripe_customer, \ add_months, next_month, next_renewal_date, renewal_amount, \ compute_plan_parameters, update_or_create_stripe_customer, \ process_initial_upgrade, add_plan_renewal_to_license_ledger_if_needed, \ @@ -747,7 +747,7 @@ class StripeTest(StripeTestCase): # If you pay by invoice, your payment method should be # "Billed by invoice", even if you have a card on file # user = self.example_user("hamlet") - # do_create_customer(user, stripe_create_token().id) + # do_create_stripe_customer(user, stripe_create_token().id) # self.login(user.email) # self.upgrade(invoice=True) # stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) @@ -761,18 +761,32 @@ class StripeTest(StripeTestCase): def test_attach_discount_to_realm(self, *mocks: Mock) -> None: # Attach discount before Stripe customer exists user = self.example_user('hamlet') - attach_discount_to_realm(user, Decimal(85)) + attach_discount_to_realm(user.realm, Decimal(85)) self.login(user.email) # Check that the discount appears in page_params self.assert_in_success_response(['85'], self.client_get("/upgrade/")) # Check that the customer was charged the discounted amount - # TODO - # Check upcoming invoice reflects the discount - # TODO + self.upgrade() + stripe_customer_id = Customer.objects.values_list('stripe_customer_id', flat=True).first() + self.assertEqual(1200 * self.seat_count, + [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([1200 * self.seat_count, -1200 * self.seat_count], + [item.amount for item in stripe_invoice.lines]) + # Check CustomerPlan reflects the discount + plan = CustomerPlan.objects.get(price_per_license=1200, discount=Decimal(85)) + # Attach discount to existing Stripe customer - attach_discount_to_realm(user, Decimal(25)) - # Check upcoming invoice reflects the new discount - # TODO + plan.status = CustomerPlan.ENDED + plan.save(update_fields=['status']) + attach_discount_to_realm(user.realm, Decimal(25)) + process_initial_upgrade(user, self.seat_count, True, CustomerPlan.ANNUAL, stripe_create_token().id) + self.assertEqual(6000 * self.seat_count, + [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([6000 * self.seat_count, -6000 * self.seat_count], + [item.amount for item in stripe_invoice.lines]) + plan = CustomerPlan.objects.get(price_per_license=6000, discount=Decimal(25)) @mock_stripe() def test_replace_payment_source(self, *mocks: Mock) -> None: @@ -923,7 +937,7 @@ class BillingHelpersTest(ZulipTestCase): def test_update_or_create_stripe_customer_logic(self) -> None: user = self.example_user('hamlet') # No existing Customer object - with patch('corporate.lib.stripe.do_create_customer', return_value='returned') as mocked1: + with patch('corporate.lib.stripe.do_create_stripe_customer', return_value='returned') as mocked1: returned = update_or_create_stripe_customer(user, stripe_token='token') mocked1.assert_called() self.assertEqual(returned, 'returned')