From e0f5fadb7980d0d268b48a0ce50414ab129e5eab Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Fri, 11 Jun 2021 16:23:45 +0530 Subject: [PATCH] billing: Downgrade small realms that are behind on payments. An organization with at most 5 users that is behind on payments isn't worth spending time on investigating the situation. For larger organizations, we likely want somewhat different logic that at least does not void invoices. --- corporate/lib/stripe.py | 42 ++++++ ...payments_as_needed--Customer.create.1.json | Bin 0 -> 1221 bytes ...payments_as_needed--Customer.create.2.json | Bin 0 -> 1221 bytes ...payments_as_needed--Customer.create.3.json | Bin 0 -> 1221 bytes ...payments_as_needed--Customer.create.4.json | Bin 0 -> 1221 bytes ...payments_as_needed--Customer.create.5.json | Bin 0 -> 1221 bytes ...payments_as_needed--Customer.create.6.json | Bin 0 -> 1221 bytes ..._payments_as_needed--Invoice.create.1.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.2.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.3.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.4.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.5.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.6.json | Bin 0 -> 3524 bytes ..._payments_as_needed--Invoice.create.7.json | Bin 0 -> 3524 bytes ...as_needed--Invoice.finalize_invoice.1.json | Bin 0 -> 3735 bytes ...as_needed--Invoice.finalize_invoice.2.json | Bin 0 -> 3735 bytes ...as_needed--Invoice.finalize_invoice.3.json | Bin 0 -> 3735 bytes ...as_needed--Invoice.finalize_invoice.4.json | Bin 0 -> 3735 bytes ...as_needed--Invoice.finalize_invoice.5.json | Bin 0 -> 3741 bytes ...as_needed--Invoice.finalize_invoice.6.json | Bin 0 -> 3735 bytes ...as_needed--Invoice.finalize_invoice.7.json | Bin 0 -> 3735 bytes ...on_payments_as_needed--Invoice.list.1.json | Bin 0 -> 83 bytes ...on_payments_as_needed--Invoice.list.2.json | Bin 0 -> 4359 bytes ...on_payments_as_needed--Invoice.list.3.json | Bin 0 -> 8633 bytes ...on_payments_as_needed--Invoice.list.4.json | Bin 0 -> 8595 bytes ..._on_payments_as_needed--Invoice.pay.1.json | Bin 0 -> 3722 bytes ..._on_payments_as_needed--Invoice.pay.2.json | Bin 0 -> 3722 bytes ...ments_as_needed--InvoiceItem.create.1.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.2.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.3.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.4.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.5.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.6.json | Bin 0 -> 1007 bytes ...ments_as_needed--InvoiceItem.create.7.json | Bin 0 -> 1007 bytes corporate/tests/test_stripe.py | 135 +++++++++++++++++- .../downgrade-small-realms-behind-on-payments | 5 + .../manifests/prod_app_frontend_once.pp | 8 ++ stubs/stripe/__init__.pyi | 6 +- .../emails/realm_auto_downgraded.source.html | 25 ++++ .../emails/realm_auto_downgraded.subject.txt | 1 + .../zerver/emails/realm_auto_downgraded.txt | 8 ++ version.py | 2 +- zerver/lib/send_email.py | 18 +++ zerver/tests/test_management_commands.py | 13 ++ ...wngrade_small_realms_behind_on_payments.py | 11 ++ 45 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.3.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.4.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.5.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.6.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.3.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.4.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.5.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.6.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.7.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.3.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.4.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.5.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.6.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.7.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.4.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.5.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.6.json create mode 100644 corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.7.json create mode 100644 puppet/zulip/files/cron.d/downgrade-small-realms-behind-on-payments create mode 100644 templates/zerver/emails/realm_auto_downgraded.source.html create mode 100644 templates/zerver/emails/realm_auto_downgraded.subject.txt create mode 100644 templates/zerver/emails/realm_auto_downgraded.txt create mode 100644 zilencer/management/commands/downgrade_small_realms_behind_on_payments.py diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 8d8a085cc9..ebafe6da48 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -12,6 +12,7 @@ import stripe from django.conf import settings from django.core.signing import Signer from django.db import transaction +from django.urls import reverse from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy @@ -26,6 +27,7 @@ from corporate.models import ( get_customer_by_realm, ) from zerver.lib.logging_util import log_to_file +from zerver.lib.send_email import FromAddress, send_email_to_billing_admins_and_realm_owners from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.models import Realm, RealmAuditLog, UserProfile, get_system_bot from zproject.config import get_secret @@ -988,6 +990,46 @@ def void_all_open_invoices(realm: Realm) -> int: return voided_invoices_count +def downgrade_small_realms_behind_on_payments_as_needed() -> None: + customers = Customer.objects.all() + for customer in customers: + realm = customer.realm + + # For larger realms, we generally want to talk to the customer + # before downgrading; so this logic only applies with 5. + if get_latest_seat_count(realm) >= 5: + continue + + if get_current_plan_by_customer(customer) is None: + continue + + due_invoice_count = 0 + for invoice in stripe.Invoice.list(customer=customer.stripe_customer_id, limit=2): + if invoice.status == "open": + due_invoice_count += 1 + + # Customers with only 1 overdue invoice are ignored. + if due_invoice_count < 2: + continue + + # We've now decided to downgrade this customer and void all invoices, and the below will execute this. + + downgrade_now_without_creating_additional_invoices(realm) + void_all_open_invoices(realm) + context: Dict[str, str] = { + "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}", + "realm": realm, + } + send_email_to_billing_admins_and_realm_owners( + "zerver/emails/realm_auto_downgraded", + realm, + from_name=FromAddress.security_email_from_name(language=realm.default_language), + from_address=FromAddress.tokenized_no_reply_address(), + language=realm.default_language, + context=context, + ) + + def update_billing_method_of_current_plan( realm: Realm, charge_automatically: bool, *, acting_user: Optional[UserProfile] ) -> None: diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.1.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..3776562c5a000191e5020e1a7747364af1de39b3 GIT binary patch literal 1221 zcmd5+%T60X5WM>cZ4_ema*ddx68}6U7p>02Va`X z!^jTikAanF%B>tBoUhO4mw*1t2Kx);ZM~1hK%;UL_rL%Ay1Ki4xVa`b?QrUL9uSCY$kzoq5O$CT`V8li>pN*X9}JpN^F++yETaC3TX$SlgiT)je0wlWIq z!%+$ZAM_ZEiM0@#e3C|t4dHGRngiEkwQE;(Q4nJJcG7{>sAY;M=FL>g|3u=EqDg3y xdjWf}J#WaBm2lz(M{eX$)K5%z)N&*mUFYX@U=m?s2LU@&a`;CmEnU1XJ^&H~W`+O& literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.2.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..770b0eb0a3e54b8ff747646758556379e926f138 GIT binary patch literal 1221 zcmd5+%Wm5+5WM><44oRFiLLe!pr-~%4+Zi7a!gPVG`Whm;6vfVa2n*_vs_BD>>o&d zvWLUv%+!=xl|ulh zUgwuJbTXSHqR)Y!nZ{}`=w33oMEp*g2s}hXQ~~>h-Vxr=TgHmZb$i)tBoUhO4mw*1t2Kx);ZM~1hK%;ULclQssS2w>Oe_fNCZ^NnEc~l_! z03GfNlUb+75mG|;T;h78^e9&vn7kDog31d3im#>yw3kzxMi;6K4;&k382m?kF z-`T6d9ayi+mJUJDb*W{NhqtTUYOMO8i4}~g3_VJ-nZ`S5%ji)?<6xfz4hcjkJL1)~ zEXHsCf+{;3ypmkT`zXlPt6jUQi-Hi#x04R6MlDlBF>j_?{wETT6iq^t x+zZ%)?Ri74tb`LUIC3M0qJCn!qn0Dl=sG{I1Ct07I|$gJlEXhjY3bs9@c}8BW{&^> literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.3.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..9b906b42f9d19b3d3a072b79c6669417d7d206bc GIT binary patch literal 1221 zcmd5+%Z?K<5WMFrqB()Yl7vMFaRLbrNU%s8LCEUZJ*1HzgC9|r)&4u}_RM6GAAp>a za=BeqUCP61z3xEkP<&Q33Dd@v&Qwn>2a<+FbAi4LTi>O^3fE<0QZhvD~I_Is){KR=JX+Cj3dSq zAH}PsBf4Pfmd-)(buf_O{C2b3j5QxTv4JVqp%-a(^LP)gj-F(69#2`(m_Wk1BU#-} z#Dx7X)OBa0SC%XIaNy-E$JFeQ8)1|U8Z6d0zW)$zI?yn1a7%h?$Rf(oT)V@! z@gfC=&w4DSELs@NA%j&@L!{e`mMHbu9Cw?xD2%CoyXwGZ(lSSs%6_ipe wN#P0IARF>^C6ai>Ng6p8^&h4?X}J(hz7NYf2#GOu#7I4982=G!OIMGpziXRjqyPW_ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.4.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..2947e819fea4a834da356285a9602a4e9fec3a23 GIT binary patch literal 1221 zcmd5+%Z?K<5WMFrqB()Yl0?7?aRLbrNU%s8LCA9K9@5B#1csDwbQ{s_h!9)Cb+c;a^~NmI496G#U#)KmMOdK^bw*dDM2vz z%gYveT^y+#OOj{iu@)SL-vt6Ozt=8<46%?+!htXb#yft?d9{1>`u4@``pI1k@U&|@ z%;I4A7>F}_TfFdc{!ejV?+aL zNYLY{GKF=0oPh|#&l=YQr^mVSK#E@V2+l5381OVCv3BuPj&a;lRsTj;YxJH^M3xjdoDu`2It<`GCW~!7b^nA&V$SbL|Q{Iq0l> zOcyCIeAeSIlW1X-B59|lhDf&=ElKLJIqo)XQIK-|cGZE+q-BmMm;GGJ|3u=2qFE^E xlY$fYK{gcXN+j`$qcn0X>OV|((sCghLm!uQ5E5hR2nl;Mdi+PIEnPjX{sOJaX1M?W literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.5.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.5.json new file mode 100644 index 0000000000000000000000000000000000000000..3987d8d629c53b0813e345a593eaad683e8a4925 GIT binary patch literal 1221 zcmd5+%T60X5WM>BGKiTJHF5qOA(r~-Bgy(7G#w~Q5+-_}>_%lX-(ckrdD zJdEsM`WRS=rrgRA!uj%idimyGHrQP#Z|i+51{#&4c>MY6$L;rr-}iUqX5F883(O+HB@hK6vr3C)4)vDmE^bx{yv`F7HQ#h_)3DCW&r%TFTlNYN-X y$-RI**p4^k%1Su#f+IJwFX{)TJ7_r)jjr|6IxvYav4emuD%t-dl$OrkXa51Z)n>l{ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.6.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Customer.create.6.json new file mode 100644 index 0000000000000000000000000000000000000000..ccdd4d1df0ecfc723a645bf9acecf317a0b3a5bf GIT binary patch literal 1221 zcmd5+%Z?K<5WMFrqB()Yl0<^liW5k1K!Qc$2tt-)_mD<@40b!pvf6*A-JY3D@&k}l zQZBcvs!MrXt=FA4CYC^IubmDCx;N|X3&E{TkTd@d#W{hlDJFp?uuR!)r;iX#NeO~^ zSYEc!>*7e|Sdu(5kG0@1{4NlP`Mq`-WQc`q5)OnhFy8T7&a2(s?dw-}>u2{d!1J#0 zFpGobW8gKqdaGneH`|-#<+p!|!{McgzTL-apfe?_`!8QVzx(v@+lTk;=5{;{$A|{h zkf6s?WeV&1I0F%epEa&WPLFftffT*!5u9D7$VXEYgMRSrT6v&DtSY8RnA68dkVcFr zK8aU{NAS_sEuDkn>)LA4=eL{PW~}+(i4DwDhhC)F&Etdib@U{o3veQZV*(lKj%0N^ z5fk^nz|@_MURkc-!-1Ex98=l;H#hm+Ncu%^*Lc^d!i9r6D}Xj!DOp-QYE4?VhOyb$As^4iyG{PsfkBh3vJKyApnkuDqvMI*D3> ziOKX*JDC_LI7uFeWAY@ELX?zr~h7 zPZa8ydO5V|r+=fQg(Sk=Sq@v?u^>G)?5^hkjarCmvfOYw*w{}85;SE&JEp4`MW_UFbQ;2;DN7o4cy+gknvw1;#S%QrGNwnor;{ z4NCZk`lJ9m;|Ns_7Zxb93RCS2`bO8AA3px`{PmN|m-}n;^TpeX_h$d+_QmDY-tkI+ zGg9lDVCW4;f}SP-q(>?R5+$0O8C_NvhdM?#r`{N=+mNb^pLFy2{PNAO=K9&yi|+pK z>0QeaMN@XIR=nWW(cEGOo_oub>$L6Zm%x-4elb3OXRww8n3;M3MVoXFmM%jNF3kWA zIT*=l%Nx&1aP2Bijjt-ZOgx z+elDl8-{^9U14JO#cZ#EL)g6A@w!v%a0J(g_XrE9Y-JpT`KZzn^-!y3);tUEBEZ^X lYeWX%5{ZyRpfJGlq^`C3%C|ch8V)LeP=h`gGI8>F@*nm8`BeY_ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.2.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..6005a53c14d42a57789c3b015906001d5c537ebe GIT binary patch literal 3524 zcma)8$!;7s5WVXw1fHX4NhByaC5oNJh%GomV#h{6aJo4fN^G)CvL})S{d-<<>1}!f z&tcGDk+r^hRZjfFA6~vVI-TAK z;(@E$ocbU_SE9tl=csz6 zv`$_#FHOyr*;)lFVYS6NigoOKoy~MU*oLo_)YBqWQ6L=|!8ac&41M2@A4`Yq)lT{nevGcX=TtgP zS~@S?(a90q(+xj8zr6V7;)l zF7F%_vzNwRWy(zP5kVLK&QhK;@OfLW$FH^U0oaSKq&}9;S;Aaz0R0m)mAemd3Wz_( zUOdb_8T(R1R)MT%6goz0F5y0l>Pg<&2SVc^TG`aXr3NV%b0v7PG9-T{Cu)ETJ-N)p zJMVJ()c>b1Ybi-o;5Hax>dT&KwUv#nm;3^JMb79`WUOq-uI|7zWP{Srqe1G5EN-P! zk$zH9@+rVpvL8!7JV^ZLNa4^=y}GgC7C^7GhKVpMfMqs!jKp^nkbsW+{&b(gA4pZwL`$@}W6x_E7W z{`L0d^sbZCMN@X|tpp{V=NV!?*vw{;Ndz93ze63pv>cs)?I*2pb@wl`s9kf(=5aMUN7k zqGpNf+41)DIJ@Zav6rik6Ia8F*wWyRUE>7`W3FCT3sF_8J^w^RCD1PG5&TeNFuy+X9vC8Hd_@RO#6Ds8uxs&%(Rd oVD-K=A~)a?iLgYV7+`r)_qF*~t~MAN9u=Tg0}h5v96TTV2fC^FsQ>@~ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.3.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..6ce4cb688d99d49b2091129c17aca7fe58c3da5e GIT binary patch literal 3524 zcma)8+in{-5PkPo7<{g4tG*!UQ)?tB8Z;?TH;Ch)AgEo+qRk~YBv*}N_}_bmm%V9K zL=VD(L(cu2ndhV9Vfwqy-aqyV7aJw!c?RqxNbOapuOUV0fJ`)j;u%{)_SS}r#^_#l_+uXC8}O4 zt&`WxOH*@Yc2>d4Sje3e-WnKSjWC_iNm^}%yPw2VS*>Liqitl^T8d&pZ3TTH`WI76 z=}oz;I_IRRVjVkQXEU7-w&5Ek^|VM;6i7!#@Xdz`L*K>tv2@5@?W8Z^$LPv?PNmbN zrSsCAoSncu-SE}rtE=;?U%qu(wLez2nJLVD)z`Dv)^4qtW+nT{{Z_SYO7{LU>O%RI zT7o<=q+{yk(59dNiIN&g#NAmAYq=4f4!jjljRZ9MLi8rf4R1Ra`)N-?rW~|Wx{5LB zE2mnjEe?kl%17)iosD02k}tc9oPg<&9d+X|ploX45{Pgkq^-s62cK7Y|``;d~F8#&z`e|zK za3$c3w)U}W`J*I)o+bd&1C@frsOoV>m%WQa9iy95Z(3*TE>)R6`P+xH+uJt}7v~|U z&AaJcCkaJUcI~YMC7tKF#r8Z8P8h$juHj!gVS);a@r9?BYRSONJSY@x(mhnV3_V<$ z0US9P$$2d+)nJN3S-i-W8x1rQ83*^~XdU^v8r)#MTQ9M6N&_QSKR^!qgkRp|pfj-N zFhf*dK<$|$^j?$I*Y4BbcOP#`gfB7DIKGgRt)QB?8A9055?c9!2NrA)IxTvX;1o4W zT+dE-tJCbF$HzggI!#;+FCwJD9s9(y-^k3!ufsqUX+?t*rIDEFnswSk@(ywWddYKT zh$cH2`vzUa*K#ZLhTF-!$-8A=a8~BHG6Ca!Y_&+d1RTsgF?Sw8MUrMew{dKfym*Y{ z&+brnmm7V05{vRB+GJUVQ8Z&sstju}lKT||Bvu0*26NjF7)dK%>*ROc1H2{H58kJ` zghG3*qxeTVL{d!)FUS=;P#L0eZs!?#GoGQta?ENcHmT|IA&>N+X!9AR_`r68&>P_% zv5j=5Y{MA1(-kIWU(NO!9AfjX$LmgChXc4Kyhm7|vOVJv%tw`us7I};S@SHsivX(+ ltq~c3OC-V)fntE=N!{1xU%B35Xn0hBpoTseGI8{B^dG8x`9S~x literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.4.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..8d293494322fe94b341c4427422e0c28c721685d GIT binary patch literal 3524 zcma)8TTdJ}6n^JdAU~G^O^|4x(uyc5Dj_P3N)lC;cgD*)czd3uph=EBN|EP@2~&7`t^_BKfk>=I-TAK z;*qP`ocbU_SE9tlm#BKB zv`$_#FHOyr*;)lFVYS6NigoOKoy~MU*oLo_)YBqWQ6L=|!8ac&41H(g$I>BtwUfStAEPVpIh9V6 zmd;CebaDjubi-f0KKuFX)thgfR_%|KZDtBHuKIfR+S-jZ)2w7ax! zF7F%_vzNwRWy(zP5kVLKUZ*@~;PbX#k6&xy1F#oeNqsDxvxK?c0Qx6pDt8~^6cB%j zy?B^=GWMm2tO8ljD0Gb0T*7@8)swul&xFQ9w6dv%OAS&k=1TBnWk~)^PSgMwdUBbG zci!dnssB%3)>4wFz-=(X)b~BpYAYLCFZl)dik#8s$XMBuU3~@9kPS*dj|QnLvbdE_ zMfyoa$)^BY$$l*T@F4M{BZWgh_3FlkTL2ZeO7)99N2FO8I3Eg7;dUV?7|XQFeYKHk zKEY$&85JTLlLA7<9;zHJEKp`IOtovtH-36~_UZ4p7eC#eZ|~aQ-oLy2@UYeA+E4B6 zuLPXY*4}q5zn4VN(*!`er&5p@Ro&0%vUhQ)V{~)sP3vskr7F`Wzx;Y~d+k>8=KSM3 zb3483Bz4i0U3)7*N#}Wn*pBDH3FFt+HTf#=-k-z zHbYbop!UoWdIx0HhpWG?uhthO!j~9n9AC)EMo>-MtV7rU39WqT2NrA)IxTvX;1o4W zT+fcTr^nevkB_}vb)2{wUc{CLckB|+E|8g#kHbI}X+?t*1xU7UfH{$+8TiXvUgU8P;MX_bUiUtVVYj%xypPNLu+?C%@|++FN4%;C-q~ zD74o)ihs02B-OO=f?Tl!l_47EcAlX(;~6?E$EBUU&LB?CERj_t+MwY|l8<=A%l-u1Bq^8F&`n#RjYQ ltr58amq>&q0>uE!le(|XzjC#~(D0}LwHk0RWa8lE;6M3}`HKJm literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.5.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.5.json new file mode 100644 index 0000000000000000000000000000000000000000..59e25e602faf3365c5a4694a289096721234c773 GIT binary patch literal 3524 zcma)8TW=IM6n^JdM1F2cQixRgR4NcHqEs}hN`orPJLAnd@z{eeOG1?Y-tYJ_x1Fun zhtu(MdXg|Hq%dzkK)G)$3QM=kpsu z+;P=NepPf0`31yVIoq1+5Yy850|=`?AH zUb@q>Q@E!){_Oe1i;HJJe~nu8-#4~fC@g(7)N|0*@2r_;rTEGHPW63G_Te+?LU~6m zL7o`WG52z8(@*~FX%@y1VE(WQ1>oF3mlt-YO%s`YKy4C?(^$wILOKIN4c`cB-w* zqoZOD(u7r}%)CER)1|-CDW&mw-)^U`weSJGmt4s>md;t~xxoPNlQLD%hd2eqpAr^N z3MLaSMPe1m21cP{Ky$6{i>RIDon28ko&d_G6)pp$UM!X1*~*ywg`B7XE)3+d5FflN z=~MrozM`cjQGwfFgsE?Prqy0{wq5fJ?JG(~9};5~OLlVurXd@Yex3{xDvG$5P9^%u zMJcCfwwnD^`td>LM@I@rJPqomhI=$BZq@2ndyYu6GH^K*pu+u1P%u_#m-}WX^L&EG zJQ@`e>azmsj3ZPzURa^bL6~dTlW+X|?ZrQT|N82Ok3a6O`(NI_`SSYZi=BDCncF*F z2{@yzy^TG;l|<0<1VDPEQji$c+%D*HaB-|-a&zuY?`#`$mFbhey*c~1@AsENUp4QV z`CTUoMRRr?tOO;U=cUCCJP%G7zqPL8pQA8Ah1K}NeNVMyU}hc^iZ<&WDqVpdE-e6# z98BcAm5u5!MWHNRWb2Ivnn{d)KlqU98Vc>T zj^dx}5Lq=XydYQXKy`?wxm{-H<#dLQ%Q0)6*yg4yhdj}PqRnTL;v?G$LhppT#WoU6 z-G(u6=POLgzFO=xIK<}NPS>5G4o7fJcu%lEWe3I~m`^I5P>)(Qi{@E*mjKotTO$ep h*GPnA0>uC;lX|Gl7rEJCXn0hBpoTseGx6l{$$tV3`VRmA literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.6.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.6.json new file mode 100644 index 0000000000000000000000000000000000000000..ca1621a0c464ea71b032e4a9f6938b85b34c10d3 GIT binary patch literal 3524 zcma)8S#KLR5PtWsF!)^8R#68=pXwro(I8G?r$KX|AgCo}(dLp257mz0|K1s1duUZe z55j^&&i#Ee&qv3{gms;XI?(j*p2#o9k}Gf|aq5I>EfrV1V>YN2ap0+cI}IiK#P6i7rLki?B5q#e&)n^o8VKN-d^0 z=dz2=3EibSM$e0x=!5C$MhZ18QkMk6p%M6|Lxn-#tMOyuAbZ>iU&Bw))%O%jr%+4u z!kwI*fP1Q^tBY6XKVSU%Eh=e$>`XsXSo*5fvuU$iqo-LZep0`c*5+jIKf^8*kJu9E ziAEh$FNZe$@=ua*NFv;w<&cXFiz-mdo_Yb$XoYAd>kV(BOZ~JbK~oO2W4fv_;X5ZS z))o$j7t$x}EuD>DM?sg}g`R_q&^1$~xtr>Z)J(~5f>VYm8P1Ifsp#!wYdo}5ZCx52 z7PCncR*BTp{zy%i{z|8m#;2BV#;+Cg0eUaFl5s4YGuU&@0N^KOs-O?zWFY>Muy|ZB znQ+MxtAMOA3LXPAm-@bl_$2S-Gj`)KKv{FFTGpbbnvO$HGaMQnwWiGFfX z$|*Ek&3-EV@F4S}LxlsLn!2%J3yli5YW1rmeBsGrEeS9)4HAkr=^iXyfgW6%0UUBL zl2a}^*~1hCW$7YWZZx2o#5izoj@FT%tHBN8<9dmua~cS;`T=s-C;a-R1YLkVhZ#im z6{x*%gxYJezpnj7_etK?2wzg9X?#IWwhU|HW(XleODO3p9!M~Osl4c6f^*a)b3HlT ztxk)J9zG6o)oJEx@FGDPxMQDq_8XaK@i+`rmRB@zqBauIv0vvsr0gIk!v-=$Qyffv z16@SdV$0Nqn#sH=yJaglsdHTF0ONdYwMx7Q0s0=9yNsYJ$+MrDG`3M*J%;4Z?qGM< z8!bJVMd=c4k|KjqG-FMy46!hh+X@0C#sM7~=C&U&l2<JUwHyUft{;~6?E$Amk!$xT-dX`%;-o6ji42euOky=U$Lwvnjo zHVgxIy27OF%h_H7hp>6`@w(IMZ~)hc_XG>5Y-1dR`KZzf^-!yB);tUE62SPOHKG7; hi9|>yP#9okQn%XtE7n^W8XhWuP=h`gGI8{B^dEeK`6~bb literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.7.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.create.7.json new file mode 100644 index 0000000000000000000000000000000000000000..a3242d50dfd88ee0678c11e8432740698d85d9cf GIT binary patch literal 3524 zcma)8S#KLR5PtWsF!)^8R!}EtpV}fp(ZqF&20`5f1wkz-ixHQkd8j_%|K1s1duUZe z55j^&&iTzZ^K^20N?6yKr~^&^?t%PpNe;l}tg52_&;KL|ha|$?Sq{0_u&4sH?4cLX8m$n`WWC`{bO}!f5;WyNJEp4|6TWlO zVr}7Ycp-i2y`{79>nP~5yMQ^!2wgK(z}-}Dq-IJ!3QifOWH>h_q@uTzt?>Y-3SAl< z7PCoHuM(-J_(-5jf3I?$3-GDsoAGPKd;shvS8^W<=L}-58G!ytnJV1}aWW8pPQ7?q zdNTDTORNI2#wd6Ut+|BzBI1+0lTV1oQ)p$)nad4QFP2KsY-LFPOpeq5E;MqPi4WeD z^vVBEU(r&ND1qC+2$5g+Ov%0I4PWvL;LA!zpAusgOJ@BAm4yiI9~~+j`l+cK8@2#cxK*oP?KvdP%E0B20Tu2Sf&ycOcB${SBF`uA zm_{u_LSt5d$T&ik!-WOPY{FbSi@wp-yUV}-`s2-uo7ekW`}6fL{NttJ@5T31d&esQ z&M0Gc(bAnjf}SP-q(>?R5-q#kj4qptLmiWwQ*W#@JmxCnCw=$j;^x)+tE;zecdzcI zcb&j4nzC!N5~OgR7Kj~q9vsttV_Z+~BGW;J#rVR5#aa?zW*Q_EZGs*wU1>eIG;46k z!AMTI=wuI56qKclWEp5cGl_BF-n?5!ey#>Lh>zra_?;&LeIRknrb7hF8 zIGAt)D57hzWokpsWZsnBvK5@vIWBd8aXz+MC0>L8xJTwLBdAL9?58G;ZIoA!A^EdA zi0*o$r6;o}U7}4=WH5?mtcjH&7DjSgL4d?Kbccqy?S~%8E1z}ZtL{O23s^sBOLYwe z_gaVXk9LTx8W&z5SJ;8-5KVKt%+L?x89FS-ggds$peu(o(SyXzXO!Y2+X;l;Gq;0n zB1*uw)D}?L#yHsKqe`c)hgx+r@GQJb4aSeH l5v2i_NQ7hpg#lJ3b*s(4V!ef-;h_T9YJh_w6DQ9n{{aEp`Q88k literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.1.json new file mode 100644 index 0000000000000000000000000000000000000000..c202e76b61e08ad810b3cf36a549250823f45cc1 GIT binary patch literal 3735 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 RNb!Jy8WF`XWMc1m??3&|G~oaM literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.4.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.4.json new file mode 100644 index 0000000000000000000000000000000000000000..881f2cd722903a7e75b56f84f69f3f75eb6f5ac1 GIT binary patch literal 3735 zcmbtX+in{-5Pi>A7<{fHtG0nOPrW#B6V!1KC#ajCAgEo+V$3DiBv-Lx_}_bmm%Zo` z1gIVafgH~5%$ylLK07#IqN;3X0*~MBfxSFnAFtR?<0~T?N#}1b-(URp*XvKO&JO2O zi6HK|sy&2Er*(SVM74ECPv=MYjE`HPYMf8boNR$9h`^B*X~aTLRp8VI5xNp31fQbn zh0;2C&Ac=G7}MI<4C0m2EZ@=DOrk;7UpMW+LA#X}>}8a)ub$s*xx=VCo=-H<5P=mdXgNzR8*k%Ev~GOKU^Kz4v2f zJu0eHFYjE$a}`4CU(RN^sGs|Q5F~*)Q!{W!nAXhE(VRs6)^V)R_;vDiZvMDV$V3ug z%4{kr(kEFgbrf&wB_d(PC*U*jCrSMuuV!Q+R)!XcA*V2rk95vbqx1(7xu4lT-sKm1PUMU}Mw-o*?BWaPk!(={X+Yx zz5Pf)owoM2Yx%9DsWMFfZucOC8>6b*4UX^qA8<@5oIuh#TXzX%`pMsXIl8`d3wib8 zmgm6<@X!*L4C90c1<)q05_Gxs zAk^y5%u<_HxxMaW%Y%Ee-D2CZ6p=W=7r*TYyM=C7dXQ#7x&L((;rS zr$f+7o>@aQIfrSkV2fSImC#FWX_ob>@mv0d~F z71*st+Ze1MerTSwwzN+EkR1A6VwvJS1|<~Q(jCRWwGq+!4%Wp+SEg$m=y|wajfd;7 z>NDF)lk!XNu_Q;@P_)L4GW^|b3-LF?-D11yOj+p|>9kIdo2a(#=;_HZKI7w7s2b;!GbbCNj50UV2W_iWC4#bsuOulOOoSkt zHoHD(OQ(|buY~q8+3kYmqTUEok&fWH;k<$NiX#RH9uYXQB8^z;sS2F>AVOE7#Kotm zda1NdUNbLE&6U|$1uJ7AcT#w3V1PBkbV4U-wHEGX5>sWhmQ{?lkzs8qiXCbz=nK)m zm|9A2%4O9#CruS`?0lWgbUxUIuawkNkg6zkmG_*W z)1;;I(j6Ti!9CsZ)8}U|&Q5>*)@ju~t86nQfF&;LeAjU?jkEQhsRiB1RJiibv`HF_X=ljVlDos0FfB_UG|+9_Sd znDmuXEn$np;f35D6=PzQTl|T=_9@+1PrQU+6iJGx`{5He0gGThJrfp!D--aFo$j zIu$uHfs!AgLrV5zWepD!sk>=|^Z<n@>8Kl%CX@zti?Tnc?% zy{o34J4szM9SOaaprrFWm)Mr)!3pD6);0V~CrnUbXMEwIB`g`n2@eXOOmv0d~F71*st z+Ze1MerTSwwzN+E&^`3M#4^Qu3`!`pr8|m$Ya^od9juFuu1wcB(DQJ;91quF)n~Sq zCgqpjV@Zy*p=ga8W%#?>7UFM&yTf+XnX=$x9H+&s_#Giod0QJD;?G1~mN+`@!PwLF z#0Ifx0B_GnL;@qYBPRf`DwS5TmYQw)F1BC2Z^_8*xr<_0qF0Q!JjZ)0U*&R*k>UXZ OH6n^(zM>vH9sCCeB{h=( literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.6.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.6.json new file mode 100644 index 0000000000000000000000000000000000000000..8eb21abd54a22d4cfab8c9cd7200383970539a37 GIT binary patch literal 3735 zcmbtX+in{-5PkPo7<{fHtEgQgPj!*PZ4f80(@UJ7AgEo+qRk~YBv-Lx_}_bmm%Zo` z1gIVafkV#i%$ylL9ULAqQB}4xfyZz6$X*_@zuvPS#v&scN#}3hU0zQfM!e-7Z)z>a{Qx=?JbH&Kqd2IAVa{34tRk(ujqgs=%oaB6KB6TzrnI z7fS2oHS^NcT$#01urd~MCxy2L23R9ZCv=ilE8%V@F;!M;S;c4@8CI5}*rB$9z7YM3 zsipL$TvnZP(o_-0&ez#Y=YwtdQb|1psfq&W$OwM(0b%Gn9Y2;1*{hxOCHxp&dCv(t zOrV3B?xN<95xx*Q7k8?Jr7}XRud?QX^05))(%KMl@BLU= zkBTbQ%R3kGT!qm3m$O+e>K8sB1W91d)C}AarZsbXJg2LE>o`_u{5tg~H-A4QWFiSL zWj2+R+qnd@ao*NTM8b+sz-Qu5>bw8r)r>5}%FqHaZxWZqZ#(Qqa$uTSrV^#zsCA;P3U0pkOS|3ht|w zOhXkO^UkOc%PuLvy?y*XT-aMWc%86|zVZ3%lbe4oU;X&uY<=7Q`u5`D>ioiY;%;hh zKN3)2k$CSbeB(1Y`mr$mk{Po@OhwJkGT+)3)9=}73U1SOs4xx}_S4^9}rw65WwJ7IzfJL3zFEn&$pPIyoNZQ?3Hms<~) z`aP%jU?k_YtW<;14`uNp+g)g&naDV}x7n>DKj(uR%y)}jqa``PXcX-shwUj<-sGTj zV{f(@qWX+^lsQ6g71^Jc{;axD*CoQ|7-^ijsEd^#Ox#9?u%RTh@}(bGutDgwol=5R z)GSY@**cCJTDLsr&R(vXC9Z}Sv8BNs+r+b7$jr!xVG9sxuY?n&keKP3MOvQn;&cRh z$unz+Cg(8L4XTJQHd&r{ z83G!DC_T@nB1xMJw{Zy}d9e>vHttZ_mm7WF620ZdPK#ObJ3^rHwl+G%pNYCGadg~+v8U^a z4Pw&(-ky($1V(U2P5@w4Dy?EIHQV%EY`=Qnl9AhU7saqduNZH6j`vo+$i)gH#RCRv NL=?l2iG$~Z{{YnpH01yQ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.7.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.finalize_invoice.7.json new file mode 100644 index 0000000000000000000000000000000000000000..585210a4d9f99a404cddd1269d3638b523173bd8 GIT binary patch literal 3735 zcmbtXS#KLR5Pr|EF!)?YR!|?!Q;!5i6W47T1a%V>1hq?9jJV{QgtFqT0Hnr}Gnh#>cHtHO?nzPBuarWp1Vq+E%Md1Z53hN>VnM2thV& zc74#6P9^DI3hiaG+Xc%-y%DA&9l>?Oc?0bgM+~@lMBvDZG-9ErDsbw92wjO1f=^NP zLTR16W?q__E3>f*R>qgyN#U)50oDl937u}Mm2fwcm@2EatYWl{3@b}f>`_}mUx@z2 z)KYp=F00NtX{v~0=j&{y^T9TJsidBQRK*MF$OwM(0b%Gn8$Xr~*{hxOCHxp&dCv(t zOGugRHrrd~C(Iv^GTCdp}mz zqoPXn^3Fv(S0S|igcv~+K2`fGUpNT(7>i>8(BMY%Iv_K3wg^7HmbCw#VPZ*j$!kZ}r;0RoQ ziUe|$i6oMVh*VfGAob~kmfLOCd{s}1g?*$JIs*TgTDa8JMKG5SPgaJC*}5k_g9|+) zZe8!Z%h0H=LzA`S>fj253?oc^+cT{;va$6(ztD3cXY?`BY_?<c-bvJF09-y(|7TpCU1^ukKb;R^#Y~(`${@&~f3dZuR;J#YP zG*sa+?~Dqu?2-b=9pd*v;9%+Cb;2(C#^-NN|M~mRS1+zFHaG3hSHIRDPHp{G{xG$7 z7zwD;)~>sjuO&^DX##M204dxURjs!;zW0B?F{N+l%LB2@_P<8(+9@2}_1?!h-^66IThk+jqWC7jh-^l3SW(y{fzr_kgpqd?*vJgFuwED&_gz(9FMxvoELbKunaD z{)v|%pdpCT^K2^0(k8=gTteu+*dM*lGF0|OqR(5RSH6!=S-!)J+M?6s8@7vnp#rUXZ OH6n^($i%a!Xa50ZD>c6W literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.1.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..e39960ab7254f5b071e40ef45d6574c984929f3a GIT binary patch literal 83 zcmb>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/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.2.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..72bbc0f46e39fd9f39a4944ee2f22ce7d35eacc4 GIT binary patch literal 4359 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 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.4.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.list.4.json new file mode 100644 index 0000000000000000000000000000000000000000..fca90201bff0cc3962fe04794e952b6328873e0e GIT binary patch literal 8595 zcmeHMTTdJ}6n^JdAU~HZ6p%=snl^|Qg^+|sA&Dx>JL6>y9?xKV7D$x;-gA5z&vl7Z zwN0eW!)miWKKIM#!~Y8{)D5v1(Sv+U20oWmamw~kl*mI0C5=wBjGeU5M0O= zo68|TI5C#lI=Umc^lKw;E~NCP5yMqZg<6#YME41(=FF4|gpz`(n#n|wEt$R=7@w6= z^31uW;C5M}sS_eH;BB4mE-o0G6ojnSnybtuMQ!qy*xJ~#pbNp}Fxr_r+D>9L zT&rcNhMo($Q?nFR<%2VR9aEj(V~(L_3V97RPxbKWujUl$5ao{eJLd~lOG_*EP;jWp zCU_kcB9$p??GknL6h&*0gl=sM=O)vl!rXv^?oDA_6PWtF&#C5g+7!rV@E)Br8LBB| z)j}vH#b=&VhQ1r|!=fy$%b$*Z%hEum#www;b~&-MwldFVvrs%wj0N*JCd5q4pt;#@ z{_XDWA*mVs>@wed9P<6k_a9O;U1uUf^%=z*t&|Q*&?;YmWhCkb@$Bg3>-?W+H$w?c zMo9+t1h@K*$h9u906PwZX5-SK)w((}0p~)R4E}$Fx!xV)6h(%^sRj{_^G>pnk65bP z&=Xb8bm}INIHVu-61&;fqrrX)Ds?(^k7V?I2&H&Dl!03esDNpL5f$HlRf-ia%6v*s zsBIR!|D_oTL25a_hW3r}Fl5vT?|HuBTDZQRf*CGD<;7^Z4DDX@LrHfFFH9!Sps!2? z6$}j9nG_^d8mCoz6ovx^vu8D7I4BV29yQsLdsG3|&{_%Gq_k%QwCm}4S|ol z)0gg@9)5WL`-`71k5*UJudhz-j-NkWs)PAZ_WB5daWbcQoRC8Y0mYgq!>JP4 z&B%;u)J}QeT|Ee8rOUkbnSx*G>GkgAs#;wz`7wK$4K30fD|R^68i`hLZD>f_QH!i* z$}CD<(A%0RE9|80_MyTYATS!K6%gow`WW(1?m*Z9hK)uo8k+M=6aasKsUs%q6Bk;b z>xMu-)+t*0?{!rprk+oYNv{~e@w%o_jZfcrH#jCVm$C8&t8ZbxhJGanNn2!ZuCCvl z+_ydOwed!SH6X{94CARss*$AhlESoWkJ!>O>7N7`nB*N2rtWOz#wfy71M_sUpU(Wu z89?8u(i$%qqWFD6Ma%*Xf}U@tIH-VA}A8M{kB(3#EpGC^cyk?|N)UO9#b}rY#lX)}zB>ic|w{`e8t) zIzI{LFnN28KL(amfUO$X2|YK5!c+jlOe)U=;400(nHD+@2e?U_=(Nqm_mM^)jp9Un zJi&Yb92{|kgERp~T=Zc?dTfF3pl}-7kPkdVT;mc5e0vdj6pvybXZi-PXRYGX3IK?Q zuWfr=*u*5Rg=2W&xovs!# PakEh!+nAEAuUmfuZfp4a literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.1.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.1.json new file mode 100644 index 0000000000000000000000000000000000000000..267220fde63792f53d198bb973536fd88fb087c0 GIT binary patch literal 3722 zcmbtX+fE!u5Pjz>&^!k(8-qmr6en0#P%u`A6>KYN_0IINZF;(g?w&Om@$Wg+m$~h% zWhp!$G*w->+T?QaqS0y*C^QLBNDbyIHG6;kvC-|H75X0W-@UgHcUuA{M>4)s{dq&tP zveG(XkB*NJo~qf)lha>LU;g^7RkC?entEa|HC6{`XRX;7J+4ZMlevv-npo`JXViw` zfqH@>(PU#BW#6Zt{|%A~MTG98hLu?JR(aO&$68=EIv_fiS;LvuhKAaakSh!8RIcny zxYEjou*Jvzg>+%~=DrPIw}Q=wi=IP8*ov#v-LVtaQgbE0iHdQm$0WzKF+Skl#j!CC z9hJM6wKm|n@xF1d7K_x>FPujTg1}tJ1<1pXd*=9fNvdw=bLi0U>*VWF|9%sZ2`o@Z zJvEZGPm)@kDArVKWK!gqfX^tNsP%uMT9Aj(8Hyu^RKmzU!dgRv(iIFtAK*=t0dP3P zp96s$CL#%B!UGp(49I-^!N~0nYZ6tXW+^_=2pxidbj59)>a3XBhea=a$4unJX9%HV zzKM9}U4lk_ADUz(bqAARWH>40k3CazD{52C#f6#UsiIGTW|Jqg`T}|+A5{M6a8S`k zSQ$7of|4F#LUQ#(XY~&vsoU{@bN~$=A`jU}efYa@N+;P35BQgD`b1#{&_ z#I1^mS*xWFZ5ItdZy&$+3VT}zuOoILHnzMt{rJzD^Pg|dwzti1@7`X#zu&4e<;MQ@ zBLQ>Dn7g)NcY?OcxB!suK?)Kr%ex83ck%Z)#uAPoX{@Q*h%)`jF1{S!T-lYlKKt-i zN9yPdSwT}YUJ0GhOJN;LEw&T6w_Llmu{FDIx%Se}=I0+9!m_|RVO|2XQK$r68a*g= zhfWv4K+Y;r${MR5#)4uo-)Nwjz&OyGPU|4fWgkZ2+SS}@i9r;!iuRDh?v~1LQqpO# z!(0y>BCC7sqr?$vr^(9qmw#VfuFrFZ&mq&WbI}(YPMFxqgcN;CDCzP%6yA8Q;(1C1 zjzJV@J1w?h-O#z^uy^)y)gp2=q6i}m?${-s-9|37c<8SH9uG?dksGPdZM}-eQ#zcE zKrd-$_1UBv#=U|wb|p4kt(l=+)|pBdvA1nJSo7^6wZ{oE){Iw)6W*ikiMZ1S%5UPm z!b~_dNS?i*_7fdy?yS*OCz2?eW6dJTu!$z5Nx&2hZiwz|0*6%?4vj;@^&=9Glg5f4 z(!+EMoJ_2%bWVkCaZCB{+&y%pgIjU8e^r#D(Ipjd^aS8`=kaVmK zD*WA>3i;RE-rtW*_#X^{NqsNgNrGL#Pl3Jcs}?KI_)j$ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.2.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--Invoice.pay.2.json new file mode 100644 index 0000000000000000000000000000000000000000..b44f8aec577d76fd7b9d377e5ff5ecb15008287c GIT binary patch literal 3722 zcmbtX+iny`5Pjz>ta*-EJH}GrDMTO=M2LtLVT__y?@X_^?Vj$TFN=-kf6uAD%xz~8 zQv6_9Q(f0nb*lXEHesz30 zec|~Xla0e89h6GSZ8X~iQawGv&-io4WrOdNZ&tKiYN@ZM3SFmU&Al|5Z3Gz`TzD_q zE{8r?OJ4=q-*Dwb@*BKiyxDPGlZ~mMh54Ex2}mB0Im#k5U#qDNe084tP}0Q1$7p*k zl}ce(PUwb7y)zPS#zJNVcSgfO)p8xUO454E?e!$7+9)OJm~Jcl)=(M?W^35$jj}_p~5fNl^iE?TiJD~*!ybKhT@KT zf+EpmV;beqr=S0gk_JV@+gS}8vEe~^*75sRU^aRndY9#eGr`7&+LMqg3+q&_;!L>O z%8szb$3a87IDAXr#@a!!<#5q+s0dqgmAgB2!bWPY5{Iq9IPEP5m+xr|lH2yjlXO|y8C1fHCR8r54 zl-Ichvvbxp8)Qyl2>?5o-G$?(+F!T}LOc?-&L;N`s z$WbPeNG3dTVZngRryq>m{;+0IGijFUibm)N{G%Ig(^Qv-xqVpjGIY#LPJD(Cdghyn z_w+I}^83(aD+M44MuwA0{+ODQJJFhEDK5+$&lP=$G@Ct{^$qBed{FsWbU3PLC#;N| znLx=(n2=Ka*ja-@B6T|*kRG71;||jWBL)3zxwFLdWo_h-9Q?goND9vKu3)a-inLV` zF$-Gy*mlVP-W}ri;laVyfp)?!#>P(Ho`3rI`>UTXf7xAkzrBC+_4Ui=Tm5W3^>-Kv zm{Z2whK}6|+A7lmKzaZvNVKePXB^+hKj4^3IDw?IrU?mU`jfrAIl0_*y9=(a>UZ@t zaw}+xrYoWM;-#>T%hR!XAy>pPOR*9<-MI32x$3F4wH*%%L-Eal)bXX#Y(nysK?K&M#`EWW0z2u!W zWRq(c`vzUa)?&-mh8fyry{UW=dmGZhT5b=iJx-A6%XF1E;XT@(h&ykfqLc0wX5y(q z^5O-xpS_{xE-(7(BobvytXX9lHqneU37D$G4bh)X;IIb6p>b%qVMNk#(pm9C?=am0 zCll){T~eW2+*1B~cMl!u;8xsiWrxPKomc3^c!drJJ?esHjyaG%PT@!nl8&`eg}-}K zA^(=!Tij4VmopwqH=SQ4?g)WgyvZQKKMqYf*hr&?h$W^AFhS vSsyLv#ZhYx9Ta&Km*}KQREkxWcXmm)`YP62tPck;Xz0)q^K^J-G03Jyture zwl}q=RS5N?_nh0ou`S1JSDgbYd zaye8_2qDUT%p5O_-j!PLIAs>nzBGX>B=uKPKa4KGNSE~X*LH*-@$+=@A7_>wU;qFB literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..e33c0caff002b01a3b8cde2a7959ab7369be8dce GIT binary patch literal 1007 zcmZuwO-~y!5WVv&BAGo*~4o0 zdFH+6H}>XeG7+lt#bxn+GCii}4MV!b2u`mF6eStagDLwCB7?dld3*Qm=EK)7kCzwJ z_O{g+m0@6Fwnk?l>4+htcij9~EQU$S${7`Va)U{;4K=e&@T6>lsyZwrLVx|P5E^Dd z+;>pr>g%L=LAUa#navezYp2B(bl&VMbdVMKR#doou4R6<0L_Vro&z&h)MQi9%<#MTzc}clYfhH8S@L*c^zuB-hs?WPJ1&PvwVF?XiuXRX+UGAEYGH% w2C*A>^ilO^e>+q$@-VH3E|uB%mJG&4R{tdR!{`ExbV+Y_ZAbV4KhH=10qId43IG5A literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.3.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.3.json new file mode 100644 index 0000000000000000000000000000000000000000..a12ccf83b8836466217325e77f3a7caff815578f GIT binary patch literal 1007 zcmZuwO-~y!5WVv&qCNLON~jRGMxd$)sMHH7Rb_eY2~6U(U4KMG5&yk2_HME%*~4o0 zdFH+6H}>vqGHGPzvQ5qPWO`1|Cx%oG0j$~**yO029*oI#5ExWBipL*6AHLpyf4;q; zwpp#wGQq&aY?V$!Q~`ZLXSw+~8}yTyq}4L?u9xpFe(Ef4PjadAl9hPvu4w#<_L& zVhwyyh7A$^AyL9Cs8s&1V zo)AJ5{g_8uDM%ZaE_C8=mNsGf&vn}R60Nf!5|Y#2496@!UJ}~VXhmpH*h#~)DW{=1 t3_Q3X`?J3vYBTaMEr%}Vc6>_)<4uxpg8E@}0Y1>MG@W;R!>t(_J((0g;J&_h<_TT$WmwU+tr2{b1n1`f>7H_`wRJ?n2N zCrSz&|J3BFHun=un~S?h>jL48JXpmk>^z0oOb&!fXEI3zy9kJ`Lu=7J%B255&x?Y5 z$l@?pS5%=RQ1nN=92+Kev=pnloG;IR{k*(*zglb_!>608<=5wpUD!0TAIps>toQ2@ zVR+jjkf_)+m=l z^^6dc9L7A+%2B!W;A1bJi*gzBf3EXOsL^@{G9x)1%y7)|?Ioc-O;)4s6eHNQmH+Zrb3q29$<*qcKxv}iumuH@p_XDB!?(@ zo_X)h8+&uKSP0eo;4+htcig-x7Q-TC<&27r++fmdLv@x3?vzbXRi}eQ=b!p(Cn^W7t8PDC^g%+QYm28ifce?vJ@ zQsDScO|EKlKfyFy+(udt2p`FVRh+`sQ;4IKj`JqGO1%sv8v1E<;ky~7iVv8-VgWT)Ai@S=cgYY+`DdKKa?9$Snt;* z$TjdqnFb>KRicDB)N7}z`B~*I=`n^DUx?n6aRdBQUOpb*3}N`8>`Gth$yEd%jdIyn z&j>New&s~uj>@GwAA9+yD3>w6aGjq*jn+Gm8Odog!!gU(kA!BLok#;3du4ey^)`sz t!lRF>-Tm!S#mvL9Ubi>&@h>WA3_nCX(sN>r$~AgZVcsKNzGRauU`&`tbo{0dwVjO2-W%GviLTcp3&!&Azfkwr{4$^B^lAclzj)0L0yu(`+0vmyZQ0-{fgSY z_BBRj7?_x?r!$ar#E{WDZhk5j!z5+pjEariVA5Hgi2>JNd>zIh;E72qI;1^|AmeZ1^JN0 zVXm&ILPwzJk9s+_OzP-Utm<-leg5m=_Ug;y#pWek-p%KW**q1s8rYBJMikcjbqR6} zd{Ksh2>+BQVGi}$scL>yxlg)}p~V-XH>KYI|CHCOv&Z|{jh!xLi}m^7Dguv2xg4rz zgqUPo^GGX4<mA69vm8^nI$ s(MQ$p{%)ya5|_5*^clde%?<$06;1oBme*a literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.7.json b/corporate/tests/stripe_fixtures/downgrade_small_realms_behind_on_payments_as_needed--InvoiceItem.create.7.json new file mode 100644 index 0000000000000000000000000000000000000000..a666db3a9a4b41e63429e8667d3f2db1c3b8b9a8 GIT binary patch literal 1007 zcmZuwO-~y!5WVv&BA4De^v~Z4pG|Q^&DH6_?U&_aIJx|8ejj>s0bd99UAYm3^?p%; zTmT=GVMBy}NR%*#df`+xKdRg$-D2qD3(=d>CcvNa{AvI8=wdcIi;EZgJh+O$TccdI z)iXj&vLEwED@WzhgO8njF3M%h|GCbup+@T+$c*H)H^VW@kC%k@G+L1cGun7ht4IdYfxI!gu(2+j|GH$sLgZ literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index ec0a5251d8..456869ba35 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1,12 +1,13 @@ import json import operator import os +import random import re import sys from datetime import datetime, timedelta, timezone from decimal import Decimal from functools import wraps -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, TypeVar, cast +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, TypeVar, cast from unittest.mock import Mock, patch import orjson @@ -19,6 +20,7 @@ from django.urls.resolvers import get_resolver from django.utils.timezone import now as timezone_now from corporate.lib.stripe import ( + DEFAULT_INVOICE_DAYS_UNTIL_DUE, MAX_INVOICED_LICENSES, MIN_INVOICED_LICENSES, BillingError, @@ -31,6 +33,7 @@ from corporate.lib.stripe import ( compute_plan_parameters, customer_has_credit_card_as_default_source, do_create_stripe_customer, + downgrade_small_realms_behind_on_payments_as_needed, get_discount_for_realm, get_latest_seat_count, get_price_per_license, @@ -273,6 +276,7 @@ MOCKED_STRIPE_FUNCTION_NAMES = [ "Invoice.finalize_invoice", "Invoice.list", "Invoice.pay", + "Invoice.refresh", "Invoice.upcoming", "Invoice.void_invoice", "InvoiceItem.create", @@ -2696,6 +2700,135 @@ class StripeTest(StripeTestCase): for invoice in invoices: self.assertEqual(invoice.status, "void") + @mock_stripe() + def test_downgrade_small_realms_behind_on_payments_as_needed(self, *mock: Mock) -> None: + def create_realm( + users_to_create: int, + create_stripe_customer: bool, + create_plan: bool, + ) -> Tuple[Realm, Optional[Customer], Optional[CustomerPlan]]: + realm_string_id = "realm_" + str(random.randrange(1, 1000000)) + realm = Realm.objects.create(string_id=realm_string_id) + users = [] + for i in range(users_to_create): + user = UserProfile.objects.create( + delivery_email=f"user-{i}-{realm_string_id}@zulip.com", + email=f"user-{i}-{realm_string_id}@zulip.com", + realm=realm, + ) + users.append(user) + + customer = None + if create_stripe_customer: + customer = do_create_stripe_customer(users[0]) + plan = None + if create_plan: + plan, _ = self.subscribe_realm_to_monthly_plan_on_manual_license_management( + realm, users_to_create, users_to_create + ) + return realm, customer, plan + + def create_invoices(customer: Customer, num_invoices: int) -> List[stripe.Invoice]: + invoices = [] + assert customer.stripe_customer_id is not None + for _ in range(num_invoices): + stripe.InvoiceItem.create( + amount=10000, + currency="usd", + customer=customer.stripe_customer_id, + description="Zulip standard", + discountable=False, + ) + invoice = stripe.Invoice.create( + auto_advance=True, + billing="send_invoice", + customer=customer.stripe_customer_id, + days_until_due=DEFAULT_INVOICE_DAYS_UNTIL_DUE, + statement_descriptor="Zulip Standard", + ) + stripe.Invoice.finalize_invoice(invoice) + invoices.append(invoice) + return invoices + + realm_1, _, _ = create_realm( + users_to_create=1, create_stripe_customer=True, create_plan=False + ) + + realm_2, _, plan_2 = create_realm( + users_to_create=1, create_stripe_customer=True, create_plan=True + ) + assert plan_2 + + realm_3, customer_3, plan_3 = create_realm( + users_to_create=1, create_stripe_customer=True, create_plan=True + ) + assert customer_3 and plan_3 + create_invoices(customer_3, num_invoices=1) + + realm_4, customer_4, plan_4 = create_realm( + users_to_create=3, create_stripe_customer=True, create_plan=True + ) + assert customer_4 and plan_4 + create_invoices(customer_4, num_invoices=2) + + realm_5, customer_5, plan_5 = create_realm( + users_to_create=1, create_stripe_customer=True, create_plan=True + ) + assert customer_5 and plan_5 + realm_5_invoices = create_invoices(customer_5, num_invoices=2) + for invoice in realm_5_invoices: + stripe.Invoice.pay(invoice, paid_out_of_band=True) + + realm_6, customer_6, plan_6 = create_realm( + users_to_create=20, create_stripe_customer=True, create_plan=True + ) + assert customer_6 and plan_6 + create_invoices(customer_6, num_invoices=2) + + with patch("corporate.lib.stripe.void_all_open_invoices") as void_all_open_invoices_mock: + downgrade_small_realms_behind_on_payments_as_needed() + + realm_1.refresh_from_db() + self.assertEqual(realm_1.plan_type, Realm.SELF_HOSTED) + + realm_2.refresh_from_db() + self.assertEqual(realm_2.plan_type, Realm.STANDARD) + plan_2.refresh_from_db() + self.assertEqual(plan_2.status, CustomerPlan.ACTIVE) + + realm_3.refresh_from_db() + self.assertEqual(realm_3.plan_type, Realm.STANDARD) + plan_3.refresh_from_db() + self.assertEqual(plan_3.status, CustomerPlan.ACTIVE) + + realm_4.refresh_from_db() + self.assertEqual(realm_4.plan_type, Realm.LIMITED) + plan_4.refresh_from_db() + self.assertEqual(plan_4.status, CustomerPlan.ENDED) + void_all_open_invoices_mock.assert_called_once_with(realm_4) + + realm_5.refresh_from_db() + self.assertEqual(realm_5.plan_type, Realm.STANDARD) + plan_5.refresh_from_db() + self.assertEqual(plan_5.status, CustomerPlan.ACTIVE) + + realm_6.refresh_from_db() + self.assertEqual(realm_6.plan_type, Realm.STANDARD) + plan_6.refresh_from_db() + self.assertEqual(plan_6.status, CustomerPlan.ACTIVE) + + from django.core.mail import outbox + + self.assert_length(outbox, 1) + self.assertIn( + f"Your organization, http://{realm_4.string_id}.testserver, has been downgraded", + outbox[0].body, + ) + self.assert_length(outbox[0].to, 1) + recipient = UserProfile.objects.get(email=outbox[0].to[0]) + self.assertEqual(recipient.realm, realm_4) + self.assertTrue(recipient.is_billing_admin) + def test_update_billing_method_of_current_plan(self) -> None: realm = get_realm("zulip") customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345") diff --git a/puppet/zulip/files/cron.d/downgrade-small-realms-behind-on-payments b/puppet/zulip/files/cron.d/downgrade-small-realms-behind-on-payments new file mode 100644 index 0000000000..e0b3d126f3 --- /dev/null +++ b/puppet/zulip/files/cron.d/downgrade-small-realms-behind-on-payments @@ -0,0 +1,5 @@ +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +USER=zulip + +0 17 * * * zulip /home/zulip/deployments/current/manage.py downgrade_small_realms_behind_on_payments diff --git a/puppet/zulip_ops/manifests/prod_app_frontend_once.pp b/puppet/zulip_ops/manifests/prod_app_frontend_once.pp index 54a2a123fb..79e0c63027 100644 --- a/puppet/zulip_ops/manifests/prod_app_frontend_once.pp +++ b/puppet/zulip_ops/manifests/prod_app_frontend_once.pp @@ -17,6 +17,14 @@ class zulip_ops::prod_app_frontend_once { source => 'puppet:///modules/zulip/cron.d/invoice-plans', } + file { '/etc/cron.d/downgrade-small-realms-behind-on-payments': + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + source => 'puppet:///modules/zulip/cron.d/downgrade-small-realms-behind-on-payments', + } + file { '/etc/cron.d/check_send_receive_time': ensure => file, owner => 'root', diff --git a/stubs/stripe/__init__.pyi b/stubs/stripe/__init__.pyi index 5ef2d1b4a0..9561f78562 100644 --- a/stubs/stripe/__init__.pyi +++ b/stubs/stripe/__init__.pyi @@ -81,7 +81,7 @@ class Invoice: ... @staticmethod - def pay(invoice: Invoice) -> Invoice: + def pay(invoice: Invoice, paid_out_of_band: bool=False) -> Invoice: ... @staticmethod @@ -91,6 +91,10 @@ class Invoice: def get(self, key: str) -> Any: ... + @staticmethod + def refresh(invoice: Invoice) -> Invoice: + ... + class Subscription: created: int status: str diff --git a/templates/zerver/emails/realm_auto_downgraded.source.html b/templates/zerver/emails/realm_auto_downgraded.source.html new file mode 100644 index 0000000000..34ca26b7e9 --- /dev/null +++ b/templates/zerver/emails/realm_auto_downgraded.source.html @@ -0,0 +1,25 @@ +{% extends "zerver/emails/compiled/email_base_default.html" %} + +{% block illustration %} + +{% endblock %} + +{% block content %} + {% trans organization_name_with_link=macros.link_tag(realm.uri, realm.string_id) %} + Your organization, {{ organization_name_with_link }}, has been downgraded to the Zulip Cloud + Free plan because of unpaid invoices. The unpaid invoices have been voided. + {% endtrans %} +
+
+ + {% trans upgrade_url=macros.link_tag(upgrade_url) %} + To continue on the Zulip Cloud Standard plan, please upgrade again by going to {{ upgrade_url }}. + {% endtrans %} + +
+
+ + {% trans support_email=macros.email_tag(support_email) %} + If you think this was a mistake or need more details, please reach out to us at {{ support_email }}. + {% endtrans %} +{% endblock %} diff --git a/templates/zerver/emails/realm_auto_downgraded.subject.txt b/templates/zerver/emails/realm_auto_downgraded.subject.txt new file mode 100644 index 0000000000..1aaae918c2 --- /dev/null +++ b/templates/zerver/emails/realm_auto_downgraded.subject.txt @@ -0,0 +1 @@ +{{ realm.string_id }}: Your organization has been downgraded to Zulip Cloud Free diff --git a/templates/zerver/emails/realm_auto_downgraded.txt b/templates/zerver/emails/realm_auto_downgraded.txt new file mode 100644 index 0000000000..2464239565 --- /dev/null +++ b/templates/zerver/emails/realm_auto_downgraded.txt @@ -0,0 +1,8 @@ +Your organization, {{ realm.uri }}, has been downgraded to the Zulip Cloud +Free plan because of unpaid invoices. The unpaid invoices have been voided. + +To continue on the Zulip Cloud Standard plan, please upgrade again by going +to {{ upgrade_url }}. + +If you think this was a mistake or need more details, please reach out +to us at support@zulip.com. diff --git a/version.py b/version.py index a4614dea0e..8bde12df69 100644 --- a/version.py +++ b/version.py @@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 75 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = "150.5" +PROVISION_VERSION = "150.6" diff --git a/zerver/lib/send_email.py b/zerver/lib/send_email.py index b4638f4c2f..5e7d020eb5 100644 --- a/zerver/lib/send_email.py +++ b/zerver/lib/send_email.py @@ -380,6 +380,24 @@ def send_email_to_admins( ) +def send_email_to_billing_admins_and_realm_owners( + template_prefix: str, + realm: Realm, + from_name: Optional[str] = None, + from_address: Optional[str] = None, + language: Optional[str] = None, + context: Dict[str, Any] = {}, +) -> None: + send_email( + template_prefix, + to_user_ids=[user.id for user in realm.get_human_billing_admin_and_realm_owner_users()], + from_name=from_name, + from_address=from_address, + language=language, + context=context, + ) + + def clear_scheduled_invitation_emails(email: str) -> None: """Unlike most scheduled emails, invitation emails don't have an existing user object to key off of, so we filter by address here.""" diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 026a73c6c2..a046cdf2e1 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -483,6 +483,19 @@ class TestInvoicePlans(ZulipTestCase): m.assert_called_once() +@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer") +class TestDowngradeSmallRealmsBehindOnPayments(ZulipTestCase): + COMMAND_NAME = "downgrade_small_realms_behind_on_payments" + + def test_if_command_calls_downgrade_small_realms_behind_on_payments_as_needed(self) -> None: + with patch( + "zilencer.management.commands.downgrade_small_realms_behind_on_payments.downgrade_small_realms_behind_on_payments_as_needed" + ) as m: + call_command(self.COMMAND_NAME) + + m.assert_called_once() + + class TestExport(ZulipTestCase): COMMAND_NAME = "export" diff --git a/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py b/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py new file mode 100644 index 0000000000..5a9abf85d4 --- /dev/null +++ b/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py @@ -0,0 +1,11 @@ +from typing import Any + +from corporate.lib.stripe import downgrade_small_realms_behind_on_payments_as_needed +from zerver.lib.management import ZulipBaseCommand + + +class Command(ZulipBaseCommand): + help = "Downgrade small realms that are running behind on payments" + + def handle(self, *args: Any, **options: Any) -> None: + downgrade_small_realms_behind_on_payments_as_needed()