From 9018ef5175f2a97efc48e6194b5937d1df819c6a Mon Sep 17 00:00:00 2001 From: Rishi Gupta Date: Mon, 28 Jan 2019 21:34:31 -0800 Subject: [PATCH] billing: Do not require a Stripe account to store Customer discounts. --- corporate/lib/stripe.py | 17 ++++----- .../0006_nullable_stripe_customer_id.py | 20 +++++++++++ corporate/models.py | 2 +- ...ach_discount_to_realm:Charge.create.1.json | Bin 0 -> 2120 bytes ...ach_discount_to_realm:Charge.create.2.json | Bin 0 -> 2121 bytes ...ttach_discount_to_realm:Charge.list.1.json | Bin 0 -> 2346 bytes ...ttach_discount_to_realm:Charge.list.2.json | Bin 0 -> 4609 bytes ...h_discount_to_realm:Customer.create.1.json | Bin 782 -> 1658 bytes ...discount_to_realm:Customer.retrieve.1.json | Bin 0 -> 2294 bytes ...ach_discount_to_realm:Customer.save.1.json | Bin 0 -> 1659 bytes ...ch_discount_to_realm:Invoice.create.1.json | Bin 0 -> 2311 bytes ...ch_discount_to_realm:Invoice.create.2.json | Bin 0 -> 2313 bytes ...t_to_realm:Invoice.finalize_invoice.1.json | Bin 0 -> 2444 bytes ...t_to_realm:Invoice.finalize_invoice.2.json | Bin 0 -> 2446 bytes ...tach_discount_to_realm:Invoice.list.1.json | Bin 0 -> 2898 bytes ...tach_discount_to_realm:Invoice.list.2.json | Bin 0 -> 5713 bytes ...iscount_to_realm:InvoiceItem.create.1.json | Bin 0 -> 469 bytes ...iscount_to_realm:InvoiceItem.create.2.json | Bin 0 -> 452 bytes ...iscount_to_realm:InvoiceItem.create.3.json | Bin 0 -> 471 bytes ...iscount_to_realm:InvoiceItem.create.4.json | Bin 0 -> 453 bytes ...tach_discount_to_realm:Token.create.1.json | Bin 0 -> 826 bytes ...tach_discount_to_realm:Token.create.2.json | Bin 0 -> 826 bytes corporate/tests/test_stripe.py | 34 ++++++++++++------ 23 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 corporate/migrations/0006_nullable_stripe_customer_id.py create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.retrieve.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.finalize_invoice.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.3.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json create mode 100644 corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 5c045e67e3..7474269ca3 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -163,7 +163,7 @@ def stripe_get_customer(stripe_customer_id: str) -> stripe.Customer: return stripe.Customer.retrieve(stripe_customer_id, expand=["default_source"]) @catch_stripe_errors -def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: +def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: realm = user.realm # We could do a better job of handling race conditions here, but if two # people from a realm try to upgrade at exactly the same time, the main @@ -183,7 +183,8 @@ def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None) -> C RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, event_time=event_time) - customer = Customer.objects.create(realm=realm, stripe_customer_id=stripe_customer.id) + customer, created = Customer.objects.update_or_create(realm=realm, defaults={ + 'stripe_customer_id': stripe_customer.id}) user.is_billing_admin = True user.save(update_fields=["is_billing_admin"]) return customer @@ -221,8 +222,8 @@ def add_plan_renewal_to_license_ledger_if_needed(plan: CustomerPlan, event_time: def update_or_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: realm = user.realm customer = Customer.objects.filter(realm=realm).first() - if customer is None: - return do_create_customer(user, stripe_token=stripe_token) + if customer is None or customer.stripe_customer_id is None: + return do_create_stripe_customer(user, stripe_token=stripe_token) if stripe_token is not None: do_replace_payment_source(user, stripe_token) return customer @@ -442,12 +443,8 @@ def invoice_plans_as_needed(event_time: datetime) -> None: for plan in CustomerPlan.objects.filter(next_invoice_date__lte=event_time): invoice_plan(plan, event_time) -def attach_discount_to_realm(user: UserProfile, discount: Decimal) -> None: - customer = Customer.objects.filter(realm=user.realm).first() - if customer is None: - customer = do_create_customer(user) - customer.default_discount = discount - customer.save() +def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None: + Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount}) def process_downgrade(user: UserProfile) -> None: # nocoverage pass diff --git a/corporate/migrations/0006_nullable_stripe_customer_id.py b/corporate/migrations/0006_nullable_stripe_customer_id.py new file mode 100644 index 0000000000..7b0348f1f6 --- /dev/null +++ b/corporate/migrations/0006_nullable_stripe_customer_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-29 01:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('corporate', '0005_customerplan_invoicing'), + ] + + operations = [ + migrations.AlterField( + model_name='customer', + name='stripe_customer_id', + field=models.CharField(max_length=255, null=True, unique=True), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 745414014f..0b399bced2 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -9,7 +9,7 @@ from zerver.models import Realm, RealmAuditLog class Customer(models.Model): realm = models.OneToOneField(Realm, on_delete=CASCADE) # type: Realm - stripe_customer_id = models.CharField(max_length=255, unique=True) # type: str + stripe_customer_id = models.CharField(max_length=255, null=True, unique=True) # type: str # Deprecated .. delete once everyone is migrated to new billing system has_billing_relationship = models.BooleanField(default=False) # type: bool # A percentage, like 85. diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..2461b7f40594fb58585bdaee08e638be48043f5b GIT binary patch literal 2120 zcmah~S#KLR5Pr|ESSo4qibpUb=xGc$#$7WXAjH&24c0odL%}wC)@ySTGN#@73Gk9k`fH|ehblR1 z<2q2IebRYuTuNAwO&%VVLdOn6t>44GLmetA%_C^^Ibj3Xc>z~eM zl=0jL!X~C5v);&;R;+`}=S0{6+WazfRxG9stG6aVqcf&+YoPY(BnvXRg*S zS8pF*?EX-D^CCy3;8)#LJmhK1vURS6)xw@rP0PV>+c`_>P}W zB=#CQ$(aHlR@2__))IhpXi0q0gDT1P2#LIx^NT^+&_>Z&N8d2Ddhdc1{-Yuiip%b6 zPA5c;=<6Aj2>oFWr5Hn<^=i3V4m0?UGRctI$~5v!KmvwygYn@5a7s8xgSkd6@ABm! znj-2+<4<%ll8?_gEhn>?l0{yBqBFV<<#L&P-=^0p=cAEEat5JE>{;^#3FMDA&XYk zz9^z|sQI31YoixKgy@6m(Ip{|ex7etYy^7lq#et;v|9%Pn_!x+QAyi;3ZMlzan$_u zba}jNZkuP-F-ZNxtc3J7hS`iZAQd&jxDf~CJ$et@xynHlOkf~aPvw96p1YK3RW0x4 zozzJzhsjC3Gm_5&D!IeH$s@f-==fy4th%S%U#0HF(oZS9L;p>*Yl~3n&^sq2-ZJe zEGXl#4}?ujK^B9RUjXNUtzLfqvORy@Er0F*{I}OPi$_4QaGc6}^VTQQ{!Zwik$2!(U)E5)NIZc_NgwLa zO#nrIp>R^>9kLyIU+h-YNkX1~yraxqep;8qoy42_Yw?}5R_g6cW}?ggVt^+I;pp=* zE<w^0CyaPQ|)xaj7NoPhy3D{-K?c8dnJcm&B^B2^}^b;pm_l;~nzrWNg77 zjC69Za+=UsO!`XqzxF-VowO<7X?hQnlJ0QIHSuz$l`H9C^vdfQI{s9J|4*k>4Bzp? ziNsz*FF8}-!`ZwyytM=%9Xb-94xmc1JwhVyljZ3s-OxtSSx4V6wff+K6i%li5{k?2 zTTUlLR`d-FN`(G6hf<8O&i3r&tPm>+f1pf~QCpctUI<9QaBdvf^Z_^}9Hqfrqn3C1 zau7`s^`!A9I;rI2GfvCN+|0=$uRqZlU59eHOuldPYn5}g$+obrkDMc>Q=bVv-(m{P UhM!!ytSYw7!~~{X*7qm>0Xj@|%K!iX literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..57497dcf4e4f4caa913af6daa07ccb167a267e49 GIT binary patch literal 2346 zcma)8+iv4F5Pi>A2tpr&_!48W-KTEbwB2H_S$KE5ML~?Ik;R50SzeqtLH@l%NtP%p z?$!^6F+6sip9Gg;hjojo1nGwE*OafLUy@L7{ze|D zM@hjF)*gyhY=afZlIfECBYJ3CGy@qMgXc7;SWrh{qP9YRtL+VQ9(;(!=3tD~J1p2{ z&pL0Plr9iQ+=SH8F5C^1xI-C(C~a$yqkGZsxV90%@QUcBLn+a+VHo*)*w?6hMsSTp zj~8hmzc(>N4#8L2ihCk#SI#gd1I?gD>^5ysCX}bx6k*gHiyVoZ3~L&C3|~P_seibS z+`J$lJp}4NdsKybGwt%h8*CfKd*fcrW|aNHdE!6DV6*O&n*ryZEq?s`$M&~htNBm$ zmw#$?JNplU%`7LFS0A?P*P{IN>Yct`|FU}f>~iwp zYA9WxvK!5KSwJM2wdtiqVhKR%@%jdpxNp`D=@aQq99z_liQ>5CEwuZ%oT@@Yr>JSP zR@ybFn}s=EeSPjco^R#Y14d@6!M`BQn+?Hc_(7C-GU@3t{<15J)Zpw+PsM= literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Charge.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..a5b0b78e86da34c79fd5c4d4b7be242d927cfabd GIT binary patch literal 4609 zcmeHK+in{-5PkPoEEah*tcwN1$y1ZojZ-SceUJ= ztfnaXkO0~TL6{+D4u^+_!^eXpNi*tb`Xc!q|0l^~^@~V@UU+t268Vm z$k5;MX)QRTjw^|F>4k{XqKo7J-*A&Qc`wME3Mv^8XQ;H4HF;8ZFUj@Wo2!?XZ@!%U zf?(q}PFnsLZIz4wGjm!=qtxpPcq4J1(EBXU47eZmW(tD3!#1a^=&!P^*057$E|-e4t>A($p{z0GRM=Sk67x0u z>m*e3KY<1AVNz-kYYRn5GKYeSoTwcAJ$fjURU6VbddG01!h#wKBex~EJ7sQ(b=3K= z*pzB*)Dki>Uy;V!CdCQJ5xYg|VCVLh2v~v$L8LMz6}@@EZ<#UyKs-e_(}ECSNZgF{ z9bJ_mU4n4600&3iKyIZ&h?II)D8nC+u+4JfHfdUIRFB=d4obLm7#kz>ierEQl9ND* zn;ya!w8YduJp^tZA&?dVR-hg#eL3H*^4jZ79mY9rUknDA{m44xKZIa|W|SRJ#vB>F z`uKTq{<;|cTz>kily`%FAlSe#gn9OUF}r$xGKcfe8pPs8&-m+Cm*>|P7qbiHK6b&j z>eQ!OEn6G(Hn8Myw^pk?aqkfjTLpkh4Zj^c029| zu0!zS@I3_r{ktK|QM4-^nQ=?IvstGiA9{{|r?Ci-E!pnG*-FxqGa{&UlkK&`y(XoS zu0YrET4yk3u&;myg&z-(yPh0b0Y)1 zjJ3v}PR5g{xafFNL(z`Qt~BFiMgz%==_c<;ECT3yoVo?cAF68y^$GPRY+KNcEyc0b zS*Y*hd|MSNIt5L`Yo)#hRdr$Zuf8^S8phXhY&~NDp}$z~NLpR`n1nRfMNi0D=fWr- zNZP_};e(_bgoC&$a*#Qo%^aHnUoxQYtcd2{W%8hom@JyqzF_FYYUZDVP^=dLCSD+1kj xHPZfvt_EANKSFDSFE)$qYxTZtMInvNo#3|aIn(B8Hdv1Ep%ot0Ha+-y@HhEh`_KRY literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.create.1.json index ea949ad0522d2d4f296d9ffb3fce8852d9450f98..efd9676112f69777bd03ce1e9e427f618abd1879 100644 GIT binary patch literal 1658 zcmb_cOK%e~5We?Utb78gqIvYhiAv>w#7k7=P=qYUo+U%Q_F{XZKGgrtjO|S}B@rCh zi<5bL{=V_dJf1X-P+I%JCAm^oISt}!LN!Mnuute6*~PV)~d6R-sIF*+IJ-dS&Yy_+~-xmga zCVq4AS!rxhXKW52R@)Hnq>!Vo9a8r+SBaV^9*U*6hdJY2I|HFfYiJ^<)1#qMuR-5( zLa*Xc#KlD9Gjfrs9Ctrp&)`e`4v`@LaSE%TT<)%~7}Yd8_h_xi)t)1FO>T~3Em`^N zjXr*{xp%5VEv<^_{7-Rk*UQekv?kY*o1tQ|{T>u|(PFlodRl4}2z|gjm1PEY6Fl}I z!-#fprnD6=3xjE@uMXQ^w?HK{@)zfe^TiZrIv06-Y68jT#}pSvag&A8>(x1#1R^kn zOa2xdJ}BNUlCamFpN+?&7UwCS-%G7%kwmIr*c(++zQeZ|N7*9zq^umb(!4?pA+Myf z&HSuv3J8&ZJzGL&<3!T^Wlh9kwOxb<2K7Hs%!{7n{4>QpMq8pu-AQz@6Pep=5rGBZ19;m7)n|L}YnPi$|R(y#6-q{2@Lo(UBw;2RgJQG$Y&ZlvG{ab;92TU~}&-9N`^WnhJcuZ#p& zg%&Cg{ICdPq6Kk-;GX+prge0hL2K+_LB?b0vg=;;nD7KY&x=N1x#Y%igYQ`XP@bnK zdi1H|%h@>iU^{RK7K3MBzJC9F_UYsG#d(~-YDR5ExsbdwyiWJDq)Zi-YPu~XEl;7;cB(+(|iB%bx@ z`gEP*I|y9zz+>iz$BAid3kOo4Nsf!*Tmsw89}vV73npu@H(=B~56T)A^R*T^;iTiB zDQ7JquF~HDsL6(F4_GRiy>P0>K&*E^Xw_G5uIa})Y~NCaS}ITDo03u_bX;kSX?dq1 ze4x;G!ggcG^eW|<*2-+H62N)~jk?7w^z75fXSatC;+y*jn9eIMNn9A#D9{qki*0VK z4YWyF-&$>_l+0a_mFNEb6iV_!(Itr) z9ce^y(e}#Bh}|#%LxQ_$^bWu?y|1lZ|b`;g=2iKQI9f H9uNKja_oKy literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Customer.save.1.json new file mode 100644 index 0000000000000000000000000000000000000000..026bfd7a7fe2fd0f7da92329963042adf1417145 GIT binary patch literal 1659 zcmb_cOK%e~5We?Utb78gBF_^iDwP8gFHx045waY6mY90&#g8a$)&I_n?M*fz5ggcy zlX-mpX8g@OT{I07Qo3X#UkM|u1opa_UXopU5D^sqpG}9aIFk2ZtCG zd2q>R5GI9J{NtxDAFkfNyL)>*K;&~iBpGdgC9n~(T44fItRTJaqjr|=cuYq3&5OHy zT*KyNP1Ye3#}J#0$(hgzZ763gIs@@dPJJa^S5h=p!(QoBRb-dMJ6qyG)=xUvd;HwRzpEFIPH_$mnz1~;dK@>_vvB$*+ zFdZ+X3kgIl#t;d#w1(|)-RdVu>Sd*bH|P7pTK7G2M$f>Fm4_NtQSbhiYieq3xjvzk zhY+~bF+cieA$6-|#-;#dwF@CB`5bj?HOi;CiKwD*Di+@!W<>VZD)3EQLlZz;o(<)4 z4f386dJ)bd&PPQakqboSy!%o21iql}6bba7r?B$EW_5klL5#DrMr%iI4jfrESsdpi zS^3M2Jb$rzu%gqF8xi92&*EU)^Um40M%RMGP%+8=0D@FBpDxCp7KH-7_c~8ynSuQT z4>}|m)+VcptY}*(9mn<6Vf*VAsDuXoY&l<+^-vw-xya*F?N~NFCc9FCMCOaut8+97 zNMKAWx_hwtQP6hbh`o07Y%~_NInVk0UV4R#Bw+vi!Kh;8dwiqAP&SJ$3d4u3G^-HF z=an?s%qC@1fDiQRXbDY*iNyQMnh0sNTZAVD^*>O|n;y&gXNm`mc0{Ata&22z8%cGu R!Nxb>XZ}JHKxJ;$BWew-3)Z6Uc2P1oQN)5H!+b4K3AZmD8EB()7aV`{_WkRpuU=lfc)nOH zmRVVVk6|x~lAjcpz%>Nrxii70v}yzbU1#MGs}{``Bhrnfngj##V!YB&3n~e3L6vG% z&RS|v@;eF+vEUQk7@rcTl#Owo#H8LD*&z>B&E-?$nbiBaj=9yWN9{qZ=i1aTj1{~l zFcHEie4ww7>8mN!#L3tr-s6Xh1ieTljbr^Otb@W`1SN;&g9k0Z=0=W!rI^sF*qMs7 z!Y&!gpJzAE4$)nRq-JGw^SNGH0m0ZQ& z0cJI~H4etF0kfy}-vDO$PXLpE^C_ER8*scg_F&l9e4v~!J!0N?Q1vdYiix}g+1Yk^ zRvPH&<<1RpmiHV*k)TTxdwbm3twCoRuRI?zDrrrp6G;f4<}*gabm-fBa>}_mrR`ge z&6HCKGoO!guj`z$ye;^#^)T{oZ-?aFVuy5{c#UElW0n;5uTTDIAO@vElg7stZPLX& zvZ139AL4~z!ooUth260@B0aT>#Iq+4SMzsHX?En7Xf?`xg>TqyjQQdyve+X#KaVFj GCqDqzIdt6s literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..d41dfb5c026d954d1502b1e5047eef25b2c9cbd4 GIT binary patch literal 2313 zcmd^B$!-%t5WVLsMtuT_$O}G1+#=wB#3r}}p;pawP2AAaJ@jH?qWnA6%Ph7Nfq)x6 z*|of`g!3-_ewV4rO~{_2%>{h8OM9< zde58q!sL*p7_nPW4zq3WhJ)CFZX$-aA#2fh;?EuK}g@EIAh=ZV{Z( zA=gtJ1(i$TW=o%~gShWaSK%^T=H5mrv1$>mD7E}^mgDTY; zHLcX3N4eii2?}lO#eD7L&i{rsNJgBkp^?Ll31ew|BG5P5lL%*gF=h$kT%} zb&pn>I61ktB|IVoi4Mt^RaAcG!A8JIJ=S^ta8G0+tAQW|xwpoq*(TL-1-}K*Uivsi z7qbvusOQ{10`&VnmV>yJh?nv!@r=<-xs~li4(ESPk@v9gQ2Xl9b7Dx9ho_Rd+N?M0 z$MY_Wzhlg1jx~(NuNiaT_TMmO^-ma+nDaS9u`M{>8GAHsY(7!WmmV>1JgD(9tqVk6 zgKV)|ElL9&eR*g_tQUFHQ51>0G_t4j&Q2zsX}s~g&#a_HosJ|?d>YUg4b!2k^U)~> z=bUzAS-ewDCCro-D;IklRF>BTzi&Sby3_TLUbomF-6uXqG0bt^Cx`bZ|7{>7N`)qk z4?EhVn|aTQjvzk78^MHyHg|l`Np z*>-_9lws$PrIKO?7iBh$!Ez8AFb#!Rf%aKZE3I=n)~vx@Ne*b*V!j5n-m>IfjJT8F ztwDd*f{CEBHv6G;D6EOjNz-h}GI~+WqQ}5|rKA>qYE261$hwK1zkT=N&9m39E?>Tw z&*zKmEFe(b3zX!)RF{w|h&pg@qf31a6$t4Yr#eE7!j>S)_fYK&qvXefOCt~%UV|>6 z<;Pno16uu%f&*c`SJBGakUAL>tgR1dtjxx$M)K%16|@ea^B1#O2ir3ruu^%}*mhQ| z=+lQO5PjqHSwKnjVanWP*qfEqomkyN>KTPe=p%0=J>(5?8i|f50{;4xepN-9GleiLa#}@o$7#j$`#h{d>Yp-2NNFEbfIc zi8!BgD7FUATkH0Qjg1G&hw?@&*Z_JskINFGtl-&Z(eGQ2U+&xxv%Kf9ibP$U*u!yW z2ZPRxybc`KJFyZ^MY`0;@P1I3iEW*4Ub%OtyT!iirZ6lo-0}~@aMbgf;K%j?E4stA zKp-$D8;RGE4@C@RC5K~o4;9!0HPi2An=bZ|)y&8?(rI9+u`#_1fC69t)$lafhe*pI*|4LnH4QaE%ohA&~-{y;HA;L5#&S%rIH`- zMca8^QG}gAIu+#G+aQu^@PXVi5eebD?VlhL@l+ zXzB7AsX@t~qUJ!D?`6~&<6|X5fVFWRm8ISoSqUDjqJ-AockXI7>tK7~JZ35`D$~xg z7QLOH0?`*v?`5hd(uYZXO0ieVp}V?ygwQi`6VXTB2)f97m{C9$sv zD*0z-N^V4J>SN(s;UNQa|1hfDlwUefea~9_ZCtYtL19>bbQP8zcFSt_SLub?-?^O`)?St zcofD&=6p%6*cu#fjos@uHts0r^Bpm7Jg8woF0+d~glC&YKXD!YI5i@cvoz^2i^yGU z*~5Nk2c6EexbqwbJh2j&Mcma$@xD`-4sDsXUfy`;yV1U@CNnG+#PVN;VXdb*!H*3F zmSu-?fk0qZR08)SACef-iU!9v9}2JrDyARKChqJbvzexm540Ro$}!}LiNWwZUK47k j73<+{{F`GX6^0#SH#PVn%C#}y941D0V-J_}*~8f{{Rf%& literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.1.json new file mode 100644 index 0000000000000000000000000000000000000000..79ad7b91fe26bf0706154a659fe13dff5bd76805 GIT binary patch literal 2898 zcmeHJOK;RL5We?UL_UE;yAM?edPBegiI?D3R9O?-+ch|L@MGyx^}jPtoOeQz+9P|| z&1T-?Z@%&ENtO`~9>_)Z4WF~@w))1mP?(@SckIHS=IDL@BMoknesl0m{OSSvI;MvLHhPdzvcM0Zjs?xt2zC~UN1 z!O@SOzI=G~{@vxcNOh&65SZT)HU1;vL(1B@#x$ygOX+T)r00>J8U+}2@8;`$>1cF)JA!K z8ykbvOo?M0yD9(ORLO%V3|~s65FQde-QG0~PEk$%U9bp05*tpd;3Jh%SZN}6bVrel z5#0EC3fx+ZC+0lZA+{3()*9nvN*oe&*hhPcR9a&z=0ov-rjq<;0PUrZT=HU+z6*JO zqGz1@V;NhAn6D

