diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 85dcb9e415..64016862db 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -231,10 +231,10 @@ def compute_plan_parameters( if discount is not None: # There are no fractional cents in Stripe, so round down to nearest integer. price_per_license = int(float(price_per_license * (1 - discount / 100)) + .00001) - next_billing_date = period_end + next_invoice_date = period_end if automanage_licenses: - next_billing_date = add_months(billing_cycle_anchor, 1) - return billing_cycle_anchor, next_billing_date, period_end, price_per_license + next_invoice_date = add_months(billing_cycle_anchor, 1) + return billing_cycle_anchor, next_invoice_date, period_end, price_per_license # Only used for cloud signups @catch_stripe_errors @@ -251,7 +251,7 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license "Customer {} trying to upgrade, but has an active subscription".format(customer)) raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) - billing_cycle_anchor, next_billing_date, period_end, price_per_license = compute_plan_parameters( + 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 @@ -294,15 +294,16 @@ def process_initial_upgrade(user: UserProfile, licenses: int, automanage_license customer=customer, # Deprecated, remove licenses=-1, - billed_through=billing_cycle_anchor, - next_billing_date=next_billing_date, + next_invoice_date=next_invoice_date, **plan_params) - LicenseLedger.objects.create( + 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, diff --git a/corporate/migrations/0005_customerplan_invoicing.py b/corporate/migrations/0005_customerplan_invoicing.py new file mode 100644 index 0000000000..d0e95ce591 --- /dev/null +++ b/corporate/migrations/0005_customerplan_invoicing.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-28 13:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('corporate', '0004_licenseledger'), + ] + + operations = [ + migrations.RenameField( + model_name='customerplan', + old_name='next_billing_date', + new_name='next_invoice_date', + ), + migrations.RemoveField( + model_name='customerplan', + name='billed_through', + ), + migrations.AddField( + model_name='customerplan', + name='invoiced_through', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='corporate.LicenseLedger'), + ), + migrations.AddField( + model_name='customerplan', + name='invoicing_status', + field=models.SmallIntegerField(default=1), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 1447bff9a3..745414014f 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -39,9 +39,12 @@ class CustomerPlan(models.Model): MONTHLY = 2 billing_schedule = models.SmallIntegerField() # type: int - # This is like analytic's FillState, but for billing - billed_through = models.DateTimeField() # type: datetime.datetime - next_billing_date = models.DateTimeField(db_index=True) # type: datetime.datetime + next_invoice_date = models.DateTimeField(db_index=True) # type: datetime.datetime + invoiced_through = models.ForeignKey( + 'LicenseLedger', null=True, on_delete=CASCADE, related_name='+') # type: Optional[LicenseLedger] + DONE = 1 + STARTED = 2 + invoicing_status = models.SmallIntegerField(default=DONE) # type: int STANDARD = 1 PLUS = 2 # not available through self-serve signup diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 57c3f21bff..1514911a70 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -392,8 +392,8 @@ class StripeTest(StripeTestCase): plan = CustomerPlan.objects.get( customer=customer, 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, + billing_schedule=CustomerPlan.ANNUAL, invoiced_through=LicenseLedger.objects.first(), + next_invoice_date=self.next_month, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE) LicenseLedger.objects.get( plan=plan, is_renewal=True, event_time=self.now, licenses=self.seat_count, @@ -477,8 +477,8 @@ class StripeTest(StripeTestCase): plan = CustomerPlan.objects.get( customer=customer, 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, + billing_schedule=CustomerPlan.ANNUAL, invoiced_through=LicenseLedger.objects.first(), + next_invoice_date=self.next_year, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE) LicenseLedger.objects.get( plan=plan, is_renewal=True, event_time=self.now, licenses=123, licenses_at_next_renewal=123)