From 69d8442ab41e161092a6d46ee2bec0feedd8c623 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Mon, 20 Nov 2023 12:01:25 +0000 Subject: [PATCH] billing: Allow user to switch between billing frequencies. --- corporate/lib/stripe.py | 90 ++++++++- corporate/models.py | 1 + ...license_management--Customer.create.1.json | Bin 0 -> 720 bytes ...license_management--Customer.modify.1.json | Bin 0 -> 745 bytes ...cense_management--Customer.retrieve.1.json | Bin 0 -> 1842 bytes ...cense_management--Customer.retrieve.2.json | Bin 0 -> 1842 bytes ...cense_management--Customer.retrieve.3.json | Bin 0 -> 1842 bytes ...cense_management--Customer.retrieve.4.json | Bin 0 -> 1842 bytes ...cense_management--Customer.retrieve.5.json | Bin 0 -> 1844 bytes ...cense_management--Customer.retrieve.6.json | Bin 0 -> 1844 bytes ...atic_license_management--Event.list.1.json | Bin 0 -> 1620 bytes ...atic_license_management--Event.list.2.json | Bin 0 -> 12691 bytes ...atic_license_management--Event.list.3.json | Bin 0 -> 21656 bytes ...atic_license_management--Event.list.4.json | Bin 0 -> 16124 bytes ...atic_license_management--Event.list.5.json | Bin 0 -> 8114 bytes ...atic_license_management--Event.list.6.json | Bin 0 -> 81 bytes ..._license_management--Invoice.create.1.json | Bin 0 -> 5625 bytes ..._license_management--Invoice.create.2.json | Bin 0 -> 5693 bytes ..._license_management--Invoice.create.3.json | Bin 0 -> 4148 bytes ...anagement--Invoice.finalize_invoice.1.json | Bin 0 -> 5960 bytes ...anagement--Invoice.finalize_invoice.2.json | Bin 0 -> 6054 bytes ...anagement--Invoice.finalize_invoice.3.json | Bin 0 -> 4509 bytes ...ic_license_management--Invoice.list.1.json | Bin 0 -> 83 bytes ...ic_license_management--Invoice.list.2.json | Bin 0 -> 13771 bytes ...ic_license_management--Invoice.list.3.json | Bin 0 -> 18919 bytes ...ense_management--InvoiceItem.create.1.json | Bin 0 -> 1116 bytes ...ense_management--InvoiceItem.create.2.json | Bin 0 -> 1098 bytes ...ense_management--InvoiceItem.create.3.json | Bin 0 -> 1126 bytes ...ense_management--InvoiceItem.create.4.json | Bin 0 -> 1124 bytes ...ense_management--InvoiceItem.create.5.json | Bin 0 -> 1105 bytes ...se_management--PaymentIntent.create.1.json | Bin 0 -> 5967 bytes ...ense_management--SetupIntent.create.1.json | Bin 0 -> 930 bytes ...icense_management--SetupIntent.list.1.json | Bin 0 -> 1116 bytes ...se_management--SetupIntent.retrieve.1.json | Bin 0 -> 930 bytes ...management--checkout.Session.create.1.json | Bin 0 -> 2152 bytes ...e_management--checkout.Session.list.1.json | Bin 0 -> 2538 bytes corporate/tests/test_stripe.py | 176 ++++++++++++++++++ corporate/views/billing_page.py | 16 +- templates/corporate/billing.html | 52 +++++- web/src/billing/billing.ts | 66 +++++++ web/styles/portico/billing.css | 18 ++ zerver/models.py | 1 + 42 files changed, 399 insertions(+), 21 deletions(-) create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.modify.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.6.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.6.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.finalize_invoice.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.5.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--PaymentIntent.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--SetupIntent.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--SetupIntent.list.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--SetupIntent.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--checkout.Session.create.1.json create mode 100644 corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--checkout.Session.list.1.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index feef2bdeaa..16dbba1f75 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -461,6 +461,7 @@ class AuditLogEventType(Enum): SPONSORSHIP_PENDING_STATUS_CHANGED = 6 BILLING_METHOD_CHANGED = 7 CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8 + CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9 class BillingSessionAuditLogEventError(Exception): @@ -1015,6 +1016,54 @@ class BillingSession(ABC): ) return new_plan, new_plan_ledger_entry + if plan.status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: + if plan.fixed_price is not None: # nocoverage + raise BillingError("Customer is already on monthly fixed plan.") + + plan.status = CustomerPlan.ENDED + plan.save(update_fields=["status"]) + + discount = plan.customer.default_discount or plan.discount + _, _, _, price_per_license = compute_plan_parameters( + tier=plan.tier, + automanage_licenses=plan.automanage_licenses, + billing_schedule=CustomerPlan.MONTHLY, + discount=plan.discount, + ) + + new_plan = CustomerPlan.objects.create( + customer=plan.customer, + billing_schedule=CustomerPlan.MONTHLY, + automanage_licenses=plan.automanage_licenses, + charge_automatically=plan.charge_automatically, + price_per_license=price_per_license, + discount=discount, + billing_cycle_anchor=next_billing_cycle, + tier=plan.tier, + status=CustomerPlan.ACTIVE, + next_invoice_date=next_billing_cycle, + invoiced_through=None, + invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT, + ) + + new_plan_ledger_entry = LicenseLedger.objects.create( + plan=new_plan, + is_renewal=True, + event_time=next_billing_cycle, + licenses=licenses_at_next_renewal, + licenses_at_next_renewal=licenses_at_next_renewal, + ) + + self.write_to_audit_log( + event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN, + event_time=event_time, + extra_data={ + "annual_plan_id": plan.id, + "monthly_plan_id": new_plan.id, + }, + ) + return new_plan, new_plan_ledger_entry + if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS: standard_plan = plan standard_plan.end_date = next_billing_cycle @@ -1079,8 +1128,12 @@ class BillingSession(ABC): switch_to_annual_at_end_of_cycle = ( plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE ) + switch_to_monthly_at_end_of_cycle = ( + plan.status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE + ) licenses = last_ledger_entry.licenses licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal + assert licenses_at_next_renewal is not None seat_count = self.current_count_for_billed_licenses() # Should do this in JavaScript, using the user's time zone @@ -1092,7 +1145,30 @@ class BillingSession(ABC): dt=start_of_next_billing_cycle(plan, now) ) - renewal_cents = renewal_amount(plan, now, last_ledger_entry) + billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule] + + if switch_to_annual_at_end_of_cycle: + annual_price_per_license = get_price_per_license( + plan.tier, CustomerPlan.ANNUAL, customer.default_discount + ) + renewal_cents = annual_price_per_license * licenses_at_next_renewal + price_per_license = format_money(annual_price_per_license / 12) + elif switch_to_monthly_at_end_of_cycle: + monthly_price_per_license = get_price_per_license( + plan.tier, CustomerPlan.MONTHLY, customer.default_discount + ) + renewal_cents = monthly_price_per_license * licenses_at_next_renewal + price_per_license = format_money(monthly_price_per_license) + else: + renewal_cents = renewal_amount(plan, now, last_ledger_entry) + + if plan.price_per_license is None: + price_per_license = "" + elif billing_frequency == "Annual": + price_per_license = format_money(plan.price_per_license / 12) + else: + price_per_license = format_money(plan.price_per_license) + charge_automatically = plan.charge_automatically assert customer.stripe_customer_id is not None # for mypy stripe_customer = stripe_get_customer(customer.stripe_customer_id) @@ -1107,15 +1183,6 @@ class BillingSession(ABC): else None ) - billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule] - - if plan.price_per_license is None: - price_per_license = "" - elif billing_frequency == "Annual": - price_per_license = format_money(plan.price_per_license / 12) - else: - price_per_license = format_money(plan.price_per_license) - context = { "plan_name": plan.name, "has_active_plan": True, @@ -1123,6 +1190,7 @@ class BillingSession(ABC): "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, + "switch_to_monthly_at_end_of_cycle": switch_to_monthly_at_end_of_cycle, "licenses": licenses, "licenses_at_next_renewal": licenses_at_next_renewal, "seat_count": seat_count, @@ -1245,6 +1313,8 @@ class RealmBillingSession(BillingSession): return RealmAuditLog.REALM_BILLING_METHOD_CHANGED elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN: return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN + elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN: + return RealmAuditLog.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN else: raise BillingSessionAuditLogEventError(event_type) diff --git a/corporate/models.py b/corporate/models.py index 50749aec9c..7d6c0cf53b 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -260,6 +260,7 @@ class CustomerPlan(models.Model): FREE_TRIAL = 3 SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4 SWITCH_NOW_FROM_STANDARD_TO_PLUS = 5 + SWITCH_TO_MONTHLY_AT_END_OF_CYCLE = 6 # "Live" plans should have a value < LIVE_STATUS_THRESHOLD. # There should be at most one live plan per customer. LIVE_STATUS_THRESHOLD = 10 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.create.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..593a07ed0c839f8849a8022e3d32ad28b51927d6 GIT binary patch literal 720 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 QknlH#A#Ty1SMurAU*3A~Jpcdz literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.6.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Customer.retrieve.6.json new file mode 100644 index 0000000000000000000000000000000000000000..36d640d850b3bfd6708d2a45fcbc70588d09344a GIT binary patch literal 1844 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.3.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.3.json new file mode 100644 index 0000000000000000000000000000000000000000..5ff0e56721f08be05787f2caac70a54f171de77f GIT binary patch literal 21656 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.5.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.5.json new file mode 100644 index 0000000000000000000000000000000000000000..90c385a60425cb1241bcb11bee4dd2a7c88ced37 GIT binary patch literal 8114 zcmeHMZExE)5dPj@Ver!iG>PpvFD@|brAdozN!q5!GPEcNTB2gs5~WC#o22M}-yL6M z(W2}W*f4ZhKBTF1JRXnFJ@=x|PX+@LGsVcW!F%|7F!-#0!&6oWy5wagatSZVcsL&J z43BrlN93s!iAv5CkKy%b=zf})R;$wo8uQsl9x0GsJ;QyXD6gfW`geJcdtJS4_CS*w z5y>)+?|)}9F4^FW&8nw^UuCo#7FXwen4MVRACh%avcO}o5 zkV4+{2~=}Y6v6jJk&1{Z0+zsyG~;TX<5dwOP8pm}o6pkpeof&9z-nAXby;#5X%yB~ zytcZoKr>AQPs6B+v;Z%)F?B^RUR|F5eD>?R(-Vk}QAimt;U?meSI%Remz+L|cuP_1 zoMkCjKRwi`D0ZVf>(OZgrRz3pAT8#(3_I3Iv>@EC1|y8$ArR3xi8{jvScT&I51vHK z6IQ2+7VJLbzyi%}b~bgARbH1GxV;j%Sqa31KJ9E*MWrbE;x87JrXg6^)o$N~wAP#g z-31-WlLUdfgkVM|b%|2rj>{OsVa8G$l|Jtzd9F09I#wkmAX3O>F2X*U=M^xtlc05J zV5C%0J=@!}3wJ98PT>fsJ+TLbtbFJKc=y1a-qR7=k54cEcrlN%>D}ea;^FMY^v$_^ zH@Uk0EkA$p%ZQ)eiTLHk%j>H*_ZO!syByE=;NH%S*Y|f~E!|kbgPVh6Jv) z;HA(>wTlT|0qB}<|KMmiJvg2mbQPFFpwx`FKR%co?N25Lt!ltn6Ed&>1`MFxnqQQL z=YnQ2&9nWN3P!oQql)I-)RJ&gT1%lk)-k(*{H6-m3E{tFJFvBoE+BBb{?g8=B?e*o z#9xCF*k?OVP$;j|9|lAtr$%DFJ;CwUO4Zwq&wO@-m~hHIquH zjgD0J1;#Cb8i^_a*bDl=henul!N8Unjo~O6RL-Cq<;mls404L=Li#8WF2Tsa77FT5 zq{3j_L6F2iBv54lt)UdU>``X49Cz6D2dU~=I~Rm+g5dR5^=X<3A&FK z+kRh9c|q@Y7_RHURgT^1%bMnNog~{UznzT8T9$on%J0E<&mp}IOf@qkdcp1x54GxX_RcC$JNN=Z%DVU;tiBxKnc)yn{?ZVJt_kKPq#Lg zJSN?SPy(*sjuRC6rqXTCN&nmFwvSKy6RuhDt)|@3H=lCD{{P}JpEqxUtDG||?aH1e z%46(my(=IpS1dK7YN&I@z5|J!uIw#GZn9FrAATX*&@;?bum!8y=<&*hjl9yId5Gk9 zt9cqTlVDqvwe2Oc&Sqv-Y3GzXV4ENKEpOOd>ZMsIW_4kmCN#%!%X)!E>=3;(g2v}G zuN5qVnU}}tlmgeTq)=Qx&v*W*D~ z$F^P8mBqTzNw!eu-=k@!DO6Q(h@JtUmFZ^j_Wq=RX5H#oFgLoL1sN z_fIhX#!+a?+3;GwdxSG?S9_ao zbnAd?^-0CVlZOb@3SSx&_M!sE+JPXj<3woCHjJRb&lxKhAXH2Dbp=LbX36?#^`$@v zZ`KDx>DX0HKFwrPUVBlpZgzMKzKJnQc>eLFHj72T<^{e}DZqG$q z6KS?|=1WCKuTF0dZca|$urj|r`B=Tax&Ct&CyOR+^d_}ds^LrIdk1q;*pDO5xV9Hb z$!}|zg2S^FZCHEYm7jX|8=Zh(fM!+z*}=y?I|C!r>2boR>~Lo`o=$fr<7B#XJYw+G zdo*UFXm~st9X8{Y)#2Ah2fHqtRCN?_4gp}e3cdCL&lD^`TBlEiEj!xfgmwv(3SXZ5 E1>98a0{{R3 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.6.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Event.list.6.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/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..ee4c6393e41c0d93f201364166a90f9cf57d17be GIT binary patch literal 5625 zcmd^DOOG5i5WeSEjC=wqBs-f~5;%bZhlubH5tl^B@^pJLX5H?NA4_(l{C7S-+wPu0 zi^L&_IjmM)<#M^|tIt*b@L;nctm{UnEDeA4E%|ywetSh8)*{6Q?!W)QRIl0QCEJ^a zn-{8k)T&wbo{HW;Q#mQ4g(2APonL=Zf0~%+m)|rWm{3B!T?))mOjGlHnxyEM6{Bjl zn?KV7=ahx*8x3puFs0MZ+r+QjdvsvZ@aUXS4(Crpr)7GMdx6kbCmLSql9u#H)2F4f5~PkgVin z2(TYEAwt}1nFPLuu|MS(3WKHae+ZnGb{hA80GECbpDP+RtinHWEdqPR45mc@Emmxw zTs^(`>gw6Jn4w-W;+%N2g-|JW3x=m zraSU@>A_>_l(2z0qE+a1XBc)?IKi})Ffgt+yS}4}R?KiX!F{p?ug>So3&-)=P4t?3 z_HV-k>6v^&qC|JSAm-xYob4FsCUKJ_!Bl zAWoTycLEnyU1@OD3a$-Jh+Uh}Oqs)|2l~!2WraN%2zxtuyc1j&)CyprIa8ammRsOJ zC8j-G9B`$>me(VvQ48+NZOj6V1VM{3n6kJ)Jq0v41_*l;7?>Jm?WO+6C%35%{N;Qe zV%)ywvNmGGhdt8eihR~rT+AfN(MyE0EdM$_OA2nHR4RKuaH4J;^NS^0pCR>?-u+qq zmw5kgK6?&2-^5FGBh+Shxx4(lf&|G|__;`B`M*u?m$ei3r}t*rdAc}92V^eze@^eE zMVJwYl0kzU>Ai?gBKUtv?{&5is`L5s!cU6c7oPt&>3zu`=LtS)ekv5doeS&}qxd#X zA48>w!3&{1GJkdJMYGv3j>%mZv7==&zFFWxxQquTaZ(|VZ?=yUB^vzGHXem$F1C;3 zH~}xhb#t=5)=Wqge?Je1EIu8;iN&a`+^}!Qt(iNMbAiIqXoTiQ9_ZLiO6i59bOpqJ zprf)MFTHmTtLxB! zmN=$zy3Q3|NqRUJo}yw4k|#&s`ruN>ebly(Bo4Z{UqRU4a%#j~XeRv|O=Jg;`G~f% z#fjLG{Hy_p1N3vhJ`3iaFt=^-Ds89#7$E-$g>eS&8F_e(jr7^sb$+%Yc=0>LvP&PS z^?kJPZy6Pq(nybHw0VyJkOro&@!6^Th=oCSr4+EeT#1K?zBX7wccyP`$Zpj%*Oic(uNAv>*LGVoj=7c%o4?aHl3*Rd^GXMYp literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.2.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..196b4cf17c6f01ca1008ff51b2eba89186f8ae41 GIT binary patch literal 5693 zcmeHLOK;pZ5WeSE2s*Vunt0cC;`Y>_4+n`KQ=3LmbWApgDJkd#D9 zyM-?WT%bM}wq`gS&f}ZMr$<#)^QP&&b}alhcl9?_{rj8xaZb{_6ZHLOu3N*aSA1t5 zS1)z*q-1mamdVzFQ+uVNgf7T#J)PfNTs_;I<=+P`TM&wGMs!?isox(xVGnW`A~BsB z1|=KrWUq^h=g({}1mi)Gg2Oq{4e8yp67j1bwf0~ z4#Fo`4JH+u?^G!QU#;WLkJbU>=OB6~mBM?N5!{9+)E(EJt7zKhz|DKQaNaOibukdcyw#`+k=rnn`C)8(3El#0#4`|giGX~mv<<=DXQI)VI{tUU=IIc)Du zh>I14gc{hNY%z(ojgiALUR!BH0)latlD_4qy49PfU{`s?nKJ=M@ zFJhCpZIlm|iaESwJC-O(LMw7#)-T#tl3%&107*hyQGM~F*j3xd)%I+&t-h*$;yV3z z{$;SuC@6V4!PM;V1)X38ubhBsL0+9p4pOHabe=0Qx=8{TaY-%Zy^bkFUe~wB^g_z; z`2Z0oS5$(^rHuPHfIEpPzkwd3A9)ABfm$NZ{st zd$qaTfINxIFCLHz;2Ps=ywAj_FRtSurNF!}HZB_>+cpOg3nMgUUQ3sm6MW4H>`m_k z!x0S5hyja9?7DhQT+zb=z2H9Hf>x*fMZ~_p_5-^XR2ND!fqKH9plI1#FCcqTarSml zv$WA*`qsx-pg+qu;=j_3pmrw?{6+={slB#?Xe3HmtZkDzP6}gLirBRn z!?oS_rehyG*G{^lhH!TS#e0o1!kk156v0@!?D_e2dy33jsMw*{Ll*JX$D5J+S6ZwlvqvCZfzP`5MIY3o z^%CGL(?2sGN_Gj(g_Wkvo*$e*9naiswkSli;ZMQ+itlt%*7v~s8Q~uy-d|*ain<-j zeT3hC#QWmdY5X{a5A}=y{{r4$UOYd)_#f}*t}FKzLHf!1OvDF;_Y2Mcn|Oa!zz%Gj z<5`CH#P?9}eshrl<--E}+)STC-3sFw?0Vo-#?7MfEWpOP1bDUU9anM3f#Q$~avkTH z`svN)X+lGbKguR#PORYKQ4I^#wtZap1MWIW${yM}hhm zM`CO!7DFiXACJX3ne}1B9UG1qUP~To1V)a@sImKJsogJalxWzb#BP5<#jrY!>#^@) z8k4S_`{%eb8jQFX-v^J=xoLn%0<$|?`wF|17Gq1;TE%u`Y&fV5!gP<(qiiMXI1Z1! z3vBaVFe^WX#*#KU5gaJyW3)|I9IQ8qp9yn+IJR)Kkuv1sY)23cM5`){ zHrRTMEl+H!-_o|G`cCz}t=>3_0+TjJbRsObYHSGiG40aEi#h74!(^PrQX2fxm=;?L z1X9P%H9mViuDUSvu9!k>FHfSMX>);-=}tge(c7*X=sc5C(;OWEX#q|shv${%_;uB@ zW;}?6+&5vl(Wt(;F1`{}m9isxjXR=M!=2D=*(~Q52lzYItk=OidknB?QlFyZFR?qI SJ+Lqg^$?gNW-mPY{OE7ay+*A7 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.3.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..94e3f9651b9952dfd449c2e452836a78fb5f0841 GIT binary patch literal 4148 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 Vmuna#3^)d-4R~?%QMmW{-e2_|vx@)# literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.finalize_invoice.3.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.finalize_invoice.3.json new file mode 100644 index 0000000000000000000000000000000000000000..64f45b12280841b1a278ef33259e9804993f4a96 GIT binary patch literal 4509 zcmc&%+in{-5Pi>A2z*W=%eIg@K+zVpQ>3-i)@fYCK|xTvl*O3K-H=>Ic94JX8D91# zxyW1fU>Ka?a4u)g%zk+?n=w%ob!$8if9*YcIb-kMvZq6l5tXF#--Icxn4OE2dpdh< zisxfBFScBj4w|ObI$EfL?Xso&hX?o#j~k&%oR7||tb{Vk-0ZvHHmYf&EM00EtqS2) zZAQ=b$9yd%7r~8@hWD~+Qq;W<@l=vmYoVQt-nPExqP!EPpfJ2``|}EK7aWm8@C7+T zLZuNaJvM<;=Y?-`ZlL%zm|iKZ5f8UgxDW!XglUD2i7ht5-q4d>K8spwSx~sVl73T@ zqb#a%0XKs?LhJiv0Cs(FWkI+7>qSX>M*ON&I6R=ON%dW{T2Mzz&sWg{vr{f`f=R?YLGt|m4T;QFwW1H9SP zW}J{L89-zA8X;3|so?auAsb`Idgy-mCrC=U7OnQY5%-lucXZkAjAsp3w>IEnM?yv| z@=v+w%(}v=W{THRxzK)4?5(7;;q6xP5(bBAa=j)%+yUei+@#LZlmxGp&vc3GJc{qP{^5=!QKE8Z-b9MRm^4E6<{^MT{*ZR#-b5tBGwxYT^l<(dz zF3#S6K0m$PUYy=8FHUc^2cVU!vrmh+$LD|i;Jz14ENx&Grp(ln)CC7eNiBxVn{tip zu}umLWgi6wS{b3$L!dt{t*xm;yOL^xo(HwuDL3Iqa85vFVk}@v z;O2+GU$ewxfy+AL?6{kbX=+4bm)kWZWX)F>j6X zgLFc#cw|(iIH9m-WDSI2bdlNJVc%)R3O5wNw^R#WJzg&o*7a@M@LNe^betwgAE_rK zMisY{3D>#US39^l&QW9QvW-}vf5$Nl;fY7Aq=dy?X~#F3Oe?t&cPdZ@YmIAUp)mR5 zD$u#(m9&m0#O|80!nk#9EB>h!#w))!5&pg*yfaK)?iDaF0^_)3-^B&7dw(G1B|NM^ z1uz9QwjZp9L?jzffs>c_V(3o$1o$e{R$@Ha!yWk zmdP7t+G-V-)3lgAgZwzaY!FjkZo(Dp&xC)y<~7azTqu^lNIK|*175ctdf1`@s}6;6 zIaJ1DM}!^LJURx_zLEDyC%s2=_84S&pTq~iWXD5@`C42Yyr0e-M?v{;}-=z1c#qDA4!B`TjG4IORRRv2x zi7^+x31i3R{e$`j%QHsRn0i>;=W=@^e>ajl^$>^%UmmpmhL)hlh>+|a*^^zCzb?=R z=!^kiIzi7Vo!cRk`_^yz7Q`^bXGLCuA`S1`fg+6QM)-PUbtBF2jC&$sMPC#HFs029 zT(;+{0C=i|y@jwf`GXR>QdG^}2hgc>e{hH|gfg3@<1T-XaNmd%cbIqB^HXbqc#!LE zrzBI6jiyxBs{jzg@`Mez&~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/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.list.2.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..3809582ab0ba928f3ea1e4bd6d4d5dfaabbc0b27 GIT binary patch literal 13771 zcmeHOTW{Pp7JkpK5cJdql6W*TPV54U-L_8BW@B&d)M+p!INa>uGNU- zu;~51!6hP;(08Ld+YPB}Nif)5*9FNCBNg?SF-z$h;}i+zJSxU;Su5GtjL8+u*lnM6s-#XI9O>JVx5+zQp<<_x)mu3cz4=h z)QV@uC88ZFj9Rh{YTj{HENx^}QQ(Y=CM_}BRQw^aBux=KL9mI6;(Blp+`$Bk$xQ$0 z`N+Da!88;0)3y0vSJ%R6>6+lew~0(F;znHFwdu)!&yK%(_1)WNM{q&n9)cSXY~J8j zOJi_5zXaB~p%nbI4Jr%F{(aLGq<)-LW#BAj(zaL>LutLKs4q0VIxx5JcJ2fCiW?7S zhVrv~W{so#{7b)@<2C9Eme%OH#Nd~`cC3puRarf-)&yg8%CZ%#I^-_$=HUu;g%x7pPX<<;riZ@=FE-~45gm395< z=;G@5=wko)DBGXSXS1JPE^eNlkfVRK|3tMh2NCjOLS1gOi_Hh5^7(0`A4GBk%s$6}m1DaS*%ioeQp!d(m%5o94uTO57y9IvAJ z!)QKB=Fy|*C8FwIi+}V=)`uyFQ<=&3JBNm64v?djha8gNer0*ZnB^^8rj#e81kxfm zKtgW4wSZtyR{0p85ap0kY}UD z3(U&}Uz9*GVGpoMNp=y)-aXsKVg&2H9P6gaJ84t=NBeEA*~5hm=0cVAWsqNmRo)Oju0gb2S52Az+9Apa8x<70uDMUeP+_Eufcx6yV-T)fhhrI3W;_ge{sBe}k&@?c2rHa| zi)8ZJflwTWj@xcEjKaQjW3DQh5rRQ6Tr)}C#ah}b5Q;cxOA|ETdzF_|Jc(M-#TJao zT1f)MLth>#PUH)`8-ldc0U^Fc-ZGi$uaLKvjuSv}CCbKIm<-b$8E-q`NM z<%;w}pPzf=0`###ysT*gMZOgbTQ&&LF{>CVB>V`ue^8vns*m5hOmq+5!Vh+_rn}_X zY`!O(L&aI2T1VYF9K2(SuMGNvB+dtn1uzWSl`Dx|DctH2aAT^+!JAbA>YokenkW~G zbz1ROepsf&3I(<5De=p+>KM=iXd;>#mq6l0gff7Q;LNxl?al^}f`*w|%ox)dduV8&ztkwY+m5p0~0`Q<7 zK$GQVN&!@AO}o?AQ&6MgThsp9!6~l3Ajo1s)6(7?ENucRnnQOguxnG{sp^j7z$svE z&{g^sek@@gvq{MN9Zp+vBWivCI5|+(yP|eVWBUE&BbtddkO}?=Ug;QOzi5hg>uEv* zV<;Ynt)ZU<1XI_SvNL+i5!vXQqU!SKHRE>Gm=?)^-Emc8YW#(t2KX@JeUU&_979#W z5;PGbfFsDRM9l>dbW~&C0Gmki;j4s@i8{wwfGRTASMVjzbSHwi)tLm_pu$+Ob&ZdJRyB z!i9#qh=0SY4HRQ72$ENA&_Nna3auy~(9UyDV=fbARKIqc375A?`yuFJux2J$uHukc z`t0GBH@_Opw(JDdPmOlNEzyYp4kw3FpdS9SUAOFpTf5DPaO?cXm(}s}uM_<2 zisUa&Ui^4|dVTVYm1pzKB7bo{J3c@8%^hx?;^pD><>7%d_4(LvYv2YNr1?{ZTc7j9 zEgfzx{T5kQnQkHuk$xITI9bIhh;p(-aA@vogk|hYU8t?><0C9@>ohzxtlsW#gtdbL z`))`pTzuIwIl^)^4K2X5t~tS0LxF)8?Vvzu5Pfs3?4uu1L78S1wC;MaXH!Ibx!bXog_`ygig+-vGEVrJ11k$E$`t_B+2qHJ`FofIT`eq$-sS!4Qb$4F;RVsnk?!riD75i#d#JivPZ^ujzB- zD`1(GKNwm4y8HFJk8VES8w`R3H7NLT@F#p83_i2p@GXktqRpF#{jSacAUJ#z+_#5w zlwmsdJIa#^4ffEizCU=G#}8f51}!6!)Bv5gX{uJr*gi>1*BNfZk3Cc(lE9ereT6eb za*}`On^-SMS@Lmha#^M%Mhz))2Ng9G%`uJ;L(PF|8l07aGA3Sp1XFD1D6O$NN!t_= zN={K8(_=POYmQ}rPaFY-U{JCEn1FLMOWoyQq;62tN?Ho0eCf8Eku-&KM-@gjJ7$of zyhW*EO1wbT2a1Zpi?~Qr98=On8EzH@JxIVoBLqiaXreSd(-wj~8iry>dB=V_JmR*h za2_-1Y3oFr)z+|DG$%N9B_d{nxMG{Pb+rGVgXu4OFOPSh!Rn#C2(EyzNrh`k4Q+S2 z35-)YN$F|}l%=@&_j#L=@?l(Lp0$KPn`99PMdhN%U82cRP2Bv`sRrOB6&{LAPSY}ZfoOws-1{Ls((#zPQjw@37^dccOGXpTe zTv2>)uhilQjEX}jJfBnHIt9g|1n|fU&J!xOGn5KNQEEFciiU}Vfx)~2DI)o4LEQ5$ ziW&q{jz(?8MWAWQ`p2y;G3jC5z@ae}hApxMMz1l&4FqlrNM(E*qEWJYaP)i;XHS*~ zFUs@1=TBZw^W*Ws`To(tyUDA=?dI>d=f~;(s2s)H;Syy>Lwxu?obJB=wD;^}Iem6A znLhil+y--ZwEHo9Gv52-=lUm9Dsf;IFCe*%jnJ^|kp{|-AC<|Riq%3K5Jbf|9EZBi zBW?kP)-h8Aq@9x-rR1EM6~NtR()V*=IRf zD46g>rk+_a)KdW648LQ6c;^dAOZy~k(n*?FO=wO;zztG?BjnOkQ&9e_Nc!*utA+&Q zK6lPc>{`tJ2IE2Zqul13OYM|;EsJJC^P(aM`!6XlZ00HT->v(~ALdy(=GggP6x>)q@*XiET;j1)x4$vMRC%W(FvyEflEq&SRoHEnv?j% zRD%afldiCVL_YMC5{P1IV$w& z&}#^;>PTpVf4K_f^}MLE=wplWhBUo|YR*ckQvpO2QA0p}9051k{8$Cq9X=XFlmONl zm?KycK!eq~0QOo&(;nFlfV6xZR&5{-qejTl4iM=C> z(~Yi6+<du^oK~CRDmS6u!GDDn)C+7cS_~>5SyZ3x?^_X9?04PQQ4?GmNHOs zoswEN4?$BjDCHR&kT6t81tR#_It^vuQ^N;S;$>*Dy*=9E9#vr0S-)PL_G@a8!ys?784vhHu>SyH#+?{zXO_WUHx+Ufs&nZ)Dro$pGdmEOqal{;%cy7@ zfQLwvc?KJR2iYWVAl8O8gRpj0&1>8=U?faA4`gf(iI^ug29C?=JhYDr7Kuw$ zkf(Lws5D9_hhN+yM#BBMWEbO*Omgq%fq)>o%R0E0Mma>{welG%MlUOTY1oe~tYY&E zEG&i;BO*sV2nrzWlffi+FTPk(sSKUPDg;4M z;@ti|YN&G|G~2TTRYc=pi+i#(pk0Ql698y)SulXulY{H0Vnill9cutGwnT10haAye zVZ#mk?)56VJ?dMX8B5@k5!2J9xfNX*`cwa5vzTQlaazPD&ImQj&Q5$AnOb|jkuc~N zpu&uPpF;~6pXU%uiojauL+}aD77&HiKtvj@ffH6-WE>IlFBRvOs)FJ8@uN_g8Sc2j z(p+2?^v{&q2t3hNRmDG>fk+K)>VliPpmfHNV~GOerY>lh6=~F{XVEK+|F3nyw?_y0 z^k{$a=BWJd^knfG{T46ZX3N*dzdfD2AHE9XtSs%i;G<+dtVZpQRWQ4#E;v+_=BA~SN z{6rAXcA3&>_&A(|n>wMS4E!5hqE2XY8fLBLH3eAfI-%!8-%p)zXEYo-I-nP~v_c_; zRS7hei!OQQLZT{x5Ed8}7kk{B(+Qv(RSNG`q0pwNK~I^I39@wEtT3brt;sZ5BC*|; z7xw0UOL?lh;I~wjQIPWTe#@D<+Lo<{-n$s3-x)m>$SPmMT9kKc6EV^%-Sx~=W-_9 zW{bm9Sj$vg+7tx21?W;XVGz12?YGv*dZg-T6sJ|Gq`z~^y#?q7Ie zujZZp;4do&Km1BxWbucH1CkHmxzKPe?^~#{oM7SQ-IO!+mfe(d?VO*8%C&Pdy}P1K zVI0NWyvw3*q}7kj>AUZyJSX~o+?3Cb0n2(iR~&lndsn)#Uwc>O(sT`*SK-%fUKw|J zyQesT_A*N;;SJkXMyZe%I^1t5s(+>(1)(mxFX6o@p&8!p8L;mTL~!rRz5fAw COSJd^ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..fcb1c3c2e9d63942394e42a1234fe7aa6bfb8a2b GIT binary patch literal 1116 zcmaJ=O^?$s5WVlOh;jl6q_pY*aYL|&mG}}z5V9P5$_{nx;E#%`+W*ds?KTcn`x*?Zl>;M8g>(jwj_*H{ZDbrZLx*+y6b?~(CRh>#FXXe4Y|2&B?2E`sC{-kp1h`s2 zbR!x=o{69yVd7*@Iz+MXCJT5ipjD-usPlZfj|TvSoMxSZAf-)O?#@|DBR z9nvfgzZUYuqL()8ou9<7ByGg_S7Y%s=jk{LA|f?Y`FK)`$;-PV zkxfcyk}*lc^DUTu*&6S>tncZ%8GWN-lQ%b{v3+?L={@&M_0YT`Iz*w2Q-{W$Rt|+1 LrETJt&ZoOS5R*JH literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..11622f30a734254b418b26d4bc58ee64f391c945 GIT binary patch literal 1098 zcmaJ=O>Yx15WV+TtbFE>kX>51Rlos>4+(BX$nx3_9pbeYe?(MK|2s3bcjM5CoU(6b z-n@A;K3#Pk6O&6evNv6Sv)OE35G0fL+R6t6Sqh3(FhndC3g4wH@o)eB5KJ?1nw{?d|sVU+`-p30cB z_TZ@h3Ee`!w55;jZn)mO9&Y-9>Jbi6cp}*DUJm{BuI~rL3;ApWd9iAmeGyp+g^VZ? z6IKh5ZUke%{^UX4>xqI4b?{^%W>uIWy~YJ(Evpe#%X2HGibPAzBz!%DMHR-2%URw3 zjmF&Pd?nbqLz?+P(>XQNPaMZw3-Vw^9#N1DA;sceY;Ug|3ciIiT`=;+&J z_bC4GQQwOrxO~ychFMG41KL;lBWNFZy_7~y^umU{^ArD_gpCmY87w}`c{|Q(9s!4` ze6*>v$;)FyiA{hj6fp@y+ocS4z8LSks849U3f3riMQvC}WBb?^*^K7JdfmJr{D@o{ TCl8G^tr&zCl`Z^Kifn literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..088916fb64e5290423831340eefe7331e8096ad7 GIT binary patch literal 1126 zcmaJ=O^?$s5WVlOh z+&||qxBdm1EfbXkBaDS~0*{XC@5x7U5-cCO!-d=Xg*m5wBm0*)3C-5QNS`%?z}2qRw#(jk+@m{n1P_CTY_T8>6kE!T$4 z8HvZ8N#t=y4yx$BxSZAf-)M@x<}1U_ZPO&Tz>7AczHppkE$XdRxw%>7X%fYz+S26R zkjjMk_gr5YKT&$%*2%Z?>QViXYj{*Uba~g44HIbGAlX;>BZ3dIZputf^vZ^f^P~Ko zl#LkwX)HcXc{94iDql5Jms3Gb-Jl>3zHpBD@+h1 z^(85B_mtA?@Cy_>Ru>KoFlEvi99piwBOl30FnnnH)ot@?b@y_`_X+kvIwolQ?yg<6 z6d*j4&swre2TilfA`78ZfkaZm!2+TiP-`?lg;x(Saj+&GA{q=y6=f&~G@7L4U_{Y! zY1n)t^0*U;IIPHC6~(j5N!|aArrb-uGHmT8&SC?+Xbb8y$0^sK+8CLdTVx(5k!>n1 z&CZT7Pl$g{_2uysr2}rAd^@ckgWRG`yB>|7LE#$7zQP|6oEK$N7IK1<#;>iL z#P29g!0=CF;c-sWu?9pyYNqhd)v_FZ=#5F?40q~_^zMt$5EXPqpM>#!B|LP6TP k+Q(t}4EM5Y?w9Vv;swzm2&JvsEUc+7t6ak88lSHI0`sswd;kCd literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.5.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--InvoiceItem.create.5.json new file mode 100644 index 0000000000000000000000000000000000000000..05b0e1fc5f595fdd1c3f384d11b0bb6440a3d549 GIT binary patch literal 1105 zcmaJ=O>Yx15WV+Ttb7J335n8jtAGO%9}?V(kmdDG=n${H_#;q7{qM}!&c>k?KJ7lw zym|9xe7x#9p+=W%6tBD0g3b$ubn+goejre!Fo+6<=tl4i>J;Rs&tE>?et3WP?k%M) zOU$4m(EXB`^2wP41nqH(=qxASC4*D<#<(;6f72-LW%a^P&1oN#^g+c2N>OOSz&~zs1HSf$HyGz`k$cL zAyGLn!dOTr@aVYyk$fZP!1B-X)#hfqU9Z*~&L=oT<(Yskwu_t1^?E~iA)kz5S7uF< zFCr_U(vd_`z-$50^=J&*A2R5B82M6=4w)>*tcoJE2O34zG8<8~JU3j+r5dXt)$0&imq>SM@2)J0Y6-tGsj~q`rN1C)I-6 cB?4$35dB1{jnn(anN|$JRb-1kk@$G^7f+Tu=Kufz literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--PaymentIntent.create.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--PaymentIntent.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..aadf902f017f5b4a24d35a244dde16b3b91eaef5 GIT binary patch literal 5967 zcmbVQ>r)gt5dZF9v6Z@yD!YJ+R$bk#?Bc1sL_x*6N@X)yb|CX0lPn9C|NC_(lbOte zg_AEpy3^_Bue;~xlfi(nBr~ZdZwA{t!{P90eMC9SwNY#)1yCE5NN5(z0*-$IKM2ur zj{p3E|9*ucl;9#>il`|*&t=S+j>k`p@4>EN9TMC*ni7upCv9w^k3=`3- zlpY?P9E|thpHC-{D)rCEy2xLTrHs=H+RhQLW)vw=)P8iw;2a+7+m6ymB^(n}-9v=8 z#Hjxsk~aHEc~)p9DbJ#=qwJeN)}B;G7|(5T;~)4#L;_XN^8iU%;xA+J_#culveXl* z^FSuyOmYKB3h8S#ut^??f~z>!nS!^-`RV@qh}qUv z^TDpnOf>kcSsF1FJste9GkiWAtOhT`=Rk;#(>6djFOn!Oa#O`7(7z<0eRC#@(DFIN z$tXelQ`=)i`G9BcO%CFz2_!MQ@WBk!m@8(YF#E&!#;6_Pi>3#wXbc;^jMHTn^PVRp zKx*Prw{0l^A}f)}L33%;tgK47#-yt>Py->2RzSz3ybZzt3GQzdd?6xZ`v3`51QtMY zkrn`)ECqL9zM_Sd%%;4O8LOX!5eGTB0zotvik3{sgB__U0a92d)VZ;xi6a%-X6YSf zTk~oG*$)-(nqB*OeVe1BiE#;MG|9u&hlJk(2nWsmX_4u zafm{vF;>u};^K-*u@ne+B+XR9Wa}<+6g*R?NYJ)GY$2os7uGdHj$H#EfZjQHQ#^0V z^5K9yT4XA|1JvquWzMXtTR`WhgE-Y?KIZYGD$ZYYV+9(TCvc#~uLvLy@i+TS8-wgV(g^y)iu;6iIfm#;) zs@qjjylc2nR?sM^8RTB6ScC4k{sLd}iUE88Z9Ooz)h$uUs*#h^&ut6DDyK=7 z>IG;w8r6Vm%|qg?U|8Cl!_Dmu72*uzQsqP6I>K_Wa zes2jZYUCyH=xFhz;tF^G$6@cv^`n|r#hjXC*4JLVFrzH?FSO2!Hyay(dC#HS#<{>Q zxKVNyc#iqEu{-8$?($A-{O;qyMD4Kb-ND8A zw`XViZ0Gvw<^J2*_4($9J@rifbG=_A2kF)c9qyjpn7j8QMz=1<(*r#>CvT5G4!@*F z`F4JH`s$^gK06bygMM!GpcW}bmwmK zoA;#xroF{4XmO3Dq-VGfW6^hy7G^$=S3P=HAYB;Fg}_C)jtH@PDQHK_aJ6i!$QLRz z_*DYwWs%_biNT-@?>&L-4{0lyWq%^wF#hF70XKa7xo+|RZEPF~moZcfB7?HkPuAQj zxv2QQKi-yO2=IZp%J5fU04sgkc3&9!QD@cQ9CNx=R-6tXI{F z^vK1k;_q9j9=S|;HapV$HR{0y3TOMy>Qo-~EKM-8_pM7+?`bYd@WJ2_E0W_1mm`<{ zsaR=ReQByIufvwe!AeFSq>D<8AWI;{qvq@i@>NY4laQ&~&(|e=abAm~(*=_Fa MGc}>nVSYXN4_yFp&;S4c literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--SetupIntent.create.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--SetupIntent.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..04376403a931b41a561236b1ac255c73ea1676ef GIT binary patch literal 930 zcmaJ=O;6)65WV+TR5^2?2v%EeK!UVVyQN4g77(%qd(s*l+w}(@s`}pWln^ztUAFe#VHlxKH8uE!Vi z+om4h=wlaPTx5B~2mu9=jt+NEdl5YeN0-X+=-Kbt_4I7=>#iyZHfoavdhVp%Z@E_H zl?myPr<-1*GFAFseoL*ur-nnc44pGc#y?BRC!|{l0UNvXgHE3unLbaZcD_2^Qpy)> zj6kBQL3zw=HlPL3b?Iq7Krx`$dXqsK->6*frVBKWPt+nsQbQzE>Of;^iy6%xaS5ts zrtaqSmmuZZ=9BK~@aMdoRlKF@r!a1GEf>+X%OXguR6URP@FveEA=b2yw5S|hnnHV` z^VS0PAGmWm!!?FiAto)4BqymI^LnJ~Kl$J8%6zhW}Hnn*+BPU zWzF@>IWyZqQHT;Eh>yi1y^CU-|73-xF?&XUt6W8NHGDY}|GN;a^m(MDNgT(~my z0~J_>#^fT>BZTu>%4jYSPmtzySwq^2Y9y{FN6E_;ePt_%eak`x0mbZ z_h0pJ_V!_NX;*N2e|Nq4d^H``OFJva;sBMZLd22^kB0lNbEZIppy#$K1^NAEdj09! z)x%^=p`)%yi9vbY{ELdpxF=a^kBYr#1ZeL3`bb?H1A&Q%sgdZtV^}A&6OTv{5F^$N zb9L&~Yph*~WUs&kJWN@GC57335>JvrtNixm5Nz?8I^<#c32wHeDap$75JqhyDc8Yw zR?ct%rjoAe=brA#>t3mxt@J$k-P)9dQ=2npPb*57)#0f90@DgB@XUW_AzRDFQNl4R zd;!8@d~*Iz4`p_FTqV{ZE6ffd1^9@rWln^ztUAFe#VHlxKH8uE!Vi z+om4h=wlaPTx5B~2mu9=jt+NEdl5YeN0-X+=-Kbt_4I7=>#iyZHfoavdhVp%Z@E_H zl?myPr<-1*GFAFseoL*ur-nnc44pGc#y?BRC!|{l0UNvXgHE3unLbaZcD_2^Qpy)> zj6kBQL3zw=HlPL3b?Iq7Krx`$dXqsK->6*frVBKWPt+nsQbQzE>Of;^iy6%xaS5ts zrtaqSmmuZZ=9BK~@aMdoRlKF@r!a1GEf>+X%OXguR6URP@FveEA=b2yw5S|hnnHV` z^VS0PAGmWm!!?FiA1nO%M$IiA$7bv!g@5we!H?sM!cJPVKg5eA-Dh?_9ptrIBiNI1W{9_8-#i;9WA;Ckw*^(v#>>v4X znM|@)h&`oA3DQ%75w(=*K?r4t@`pqnJ{;pbBh=6T!y`%Ay;4WELtqH?g$~A==_p`^ zcbts2?JA&qU zP_a|A2@F`Lae?+ zsNvahjr*T7;6+JNmxO*V_RZ|LIaX|IU=j@hA`+uV?^BfV|4yl&2Pe%@_xYDer~4Px z^11V5|SYbj{21&1p?VT2>pin|?KcrCIdvzBY zrg=7u8aRheg!ca*4qtpN8t~#(miPT)EEWXxv`mO7_qPah#NbUrOWsJe%hpAcNl=R>?kVdWHtoU=5T)A<2F+_t-A}FET^+Uw3`iB zdVkToVErqMi&flLYc}a!M$;=_w-xp+r_*aXPn2T)rM}s&(plfG<}I<9OvP%RTw8SE WCDv-)?0(xs7uO4Dk413ucJdvg2)K3t literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--checkout.Session.list.1.json b/corporate/tests/stripe_fixtures/switch_from_annual_plan_to_monthly_plan_for_automatic_license_management--checkout.Session.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..49eef5d590d907ddabd85a63e9e375a8781b6444 GIT binary patch literal 2538 zcmbtW+iv4F5Pi>A80u>g$IiA$7bv!g@5we!H?;l;Ng|Gq<6l4Z+o zADV|Cn4yMe&YU^S`$eth1=wKkbL~6)U8}vPzaff4LlvTH$rLtBByb|Joc~NuSPHBVxFjAcNQNVGazS(FFUP1`rG}*EEL&8Evq! zIf@MFN1pe5#yLyo2nT_pS_5RxDLK^~!3m)pS>+&mkumafy}q@a$xB1E(Nry|s#`f% zIG}Z}xPfp+pfw=^CdF)2$tA6@IlWc`>OcL{M&T&$4@ zkt3J^eV7m`jT=T*q%zYTK&l|`!Qbd{`0dZzuY*~q4Ky@LlSvx^MFZ_8^_&+%0X3L$ z*KDb$OfuD0Xbgwl*g6S{^<-3eJ`^20MZ84i?IrHh?0e{=2C-`xW&l_ z>q)$coe8kSoLJ>N3r4g!<|WTTLy-s$x81{hg*Uo-O>sC0+1&2Dx}@O)1sq)3fqrI^$?e7eAv2sB?_wjP30p|E{oLFA*pYD*9%b0)N`G8~bi2%SK$9;gQi- zrlcqlU|cjnA-qG!bf&Z0kHO87p~kTD_RF~4`3o!Y)_&Dudh_ya)NAjN<@Qx6cVuga zs1JC~OQ1c&CF3lzFszE0_w)%#ORCygCdyQOc$@V#Q7j#11+)bRg{SU6Ub5_Etkb1a z?heBjZJ;OxFHQvx%kukstKVro3`c*X=g#xTUBVG z1Wv;{ON!M>0bGKa)=3?tRR90p^i#-!MKD}e$I)ck54O`jNuI8|*Q9p~alQz9az)17n_zP5 z>NfqJVR&+fXR(x|7peQrBANEgV%FsI@q{mC@tuJeBZ6AZ`|WS*;QDS(TO+<(tPhZS ox?1q8Xy8pbofu`)|8+_(#F;iJk6y-Mec34Zt7ntb-o?ko56lqv-v9sr literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 7fbc9d992b..5fb3dbf162 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -2628,6 +2628,182 @@ class StripeTest(StripeTestCase): for key, value in annual_plan_invoice_item_params.items(): self.assertEqual(invoice_item[key], value) + @mock_stripe() + def test_switch_from_annual_plan_to_monthly_plan_for_automatic_license_management( + self, *mocks: Mock + ) -> None: + user = self.example_user("hamlet") + self.login_user(user) + self.add_card_and_upgrade(user, schedule="annual") + annual_plan = get_current_plan_by_realm(user.realm) + assert annual_plan is not None + self.assertEqual(annual_plan.automanage_licenses, True) + self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL) + + stripe_customer_id = Customer.objects.get(realm=user.realm).id + new_plan = get_current_plan_by_realm(user.realm) + assert new_plan is not None + + with self.assertLogs("corporate.stripe", "INFO") as m: + with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + response = self.client_patch( + "/json/billing/plan", + {"status": CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}, + ) + expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}" + self.assertEqual(m.output[0], expected_log) + self.assert_json_success(response) + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.status, CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE) + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): + response = self.client_get("/billing/") + self.assert_in_success_response( + ["Your plan will switch to monthly billing on January 2, 2013"], response + ) + + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=20): + update_license_ledger_if_needed(user.realm, self.now) + self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 2) + self.assertEqual( + LicenseLedger.objects.order_by("-id") + .values_list("licenses", "licenses_at_next_renewal") + .first(), + (20, 20), + ) + + # Check that we don't switch to monthly plan at next invoice date (which is used to charge user for + # additional licenses) but at the end of current billing cycle. + self.assertEqual(annual_plan.next_invoice_date, self.next_month) + with patch("corporate.lib.stripe.timezone_now", return_value=annual_plan.next_invoice_date): + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=25): + assert annual_plan.next_invoice_date is not None + update_license_ledger_if_needed(user.realm, annual_plan.next_invoice_date) + + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.status, CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE) + self.assertEqual(annual_plan.next_invoice_date, self.next_month) + self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL) + self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3) + + invoice_plans_as_needed(self.next_month + timedelta(days=1)) + + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1)) + self.assertEqual(annual_plan.invoicing_status, CustomerPlan.DONE) + self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3) + + customer = get_customer_by_realm(user.realm) + assert customer is not None + assert customer.stripe_customer_id + [invoice0, invoice1] = iter(stripe.Invoice.list(customer=customer.stripe_customer_id)) + [invoice_item1, invoice_item2] = iter(invoice0.lines) + annual_plan_invoice_item_params = { + "amount": 7322 * 5, + "description": "Additional license (Feb 2, 2012 - Jan 2, 2013)", + "plan": None, + "quantity": 5, + "subscription": None, + "discountable": False, + "period": { + "start": datetime_to_timestamp(self.next_month), + "end": datetime_to_timestamp(self.next_year), + }, + } + + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(invoice_item1[key], value) + + annual_plan_invoice_item_params = { + "amount": 14 * 80 * 1 * 100, + "description": "Additional license (Jan 2, 2012 - Jan 2, 2013)", + "plan": None, + "quantity": 14, + "subscription": None, + "discountable": False, + "period": { + "start": datetime_to_timestamp(self.now), + "end": datetime_to_timestamp(self.next_year), + }, + } + + for key, value in annual_plan_invoice_item_params.items(): + self.assertEqual(invoice_item2[key], value) + + # Check that we switch to monthly plan at the end of current billing cycle. + with patch("corporate.lib.stripe.timezone_now", return_value=self.next_year): + with patch("corporate.lib.stripe.get_latest_seat_count", return_value=25): + update_license_ledger_if_needed(user.realm, self.next_year) + self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3) + customer = get_customer_by_realm(user.realm) + assert customer is not None + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.status, CustomerPlan.ENDED) + self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1)) + monthly_plan = get_current_plan_by_realm(user.realm) + assert monthly_plan is not None + self.assertEqual(monthly_plan.status, CustomerPlan.ACTIVE) + self.assertEqual(monthly_plan.billing_schedule, CustomerPlan.MONTHLY) + self.assertEqual(monthly_plan.invoicing_status, CustomerPlan.INITIAL_INVOICE_TO_BE_SENT) + self.assertEqual(monthly_plan.billing_cycle_anchor, self.next_year) + self.assertEqual(monthly_plan.next_invoice_date, self.next_year) + self.assertEqual(monthly_plan.invoiced_through, None) + monthly_ledger_entries = LicenseLedger.objects.filter(plan=monthly_plan).order_by("id") + self.assert_length(monthly_ledger_entries, 2) + self.assertEqual(monthly_ledger_entries[0].is_renewal, True) + self.assertEqual( + monthly_ledger_entries.values_list("licenses", "licenses_at_next_renewal")[0], (25, 25) + ) + self.assertEqual(monthly_ledger_entries[1].is_renewal, False) + self.assertEqual( + monthly_ledger_entries.values_list("licenses", "licenses_at_next_renewal")[1], (25, 25) + ) + audit_log = RealmAuditLog.objects.get( + event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN + ) + self.assertEqual(audit_log.realm, user.realm) + self.assertEqual(audit_log.extra_data["annual_plan_id"], annual_plan.id) + self.assertEqual(audit_log.extra_data["monthly_plan_id"], monthly_plan.id) + + invoice_plans_as_needed(self.next_year) + + monthly_ledger_entries = LicenseLedger.objects.filter(plan=monthly_plan).order_by("id") + self.assert_length(monthly_ledger_entries, 2) + monthly_plan.refresh_from_db() + self.assertEqual(monthly_plan.invoicing_status, CustomerPlan.DONE) + self.assertEqual(monthly_plan.invoiced_through, monthly_ledger_entries[1]) + self.assertEqual(monthly_plan.billing_cycle_anchor, self.next_year) + self.assertEqual(monthly_plan.next_invoice_date, add_months(self.next_year, 1)) + annual_plan.refresh_from_db() + self.assertEqual(annual_plan.next_invoice_date, None) + + assert customer.stripe_customer_id + [invoice0, invoice1, invoice2] = iter( + stripe.Invoice.list(customer=customer.stripe_customer_id) + ) + + [invoice_item0] = iter(invoice0.lines) + + monthly_plan_invoice_item_params = { + "amount": 25 * 8 * 100, + "description": "Zulip Cloud Standard - renewal", + "plan": None, + "quantity": 25, + "subscription": None, + "discountable": False, + "period": { + "start": datetime_to_timestamp(self.next_year), + "end": datetime_to_timestamp(add_months(self.next_year, 1)), + }, + } + for key, value in monthly_plan_invoice_item_params.items(): + self.assertEqual(invoice_item0[key], value) + + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): + response = self.client_get("/billing/") + self.assert_not_in_success_response( + ["Your plan will switch to annual billing on February 2, 2012"], response + ) + def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle(self) -> None: user = self.example_user("hamlet") self.login_user(user) diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 29b968808e..4af5c24938 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -132,6 +132,7 @@ def update_plan( CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE, + CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE, CustomerPlan.ENDED, ] ), @@ -160,14 +161,23 @@ def update_plan( if status is not None: if status == CustomerPlan.ACTIVE: - assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD do_change_plan_status(plan, status) elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: - assert plan.status == CustomerPlan.ACTIVE + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD downgrade_at_the_end_of_billing_cycle(user.realm) elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: assert plan.billing_schedule == CustomerPlan.MONTHLY - assert plan.status == CustomerPlan.ACTIVE + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + # Customer needs to switch to an active plan first to avoid unexpected behavior. + assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE + assert plan.fixed_price is None + do_change_plan_status(plan, status) + elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: + assert plan.billing_schedule == CustomerPlan.ANNUAL + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + # Customer needs to switch to an active plan first to avoid unexpected behavior. + assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE assert plan.fixed_price is None do_change_plan_status(plan, status) elif status == CustomerPlan.ENDED: diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index 637ba46e45..7f18c114cf 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -29,15 +29,44 @@ {% endif %} -
- -
+
+ + {% if free_trial or downgrade_at_end_of_cycle %} +
{{ billing_frequency }} - {% if switch_to_annual_at_end_of_cycle %} -
- Your plan will switch to annual billing on {{ renewal_date }}. - {% endif %}
+ {% elif switch_to_annual_at_end_of_cycle %} + +
+ Your plan will switch to annual billing on {{ renewal_date }}. +
+ {%elif switch_to_monthly_at_end_of_cycle %} + +
+ Your plan will switch to monthly billing on {{ renewal_date }}. +
+ {% else %} + + {% endif %} + +
{% if automanage_licenses %}
@@ -130,7 +159,14 @@
Expected charge: ${{ renewal_amount }} {% if not fixed_price %} - (${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x {{ "1 month" if billing_frequency == "Monthly" else "12 months" }}) + (${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x + {% if switch_to_annual_at_end_of_cycle %} + 12 months + {% elif switch_to_monthly_at_end_of_cycle %} + 1 month + {% else %} + {{ "1 month" if billing_frequency == "Monthly" else "12 months" }} + {% endif %}) {% endif %} {% endif %} {% else %} diff --git a/web/src/billing/billing.ts b/web/src/billing/billing.ts index ec8dbb1e41..4aebbdfb61 100644 --- a/web/src/billing/billing.ts +++ b/web/src/billing/billing.ts @@ -1,9 +1,20 @@ import $ from "jquery"; +import {z} from "zod"; import * as portico_modals from "../portico/portico_modals"; import * as helpers from "./helpers"; +const billing_frequency_schema = z.enum(["Monthly", "Annual"]); + +enum CustomerPlanStatus { + ACTIVE = 1, + DOWNGRADE_AT_END_OF_CYCLE = 2, + FREE_TRIAL = 3, + SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4, + SWITCH_TO_MONTHLY_AT_END_OF_CYCLE = 6, +} + export function create_update_current_cycle_license_request(): void { $("#current-manual-license-count-update-button .billing-button-text").text(""); $("#current-manual-license-count-update-button .loader").show(); @@ -255,6 +266,61 @@ export function initialize(): void { } }, 300); // Wait for 300ms after the user stops typing }); + + $(".billing-frequency-select").on("change", function () { + const $wrapper = $(".org-billing-frequency-wrapper"); + const switch_to_annual_eoc = $wrapper.attr("data-switch-to-annual-eoc") === "true"; + const switch_to_monthly_eoc = $wrapper.attr("data-switch-to-monthly-eoc") === "true"; + const free_trial = $wrapper.attr("data-free-trial") === "true"; + const downgrade_at_end_of_cycle = $wrapper.attr("data-downgrade-eoc") === "true"; + const current_billing_frequency = $wrapper.attr("data-current-billing-frequency"); + const billing_frequency_selected = billing_frequency_schema.parse(this.value); + + if ( + (switch_to_annual_eoc && billing_frequency_selected === "Monthly") || + (switch_to_monthly_eoc && billing_frequency_selected === "Annual") + ) { + $("#org-billing-frequency-confirm-button").toggleClass("hide", false); + let new_status = CustomerPlanStatus.ACTIVE; + if (downgrade_at_end_of_cycle) { + new_status = CustomerPlanStatus.DOWNGRADE_AT_END_OF_CYCLE; + } else if (free_trial) { + new_status = CustomerPlanStatus.FREE_TRIAL; + } + $("#org-billing-frequency-confirm-button").attr("data-status", new_status); + } else if (current_billing_frequency !== billing_frequency_selected) { + $("#org-billing-frequency-confirm-button").toggleClass("hide", false); + let new_status = CustomerPlanStatus.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE; + if (billing_frequency_selected === "Monthly") { + new_status = CustomerPlanStatus.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE; + } + $("#org-billing-frequency-confirm-button").attr("data-status", new_status); + } else { + $("#org-billing-frequency-confirm-button").toggleClass("hide", true); + } + }); + + $("#org-billing-frequency-confirm-button").on("click", (e) => { + e.preventDefault(); + void $.ajax({ + type: "patch", + url: "/json/billing/plan", + data: { + status: $("#org-billing-frequency-confirm-button").attr("data-status"), + }, + success() { + window.location.replace( + "/billing/?success_message=" + + encodeURIComponent("Billing frequency has been updated."), + ); + }, + error(xhr) { + if (xhr.responseJSON?.msg) { + $("#org-billing-frequency-change-error").text(xhr.responseJSON.msg); + } + }, + }); + }); } $(() => { diff --git a/web/styles/portico/billing.css b/web/styles/portico/billing.css index ef52e1d61d..0f9fd4bd7d 100644 --- a/web/styles/portico/billing.css +++ b/web/styles/portico/billing.css @@ -458,6 +458,7 @@ input[name="licenses"] { bottom: 15px; } +#billing-page #org-billing-frequency-confirm-button, #billing-page .license-count-update-button { margin: 0 auto; font-size: 1.1rem; @@ -465,10 +466,19 @@ input[name="licenses"] { width: 100px; } +#billing-page #org-billing-frequency-confirm-button.hide, #billing-page .license-count-update-button.hide { display: none; } +#billing-page #org-billing-frequency-confirm-button { + margin: 0; + display: block; + position: absolute; + top: 25px; + right: 0; +} + #billing-page #current-license-change-form, #billing-page #next-license-change-form { margin-bottom: 0; @@ -535,6 +545,7 @@ input[name="licenses"] { } } +#billing-page-details .billing-frequency-message.not-editable-realm-field, #upgrade-page-details #onboarding-free-trial-not-ready, #onboarding-go-to-org .not-editable-realm-field, #free-trial-top-banner .not-editable-realm-field, @@ -592,6 +603,13 @@ input[name="licenses"] { margin-right: 0; } +#billing-page-details + .org-billing-frequency-wrapper.input-box + .billing-frequency-select { + width: 150px; +} + +#billing-page-details .org-billing-frequency-wrapper.billing-page-field, #upgrade-page-details .upgrade-add-card-container { text-align: left; } diff --git a/zerver/models.py b/zerver/models.py index 454fd820f3..3ab6b9fb50 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -4782,6 +4782,7 @@ class AbstractRealmAuditLog(models.Model): CUSTOMER_CREATED = 501 CUSTOMER_PLAN_CREATED = 502 CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503 + CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504 STREAM_CREATED = 601 STREAM_DEACTIVATED = 602