From 1d579ec567d27aba514db9a519481886bdc5349c Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Fri, 18 Jun 2021 19:10:45 +0000 Subject: [PATCH] billing: Fix the type annotation of Customer.stripe_customer_id. This also fixes a bug in void_all_open_invoices function. If a realm with a local Customer object but without an associated stripe.Customer is passed to void_all_open_invoices, then the function will end up voiding the last 10 invoices created by billing system instead of voiding no invoices at all. This is because stripe.Invoice.list(customer=None) return last 10 invoices across all customers. But this bug won't cauuse any issue in production since void_all_open_invoices can be only invoked from /support page. And we show the option to void invoices in support page only if the realm has a paid plan. And it's not really possible for a realm to have a paid plan without having an associated stripe_customer_id. Plus I went through the void events in stripe stream since the PR to add void invoices was merged and there does not seems to be any suspicious events. --- corporate/lib/stripe.py | 10 +++ corporate/models.py | 2 +- ..._all_open_invoices--Customer.create.2.json | Bin 0 -> 1195 bytes ...d_all_open_invoices--Invoice.create.1.json | Bin 3253 -> 3508 bytes ...d_all_open_invoices--Invoice.create.2.json | Bin 0 -> 3507 bytes ..._invoices--Invoice.finalize_invoice.1.json | Bin 3466 -> 3719 bytes ..._invoices--Invoice.finalize_invoice.2.json | Bin 0 -> 3724 bytes ...oid_all_open_invoices--Invoice.list.1.json | Bin 4040 -> 4343 bytes ...oid_all_open_invoices--Invoice.list.2.json | Bin 4047 -> 4350 bytes ...oid_all_open_invoices--Invoice.list.3.json | Bin 0 -> 4342 bytes ...oid_all_open_invoices--Invoice.list.4.json | Bin 0 -> 4349 bytes ...open_invoices--Invoice.void_invoice.1.json | Bin 3473 -> 3732 bytes ...open_invoices--Invoice.void_invoice.2.json | Bin 0 -> 3731 bytes ...l_open_invoices--InvoiceItem.create.1.json | Bin 1006 -> 1006 bytes ...l_open_invoices--InvoiceItem.create.2.json | Bin 0 -> 1006 bytes corporate/tests/test_stripe.py | 59 ++++++++++++++++-- corporate/views.py | 1 + 17 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Customer.create.2.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.3.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.4.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.void_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/void_all_open_invoices--InvoiceItem.create.2.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 723f69b174..afc41b58ba 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -319,6 +319,7 @@ def do_replace_payment_source( ) -> stripe.Customer: customer = get_customer_by_realm(user.realm) assert customer is not None # for mypy + assert customer.stripe_customer_id is not None # for mypy stripe_customer = stripe_get_customer(customer.stripe_customer_id) stripe_customer.source = stripe_token @@ -533,6 +534,8 @@ def process_initial_upgrade( ) -> None: realm = user.realm customer = update_or_create_stripe_customer(user, stripe_token=stripe_token) + assert customer.stripe_customer_id is not None # for mypy + charge_automatically = stripe_token is not None free_trial = is_free_trial_offer_enabled() @@ -710,6 +713,11 @@ def update_license_ledger_if_needed(realm: Realm, event_time: datetime) -> None: def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None: if plan.invoicing_status == CustomerPlan.STARTED: raise NotImplementedError("Plan with invoicing_status==STARTED needs manual resolution.") + if not plan.customer.stripe_customer_id: + raise BillingError( + f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer." + ) + make_end_of_cycle_updates_if_needed(plan, event_time) if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT: @@ -957,6 +965,8 @@ def void_all_open_invoices(realm: Realm) -> int: customer = get_customer_by_realm(realm) if customer is None: return 0 + if customer.stripe_customer_id is None: + return 0 invoices = stripe.Invoice.list(customer=customer.stripe_customer_id) voided_invoices_count = 0 for invoice in invoices: diff --git a/corporate/models.py b/corporate/models.py index 5ed704d0a6..46570f98b9 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -16,7 +16,7 @@ class Customer(models.Model): """ realm: Realm = models.OneToOneField(Realm, on_delete=CASCADE) - stripe_customer_id: str = models.CharField(max_length=255, null=True, unique=True) + stripe_customer_id: Optional[str] = models.CharField(max_length=255, null=True, unique=True) sponsorship_pending: bool = models.BooleanField(default=False) # A percentage, like 85. default_discount: Optional[Decimal] = models.DecimalField( diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Customer.create.2.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Customer.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..bedc055ea8e83dc5cda60f61de24461a8cc2fa9c GIT binary patch literal 1195 zcmd5*%Z}4P5WMFrEFTa^XtUwS2?QLF7LYiC(5N%h-Zad^%#5R~R{QUCw;yrx1B(xl zQ&Ur2UEO}%G))WErXg0d2M^I=`?k4#t+;a;OX2@<@Lr*7ip>ZW9aDC<^P@yNw4VDQzHG-NHa$U5{C{|*={^E>bbRm5Sjnfgj8GTwTG+`PP#-xtk?bn~hmSya^j zx)cT=FL(Y>yniPbZ&JP=`6PB*I~4Qv+xIW;?>_(d^pU;4nNH&|kww#I?C2y+WnCW^ ztd*rDoJY=zQ)I!Kg6RmoTW7{cbK^Zc1n$uUtcN5n<^Y(}rBpFb7%M($Z=Q}Al9QR7 zW8v%Ipu)K}Pr(N>Ha7ObiX&L6h-!B2A{!xyc!fJSKrtcZi|!dd`|(t& zdSSGt3f|0{pc~BSkaZ2V$K7@u5auFpSF_*FUY3Z;uwRz%Ke2e>XfayhWblNc*9k=# oi6$;M=_XDK`i$w$UM@^i?9zH2l*E`hVy2EPOy9k1>E>zk5A{%P4wr(0Qetv)eraAwd`V(Od}d1VM4gRxiKQj^ zxrrs2$sj=`D}`#HI-ttbyu_rO)D)mVT4GLdDo8g_w74X(q_h|)m{*#U0~D&=ctf07 zEU~yGJ}omZF(iqe$nPf%)6Lmyo~eGeO!zpEXvXgoc!GVGI9(JT(i=^ z&Y7&osyz8NTMM68hO>#KpNU0QWNuWT#pYc035?=s8Y?Rcb2EG*Q#Xrn3NZ=;?MO<^ zNX$u#&rgH8X0jreVN7CWZYt1w#i=DFnR)3TheEwm0F}v2Ey>7FiO(+pxfA9QsOQks XlvEa^!c>5~y4i;-hKU{Q@mekbq_=dD delta 119 zcmdlYy;X98{=|@Vn_U@&m^W9ktYs4SaLbG^cQJ~vC`&JJ@^g>!F08CfHOQN6$G(Qo zqrlI@ILh17sUX?HGGOx$_6dyW>LaTn%l(W^b2rcD6k?owl1pcEICmHmub}}9aMf}F E08EA@GXMYp diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.create.2.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..a541f9754a649b31d11c5a751ccbc649650892a3 GIT binary patch literal 3507 zcma)8+ioN^5Pi>AjQq@!%LWwj1Z-fJ09lC$3GE75o^H>KcieWb?H&@M{5z+7>+ZRr zorh7&W!Lr8sd_$JESP8-JD9-Zzk6ceE!dwA?5nBBh)&Y^{rf+D|KYb^KKyk1>T-S~ zh)1qkk4t9IIz8^9+IFC)-(Fu_Oiz2ES{%>Loa}@$%B*Mi`(CR?1Z53hNm4Z$2tjsz zc6?NpP9>>d3GHPvI|R!`yA!4%9l;Ibc?azcM+6W&BW&bE8nM)K6*%=lgrUTUi!V|2 zQfZx>W?q_>E3>l-R;EJkr0~|j0PBPqgig|CBiwoxQ)9K3O^mjaVPh$ZBWfGy3(>#J zi}H*1n!!0~npnlbx7p0#gYEcANj)!769v+d5q$HpzEFBKeJmZa*E;D-_}LmMhKwqw zNy`?Q_+CrlzP4_?ry!%K)~{IS{l=Pk7P4R5Z&cr>R;J%K`9x}tqrlr^<-l`;8UQ=2S>%L(%70znK>Q` z&MGo8(0SjkrmwZ|0a%N!q`sBTS;AN~fPRUY%H4)I1;ig??=5l<#y%90Lm(^sL8s`% zV=&L6c9wSbVbH2ypmR+tT)PjP;$AuxStr#dp8|7u zG4GrD1j%)za1>W{Q^P&*idzK)#hxRsEX11+1sHI5Bq&rqvn}_{My8nIVg(FN^g{gKu`Nl8rUYqs%+pE8BcI*Df`?uGuM-Oufvt^}Oc)^3NMZzb{a zJOPlNs1zhdHQPPbtS*jqjBd`o>78wdRAu_)cbhlYpMHJQJc!rZZ5r-Xk(1O-bCRvX zprrFWL+rrw;Dqrj>pK2<5GJT_G`{fEQ!NidA=mPV@@)%Jk7Z?%y31T?p{PHFToSS;T#SqhHuzThQ zeE_g;etx%lySu(C5x&Go;{-##HiGKm_PT?OaL~$^USPonq0?rEgj}YmS)zM(zPmin zE_!^Nqtsz_TIoG2h-X6Tk_^~h_-t8wWVqsb0N%m6`r zDK|o|xSfrhyiL}Evogw+37F_pt3~2v2*5nCcAi2-lEy!`adMNqc#P!l-63R`8#O(N zLHRMtWLb8|m{b|oV=C7z1O(QiGYm$zANwOM%)OIebPug9v4QZK>JkdAvX0`PtPx2y z1QTevH)Y) z&bOGDeYLmR;1J7pJKcC{9ZvK$^?Pg!RJLpt)a8>x$EHWAnmz0sK8x+uo?0Ms`yHbY emZ%e>E6?a!nSbPRgK^=Je`++KV9dkW%h`XqH1)^; literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.finalize_invoice.1.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.finalize_invoice.1.json index 475bef780bdbe8733b5041913d8f151d7840ead8..0bd2da21baaf8978b75e49525f820c79ed0258ef 100644 GIT binary patch delta 439 zcmeB@ZkL^)ucYMXALQ%k;~C}ZqHCz6!=<30l$e~HUz%4EUy@i6pP5oTQD>uFVrfZ! zZemGhGDuL#N}(F44yZCUFEJ@6H3cY;mY7qV3epV}EiOqcDJ=#H=9T8;0EKEd-nh*q zl9^YQpP8JhS6ot*S&*uioS&%p~q*oR{w7ViaLfmR{iG=kAx0V`$)-l{PtxeGQ*i zhO>#KpNU0QWNuWT#bygS%WaOvB=NEwd01Fpr dSfQyYsVqo^sQ`r>I6gKja>p>Sg9Edc3jiF+kM#fm delta 178 zcmZpd?UJ6LKQUz8W>>~*Oq18M__8{Onx_X(&Ssg&1!n0Nq@*cr)@0qxB;(322}pg%rpzQ`%uP@;S1tT1QfKF-bG|@9gSw?(TF3 zKX^P^X>Z@|x9i7~#e%S^(m{C|zwHBgu^@llk+1q9#Tt(1w{QOZ?T24~dH2)Rv(@m1 zXZKXp4lb!6WqRC1wROO!-&~xX^-m2GH5?DlEN_@lLTv~4jgg{aUT8%(994A&yyuO{ zj(5t^DM$4iCLK>^gV&VR2c|02;ce*78_-@+hyVnS5H{q5E4G$H6>#c2^P$8D7oVc) zwU9D7O&nJ>73!b`Sm_I?<;-aX21vtHU@}Ro9kbg(OqG_BS25a#`<=!pX4F=o&qx1O zXXO*^Rl!=Ws)%B6bv6^6*A3kWE{70RQNS%4f#0;l7nGj&A9D-Yt1WjW{A>*sgA9?A zsAUhC_+D~mPqp2S6yz#u@hjG;*=aS*LiUTgoiHXPclr!#j^ASmpk;+RhF*0=`Tn0M zsUc}_ca*c5Z&;9?8urj|XoNQLt;ur3>0o31OeAQ^f_6+#F~(hGg+VOguzMj~?6kSF z{_DW$yt`0ikP*6OG8c1dqK!~Yich?z4D&IHaig`5IJbW6w1WyN)k=en`0czm?!|JM zi}=)e2!Wqgx><@PFohr1$l2Knue!-`DM~1#ZyWaMIw2E#o(MHoQf}vx%f_jxHxLOa zJ^?-he^Q_QAFq~ZAy$TD5JOI3A{%b4#>Qw922CE}%@hIPFc80wgt5rP5y^u^;saSB zJv;^anCs0fst2_~-UTVl0_aB7%+`xwwwBeYy32!HMKl@f3TGro8Us&SA{>b)?=nK- z^9W@v1t$SK3`U6fvS(5pc%vbnsbA1~EGPCcvTL@a*B^it(FUSGqd~+iiw(CTQf0y> zpF*R+i(w;sXZSrI~Rxx>$&NYTp&1&ju69iP?a}1;M~;X7K4~R z!br;eASVU(_0QLvSBHz665&&fG>%=!*N!0`cGMlDD+eiD=>_7oXELp$kdRf1n&bhs zJj98E8|9@Y@JN{mQ({=IADzh9d_-tyIVNq2eTxcATu zNauMx6-ioVsE&IG%8PwKWpoEizT9Z@mZ+7^aVg2NDU~8*#K3~l?o_~RHMER^EyQ(= zlQtJ)`4`=Tp69Sn(H4Ue3NGdrt*<}3m3%SFuR8ZE2y&5!zfO>S@AnUAo9Z2JB0rab=l+axCP@#*CQLmrU7`% z&Vd}zLpxFe;8mr{D#lW;4W7m3tBh|6Or{r<#(Ve~_=Ek7tyti>{%P4wnKLC?zH*=a=S{#Fr#i#Al`yPjtKJl~`Jm zpPN{cnG6zCvQnsqs#Q=>O3h15%1KQD3Z^CI6sN*W0m>AYB$kvG110iGb8?^>YB%#S zx-*Mp=9T4VCa3BZmlS0dr0ON-=jvxp&So)ZO)T@sh?u;bWu_sRrC*SehHzL;VsS}) zT4r8iPG%L*G5LA%sYONkMNk_yd$Ec$i+dU8rTe%TMOc)j7dZL3`(@-98n|YqO|IZL z!snIYY+~tWVv!Y@8x?4=*^Bc8Bf9#^%EH_XpUBkBn%r)T!ay66QZo{B(&F>eptej- z<1wiRS>lJV#J?mY$lKUE58={+#LC=MU=S9kmXu`XrGo+iDac^5xv3=?`6==F1t7n` Y0tzVr(bSbx7NmkzaU+N4CLS?n0D6g$9smFU delta 155 zcmeyactU=H=ft9Onv|4(s(r<#(Ve~_=Ek7tyti>{%P4wnKLC?zH*=a=S{#Fr#i#Al`yPjtKFl~`Jm zpPN{cnG6zCvQnsqs#Q=>O3h15%1KQD3Z^CI6sN*W0m>AYB$kvG110iGb8?^>YB%#U zx-*Mp=9T4VCa3BZmlS0dr0ON-=jvxp&S5cUO)T@sh?u;BWu_sRrC*SehHzL;VsS}) zT4r8iPG%L*G5LA%sYONkMNk_yd$Wo&i+dU8rTe%TMOc)j7dZL3`(@-98n|YqO|IlP z!snIYY+~tWVv!Y@8x?4=*_-nOBf9#^%EH_XpUBkBTHJ1o!ay66QZo{B(&F>eptej- z=P{`VS>lJV#J?mY$lKUE58={+#LC=MU=S9kmXu`XrGo+iDac^5xv3=?`6==F1t7n` Y0tzVr(bSbx7NmkzaU+N4W*!-40G%w6JOBUy delta 159 zcmeyTcwT;j=j0ijJQM2WY)tA*d)I2?SvJ&e|E-*{KASF#{^BY!iW^oU< z%m{NAqX>(#^a3Y8_bBhe%F0xOyvb6WNBBGn{5*`Kye*vyk}WI)HoxIK!HBLtvMRFN m&)76~^J#83#>u+8+7ciGEf5B}XE}NVBsmo+ZGOZn!wdk!k~A{_ diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.3.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.3.json new file mode 100644 index 0000000000000000000000000000000000000000..0ccddad514e3b158290d953b727dc674d834e4d7 GIT binary patch literal 4342 zcmbtXZF3tn5dNND;n8Or$7z7jPe20=DWpJ#nbHoUk?&+%wN6qcok^O>e`iOCZ};}~*=P42PZkSOGSA41#e4X@SUiTm@RSvWu9c_ZuYCXo^4A^tYA9B$;wZa) z^XG3r{QAqgpRS&*##)};Q&BolRJD}ZNEKX^HJbS5;_PfFF-(*o8a=nXVnPYE9ld9a z6b17_E4txmw(rn;UYV3s-*A+0bhlyB@f=F+HD%?2sRHeJTlZxZcq=Gm2*Rd%P(QBN zT8_YxR99x71FnbQiKl8ULC$*fWAnB9(YFSL}r z2$iY0-)YQr#zg_PeTaQ^*8FSz7qzuq6#)^oD?4s$=XFIlg3EEMiy-6{ox*RL@d6Ic zheo)CikFtVrc530=mg}50EyQ2P_6GJXZBRv?S!F(&>jw&XsFp~HLg^OmAaiUCN^XG zb;LElM@V5L3T=%e&ES9kPb-C?L%$RCE%}Dk(o@48Dh>nKHmesxqvdpM11=|}=-PrO zEMwO>cZC%Ou>gYpo5F>OpL;&ERC79?3g8#2N7qcI;mL!%5sFFiiI1!+p^!$$ zeb5bbkY*vW9%&C$Cf`pT6`${Dvexufzz)I2iZ6dviUY4SWUqYmjq-(YbAV(>^9hAYbh(utfy8X_T6B-banGwZQop-zr8$c z&5yUQE{rUpJ>T#1<<=Us94B|Y-C6}NxOFtab}}PxnQ|L#EBdi!$_qc6yMHi< z0RkJ6dI62@Lm2YMmZxzC#mjsPw()c{Xvqsv!Oa6EL6sZ_EwEr9M9`jMiB9a7{ji9u z*K?aPhDC709wDje1>C%wijs(BYP2EekFa7BSIH4zzyA4p^XhPM)2+Z$TaEVIfcowj z_R>yFBFWH_!gc44czhqWFD2 zj95I`A+RnuiK?r0yff2{`K*6o=4?~lTPy-P>6-7D+)$0HyJt3iiu3?Pnj1|h52qTu zLX3z#`aP4%->=OdZ~5xG?5BCnaqnROaXn?~ZjHvnkLqxzp-wj*=xAdl>EVbqTBAD0 z6+!3r0ZDS$%Y${$w44|?yxN--n7D*-RdAuXJhb_`GnRj`F7ObCTa>na>!yhxOBQQ1 zc|_uu6l@JRlA9|d+&k&WxJySyt~=_7FE64ESHtHt(3!xic+ip09;?ueirGCJrnPFm zv*0X=w{16{&^D2d-hlwV^UCHPM$xE=N7x9NDhw;YqsM8tN3i1R)& z5lEmsc9JEcm?J|H7X>W#bmjD%{>#@pSe_1=h;#tZ8h#UmN+7KTLYt(rDH59^DRT03 F@*f%0i=O}h literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.4.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.list.4.json new file mode 100644 index 0000000000000000000000000000000000000000..b526043a90c58fd7c8158b9a3a8888b7913fb569 GIT binary patch literal 4349 zcmbtX-)|c?41UjFVd!~{<22i{K5c8ZX6cr03kGy820^})ZPhwSm2_UxApiSFNq4%R zZh`e7aE}y8ksm)&k0*--DVb;F#o|5uUMwENU--(3Lf6XE@YgwMas&6<-IJ(;~>39yM_L{Qtz*K?u#=;8R6%?)r(k6Y7K(5$Y zj_rZOdFJaT87ZMpt-rO9GTw|hu1YG@K?`s_RG^kKrxiFQ6;m~nS)PJtuPeF{T#jp91R=NR6n@hT7;tbt zG{P;UytLdkY3g`KCm=@%NVK+xY<({|v!~i_Ck!Qs_JGhtL(NXBai&tN)a`^Zu^7|G z5!n15F@=#Rv^9=2qyPOstrWTr{Z8b!F>meaKjz?_t#YYUz* zja}#56;>F;0tosig$pe|_kC!o=5+olfM3WST{D@wCwKBjC?>@xUQ&j+o5;gPYaj5^ zi*KhLAhYQTTHAnuo%hDQST0kuoH`HJaui_&GA7Jmp!bJceNB|0Tif8X3Irwww9H-Ynnp|g)}T|T2og6Lj(&ezWk^Z2VQBo=X9LV;w%;6v7H3Tt6qNq z)<%C=IU1ZdHBTyrTM_6y*EC6>0paAhxuQ7EBHQo3G?1dkFc<@k3 ze!P8kVbu9maeX`XeY}Ej5vldQHgwN%Fpd)h8b_)OQYDK0#Heb+&Nv96k0Ufzmvux6 z{-ig%mlq#izbx+9^Zh=rZmmJfv2(}MtyS=XTSpUYCvW5}Q*NVeML*U|dEsa8?jH$y2+7;$>b2+ju$}wB&`TVCMmoph}K|7FaM4B4|&sL?`ykeptlS z>$yo8!y-6gkC4>#0B+8vq$FaQYHhglM;Nh*tK1N=m%|^R!Kz?@& zYiTDskz{B|;ktcCy!K4SHvlAWm9sR^H zh!L?yzh_eU`?dMwEnj_?{WQlM_Z}J$$5W>6)@WS(s1AD?>U86QiZ)h~9=2GcHOg}w z5p-@IkR*q>JQxQ}$%%o*tKCU~iA!i#1sj^nO`EqnWBC{B0uOQ6MQK~N?ltkTWHCpR zOC-LeU~0gU+-wFhIxm;_od!A+xD^j7^4Vn-s!=h!hsCs3 z%{vR$lDOM;?-R-<($YH+U`*qEcvQ+GoP>N8S{A_4-PDk!2KWiY1ywI1Zo8qxt{<5Q zR8SsU%o1J9Q6h=s0>*pVb$U+!{%P4wr(0Qetv)eraAwd`V(Od}d1VM4e4`iKQj^ zxrrs2$sj=`D}`#HI-ttbyu_rO)D)mVT4GLdDo8g_w74X(q_h|)m{*#U0~D&=c=I-s zNM>GHer9s2UU5lLW-n@`yGn2TNabCKQi&2C{S$ct!pSxd1j-i2TR@&rj_BDK7 z8O|n_ekK-Kk-1TU7MoQ$CNQF_udFQ0&G3m#-F${qh*215LsDu+Voq9oej3!4$v3%l z>Oq$HAuRDP2?_Ex_RfR2vLLZCHx(FK#i=DFnR)3Te?vnDCX<_5l98VhpI-p-11wyi YVTGoqq_Q9trUDdl;P}|A#GTIs0M7f3(*OVf delta 164 zcmbOtJ5hRq{=|^=o81_%F-_jU;>+qBYMvfEIfrE;7nr4AkdmgfS&MZulZ=O3W`wzm zQG`WVdV!Oldz5!!Wo4>Co{|oig2Lo{Hs#4O94&kv1%4jJQQnqL1<4kc0h`xxOkfm8 r(->J5S?*_Sn!7oaONeoDJ-509NPz{yWcMsbkANhnBBjj=JS9v3;uA8~ diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.void_invoice.2.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--Invoice.void_invoice.2.json new file mode 100644 index 0000000000000000000000000000000000000000..c72adf7ad6405b7a77e14419325aef8dde5c7789 GIT binary patch literal 3731 zcmbtW+in{-5PkPo7<_Il%W+#bPi^BiuG2bgjRI+cf}nOOi_tF0kX+S{;eYQLUUs!w zDF{$K2n!B5w=;8Q9*>TXnW!q$>A>T^dtfh)*_-*`|0x8 ze0(E_d#-AaOS)^C^tg>`>yDm&bAEcdeQJfOaXdP6vK2}zeK)#qZKEm?l+k=CN!4H= z1lii`_@FGEN>aZRjhD%67YrBmM(B!kj0G*!R~#Wg@`$*R6sg6c8SB8Q4Ac^3Fy4Rv}pb zVm8Y~eBuK_kSCRCXKDtj2;&+#J)P54KRGT%i8A`O6`!sWGO_8I(z{B^?Obx%IJfl@ zk+9+u@EQ4&`t1LBH6shLG8%yxatagMNaqYSMxQV=d4xAp1i%pxzmJ4*oQWfnhltDv zSz$do1%2%6%`B=%wZh(Yjk3p}8(j-mA9wc7i8@txd627!Cu2k5?8u46;7Lz-JL1W^ zjF9>~LRm|}Nr8xAgsCrk)~Jna4dR*lh1L@}v5%2mvn8|m09GU$gaVHS3AZe^(y2(5 z37dS1M!}15BlJ&@TsQ8MUfs4~izb7TQUt}GBZ4o}A|DFS_GV8|Kt7KM?yHqdqZA(V zPOA{BuXpMYtq&LW(0eaT*je(8&##~ByEm6-Z!b1?_Q#u7=T@IxR#!J;dxw#L(~U9f z&hoXSVKPntqz90KM5}7OL-oCj1CG(nu{YM4x=Se2C%<03Jpb_eWpyi_uh(fM=@mIi z-87yIy)Y>0JkKRI@jN)8{nEIWf9!+~D(sCfJXpe#VXE+;0NPM)h>@Vnt%pk&X}Rg` zZ%KJAE7fA&Llv&E{Y3_*iKK&DyPdk_fIJY zOdm0lGCwqv0{i;stL3ZB`E`l#DMlK{F7mY!gooSd4mOlSqkQQF7EBOL+DDO)d5W6l z0X5sii9;Kf$LKl8Q?o?Z@FKP|cwm*=n?m#unm- z#z~utb@Geuq30#`Dc)mHLZQXnQT&so2m;I|TI$M#jWazD*xNi{Q!eRl-0m6n_q;i$ zGae~J(I&T*;mg9Q84*bU delta 55 zcmaFI{*HZv0F$_fTV{m0i&2C{S$ct!pL>*dVP$2iLEdC_rVc)j0zVJqC~r%rf@BNJ KfX#cE3>g9ax)G`X diff --git a/corporate/tests/stripe_fixtures/void_all_open_invoices--InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/void_all_open_invoices--InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..54b979e06a3e5294604ec0fc09d5ebdc0a63a38b GIT binary patch literal 1006 zcmaKrPj3@35XJBP6stXR*rpMRxIu$}0;xnVs0dk(cbZPuYa9QGsG@#%X4c-#1|o9W zJdfY}{Kh_9R8=i|n@p_VRIis!^NJOfya%J6Nvl(6>mr6|d+;o(6vXEC+t;^WKHa}x zH%+t1HLZ+5>zAbD->|cVpgg*W*6`;0q|sF&O4CZ;R>|$WY@rxnP)sAYnvEW$4v;;{ zLLl|W&q2bL-4IVn#?BKHn!QEEZYF6x?9{l2-crwbhaRFNeo5ltWhk@TCr}(rT|6*C zpE(_PwA}wdc~N3u_|NINNv1=3HaIR&c{X=iavBqp^BJ-jN)-u`3Z@9iZi`x@*@>VY zq36Y#I7BiS2P&#i9%uw-SpNgHgZORkiPjT~jy?pw7t?uMi*WHa& z9{6aK%W3zB;Dab*p7n}en($zKFMcIyBF4X{DnAv1oav$k zg}u}~87ioc!}iw8a{s4+*0cVr;lhQqoxhY3x{mUlfJnfxBi literal 0 HcmV?d00001 diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 9e28210e4a..ec0a5251d8 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1990,6 +1990,7 @@ class StripeTest(StripeTestCase): monthly_plan.refresh_from_db() self.assertEqual(monthly_plan.next_invoice_date, None) + assert customer.stripe_customer_id [invoice0, invoice1, invoice2] = stripe.Invoice.list(customer=customer.stripe_customer_id) [invoice_item0, invoice_item1] = invoice0.get("lines") @@ -2154,6 +2155,7 @@ class StripeTest(StripeTestCase): self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 12)) self.assertEqual(annual_plan.invoicing_status, CustomerPlan.DONE) + assert customer.stripe_customer_id [invoice0, invoice1] = stripe.Invoice.list(customer=customer.stripe_customer_id) [invoice_item] = invoice0.get("lines") @@ -2632,12 +2634,17 @@ class StripeTest(StripeTestCase): @mock_stripe() def test_void_all_open_invoices(self, *mock: Mock) -> None: iago = self.example_user("iago") - self.assertEqual(void_all_open_invoices(iago.realm), 0) - customer = update_or_create_stripe_customer(iago) + king = self.lear_user("king") + self.assertEqual(void_all_open_invoices(iago.realm), 0) + + zulip_customer = update_or_create_stripe_customer(iago) + lear_customer = update_or_create_stripe_customer(king) + + assert zulip_customer.stripe_customer_id stripe.InvoiceItem.create( currency="usd", - customer=customer.stripe_customer_id, + customer=zulip_customer.stripe_customer_id, description="Zulip standard upgrade", discountable=False, unit_amount=800, @@ -2646,14 +2653,45 @@ class StripeTest(StripeTestCase): stripe_invoice = stripe.Invoice.create( auto_advance=True, billing="send_invoice", - customer=customer.stripe_customer_id, + customer=zulip_customer.stripe_customer_id, + days_until_due=30, + statement_descriptor="Zulip Standard", + ) + stripe.Invoice.finalize_invoice(stripe_invoice) + + assert lear_customer.stripe_customer_id + stripe.InvoiceItem.create( + currency="usd", + customer=lear_customer.stripe_customer_id, + description="Zulip standard upgrade", + discountable=False, + unit_amount=800, + quantity=8, + ) + stripe_invoice = stripe.Invoice.create( + auto_advance=True, + billing="send_invoice", + customer=lear_customer.stripe_customer_id, days_until_due=30, statement_descriptor="Zulip Standard", ) stripe.Invoice.finalize_invoice(stripe_invoice) self.assertEqual(void_all_open_invoices(iago.realm), 1) - invoices = stripe.Invoice.list(customer=customer.stripe_customer_id) + invoices = stripe.Invoice.list(customer=zulip_customer.stripe_customer_id) + self.assert_length(invoices, 1) + for invoice in invoices: + self.assertEqual(invoice.status, "void") + + lear_stripe_customer_id = lear_customer.stripe_customer_id + lear_customer.stripe_customer_id = None + lear_customer.save(update_fields=["stripe_customer_id"]) + self.assertEqual(void_all_open_invoices(king.realm), 0) + + lear_customer.stripe_customer_id = lear_stripe_customer_id + lear_customer.save(update_fields=["stripe_customer_id"]) + self.assertEqual(void_all_open_invoices(king.realm), 1) + invoices = stripe.Invoice.list(customer=lear_customer.stripe_customer_id) self.assert_length(invoices, 1) for invoice in invoices: self.assertEqual(invoice.status, "void") @@ -3207,6 +3245,17 @@ class InvoiceTest(StripeTestCase): with self.assertRaises(NotImplementedError): invoice_plan(CustomerPlan.objects.first(), self.now) + def test_invoice_plan_without_stripe_customer(self) -> None: + self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL) + plan = get_current_plan_by_realm(get_realm("zulip")) + assert plan and plan.customer + plan.customer.stripe_customer_id = None + plan.customer.save(update_fields=["stripe_customer_id"]) + with self.assertRaisesRegex( + BillingError, "Realm zulip has a paid plan without a Stripe customer" + ): + invoice_plan(plan, timezone_now()) + @mock_stripe() def test_invoice_plan(self, *mocks: Mock) -> None: user = self.example_user("hamlet") diff --git a/corporate/views.py b/corporate/views.py index 7f5754702a..887c5ba4ff 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -317,6 +317,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: ) renewal_cents = renewal_amount(plan, now) charge_automatically = plan.charge_automatically + assert customer.stripe_customer_id is not None # for mypy stripe_customer = stripe_get_customer(customer.stripe_customer_id) if charge_automatically: payment_method = payment_method_string(stripe_customer)