On8hL6*ZFbbvd3FycPLu@Ks4PhKP7vYIzCsk8I#`Sxi$7pY$U zZ~$({J{v;0AAl3n{{ZX(aP#N@Y{7Wdxh8vXv@mvN=t+O5o$sA9@y3H{uL#?oyR=4D zhfT9H96l_v*Q(~{lvz6>$RhJpL}p2dXmR5yuB?cBT!YmOX2bwQha#`{ecJV>!-dsV zn-JU>I>D&u}njem)JZ_E#eNyqcG(<@7bgFF1L R5IT#^IaHDv)RB|BlRxl4-zfk9 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Invoice.list.2.json new file mode 100644 index 0000000000000000000000000000000000000000..ab7c8480b1629a6c36294f6ac2bbb48387c7bce3 GIT binary patch literal 5713 zcmeHKOK;RL5We?UL_UE;yAM{V^oD=~5--862wC35+ch|L@S}98`rnyJoH$Qf)T%{d zd+6?F9-bM0GxO#0AW2BZ93v;m7xsmNTA56Yn%-**Q}wy4x^mqi`*xtU@5^ zmYFk78K~!Qe#)fWk;Y33S7zEdaI38YBcaZ< zNICUJVxU{+3VSk}6>N889b~|dQk~C&7JgdSEmmC^{9ML7OGpx-rj*;NA%r5^+(8I4 zbkp1tV2|VF*R9lVF>JHgXw|RKZeX=KhNNm=K{$oCAnslKsh6TiK0Br|W+FIiIs*X>aTx)|+O#yw3ba~aQie7)?DdLY{#c6qX zPo>~SXdE8#E<(tFZg@OIZY72*vyPcAxWfpnH2T35&?Vro4fk+TVYDenABa11D#(9$ zrkrp%OpbfuTfc6%^oVn}E3k9``m&|U8noi$dko@$=aso_O}d^o%6ViChr4jh3tfwhN9HT0&oK6r&3 zG1+?I=#tA!>>G6EJTAg2l_rEgOf>?s@|wY_S%So|i~es4nWhj*zurURpM1U5`n!do z@qW#aw3IjqD5z)Tp8n!?3iP>ORr;`#a+_h=g2;9xN$G~)I;{?y6dnd@^=Ge2}Te06;BksP*Z+wmYYXA;R_W=CQ$nWBz$#0y2 kEvmmKztGALNPd9=m#=4|FJ`TKk|Mm4k&#y}a&UF<8`31FTL1t6 literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..604c1d0167db7c86a92f51a27659086e794d9c0f GIT binary patch literal 469 zcmY+BOH0Hs6ovQw6`?Byg|rNU?nGS(zHsSgxJ|BdC6AiNz)W- z)Ckdn_1UHNMqFNBNqNDN$|0ar`y#eE>UI%h@)iS&D#z{1>-+QV)8og(y_B+>XEY=% z_gzuuuR1kG6=+Q49h=`^Z_y=jb_YQVbei0kASX8craxae6cwj}!7%R=OvH*J%><(L z_P=dX_OqYd&XZ!R%f?ivp&eSU*Eov_N@gg7)1i*};TsjV(Jl;1XlLk%fjsY@N=K!o zlDr(&Z!L065EyW}DC0AHG{6lUGmXIqSZX=F@h5{zl(_w_W{P?EU*OT69i{F1_ce#+ E7p@M0?EnA( literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..1768a0f9bb3784f56a4b93eaec1dbb0cc4037971 GIT binary patch literal 452 zcmY*WF;2uV5bX1c$TfvSfetz(D3A_Lg6^8r#$KdFjvegvNhrF%<8?xgxbt{tc4j;^ zZL44i*|U0R@9(s}5ya#e(VL%wshq4@z{Fv|D4@yd^mcfEecZnspP#hW-8^G~vAlN$ znNMX}OeWIE6ugj++0od3Fn9}b5x10#xPab?z$dtbl}lDAMB8mr8grYPuCHq< zW2ICLE)<{ZYjxROHBECNnl3S-*Lzm$l1#mbDF=s9L|xMM`Q`2D=JDbE{*GQlfioEJJ-R6AZP|^|$dMmsSun8v=86gm@ zxBqTynr`-!-v!cQ^~KuBX=#Tpm^IE}2F?sTI3A|4(0`$pI_iZ%8QlyWFjA2HBX`6t zadJMa-^y~z5Cw3$;PDwgSda#enT8l5EVZ28_+7wf%F=#2WlE+0Tj0@NJmu}b|5qKV EABKT~NdN!< literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:InvoiceItem.create.4.json new file mode 100644 index 0000000000000000000000000000000000000000..2e5300ced6c1c3f8c8e06cbaded1f74ded8f3f70 GIT binary patch literal 453 zcmY*WO-sZu5WV+Tl%73wQDpHZ=t1xU^tP8}nv5`#CN-HXNZJ4HOj5fxx4gXfX5P!( zv~34t$ez2q_GaJr{gpx{$B5qi2-xLhy9G=fMvMxYoQ|(=?=SbyPlv|`k?ktS0;9a| z1gWPeEhZCbVhUcxhwNx-KN!4)xR_f?M%RGBslg|>ghfkMB}ChCfA)QUT?dmt1u`o2 z*}0;0biy&%t=)(WvLpy`J{MXTzR+m=Zt)<-v6@a8Dd_xJN|9R7>t!2G;KUHs;PN2! sYCbv8$m>p13=x(dE*ZZwcqZ1m_hqM)hyRVO^6ZI^>wn!!3(dLt1rADm;{X5v literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.1.json new file mode 100644 index 0000000000000000000000000000000000000000..60d47536e7c17c03b5152a712f27efc6362cb64e GIT binary patch literal 826 zcmaJe?5488L!s+`eO3a8zWw!@@-X^0(msZ3&4k2b9m7p)-v`{F>^Rw2?$YCk`} z7kfWS5~aZzb(`$NJ^X<&4!zGcaTlWM6*R3$tMMRRx0YQ*EG;pbWWm7bhq#g~PK0M^ z4Z4I2OgzH)A}rSh>WzR~8?R_C;j)0RZCMT!dFg*SwWCH@r*ejjA$O(k%7K-~QSaoT zFV%m%FV4ZOzQSWV3l-_S1kbboBJsP)tJ-oA!qbx{iFF7rgtBznN7rOO(KU`Ni=^k5 zx2O5z!^izyDreFwS+~{%q(HPoL)<4QI+1bbgW5>&m!sFIX4x$3>nmh1zz~@Chm+D^ zOCDX#4af#K*MsZT7wV`p?$-&<^#(2NfT^~5;45m;B-0G*6*-kI2`%Txk)=2Bi=^Tv zj93aLz2H=M)zMhszV1tvw8g46B6tm@$5{L;G>SX6;;xCd&e~P$W9L{mI*fh*S9axJ literal 0 HcmV?d00001 diff --git a/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json b/corporate/tests/stripe_fixtures/attach_discount_to_realm:Token.create.2.json new file mode 100644 index 0000000000000000000000000000000000000000..bc7b308cf4c06107005340f9326b66c7cc95bea2 GIT binary patch literal 826 zcmaJe?5488L!s+`dTh0|_G+hNkaG{g?OR3>p(k2b9m7p)-v`{F>^Rv}WUO6=$7 z_u}kFNuo43qi&OZ*oPAsgVr7ZZBufTHKg5+}aVk73 zYtSWJVB!(R7h#z$P;Ug>+PFn?36}mrsU0=KI+HbI47n?P*AA>)j(R5# zeX0KAeR&RU^%Wk|S*S>7C3v3y7m43ZR@Ih^5T2bpNvuO~A(T#LeKaTgiLP;EStLEb zyge-*A3pByCbFizlDxGhAO)fw8sa`d(TR*Z8`MUE@JoAWuT#y_`8il2g8_!Xyg!_j z23vCJYGFWLfO9=Vz5YTSb;kE~f^)q=OFLkyZ5H^7S~STt!}E%qN|%I|v*XAnH}Q+4 z;wFq(3Z}i_iSVkUvA})ZmnvzCRc%CY4yDIf{3|qyJGSDkiMGz#RqJEtST{P1egIhV B None: # Attach discount before Stripe customer exists user = self.example_user('hamlet') - attach_discount_to_realm(user, Decimal(85)) + attach_discount_to_realm(user.realm, Decimal(85)) self.login(user.email) # Check that the discount appears in page_params self.assert_in_success_response(['85'], self.client_get("/upgrade/")) # Check that the customer was charged the discounted amount - # TODO - # Check upcoming invoice reflects the discount - # TODO + self.upgrade() + stripe_customer_id = Customer.objects.values_list('stripe_customer_id', flat=True).first() + self.assertEqual(1200 * self.seat_count, + [charge for charge in stripe.Charge.list(customer=stripe_customer_id)][0].amount) + stripe_invoice = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer_id)][0] + self.assertEqual([1200 * self.seat_count, -1200 * self.seat_count], + [item.amount for item in stripe_invoice.lines]) + # Check CustomerPlan reflects the discount + plan = CustomerPlan.objects.get(price_per_license=1200, discount=Decimal(85)) + # Attach discount to existing Stripe customer - attach_discount_to_realm(user, Decimal(25)) - # Check upcoming invoice reflects the new discount - # TODO + plan.status = CustomerPlan.ENDED + plan.save(update_fields=['status']) + attach_discount_to_realm(user.realm, Decimal(25)) + process_initial_upgrade(user, self.seat_count, True, CustomerPlan.ANNUAL, stripe_create_token().id) + self.assertEqual(6000 * self.seat_count, + [charge for charge in stripe.Charge.list(customer=stripe_customer_id)][0].amount) + stripe_invoice = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer_id)][0] + self.assertEqual([6000 * self.seat_count, -6000 * self.seat_count], + [item.amount for item in stripe_invoice.lines]) + plan = CustomerPlan.objects.get(price_per_license=6000, discount=Decimal(25)) @mock_stripe() def test_replace_payment_source(self, *mocks: Mock) -> None: @@ -923,7 +937,7 @@ class BillingHelpersTest(ZulipTestCase): def test_update_or_create_stripe_customer_logic(self) -> None: user = self.example_user('hamlet') # No existing Customer object - with patch('corporate.lib.stripe.do_create_customer', return_value='returned') as mocked1: + with patch('corporate.lib.stripe.do_create_stripe_customer', return_value='returned') as mocked1: returned = update_or_create_stripe_customer(user, stripe_token='token') mocked1.assert_called() self.assertEqual(returned, 'returned')