From 11908c4c2e391223f768d88dacc12a6366b4e580 Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Mon, 8 Jan 2024 17:58:06 +0530 Subject: [PATCH] stripe: Add cron-based plan invoicing to remote realm billing system. --- corporate/lib/stripe.py | 35 ++++++- ...customerplan_invoice_overdue_email_sent.py | 17 +++ corporate/models.py | 5 + ...ce_plans_as_needed--Customer.create.1.json | Bin 0 -> 795 bytes ...ce_plans_as_needed--Customer.modify.1.json | Bin 0 -> 820 bytes ..._plans_as_needed--Customer.retrieve.1.json | Bin 0 -> 1916 bytes ..._plans_as_needed--Customer.retrieve.2.json | Bin 0 -> 1916 bytes ..._plans_as_needed--Customer.retrieve.3.json | Bin 0 -> 1916 bytes ..._plans_as_needed--Customer.retrieve.4.json | Bin 0 -> 1916 bytes ...invoice_plans_as_needed--Event.list.1.json | Bin 0 -> 1695 bytes ...invoice_plans_as_needed--Event.list.2.json | Bin 0 -> 15785 bytes ...invoice_plans_as_needed--Event.list.3.json | Bin 0 -> 27373 bytes ...invoice_plans_as_needed--Event.list.4.json | Bin 0 -> 29816 bytes ...invoice_plans_as_needed--Event.list.5.json | Bin 0 -> 81 bytes ...ice_plans_as_needed--Invoice.create.1.json | Bin 0 -> 7106 bytes ...ice_plans_as_needed--Invoice.create.2.json | Bin 0 -> 7144 bytes ...as_needed--Invoice.finalize_invoice.1.json | Bin 0 -> 7447 bytes ...as_needed--Invoice.finalize_invoice.2.json | Bin 0 -> 7505 bytes ...voice_plans_as_needed--Invoice.list.1.json | Bin 0 -> 83 bytes ...voice_plans_as_needed--Invoice.list.2.json | Bin 0 -> 17103 bytes ...plans_as_needed--InvoiceItem.create.1.json | Bin 0 -> 1111 bytes ...plans_as_needed--InvoiceItem.create.2.json | Bin 0 -> 1085 bytes ...plans_as_needed--InvoiceItem.create.3.json | Bin 0 -> 1115 bytes ...plans_as_needed--InvoiceItem.create.4.json | Bin 0 -> 1119 bytes ...plans_as_needed--InvoiceItem.create.5.json | Bin 0 -> 1095 bytes ...plans_as_needed--InvoiceItem.create.6.json | Bin 0 -> 1116 bytes ...ans_as_needed--PaymentIntent.create.1.json | Bin 0 -> 6064 bytes ...plans_as_needed--SetupIntent.create.1.json | Bin 0 -> 930 bytes ...e_plans_as_needed--SetupIntent.list.1.json | Bin 0 -> 1116 bytes ...ans_as_needed--SetupIntent.retrieve.1.json | Bin 0 -> 930 bytes ..._as_needed--checkout.Session.create.1.json | Bin 0 -> 2379 bytes ...ns_as_needed--checkout.Session.list.1.json | Bin 0 -> 2769 bytes corporate/tests/test_stripe.py | 99 ++++++++++++++++++ templates/zerver/emails/invoice_overdue.html | 10 ++ .../zerver/emails/invoice_overdue.subject.txt | 1 + templates/zerver/emails/invoice_overdue.txt | 3 + 36 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 corporate/migrations/0033_customerplan_invoice_overdue_email_sent.py create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.modify.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.3.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.4.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.3.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.4.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.5.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.5.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.6.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--PaymentIntent.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--SetupIntent.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--SetupIntent.list.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--SetupIntent.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--checkout.Session.create.1.json create mode 100644 corporate/tests/stripe_fixtures/invoice_plans_as_needed--checkout.Session.list.1.json create mode 100644 templates/zerver/emails/invoice_overdue.html create mode 100644 templates/zerver/emails/invoice_overdue.subject.txt create mode 100644 templates/zerver/emails/invoice_overdue.txt diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index dd33c70354..6779d522be 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -2478,7 +2478,8 @@ class BillingSession(ABC): stripe.Invoice.finalize_invoice(stripe_invoice) plan.next_invoice_date = next_invoice_date(plan) - plan.save(update_fields=["next_invoice_date"]) + plan.invoice_overdue_email_sent = False + plan.save(update_fields=["next_invoice_date", "invoice_overdue_email_sent"]) def do_change_plan_to_new_tier(self, new_plan_tier: int) -> str: customer = self.get_customer() @@ -4341,10 +4342,40 @@ def get_plan_renewal_or_end_date(plan: CustomerPlan, event_time: datetime) -> da def invoice_plans_as_needed(event_time: Optional[datetime] = None) -> None: if event_time is None: # nocoverage event_time = timezone_now() - # TODO: Add RemoteRealmBillingSession and RemoteServerBillingSession cases. + # TODO: Add RemoteServerBillingSession cases. for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): if plan.customer.realm is not None: RealmBillingSession(realm=plan.customer.realm).invoice_plan(plan, event_time) + elif plan.customer.remote_realm is not None: + remote_realm = plan.customer.remote_realm + remote_server = remote_realm.server + billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) + + assert remote_server.last_audit_log_update is not None + assert plan.next_invoice_date is not None + if plan.next_invoice_date > remote_server.last_audit_log_update: + if ( + plan.next_invoice_date - remote_server.last_audit_log_update + >= timedelta(days=1) + and not plan.invoice_overdue_email_sent + ): + context = { + "support_url": billing_session.support_url(), + "last_audit_log_update": remote_server.last_audit_log_update.strftime( + "%Y-%m-%d" + ), + } + send_email( + "zerver/emails/invoice_overdue", + to_emails=[BILLING_SUPPORT_EMAIL], + from_address=FromAddress.tokenized_no_reply_address(), + context=context, + ) + plan.invoice_overdue_email_sent = True + plan.save(update_fields=["invoice_overdue_email_sent"]) + continue + + billing_session.invoice_plan(plan, event_time) # TODO: Assert that we never invoice legacy plans. diff --git a/corporate/migrations/0033_customerplan_invoice_overdue_email_sent.py b/corporate/migrations/0033_customerplan_invoice_overdue_email_sent.py new file mode 100644 index 0000000000..17fc8b64f6 --- /dev/null +++ b/corporate/migrations/0033_customerplan_invoice_overdue_email_sent.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2024-01-10 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("corporate", "0032_customer_minimum_licenses"), + ] + + operations = [ + migrations.AddField( + model_name="customerplan", + name="invoice_overdue_email_sent", + field=models.BooleanField(default=False), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 7f933164c9..d70bd61607 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -249,6 +249,11 @@ class CustomerPlan(models.Model): # next_invoice_date. next_invoice_date = models.DateTimeField(db_index=True, null=True) + # Flag to track if an email has been sent to Zulip team for + # invoice overdue by >= one day. Helps to send an email only once + # and not every time when cron run. + invoice_overdue_email_sent = models.BooleanField(default=False) + # On next_invoice_date, we go through ledger entries that were # created after invoiced_through and process them by generating # invoices for any additional users and/or plan renewal. Once the diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.create.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..eb66e356bb32d774364b81696ad1c9b5e0d61b10 GIT binary patch literal 795 zcmaKq$xg#S42JK0iYjM{nzj_m2_!fmu}B<2Xync~WMGysOVv{K-SJGCE+COx?BBob z&$I6J`vnN$(EH-DZ=zBsDKrNKh9iZB`JX$4fP((ZVfVF(=o}ipIxH4gK~#b9e{$YN zm;HAw1uAL2BAP&66)2Cnfk4llY=X3g;>CAVvN;XtgGaYO*T-ry0pRC@(Z#%IE2QT( zW}BTkYLF_`*Ps=KtJa2F-ImC-S;3=ck5A7J*Y|fXw>PwAxy_n|l^j{)u#(H9%&OmM zk0D6&o;a?FAyFj5U^S~GDshOHQXQiPR+?638bfW<6y3JNo7bm7j>Ivxv<|_K29t6;AI!$( z+2EoY4y)-%l+)RHu^Y>PP9t^O6$Y1q$t_rq>F)VClz9m||3IEcY7r8J9B*oCj%?CN ukY1o*%5qScaJ;@{t*>R%5U9hh0hSDxsGICYx15Qgvh715pv+D$`0I29>96af{S5C~Zld$#N1uiE1%rRslY?9HYrNZs4c z$2;#lz9!sQABYr*BN;7}3dx<7S0DWJ5x} zl1o0vs(%=%&}es`(GhF)U=r2=3X_aFkoJz_&7W*^cuSZlVccSzW4~Gf$g{=reAA2_ zI!T|a%t0J2XjA$JumUROKZ+lc$Qs=q*~W%1xi?@>ozn@a#Eq5uxCF z8!HB%$~FF@J{fsYob!nB_#(^2syo>%E>604i}Sv1`_rZBPA|@ygDnTPSeiCz9PUWy zJ@JI)LDYBB)g}Dq4rR48vSkW29tQ7T$dp?lKTRc!mte5q`1z~Wra_0mppK^!>;&%6 WhN|s~SgkD&O-Ppby|11<&He(jGy9_e literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.1.json new file mode 100644 index 0000000000000000000000000000000000000000..364fe0554b48a2361fe0ebea053ffbcb66ecbebe GIT binary patch literal 1916 zcmaJ?-EY${5P#>dsQgS(({*1wfwVmg0TUo3#57s1b7{Oec4j+W$F%>QJ3C3;(vi2^ z_xZ(`nVd3*dtXw+1(XYOxSPo=)dy zOO`y8m$r*x#^BHhrOCe*jfU_cg^{*NE{Zy1vh)1w_4h9yKVSX6yudb#UTfA?N#L!6 zN^Qt8TJ5VngrLldaC}A#5k(9bY{M%By3FB4)qqheo*HbKHxOz|RU}s$fzk-bikA?u z_HEP{c{{SfJ|$Ekp9IP1V-XK`P=z_5z`O%g$pd{zF+Zeetq($TX-nFVfBaAx21SH{ zBEHaM(#M8b?0PaIn!znIZfk1>aUWug#iVdKWCWTMCeCuLyol55lgb+8{Vs@T-owZv z2t3*xYH2XbI)B|*uH~p?t-!k)TOTXO)Z3A;pFi?SVbk))nowgUMTP{)pFlVi&u8=F zOjF^SfNPzKJD-+wSm9xbdl;(DlxU4H7@l;gLY{dA1{|F^uN+>+{%n~RJ}k&}F<;Dc zM+4!}x&!d8adX8|(*SQq6sQyy=aY$UxBZ}&E)9@aXq2=jqhXFarWz9wP6w$kwvjFzXeh7IN3=@>(>A=g{xzGG7nk3J zvkw<{eie-C!l$bdPQ1G{H0icR7K|~FB19>IpdPp7+2tKbDjs)jhe`%OS$Jv*mvk)= zfZm%`)`nR7_7oYic?q~mD{dp{4!nhq(d}S;EUzu5z&H&C=Lnu|c#V`nCG4|z-}SC> z*Cn)aW}iTn$@wYWRw`?cKA3?`;HqbC5A?9a_a>ec+oCp3 zknmzT-6X-ViCkMLG!Ps&w^8M5)wb!F2K-nM8x9+2+Sslk?uxebU~!2d?$JMP^vj#S D;o}NE literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.2.json new file mode 100644 index 0000000000000000000000000000000000000000..364fe0554b48a2361fe0ebea053ffbcb66ecbebe GIT binary patch literal 1916 zcmaJ?-EY${5P#>dsQgS(({*1wfwVmg0TUo3#57s1b7{Oec4j+W$F%>QJ3C3;(vi2^ z_xZ(`nVd3*dtXw+1(XYOxSPo=)dy zOO`y8m$r*x#^BHhrOCe*jfU_cg^{*NE{Zy1vh)1w_4h9yKVSX6yudb#UTfA?N#L!6 zN^Qt8TJ5VngrLldaC}A#5k(9bY{M%By3FB4)qqheo*HbKHxOz|RU}s$fzk-bikA?u z_HEP{c{{SfJ|$Ekp9IP1V-XK`P=z_5z`O%g$pd{zF+Zeetq($TX-nFVfBaAx21SH{ zBEHaM(#M8b?0PaIn!znIZfk1>aUWug#iVdKWCWTMCeCuLyol55lgb+8{Vs@T-owZv z2t3*xYH2XbI)B|*uH~p?t-!k)TOTXO)Z3A;pFi?SVbk))nowgUMTP{)pFlVi&u8=F zOjF^SfNPzKJD-+wSm9xbdl;(DlxU4H7@l;gLY{dA1{|F^uN+>+{%n~RJ}k&}F<;Dc zM+4!}x&!d8adX8|(*SQq6sQyy=aY$UxBZ}&E)9@aXq2=jqhXFarWz9wP6w$kwvjFzXeh7IN3=@>(>A=g{xzGG7nk3J zvkw<{eie-C!l$bdPQ1G{H0icR7K|~FB19>IpdPp7+2tKbDjs)jhe`%OS$Jv*mvk)= zfZm%`)`nR7_7oYic?q~mD{dp{4!nhq(d}S;EUzu5z&H&C=Lnu|c#V`nCG4|z-}SC> z*Cn)aW}iTn$@wYWRw`?cKA3?`;HqbC5A?9a_a>ec+oCp3 zknmzT-6X-ViCkMLG!Ps&w^8M5)wb!F2K-nM8x9+2+Sslk?uxebU~!2d?$JMP^vj#S D;o}NE literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.3.json new file mode 100644 index 0000000000000000000000000000000000000000..364fe0554b48a2361fe0ebea053ffbcb66ecbebe GIT binary patch literal 1916 zcmaJ?-EY${5P#>dsQgS(({*1wfwVmg0TUo3#57s1b7{Oec4j+W$F%>QJ3C3;(vi2^ z_xZ(`nVd3*dtXw+1(XYOxSPo=)dy zOO`y8m$r*x#^BHhrOCe*jfU_cg^{*NE{Zy1vh)1w_4h9yKVSX6yudb#UTfA?N#L!6 zN^Qt8TJ5VngrLldaC}A#5k(9bY{M%By3FB4)qqheo*HbKHxOz|RU}s$fzk-bikA?u z_HEP{c{{SfJ|$Ekp9IP1V-XK`P=z_5z`O%g$pd{zF+Zeetq($TX-nFVfBaAx21SH{ zBEHaM(#M8b?0PaIn!znIZfk1>aUWug#iVdKWCWTMCeCuLyol55lgb+8{Vs@T-owZv z2t3*xYH2XbI)B|*uH~p?t-!k)TOTXO)Z3A;pFi?SVbk))nowgUMTP{)pFlVi&u8=F zOjF^SfNPzKJD-+wSm9xbdl;(DlxU4H7@l;gLY{dA1{|F^uN+>+{%n~RJ}k&}F<;Dc zM+4!}x&!d8adX8|(*SQq6sQyy=aY$UxBZ}&E)9@aXq2=jqhXFarWz9wP6w$kwvjFzXeh7IN3=@>(>A=g{xzGG7nk3J zvkw<{eie-C!l$bdPQ1G{H0icR7K|~FB19>IpdPp7+2tKbDjs)jhe`%OS$Jv*mvk)= zfZm%`)`nR7_7oYic?q~mD{dp{4!nhq(d}S;EUzu5z&H&C=Lnu|c#V`nCG4|z-}SC> z*Cn)aW}iTn$@wYWRw`?cKA3?`;HqbC5A?9a_a>ec+oCp3 zknmzT-6X-ViCkMLG!Ps&w^8M5)wb!F2K-nM8x9+2+Sslk?uxebU~!2d?$JMP^vj#S D;o}NE literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Customer.retrieve.4.json new file mode 100644 index 0000000000000000000000000000000000000000..364fe0554b48a2361fe0ebea053ffbcb66ecbebe GIT binary patch literal 1916 zcmaJ?-EY${5P#>dsQgS(({*1wfwVmg0TUo3#57s1b7{Oec4j+W$F%>QJ3C3;(vi2^ z_xZ(`nVd3*dtXw+1(XYOxSPo=)dy zOO`y8m$r*x#^BHhrOCe*jfU_cg^{*NE{Zy1vh)1w_4h9yKVSX6yudb#UTfA?N#L!6 zN^Qt8TJ5VngrLldaC}A#5k(9bY{M%By3FB4)qqheo*HbKHxOz|RU}s$fzk-bikA?u z_HEP{c{{SfJ|$Ekp9IP1V-XK`P=z_5z`O%g$pd{zF+Zeetq($TX-nFVfBaAx21SH{ zBEHaM(#M8b?0PaIn!znIZfk1>aUWug#iVdKWCWTMCeCuLyol55lgb+8{Vs@T-owZv z2t3*xYH2XbI)B|*uH~p?t-!k)TOTXO)Z3A;pFi?SVbk))nowgUMTP{)pFlVi&u8=F zOjF^SfNPzKJD-+wSm9xbdl;(DlxU4H7@l;gLY{dA1{|F^uN+>+{%n~RJ}k&}F<;Dc zM+4!}x&!d8adX8|(*SQq6sQyy=aY$UxBZ}&E)9@aXq2=jqhXFarWz9wP6w$kwvjFzXeh7IN3=@>(>A=g{xzGG7nk3J zvkw<{eie-C!l$bdPQ1G{H0icR7K|~FB19>IpdPp7+2tKbDjs)jhe`%OS$Jv*mvk)= zfZm%`)`nR7_7oYic?q~mD{dp{4!nhq(d}S;EUzu5z&H&C=Lnu|c#V`nCG4|z-}SC> z*Cn)aW}iTn$@wYWRw`?cKA3?`;HqbC5A?9a_a>ec+oCp3 zknmzT-6X-ViCkMLG!Ps&w^8M5)wb!F2K-nM8x9+2+Sslk?uxebU~!2d?$JMP^vj#S D;o}NE literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..3770b6ff5c17914920fa2a1bf3bfb6974cf3e14b GIT binary patch literal 1695 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.5.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Event.list.5.json new file mode 100644 index 0000000000000000000000000000000000000000..6d922067afeefefcba3c644d2dd8510c1d8984a9 GIT binary patch literal 81 zcmb>CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Xv?vE8pkHRFpIVlhS5mCRRm%kck@XgY literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..f07b172fd39062c97f97ed6568bf85f592d8446d GIT binary patch literal 7106 zcmeHMOOF&c5We#(T2>B;1T3=;1?80I0fi(eQ7#~4dAdD}vu=0e$HId6@2T?J-7{!% zh**^D0U=kpT&~CGu5aGo>pH^vesaoE|F6C!-*n{9Kgj)Bq}a%D{b!~I&AO*-YwmYX zRR6G5v+OMug8@zDqzo2DZ+mdK|53d(GchdJG#{BzLcLlF%wEiMvp&yK^vsG$HOG}@ zy5pQOe|)21Eg$E2+j2};<>=9lNyCGu&Q6pKH%#^DzSXX*k8pNR;l?0%53zzka>cf? zH36%}GV4-IK=Fe&y%kczeW>Ql_$!c+DaT|8XuoIr6&{)6*iTaO9r{@mVa%~kKS?933godFAUC%A00*Lwli{xE0gufP5N%Xd$o zJpbuOFuJKXa}9wSG&kv{@ia6HGR&()D|Yx*|tA8&4W$a@yC$N`gi;#2;gk z&6-||d0DT6Fuv_Tu~>;~^=`-M00wI{zT06z+(41z3pO@4=>dY zHA=KF@H&^|3q0U#oRr;niofYn({wqx)#+(1B2&q6vK^C;NCFqjShNtY_QjmYkLM&> zNtqDVQF;-qa7ifx>RLw-S*u$YVY9^m3h)AlbYQal3_NBx0SG-j@ahq92)xCts4 zcC^41L;+4U8Z6?R+WO!bsvopcNkz-E4u#$v$9xxo@*>!9SVC#j?|FCu!DH%_upUOT zQ~eRREeZ!&UD5ixFCLt7g&f`Jd{ zxq_edix;v$Qv4$1ECHYn&=Qf`Fy-bhH;zya_q=#~bppgMv`$C(=lHeKeb0>OtNfEs zh2`h96Q^T&v+X!G9P52jy8EAF`NjF;&DG}dTG>p(7y2e z^Mv*Vd>jXNuXtzRy|mX|`%FjFPGS1s+x%s47T7+Vzx0AJXuc#(8l{PyER%tS#!vca zB5+US>}GQo@zB73+UYSh7En)Tfgl1e{7ZB2yJ-!BMDe$U@?(Jv1}6&1fZh=0#%)WW zm2cwcoX2mN7X5@OcXVpFugH1l!H(bpotyE!9iG$+P*FLZEzOBy+-fwrNCcp4IA`O`kO&q~+10^WN7zRCZ%5QfOa1>*DyFFwo}EeAjX zb(tnkh~Fztjd7A6oKf*D8+>p-5bP^_&O1ub?)WVUpK3W zOW9&TsiWVA5yh5VMm)|V+#wo;_)P48T#PJgE`T?DyNB`4KoIc54j7&i@p~Wc{SB`q Bvi|@8 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.2.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..234942fd25dc56fcaba759b3acdbc2c965eca77e GIT binary patch literal 7144 zcmeHM%Z?N`6y5U`B`b?W0;V6+4azD+NhA_Nq(s?(kmas&)6TeDh3y&`5dWUzN4f0k zZZug$3`%A(nz661ukYj7_tb}bRYh1`cZ0Ij|JFC;+lu`82f3e5Db{i~{tHu$X4Nyc zGxw{fs(x6CS@w>K#z0aHQU(pJ*WC%mnX26(tRANE7JJte z=f=bbcnt33itS{n0;9$8=3nJhJqm$|&;?_9v$=E~_P4Vx18O=1s_Mfv~TwL zYRQa6swpPLdtbklYu&jNOB+b%JB!-57G_r9C>rn9SaNU*!^)Sw0IOJtz8F*GKUl#s z7uxVc!UVFSmV@_HsNuQTsX~bw8lK0Je6?CUTr4iyPT51H_}gl%nkq#%+kPC2&@=^k zR34-6padsNU^JUPS{H*N&jsbu2PJA5LnDBN*n|jit7Q_|)^^P)yHF-92IGTAxVTfd z`wLm>_HdD-e#6WgJ=elxS%_y`gcQb#61l=uz*FrMPSYv2`NlocA5vpZMa{hqnOKJ_=4@;>!nCOmJY0mZm<~$V%$<;{&){vc`g}UM)d7E<+T$B5sR$AFC%8?tp!a;d z++^2X4L!Z$?k*_ZgxNFk#EcU4)eNSSiBq(Lqo+t?VXwPp2)W??EYFaqCuB8u4ht9l zk!=oMcl^Mvg@+9?DAV&=;Q9*pSu)28t__VDd#pw?W%iwJ>6?KmE9^-}*qh$vJHgdK ztw0VGXDUNB!w{S!o2LzIY;nUwnHMCd^A=b*K5J(A=sXzapv&Sybs$h=GSaX?i2=D$ z*lyMx`s6m*k(bknh#7V{wv|33&hY_0kKpI{$_r5-qyHl0%mE;G&>WIWH>Kj9P8_iu z=iF>>gYh$|^EF9k5dXqGSE}!s5q0&T!UKxGjZd|B%Fl)Bn=Bo1Bm0Xyfb1txy;yc0 zCC;G$ndvC&+&0g>8)uX$D^w}foj=kM8Cw=r!xmUrpU;tf9 zre`EV@uW0kcTH?(nGF0iUis^&BJfe<;(B=zkTiW7;Z^=krBgK*Q z6Av>@Or8GNjE5VvSl}dq@aN(1d~_|l`l=Xp=#=XXaCP>H7~yQrb4;6hv^&MJ5FeF2xW`Dc Zret`_cL(%*1`cu0I}kvV_wRkY_cx#Eyu$zh literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000000000000000000000000000000..470ec1c8a2d833c567d4ed758a683780c17e27be GIT binary patch literal 7447 zcmeHMTW=IM6n^Jdj7EKErNU;ji3&(nTN;AWh7>}e1%xc`jF)wH#xvNSunFS7_Z;8G zo}EQosh6NO4^iTCe0*;HY=86NR+ib z5y^7>Fj+80s98SI(3;h)i?(0JjxzK}%Z^z`JKYdkUQ$t@_X}F7UQol@1%Ze`@&R%M ziDZJ#D`O(4!l=?zcRnYe_|e#&^QwY)h-6fm5b~Oej#f6I#gfXmxMs*>(Nq;HFkDhI zy=>6Y+^P`@ej0aZRb4p)U{@PgrhER!tXp(aGEtZ+cdB&SodOGF!C947uP_28&0?J* zXTO|3{^9iG)x#q&I*lhY37IM-Q$A9+8X5)}*0n+_OIlYq11JM7nzE3@rDvA5#J;@KS|B^Zp=*kXYVaS2V1!42xnM2YP+r7*^6)~&ifnYamel!l-f7IMNT03FIJX$_I+dCF#*X2?1+lvR!eqBGgSiQJ7ul32x zRn?t8Kc1D(*Y8e`R@1YiliA~ohqJx?y}f%svEt>?#ryBnx3smTHK>wvG4#Z51J|}L z0@9WX$X>p5fkD}mplo9P&Cn*`VH93PA)N~@XvMD${1GN)gF7^ow8GZY04I91NMH44CDyJ5Lkr6sE~V;8}nS#RG)4dw}@{Y4kror z4(B^ft6;CzO*!TlfDCoD_+ZWlw^B3z9rie`frTSx!6+LoliZvHww!JfY<-xK;(-;} z(@3Up5t17(+yZQX5#LhIaw{!JB+F3)LZCcIHZ0~3IaMr~y+DA+q!V15ts0yvwwzYI z!pNx=3C-z+W;f6Kpk*s;BG{!`;QjV`p|ELIT}xIBPtr6^Al*_=kO*F^28Ud5v9GpG zbYE$h*vh8tY%k!SX$%(Af+5Lp``PQ#hHrQ{&DoM(a)TOqCsfM{4u`+J4FoHiGpUH{ z*iAE%3bkltP2P1>Xs$;Sp|4u(_Z$vhqB-
r~T0fa!zk(`v^V-5EsFlH3<@xBEp z^q0axKVk;03~Xt*jvxpmoeXVQ!WM%=4>mSRG3e1{qL8P3M7iqbF|?&Q%Q5j5JvZQI z^TJ@Oko){O$Qc4aoS-2hr)dhCyN}pGIm9`;d$UqqjJGAl zgY{pH<@>P{yJLAWZ96t>+xvOHyZ=0v&-U+552tqz4*O<T2M33v z&HA6j^0-=%s_pec;f=!bf#;tmmd|29*^cC(c4Kh-<^p+?rBA~6?deW9K2+Ns$e-9Z zarWhpRF;{2VrE=q2m1&6U&W~Eq5GeP_F3%2?$Dl0+YS|f3$*)ZX>qD9L%UB! zT&>Rw+6SJ0p3pvkkL}=Y6mJZ?_cy1Tc1=gr_G$WPw)q?DERd!6{*nX6Ah}5@|CG*k zqiJPHe7w>-6OOl=Tuwb_r{ItD_2@SivUQdq0x!%-GkR~@guz^}b{OjOLYEEl7g|oqtQ^?z2H4>G6NkbQ;uv?KIR!c=&;` zC2_gXz~!c9tVA)b;i25W1#?gD0v|u1Tp9q^AS%0SkvdJT+1pAh-0Ye{@3r-ZG;2wIK!ILjaP|rsc8mZOes4MzWiGvLE^! zIzMWQAn4!r!EKYtF*Y1O%>Xe4EWhU@EhRF1u=d|@Z2Or);AHqDqLByT$m>Tvj z5=OXrXrr3S6$m@wKkEQpX;r<`rq`ge$L9lEUwY#>BP4GN1Cc=92K^Cl=%F=(Dv)T~ zQ()f0%#dubV(5anCWeJBs-{>aqli<%qBXbmzUWYG%W?+s#%Pt?RAm8VqhnQlTJ!g8 VzJ%wVf+XNY4;Y_eif?_q^#@cw8bSa7 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.2.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.finalize_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..65c7c18c9d81bd4957fa9113e00e0b27f2a66c2e GIT binary patch literal 7505 zcmeHM*>2o66n)QE2tprPAn|M-Ck{}wDbfUK9T!>Z01g6zBXK;eB}x$~Z$bXOhnqxd zMuiJ>anSm~u*u6y-tF-6=+nI4J#=ZT%7HYmm6N!cFL`( z>w;%g@k&IS_QPb&7@_76k%UsLtewsM5$q__hg#D@GJCLARfOgjRAkx+)wKP1iM=xd zyD{+z9)mlXpsONQfl;ZbYQ1M5_}Qpl@uGk|q+wJV6Vj53mKN5n*@iY}ddtq6SydD) z)8>+rsZFI1np!bJaZGa@S`^n#0rVRhX_&}NtXr8o=~hAmS#wszG0e;a zYs_Y?B*(wJe);6+#p#bvp>!55W(^#bH%xj<-ELYj=5%Natz|>Yf~oIs+Je`6Syc{n zxOD5yb_XE!rV_D3{7jG<+skxbvMU{My}8gj;)C^x`~HKO$=RB=g(5Y*E*Wa0^LD4) zOUSBiOkQlwXr&GOuYJ*}m082En&N|Cuf>gpt6N5LC=A8edaV=U0!glo8^oE3 zT&LHH7A|>h*{rKdnQ(Tyts6w1i;IfKwrnbi1oAVaZEX0Is^#O!#4Xs93Pv#<$O)eS zPE?#CNRkQM5RK00BLDIA$+JyX9$vkEUf&!&JAC!_`R3Kjlhcdi#qs{h{P5kI@{qm} zwK`cmtLgL8I)68Nm7Sbj9z8vu9Y1}sc=`6l^>jX+ZjR1#e*Nh5JNYfGZD%#}#H`7r&e!l>x^adf?2%Dc>fbbNf)fVj6%_7Vb3qG!W8jalrm1v=b|Ecv>M20W z1(ySS1m>8mbcltVqFDLf;8)^s%wSmuoDbk-lRJ7?-x(Ix*;Q7wCdDiS4#jrijMI>_ zuoTj-=F`3DbW&DAZ4$vQ6AwB`R~))9{bE-f4%zz}8O&=WhGiX=52m-)xvVM1vS#08 zjnm??pKx3yI(az1)v^M`T2}e4a{*{kz)%N<*YPT=^PKAxIvEVk*g5L3X>B*`(Z)+W39}>d#EjtC z`QVo8j@U=rI=YWE7S>Ibw>B5_pRo;LdP25g+F`a|+B%!QomOl^FSvn?v=y>u8OQB! z+kx&linB%%m$BPw8Y<+vYD)64r9yGF(-G>r*7=^|<|T?F2O^u|kPR>db_DYz#~DjB zNKj@3)7^OsEc9LBARn~{?HqI|^dlMsqD)2_=BR1hdMIo?tBu~dOdPP<(K47O=oY9REgMIPF%(GqI{)*izF15T zmJj#$&2y?NRXj2*sxRh`X3N>5{pHTI{ZFEL7%gDRc)axFokI1&$-higUo3<6aU9bj zc4rX%AVAo=1?hp8{t(b-2bOPf_&U$A1xb4j=u-#J(Ty^i1G=LetEEeF#jLv{lk7b z4WspCA^qUwUntVgA4aJfHm5=NzL-C}D@Y$SQFp9&51jPbv~nM;YeAsuCs{Kf5u%n- z`qy=;Dn$!R@1x6KI2De|n_SF1k0Xv>x5mmRnK06Q-XSO+#az;?sJJYS17sWf%xA6cpUrqI3m%mArc1vwC*h7<`W zEx*f!0?i9OX+6r~ILgpd!jdV4w{`z~#Cyk*#^CS=G%aArFSATCsSFA1UzaW<0~!75 z^AerkE!jhMu{)`o>n?{bd|H26gUjs*ZTrY20z|Ov=@Ls1*A2Us+{YHHL*E0i7}#yn zg|mwaUVAxMD)UvxK0Q5!sNcM32k<6|nS<0)vi_Y4|D7=+`HkTb@bN$s*rp_OVc3-H zHre6b)PEpA9iTFTgy|-FI-onY@#HQB9TOl2^p^qu$h9m=@W!=iss+t=pR}r(2*-%h z)QM@Jf*f+3F4%`1d_IC$(&ikKt?-|Mz;Ieoe`u3y+^r7=@vP0gT0PvkZKSu2pm2*T z2}^yb&EO4O+CQczGzNi0cJvQmhS)dBG`5{u(=^NUh}B58>^#i?My{G_bZ Zv?vE8pkHRFpP5&dpP8Imti)Bz1pvYK7f%2H literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.list.2.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..c8dce9c1b91d6541bc879e0ef45f22a36cfbb761 GIT binary patch literal 17103 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 Nc=6fd7hFDF{Q-`~J5B%q literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..82e3bb8ef9179aeda7b9b4bf60059921ce515cae GIT binary patch literal 1085 zcmaJ=%WfMn47}$n44r$}jlHPRTZ`sUpbwzO6onvb>4vdZ+Uj96F!JvuC3!73L4EQL z$>DHFoo-eup(dAX6z^8+?a;kpL?`dT>L=^2 z*O#18MWA?3NcrW;5rXzOM|75p?~}oK^+5&HwF-k~32JA!daq1?Qdk3l&>ufDgd$=-QiT3DFGn-Au~NAi5nI zgZ7sU`T-^m3eq8y#h6u5hW0?dXDy2nRm*FeKBevu0?&YDi7CWUN%u|sw2(bjj3#i|6c1W>nBPNJUaPy-94&5at9CU zh%O&;vS9{|dnEfRe?;&>)=O#RM6YbvJ3qO6VOkS5K zMhY8Tj-+8yhL=kT;&LzEc~#%daucHI-}l@_gtTj)*jcsUaftw$2SguHYUA{wai$eR Na2462FC;$S`~^uf&U488YP7{-npmh`w=f$cD0Yk?hC41z2(3m1KO5|yRmjzma?1yWnZxdHB;YSt0I5Or~h5Rr5&?RycRsCRimB zM<0Kb5RO8OxE6I5vB*Pz3}}SSXKPEP#Ur>du5WNanp@EfJiQcH7=D2fllqwh30&=Y zf`~!xKe2Ue9(XydE#Pn>9fjbMV0a~;tQKPyO;fBQXF_d=MKWNqfawOb7X43^%pRO1 zd)A?{M=GiqVIuG}u4=h9U#TdAC?smxinA)tSC@;r{~OKNYraZ2ghQUx0c5ut>Q|04 z)}uLiU7BlViOo)$L?d1ZkapTi;IlNe!^qM+{}XvriKk+#%aH z^Akc$YTlGa4n^(LE<~q(XYC2;pT_8UD$@xb6d^Y=^U0(xCNEEqH8vSFvI?iIjJH7g z%g)3Q_57liz33Ya+p@PQ5AEAK3-6_4u7~Cg@qv`F!5kWMS~VPAbhhy;Dqn8?0z0xj AivR!s literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..c0d44abd080a6107cbdff69f0c96fccb4990fa38 GIT binary patch literal 1119 zcmaJ=+m6#P5PjdTi1Gx|A|>gC6>lp65{Qe$BM4cJGi8T5cJQTQRqeksW4mp-t;kb+ ze0=828GCu~H?Xuws{qZwFIB*rjbB6~j{uMnu$Q{~r*Q4E<6xpFM;C literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.5.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.5.json new file mode 100644 index 0000000000000000000000000000000000000000..54f069ed03b24e7bb16780fb3fea446003a9669c GIT binary patch literal 1095 zcmaJ=O>f&U488YP7(Q=+y~J6uTZi>9U?0Ga9R@*>nU#$!dHTp!1pV(LCB;$JVmZZ6 zOGv$oPQ%xjdS%i}+de1H= zb^kY-Vqfx=VQcqs7JJ}*s;HkiPO%2n-pJHk6?vXSwyCrQbvAi5q+=t66)r~7Fwr^Ca4DcY?~AiemJ3>TqOI!p8?Ms&xUG-vq`KvHi2$kt YqK_bywrXEFQ;Q+Eh^*0P5}&UA0!i{XNB{r; literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.6.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--InvoiceItem.create.6.json new file mode 100644 index 0000000000000000000000000000000000000000..fba693008785fdf6f131fa637918234329644091 GIT binary patch literal 1116 zcmaJ=%dXTg4Bg+as3Mj;I#;aNA;AKPhr|+uqBQj|X40f2&cM~^{yTQk>7+2CtHwS) zIX-sIcU`B<6tXAvs(W_d_x)1|*&HKy`@lh!)2NnUqA)>}pv_5t{Pg+5>-X=zynVxE zyNVeN0q1udsb8raA=!vCVenG?Ck!j)iGOP?nX?uh^b}E$qY_N_r@jI zBoarzek&myg&A?H>Y}3WUsiqSPXVp4`RrV&ba;X(jN2O~Aj7>F2A&@aEDS%uic4zY zK!T}~&JZ!k{b%-$-2*R&tp^-VWTFsU6AZ89voqq%s%ef@j9SCiMb#~PcA9$Cd? zoQ$_X)OBZKh-P_G>t3iv!>;UY%0v73F2a>`%=OT`AwH0{F4$vZPOFB)tIjrlL*?V$ EA0ucz!T`$7#uKGLdVV3B4#1 z)T3eIEaxH>31w(f=Qy=esS>P;g*#J4{UBT<%nKt{JHB9QE&_`arWx>kv-Xc_KUiNh zihF z`R?lN@x}S|>B%_!n^*_=>oQGcKBwIT@oFZC5>@L(cMQJaQs4BHMk?W$oaz=L$P$y` zdq_GMCY42{SxR}4^c`ho0$F=f9bY`RnT`L!KO!wN^H2GA{6-&fO7o%&F%5wCaRgym#-9Owr z8f`|C$>`ze{=s-}j6Xt&@Q#*w7Z{xXNFuA!G*Jmms0nD|jHOj*{0t&wl%T^&?XjU2 z;F$;0f_Q2M>C5ImjDZ?6#Y__BeHdAZ+7Z5po!A6q9JxW}t3vVt5h);P=90E+DF7j> z5y>HPZPc``N4UmPS7V?CLK$rUj-}~M5C%wXf2-mPkr)gAPM(djr<4cSy z6*341#q_)2))W`9Ko3&nbSf4sozY?j4|P(xP>J$gt`MNN;P@M4 z75X4RcKk_ZF!2ZlP~||dGbhEsfb1~c!41$7yK^FS*G8GcIYKH7DL&jgC~I=If`(`3 z^{oz;InF;RO2Ay`2brX-(vMKVM+c8$;0OHy@}brA`;+xWW>Of??c&e{8cT0|yR*6; zfk2(1oc8dgi>1JYq8Cm;_hvZQ>KA}KwMF%eZDeM$dUTGrqj=OuOQSs6*;c(!u=>FK zCpZi)!@*fY>;(1&3|wGPl$OLpqXmzB*#$=Q?5<>B=w zc9@)AeLh?8?CIv}W%={Xo9y`EkM+y9bGCf^=JdtQm-*w1=U=}(JzK9YpM04-T^zhR zdHj7jeV-Mt)>U~StD`G9JIuxDe5p^aAOEdC&Of}JY_1+=&xMgeErMCiBG#R^9I1<> z#4m;ahS&oGMFp5;!`XQUlBDhOsf58Lx~L1cr?@F1?v=GK_g9nsdN+lCZ|z>-T(l)X z-ZKJclDm;uzK~i}a)S zr=JbnH1fx`Q3SNH;UheiP$-BN$|3^Ua;xT|NAhR(?(O9(^(Kmrs^*nVw9o zub}(pGYL~0qQaKXA-AlR1YoPTP{QGzt0eL9{ohL@adY?<4>$w+>y>SH%{L{Xl-;#` zdH@xd2$*1SZGybGbB`lKiyG;zXw90n<8s#U53OY&bC-q8vmtV|Tgh!p7`bEpinpjc z7O$9jr|lQOv93R*kS*TS)5e3S`S{=Lfgo5iiD$EA5;W9V_v}VE*?;lVIDo zy0jmEJ4)jyf(iF6P#OMf;S^G=;P6c)?lKVgUB(Y!>ohJ%2yIF_Gw|VBP}BKTGt|)f jt!%LAU~q%4SuQVznpwX)1^YzYmu~GyEg|YfzwiAA3AL8? literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--SetupIntent.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..1279fca04af737770823785f03e9b3cbe88d1658 GIT binary patch literal 930 zcmaJ=O>fgc5WVMDL_Tv!R6!9psH;-akU)ggg3xNZJ5Dz2hrJ(|66L>lcAZc=t?8rMlA8s!`Zp&4> zn&I->*_mFQziBRtERPr=pdixezrVTuepwQ1+9eD0+{ya1 z3F(lhn?a*8RrDIJmxkv7(sM>dYTVV3@EnSXOO`^R4#YZ2ip5BwMdcF5DAqg(3rYn*06ir zfU20S`Z>cTNV&Csrh6TXi{aCCM>R}g+?qYx15WVMDtb9glQVE2(AyNrMASqCRK*(~uvx$q>-g@k|NtFN2jI+rmo3?vc zS@Zeky_wCRC`75E67PyX^e&1`{*zTTjWt?D;~hC&vi8-%m7=f7qh+I3HMl5ih)Z8$ zKTs>D!P;Cz22gljOIyPQ5Ojb@`2rytIhy_&4)yrzS_OYMzD4{wdmGjh)yB!$WI0eOrQq4p(^y;2t8FlDVADa`hhc#=%D%5S!ZU6x;UIvD*V3R}C=a-R0#58FHKV z_n-fgc5WVMDL_Tv!R6!9psH;-akU)ggg3xNZJ5Dz2hrJ(|66L>lcAZc=t?8rMlA8s!`Zp&4> zn&I->*_mFQziBRtERPr=pdixezrVTuepwQ1+9eD0+{ya1 z3F(lhn?a*8RrDIJmxkv7(sM>dYTVV3@EnSXOO`^R4#YZ2ip5BwMdcF5DAqg(3rYn*06ir zfU20S`Z>cTNV&Csrh6TXi{aCCM>R}g+?qAP?OqyTCisf38ae3F4!hG#13}wZEH&39x zL!NedGA*h5^y$-Qp57fCcr>!039J*YspYZqJ{_o3h(EG3A>s#;=vcCO2GQ{`rR?Kj za5@VPkNw8%@T}pV9G*t?dh{U({SRj!YiTmqG=$n`NmWV(5uAZCK=5BW2MNlaXC3@* zIp}lW%Mdfc@qlQEKm)=e+;v*l1^S4zksg2uRqze%^(|Mu6-^|aN*=1cCgHc;b8?kk0t2}lT* za^c!6JIcki3l}6_a>W)@3b21Zrh+F&ESA}ru^&Zc3DS{;kz^68y$}i?7Y<~yN0p27 zjB2NhS51<#dZn9eYrzoe673BxB@NG-){ z1?w80S-?fXeIzLgAm0=(pC!_1H;GH(F{(q7Cioh>im9TcXTYXO4m_UyL~F=iEPyTJ z)E*i13@7zW%?aJOoh4k^y+tce&}x?Wz9PMhBltxe?(U&S#RJ`#-PF}Z74dnRyX=*oTFum%x!P4uwx=y%UoxU5XVHmq~+bpKs^ zZELRB*1%%44G2kuUx;mrGX7sEb@SlF?~Utq)@tgTZyLORk&GIhENb_bq{?oRk~lzJa9x zwmLBQiDcEDWJR63)mRO+Q~U2`@;>6B9|ji*mHowSkZ3kwzXk1%KV7jep`2U}N7E}C zbcVwQ;uw?8Il1J&(M>SyDr6v5rN zd)wJ8)ua)wXnQ&j8{K)haT(Y+e3)D=6&>|Ysk&I-jo1F9$UQYQ=p>VNBGK9(=#^1l n2jQFseY|}!#20dM<*(=n>o+Fqd!yZ8R~N^f!JY8lJ-_=GHs%1O literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/invoice_plans_as_needed--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/invoice_plans_as_needed--checkout.Session.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..6d572a8fd56fcaf87e029aa6dc57fe8578b7add2 GIT binary patch literal 2769 zcmb_eU31zv6n*cn$hfonvy!Xh8^-li~0q#-&PnaEIaBz!oQm?*@X88LK$2eZPkG>~*8z{kaa3JqCF z7}18BB&%Xw>01_eMqCIk$AtR60nBvTU zHX76%R7n99VF`lsb4uCAQ}1%*ou0du(dkviy*RxL%H`mL=er-SK9-Vrtf&vA&molw z6-00bQUk$%=@>$g<~r@@cf&zlyUwO^CO8I(f&dg?X@uMExA<6Z(ZCd#y_LEnYC|k{P@3!?i=9P$$KA;|!Xrz|}9E5NA`uTSuYZ@}Ju=N&bUnvr` z6!P6pEcFu}#|3tGin2S7oJ5_6nnVGygd!t^G6u1+5?CkMQ_`w@P#AacZ_;eFzy0-a zKde;|q#|!=n>a^E%E2kh5)mH^<-$Ty?06;A{Z=5v&GzyM4F#A#UlPINGZuv#$zZ>d zhFJp{xVRcmB6(mLhYW`u!uSBRHpWw$tm2M>L9(j0beTE`XhLn`10qV5>@ZF2NELzf zTH)jZsvgX$)4{_-?Y_q!+<0l4(~k7Jt&yC^k9UjZRcqW1T1jQoxx?i=LK=5a%a)H) zA^03ZG%-B4Wh;(|7!`ZzLL_YMoCU@drZ9kmik?6c6x3=`om>-0fmy~jox`Z|NfKig zaQ0ALm*QwJNt`zqjeenJrKcyrrcq+fG1+#=Jbg~>JI|xK3;mFQs@hK?qx%1TQv7~@SXmQ|^0AIpgI8*L&8Z4Kb zxUGJ<%&N6Vs-ju-m)5=Sm+Iw#=9OhuwwG?l;%fz3bQ8FSXmR zt*0NPQ|ol@TTOCbeaoRvKRu3#HQ^+iLi_F)#Z%};b2$q4rAu-Pqm- z+;@HNCZ=I~^615ib=YrSz2OdL?28``Zo9qVo$(soZiR9dH@vnxs$GnO+jLi@oAhLT z?8`dsciZ8&r;C^7RQQiS23H-~Y!Av;{b4A)r+)LXv7E|5#h=moaO_u_V}EHgu)hC1 zxSdMcYhO}%vv}$++`&5cP*blF59)D<&(Jfy)ADP_A5*W5yKlO9A{^YgGup%ZD+Bqx zQm?SPoAXBJNvx*#4d*GHUwp?jVy&zgjY`r_Iw!X}!L>;_bJn<*=I#b_{4Pt`*3P@P GcmDv-^FWRO literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 18311945d9..c89285c310 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -6378,6 +6378,105 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): for key, value in invoice_item_params.items(): self.assertEqual(invoice_item2[key], value) + @responses.activate + @mock_stripe() + def test_invoice_plans_as_needed(self, *mocks: Mock) -> None: + self.login("hamlet") + hamlet = self.example_user("hamlet") + + self.add_mock_response() + with time_machine.travel(self.now, tick=False): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + + self.execute_remote_billing_authentication_flow(hamlet) + with time_machine.travel(self.now, tick=False): + stripe_customer = self.add_card_and_upgrade( + tier=CustomerPlan.TIER_SELF_HOSTED_BASIC, schedule="monthly" + ) + + customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) + plan = CustomerPlan.objects.get(customer=customer) + assert plan.customer.remote_realm is not None + self.assertEqual(plan.next_invoice_date, self.next_month) + + with time_machine.travel(self.now + timedelta(days=2), tick=False): + for count in range(5): + do_create_user( + f"email - {count}", + f"password {count}", + hamlet.realm, + "name", + role=UserProfile.ROLE_MEMBER, + acting_user=None, + ) + + # Data upload was 25 days before the invoice date. + last_audit_log_update = self.now + timedelta(days=5) + with time_machine.travel(last_audit_log_update, tick=False): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + invoice_plans_as_needed(self.next_month) + plan.refresh_from_db() + self.assertEqual(plan.next_invoice_date, self.next_month) + self.assertTrue(plan.invoice_overdue_email_sent) + + from django.core.mail import outbox + + messages_count = len(outbox) + message = outbox[-1] + self.assert_length(message.to, 1) + self.assertEqual(message.to[0], "sales@zulip.com") + self.assertEqual(message.subject, "Invoice overdue due to stale data") + self.assertIn( + f"Support URL: {self.billing_session.support_url()}", + message.body, + ) + self.assertIn( + f"Last data upload: {last_audit_log_update.strftime('%Y-%m-%d')}", message.body + ) + + # Cron runs again, don't send another email to Zulip team. + invoice_plans_as_needed(self.next_month + timedelta(days=1)) + self.assert_length(outbox, messages_count) + + # Ledger is up-to-date. Plan invoiced. + with time_machine.travel(self.next_month, tick=False): + send_server_data_to_push_bouncer(consider_usage_statistics=False) + invoice_plans_as_needed(self.next_month) + plan.refresh_from_db() + self.assertEqual(plan.next_invoice_date, add_months(self.next_month, 1)) + self.assertFalse(plan.invoice_overdue_email_sent) + + assert customer.stripe_customer_id + [invoice0, invoice1] = iter(stripe.Invoice.list(customer=customer.stripe_customer_id)) + + [invoice_item0, invoice_item1, invoice_item2] = iter(invoice0.lines) + invoice_item_params = { + "amount": 16 * 3.5 * 100, + "description": "Zulip Basic - renewal", + "quantity": 16, + "period": { + "start": datetime_to_timestamp(self.next_month), + "end": datetime_to_timestamp(add_months(self.next_month, 1)), + }, + } + for key, value in invoice_item_params.items(): + self.assertEqual(invoice_item1[key], value) + + invoice_item_params = { + "description": "Additional license (Jan 4, 2012 - Feb 2, 2012)", + "quantity": 5, + "period": { + "start": datetime_to_timestamp(self.now + timedelta(days=2)), + "end": datetime_to_timestamp(self.next_month), + }, + } + for key, value in invoice_item_params.items(): + self.assertEqual(invoice_item2[key], value) + + # Verify Zulip team receives mail for the next cycle. + invoice_plans_as_needed(add_months(self.next_month, 1)) + self.assert_length(outbox, messages_count + 1) + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase): diff --git a/templates/zerver/emails/invoice_overdue.html b/templates/zerver/emails/invoice_overdue.html new file mode 100644 index 0000000000..8e8b20124b --- /dev/null +++ b/templates/zerver/emails/invoice_overdue.html @@ -0,0 +1,10 @@ +{% extends "zerver/emails/email_base_default.html" %} + +{% block content %} +Support URL: {{ support_url }} + +

+ +Last data upload: {{ last_audit_log_update }} + +{% endblock %} diff --git a/templates/zerver/emails/invoice_overdue.subject.txt b/templates/zerver/emails/invoice_overdue.subject.txt new file mode 100644 index 0000000000..56ace1e4a2 --- /dev/null +++ b/templates/zerver/emails/invoice_overdue.subject.txt @@ -0,0 +1 @@ +Invoice overdue due to stale data diff --git a/templates/zerver/emails/invoice_overdue.txt b/templates/zerver/emails/invoice_overdue.txt new file mode 100644 index 0000000000..7c7633be97 --- /dev/null +++ b/templates/zerver/emails/invoice_overdue.txt @@ -0,0 +1,3 @@ +Support URL: {{ support_url }} + +Last data upload: {{ last_audit_log_update }}