PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB`Mini Shell

HOME


Mini Shell 1.0
DIR:/home/htlwork.com/www/dev/pricelow/app/code/StripeIntegration/Payments/Helper/
Upload File :
Current File : //home/htlwork.com/www/dev/pricelow/app/code/StripeIntegration/Payments/Helper/Subscriptions.php
<?php

namespace StripeIntegration\Payments\Helper;

use StripeIntegration\Payments\Helper\Logger;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use StripeIntegration\Payments\Exception\SCANeededException;
use StripeIntegration\Payments\Exception\CacheInvalidationException;
use StripeIntegration\Payments\Exception\InvalidSubscriptionProduct;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\CouldNotSaveException;


class Subscriptions
{
    public $couponCodes = [];
    public $coupons = [];
    public $subscriptions = [];
    public $invoices = [];
    public $paymentIntents = [];
    public $trialingSubscriptionsAmounts = null;
    public $shippingTaxPercent = null;

    public function __construct(
        \StripeIntegration\Payments\Helper\Generic $paymentsHelper,
        \StripeIntegration\Payments\Helper\Compare $compare,
        \StripeIntegration\Payments\Helper\Address $addressHelper,
        \StripeIntegration\Payments\Model\Config $config,
        \StripeIntegration\Payments\Model\SubscriptionProductFactory $subscriptionProductFactory,
        \StripeIntegration\Payments\Model\Stripe\SubscriptionFactory $stripeSubscriptionFactory,
        \StripeIntegration\Payments\Model\ResourceModel\Subscription\CollectionFactory $subscriptionCollectionFactory,
        \StripeIntegration\Payments\Model\ResourceModel\Coupon\Collection $couponCollection,
        \Magento\ConfigurableProduct\Api\LinkManagementInterface $linkManagement,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
        \Magento\Framework\App\CacheInterface $cache,
        \Magento\Tax\Model\Sales\Order\TaxManagement $taxManagement,
        \Magento\Sales\Model\Service\InvoiceService $invoiceService,
        \Magento\Quote\Api\CartRepositoryInterface $quoteRepository,
        \Magento\Sales\Model\Order\CreditmemoFactory $creditmemoFactory,
        \Magento\Sales\Model\Service\CreditmemoService $creditmemoService,
        \Magento\Sales\Model\Order\Creditmemo\ItemFactory $creditmemoItemFactory,
        \StripeIntegration\Payments\Model\SubscriptionFactory $subscriptionFactory,
        \Magento\SalesRule\Model\CouponFactory $couponFactory,
        \StripeIntegration\Payments\Model\CouponFactory $stripeCouponFactory,
        \StripeIntegration\Payments\Helper\TaxHelper $taxHelper,
        \StripeIntegration\Payments\Helper\RecurringOrderFactory $recurringOrderFactory
    ) {
        $this->paymentsHelper = $paymentsHelper;
        $this->compare = $compare;
        $this->addressHelper = $addressHelper;
        $this->config = $config;
        $this->subscriptionProductFactory = $subscriptionProductFactory;
        $this->stripeSubscriptionFactory = $stripeSubscriptionFactory;
        $this->subscriptionCollectionFactory = $subscriptionCollectionFactory;
        $this->couponCollection = $couponCollection;
        $this->linkManagement = $linkManagement;
        $this->priceCurrency = $priceCurrency;
        $this->eventManager = $eventManager;
        $this->customer = $paymentsHelper->getCustomerModel();
        $this->cache = $cache;
        $this->taxManagement = $taxManagement;
        $this->invoiceService = $invoiceService;
        $this->quoteRepository = $quoteRepository;
        $this->creditmemoFactory = $creditmemoFactory;
        $this->creditmemoService = $creditmemoService;
        $this->creditmemoItemFactory = $creditmemoItemFactory;
        $this->subscriptionFactory = $subscriptionFactory;
        $this->couponFactory = $couponFactory;
        $this->stripeCouponFactory = $stripeCouponFactory;
        $this->taxHelper = $taxHelper;
        $this->recurringOrderFactory = $recurringOrderFactory;
    }

    public function getSubscriptionExpandParams()
    {
        return ['latest_invoice.payment_intent', 'pending_setup_intent'];
    }

    public function getSubscriptionParamsFromQuote($quote, $paymentIntentParams, $order = null)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return null;

        $subscriptions = $this->getSubscriptionsFromQuote($quote);
        $subscriptionItems = $this->getSubscriptionItemsFromQuote($quote, $subscriptions, $order);

        if (empty($subscriptionItems))
            return null;

        $stripeCustomer = $this->customer->createStripeCustomerIfNotExists();
        $this->customer->save();

        if (!$stripeCustomer)
            throw new \Exception("Could not create customer in Stripe.");

        $metadata = $subscriptionItems[0]['metadata']; // There is only one item for the entire order

        $params = [
            'customer' => $stripeCustomer->id,
            'items' => $subscriptionItems,
            'payment_behavior' => 'default_incomplete',
            'expand' => $this->getSubscriptionExpandParams(),
            'metadata' => $metadata,
            'payment_settings' => [
                'save_default_payment_method' => 'on_subscription'
            ]
        ];

        $couponId = $this->getCouponId($subscriptions);
        if ($couponId)
            $params['coupon'] = $couponId;

        if ($paymentIntentParams['amount'] > 0)
        {
            $stripeDiscountAdjustment = $this->getStripeDiscountAdjustment($subscriptions);
            $normalPrice = $this->createPriceForOneTimePayment($quote, $paymentIntentParams, $stripeDiscountAdjustment);
            $params['add_invoice_items'] = [[
                "price" => $normalPrice->id,
                "quantity" => 1
            ]];
        }

        foreach ($quote->getAllItems() as $quoteItem)
        {
            try
            {
                $product = $this->subscriptionProductFactory->create()->fromQuoteItem($quoteItem);
                if ($product->hasTrialPeriod())
                {
                    $params["trial_end"] = $product->getTrialEnd();
                    break;
                }
            }
            catch (InvalidSubscriptionProduct $e)
            {
                // Ignore non subscription products
            }

        }

        // Overwrite trial end if we are migrating the subscription from the CLI
        foreach ($subscriptions as $subscription)
        {
            if ($subscription['profile']['trial_end'])
                $params['trial_end'] = $subscription['profile']['trial_end'];
        }

        return $params;
    }

    public function filterToUpdateableParams($params)
    {
        $updateParams = [];

        if (empty($params))
            return $updateParams;

        $updateable = ['metadata', 'trial_end', 'expand'];

        foreach ($params as $key => $value)
        {
            if (in_array($key, $updateable))
                $updateParams[$key] = $value;
        }

        return $updateParams;
    }

    public function invalidateSubscription($subscription, $params)
    {
        $subscriptionItems = [];

        foreach ($params["items"] as $item)
        {
            $subscriptionItems[] = [
                "metadata" => [
                    "Type" => $item["metadata"]["Type"],
                    "SubscriptionProductIDs" => $item["metadata"]["SubscriptionProductIDs"]
                ],
                "price" => [
                    "id" => $item["price"]
                ],
                "quantity" => $item["quantity"]
            ];
        }

        $expectedValues = [
            "customer" => $params["customer"],
            "items" => [
                "data" => $subscriptionItems
            ]
        ];

        if (!empty($params['add_invoice_items']))
        {
            $oneTimeAmount = "unset";
            foreach ($params['add_invoice_items'] as $item)
            {
                $oneTimeAmount = [
                    "price" => [
                        "id" => $item["price"]
                    ],
                    "quantity" => $item["quantity"]
                ];
            }

            if (empty($subscription->latest_invoice->lines->data))
                throw new CacheInvalidationException("Non-updateable subscription details have changed: Regular items were added to the cart.");

            $hasRegularItems = false;
            foreach ($subscription->latest_invoice->lines->data as $invoiceLineItem)
            {
                if (!empty($invoiceLineItem->price->recurring->interval))
                    continue; // This is a subscription item

                $hasRegularItems = true;

                if ($this->compare->isDifferent($invoiceLineItem, $oneTimeAmount))
                {
                    throw new CacheInvalidationException("Non-updateable subscription details have changed: One time payment amount has changed.");
                }
            }

            if (!$hasRegularItems && $oneTimeAmount !== "unset")
                throw new CacheInvalidationException("Non-updateable subscription details have changed: Regular items were added to the cart.");
        }
        else
        {
            if (!empty($subscription->latest_invoice->lines->data))
            {
                foreach ($subscription->latest_invoice->lines->data as $invoiceLineItem)
                {
                    if (empty($invoiceLineItem->price->recurring->interval))
                        throw new CacheInvalidationException("Non-updateable subscription details have changed: Regular items were removed from the cart.");
                }
            }
        }

        if (!empty($params['coupon']))
        {
            $expectedValues['latest_invoice']['discount']['coupon']['id'] = $params['coupon'];
        }
        else
        {
            $expectedValues['latest_invoice']['discount'] = "unset";
        }

        if ($this->compare->isDifferent($subscription, $expectedValues))
            throw new CacheInvalidationException("Non-updateable subscription details have changed: " . $this->compare->lastReason);
    }

    public function updateSubscriptionFromQuote($quote, $subscriptionId, $paymentIntentParams)
    {
        $params = $this->getSubscriptionParamsFromQuote($quote, $paymentIntentParams);

        if (empty($params))
            return null; // The cart may not include subscriptions

        if (!$subscriptionId)
            return $this->config->getStripeClient()->subscriptions->create($params);

        try
        {
            $subscription = $this->config->getStripeClient()->subscriptions->retrieve($subscriptionId, [
                'expand' => $this->getSubscriptionExpandParams()
            ]);
        }
        catch (\Exception $e)
        {
            $this->paymentsHelper->logError("Could not retrieve subscription $subscriptionId: " . $e->getMessage());

            return $this->config->getStripeClient()->subscriptions->create($params);
        }

        try
        {
            $this->invalidateSubscription($subscription, $params);
        }
        catch (CacheInvalidationException $e)
        {
            $this->paymentsHelper->logError("Will re-create subscription: " . $e->getMessage());

            try
            {
                $this->config->getStripeClient()->subscriptions->cancel($subscriptionId);
            }
            catch (\Exception $e)
            {

            }

            return $this->config->getStripeClient()->subscriptions->create($params);
        }

        $updateParams = $this->filterToUpdateableParams($params);

        if (empty($updateParams))
            return $subscription;

        if ($this->compare->isDifferent($subscription, $updateParams))
            $subscription = $this->config->getStripeClient()->subscriptions->update($subscriptionId, $updateParams);

        return $subscription;
    }

    public function updateSubscriptionFromOrder($order, $subscriptionId, $paymentIntentParams)
    {
        $quote = $this->paymentsHelper->loadQuoteById($order->getQuoteId());

        if (empty($quote) || !$quote->getId())
            throw new \Exception("The quote for this order could not be loaded");

        $params = $this->getSubscriptionParamsFromQuote($quote, $paymentIntentParams, $order);

        if (empty($params))
            return null;

        if (!$subscriptionId)
        {
            $subscription = $this->config->getStripeClient()->subscriptions->create($params);
            $this->updateSubscriptionEntry($subscription, $order);
            return $subscription;
        }

        $subscription = $this->config->getStripeClient()->subscriptions->retrieve($subscriptionId, [
            'expand' => $this->getSubscriptionExpandParams()
        ]);

        try
        {
            if (!$order->getPayment()->getAdditionalInformation('is_migrated_subscription'))
                $this->invalidateSubscription($subscription, $params);
        }
        catch (CacheInvalidationException $e)
        {
            $this->paymentsHelper->logError($e->getMessage());
            throw new LocalizedException(__("The cart details have changed. Please refresh the page and try again."));
        }

        $updateParams = $this->filterToUpdateableParams($params);

        if (empty($updateParams))
        {
            $this->updateSubscriptionEntry($subscription, $order);
            return $subscription;
        }

        if ($this->compare->isDifferent($subscription, $updateParams))
            $subscription = $this->config->getStripeClient()->subscriptions->update($subscriptionId, $updateParams);

        if (!empty($subscription->latest_invoice->payment_intent->id))
        {
            $params = [];
            $params["description"] = $this->paymentsHelper->getOrderDescription($order);
            $params["metadata"] = $this->config->getMetadata($order);
            $shipping = $this->addressHelper->getShippingAddressFromOrder($order);
            if ($shipping)
                $params['shipping'] = $shipping;

            $paymentIntent = $this->config->getStripeClient()->paymentIntents->update($subscription->latest_invoice->payment_intent->id, $params);
            $subscription->latest_invoice->payment_intent = $paymentIntent;
        }

        $this->updateSubscriptionEntry($subscription, $order);

        return $subscription;
    }

    public function getSubscriptionItemsFromQuote($quote, $subscriptions, $order = null)
    {
        if (empty($subscriptions))
            return null;

        if (!$this->renewTogether($subscriptions))
            throw new LocalizedException(__("Subscriptions that do not renew together must be bought separately."));

        $recurringPrice = $this->createSubscriptionPriceForSubscriptions($quote, $subscriptions);

        $items = [];
        $metadata = $this->collectMetadataForSubscriptions($quote, $subscriptions, $order);

        $items[] = [
            "metadata" => $metadata,
            "price" => $recurringPrice->id,
            "quantity" => 1
        ];

        return $items;
    }

    /**
     * Returns array [
     *   [
     *     \Magento\Catalog\Model\Product,
     *     \Magento\Sales\Model\Quote\Item,
     *     array $profile
     *   ],
     *   ...
     * ]
     */
    public function getSubscriptionsFromQuote($quote)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return [];

        $items = $quote->getAllItems();
        $subscriptions = [];

        foreach ($items as $item)
        {
            $product = $this->paymentsHelper->getSubscriptionProductFromQuoteItem($item);
            if (!$product)
                continue;

            $subscriptions[] = [
                'product' => $product,
                'quote_item' => $item,
                'profile' => $this->getSubscriptionDetails($product, $quote, $item)
            ];
        }

        return $subscriptions;
    }

    /**
     * Returns array [
     *   [
     *     \Magento\Catalog\Model\Product,
     *     \Magento\Sales\Model\Order\Item,
     *     array $profile
     *   ],
     *   ...
     * ]
     */
    public function getSubscriptionsFromOrder($order)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return [];

        $items = $order->getAllItems();
        $subscriptions = [];

        foreach ($items as $item)
        {
            $product = $this->paymentsHelper->getSubscriptionProductFromOrderItem($item);
            if (!$product)
                continue;

            $subscriptions[$item->getQuoteItemId()] = [
                'product' => $product,
                'order_item' => $item,
                'profile' => $this->getSubscriptionDetails($product, $order, $item)
            ];
        }

        return $subscriptions;
    }

    public function getSubscriptionIntervalKeyFromProduct($product)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return null;

        if (!$this->isSubscriptionProduct($product))
            return null;

        $key = '';
        $trialDays = $this->getTrialDays($product);
        if ($trialDays > 0)
            $key .= "trial_" . $trialDays . "_";

        $interval = $product->getStripeSubInterval();
        $intervalCount = $product->getStripeSubIntervalCount();

        if ($interval && $intervalCount && $intervalCount > 0)
            $key .= $interval . "_" . $intervalCount;

        return $key;
    }

    public function getQuote()
    {
        $quote = $this->paymentsHelper->getQuote();
        $createdAt = $quote->getCreatedAt();
        if (empty($createdAt)) // case of admin orders
        {
            $quoteId = $quote->getQuoteId();
            $quote = $this->paymentsHelper->loadQuoteById($quoteId);
        }
        return $quote;
    }

    public function isOrder($order)
    {
        if (!empty($order->getOrderCurrencyCode()))
            return true;

        return false;
    }

    public function getSubscriptionDetails($product, $order, $item)
    {
        // Get billing interval and billing period
        $interval = $product->getStripeSubInterval();
        $intervalCount = $product->getStripeSubIntervalCount();

        if (!$interval)
            throw new \Exception("An interval period has not been specified for the subscription");

        if (!$intervalCount)
            $intervalCount = 1;

        $name = $item->getName();
        $qty = max(/* quote */ $item->getQty(), /* order */ $item->getQtyOrdered());
        $originalItem = $item;
        $item = $this->paymentsHelper->getSubscriptionQuoteItemWithTotalsFrom($item, $order);

        // For subscription migrations via the CLI, we set the trial period manually
        if ($order->getPayment() && $order->getPayment()->getAdditionalInformation("subscription_start"))
        {
            $trialEnd = $order->getPayment()->getAdditionalInformation("subscription_start");
            if (!is_numeric($trialEnd) || $trialEnd < 0)
                $trialEnd = null;
        }
        else
            $trialEnd = null;

        // Get the subscription currency and amount
        $initialFee = $product->getStripeSubInitialFee();

        if (!is_numeric($initialFee))
            $initialFee = 0;

        if ($this->config->priceIncludesTax())
            $amount = $item->getPriceInclTax();
        else
            $amount = $item->getPrice();

        $discount = $item->getDiscountAmount();
        $tax = $item->getTaxAmount();

        if ($this->isOrder($order))
        {
            $currency = $order->getOrderCurrencyCode();
            $rate = $order->getBaseToOrderRate();
        }
        else
        {
            $currency = $order->getQuoteCurrencyCode();
            $rate = $order->getBaseToQuoteRate();
        }

        $baseDiscount = $item->getBaseDiscountAmount();
        $baseTax = $item->getBaseTaxAmount();
        $baseCurrency = $order->getBaseCurrencyCode();
        $baseShippingTaxAmount = 0;
        $baseShipping = 0;

        // This seems to be a Magento multi-currency bug, tested in v2.3.2
        if (is_numeric($rate) && $rate > 0 && $rate != 1 && $amount == $item->getBasePrice())
            $amount = round(floatval($amount * $rate), 2); // We fix it by doing the calculation ourselves

        if (is_numeric($rate) && $rate > 0)
            $initialFee = round(floatval($initialFee * $rate), 2);

        if ($this->isOrder($order))
        {
            $quote = $this->paymentsHelper->getQuoteFromOrder($order);
            $quoteItem = null;
            if (!$quote || !$quote->getId())
            {
                $quote = $this->createQuoteFromOrder($order);
            }

            foreach ($quote->getAllItems() as $qItem)
            {
                if ($qItem->getSku() == $item->getSku())
                {
                    $quoteItem = $qItem;

                    if ($quoteItem->getParentItemId() && $originalItem->getParentItem() && $originalItem->getParentItem()->getProductType() == "configurable")
                    {
                        $qty = $item->getQtyOrdered() * $quoteItem->getQty();
                        $quoteItem->setQtyCalculated($qty);
                    }
                }
            }

            if ($item->getShippingAmount())
            {
                $shipping = $item->getShippingAmount();
            }
            else if ($item->getBaseShippingAmount())
            {
                $shipping = $this->paymentsHelper->convertBaseAmountToStoreAmount($item->getBaseShippingAmount());
            }
            else
            {
                $baseShipping = $this->taxHelper->getBaseShippingAmountForQuoteItem($quoteItem, $quote);
                $shipping = $this->paymentsHelper->convertBaseAmountToStoreAmount($baseShipping);
            }

            $orderShippingAmount = $order->getShippingAmount();
            $orderShippingTaxAmount = $order->getShippingTaxAmount();
            $shippingTaxPercent = $this->taxHelper->getTaxPercentForOrder($order->getId(), "shipping");

            if ($orderShippingAmount == $shipping)
            {
                $shippingTaxAmount = $orderShippingTaxAmount;
            }
            else
            {
                $shippingTaxAmount = 0;

                if ($shippingTaxPercent && is_numeric($shippingTaxPercent) && $shippingTaxPercent > 0)
                {
                    if ($this->config->shippingIncludesTax())
                        $shippingTaxAmount = $this->taxHelper->taxInclusiveTaxCalculator($shipping, $shippingTaxPercent);
                    else
                        $shippingTaxAmount = $this->taxHelper->taxExclusiveTaxCalculator($shipping, $shippingTaxPercent);
                }
            }
        }
        else
        {
            $quote = $order;
            $quoteItem = $item;

            // Case for configurable and bundled subscriptions
            if ($quoteItem->getProductType() != $originalItem->getProductType())
            {
                $qty = $quoteItem->getQty();
                $name = $quoteItem->getName();
            }

            $baseShipping = $this->taxHelper->getBaseShippingAmountForQuoteItem($quoteItem, $quote);
            $shippingTaxRate = $this->taxHelper->getShippingTaxRateFromQuote($quote);
            $shipping = $this->paymentsHelper->convertBaseAmountToStoreAmount($baseShipping);

            $shippingTaxAmount = 0;
            $shippingTaxPercent = 0;

            if ($shipping > 0 && $shippingTaxRate)
            {
                $shippingTaxPercent = $shippingTaxRate["percent"];
                $shippingTaxAmount = $shippingTaxRate["amount"];
                $baseShippingTaxAmount = $shippingTaxRate["base_amount"];
            }
        }

        if (!is_numeric($amount))
            $amount = 0;

        if ($order->getPayment()->getAdditionalInformation("remove_initial_fee"))
            $initialFee = 0;

        if ($this->config->priceIncludesTax())
            $initialFeeTaxAmount = $this->taxHelper->taxInclusiveTaxCalculator($initialFee * $qty, $item->getTaxPercent());
        else
            $initialFeeTaxAmount = $this->taxHelper->taxExclusiveTaxCalculator($initialFee * $qty, $item->getTaxPercent());

        $params = [
            'name' => $name,
            'qty' => $qty,
            'interval' => $interval,
            'interval_count' => $intervalCount,
            'amount_magento' => $amount,
            'amount_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($amount, $currency),
            'initial_fee_stripe' => 0,
            'initial_fee_magento' => 0,
            'discount_amount_magento' => $discount,
            'base_discount_amount_magento' => $baseDiscount,
            'discount_amount_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($discount, $currency),
            'shipping_magento' => round(floatval($shipping), 2),
            'base_shipping_magento' => round(floatval($baseShipping), 2),
            'shipping_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($shipping, $currency),
            'currency' => strtolower($currency),
            'base_currency' => strtolower($baseCurrency),
            'tax_percent' => $item->getTaxPercent(),
            'tax_percent_shipping' => $shippingTaxPercent,
            'tax_amount_item' => $tax, // already takes $qty into account
            'base_tax_amount_item' => $baseTax, // already takes $qty into account
            'tax_amount_item_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($tax, $currency), // already takes $qty into account
            'tax_amount_shipping' => $shippingTaxAmount,
            'base_tax_amount_shipping' => $baseShippingTaxAmount,
            'tax_amount_shipping_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($shippingTaxAmount, $currency),
            'tax_amount_initial_fee' => $initialFeeTaxAmount,
            'tax_amount_initial_fee_stripe' => $this->paymentsHelper->convertMagentoAmountToStripeAmount($initialFeeTaxAmount, $currency),
            'trial_end' => $trialEnd,
            'trial_days' => 0,
            'expiring_coupon' => null,
            'product_id' => $product->getId()
        ];

        if (!$trialEnd)
        {
            // The following should not be used with subscriptions which are migrated via the CLI tool.
            $params['trial_days'] = $this->getTrialDays($product);
            $params['expiring_coupon'] = $this->getExpiringCoupon($discount, $order); // @todo - we should check if the coupon is still active and recreate a custom one with less duration_in_months
            $params['initial_fee_stripe'] = $this->paymentsHelper->convertMagentoAmountToStripeAmount($initialFee, $currency);
            $params['initial_fee_magento'] = $initialFee;
        }

        return $params;
    }

    public function getTrialDays($product)
    {
        $trialDays = $product->getStripeSubTrial();
        if (!empty($trialDays) && is_numeric($trialDays) && $trialDays > 0)
            return $trialDays;

        return 0;
    }

    public function getExpiringCoupon($discountAmount, $order)
    {
        if ($discountAmount <= 0)
            return null;

        $appliedRuleIds = $order->getAppliedRuleIds();
        if (empty($appliedRuleIds))
            return null;

        $appliedRuleIds = explode(",", $appliedRuleIds);

        $foundCoupons = [];
        foreach ($appliedRuleIds as $ruleId)
        {
            $coupon = $this->couponCollection->getByRuleId($ruleId);
            if ($coupon)
                $foundCoupons[] = $coupon;
        }

        if (empty($foundCoupons))
            return null;

        if (count($foundCoupons) > 1)
        {
            $this->paymentsHelper->logError("Could not apply discount coupon: Multiple cart price rules were applied on the cart. Only one can be applied on subscription carts.");
            return null;
        }

        return $foundCoupons[0]->getData();
    }

    public function getCouponId($subscriptions)
    {
        if (empty($subscriptions))
            return null;

        $amount = 0;
        $currency = null;
        $coupon = null;

        foreach ($subscriptions as $subscription)
        {
            $profile = $subscription['profile'];
            $coupon = $profile['expiring_coupon'];

            if (empty($coupon['coupon_duration']))
                return null;

            $amount += $profile['discount_amount_stripe'];
            $currency = $profile['currency'];
        }

        if (!$coupon)
            return null;

        $couponId = ((string)$amount) . strtoupper($currency);

        switch ($coupon['coupon_duration'])
        {
            case 'repeating':
                $couponId .= "-months-" . $coupon['coupon_months'];
                break;
            case 'once':
                $couponId .= "-once";
                break;
        }

        if (!empty($this->coupons[$couponId]))
        {
            return $couponId;
        }

        try
        {
            $this->coupons[$couponId] = \Stripe\Coupon::retrieve($couponId);
            return $couponId;
        }
        catch (\Exception $e)
        {
            try
            {
                $params = [
                    'id' => $couponId,
                    'amount_off' => $amount,
                    'currency' => $currency,
                    'name' => "Discount",
                    'duration' => $coupon['coupon_duration']
                ];

                if ($coupon['coupon_duration'] == "repeating" && !empty($coupon['coupon_months']))
                {
                    $params['duration_in_months'] = $coupon['coupon_months'];
                }

                $this->coupons[$couponId] = \Stripe\Coupon::create($params);
                return $couponId;
            }
            catch (\Exception $e)
            {
                $this->paymentsHelper->logError($e->getMessage(), $e->getTraceAsString());
                return null;
            }
        }
    }

    public function getSubscriptionTotalFromProfile($profile)
    {
        $subscriptionTotal =
            ($profile['qty'] * $profile['amount_magento']) +
            $profile['shipping_magento'] -
            $profile['discount_amount_magento'];

        if (!$this->config->shippingIncludesTax())
            $subscriptionTotal += $profile['tax_amount_shipping']; // Includes qty calculation

        if (!$this->config->priceIncludesTax())
            $subscriptionTotal += $profile['tax_amount_item']; // Includes qty calculation

        return round(floatval($subscriptionTotal), 2);
    }

    // We increase the subscription price by the amount of the discount, so that we can apply
    // a discount coupon on the amount and go back to the original amount AFTER the discount is applied
    public function getSubscriptionTotalWithDiscountAdjustmentFromProfile($profile)
    {
        $total = $this->getSubscriptionTotalFromProfile($profile);

        if (!empty($profile['expiring_coupon']))
            $total += $profile['discount_amount_magento'];

        return $total;
    }

    public function getStripeDiscountAdjustment($subscriptions)
    {
        $adjustment = 0;

        foreach ($subscriptions as $subscription)
        {
            $profile = $subscription['profile'];

            // This calculation only applies to MixedTrial carts
            if (!$profile['trial_days'])
                return 0;

            if (!empty($profile['expiring_coupon']))
                $adjustment += $profile['discount_amount_stripe'];
        }

        return $adjustment;
    }

    public function updateSubscriptionEntry($subscription, $order)
    {
        $entry = $this->subscriptionFactory->create();
        $entry->load($subscription->id, 'subscription_id');
        $entry->initFrom($subscription, $order);
        $entry->save();
        return $entry;
    }

    public function findSubscriptionItem($sub)
    {
        if (empty($sub->items->data))
            return null;

        foreach ($sub->items->data as $item)
        {
            if (!empty($item->price->product->metadata->{"Type"}) && $item->price->product->metadata->{"Type"} == "Product" && $item->price->type == "recurring")
                return $item;
        }

        return null;
    }

    public function isStripeCheckoutSubscription($sub)
    {
        if (empty($sub->metadata->{"Order #"}))
            return false;

        $order = $this->paymentsHelper->loadOrderByIncrementId($sub->metadata->{"Order #"});

        if (!$order || !$order->getId())
            return false;

        return $this->paymentsHelper->isStripeCheckoutMethod($order->getPayment()->getMethod());
    }

    public function formatSubscriptionName($sub)
    {
        $name = "";

        if (empty($sub))
            return "Unknown subscription (err: 1)";

        // Subscription Items
        if ($this->isStripeCheckoutSubscription($sub))
        {
            $item =  $this->findSubscriptionItem($sub);

            if (!$item)
                return "Unknown subscription (err: 2)";

            if (!empty($item->price->product->name))
                $name = $item->price->product->name;
            else
                return "Unknown subscription (err: 3)";

            $currency = $item->price->currency;
            $amount = $item->price->unit_amount;
            $quantity = $item->quantity;
        }
        // Invoice Items
        else
        {
            if (!empty($sub->plan->name))
                $name = $sub->plan->name;

            if (empty($name) && isset($sub->plan->product) && is_numeric($sub->plan->product))
            {
                $product = $this->paymentsHelper->loadProductById($sub->plan->product);
                if ($product && $product->getName())
                    $name = $product->getName();
            }
            else
                return "Unknown subscription (err: 4)";

            $currency = $sub->plan->currency;
            $amount = $sub->plan->amount;
            $quantity = $sub->quantity;
        }

        $precision = PriceCurrencyInterface::DEFAULT_PRECISION;
        $cents = 100;
        $qty = '';

        if ($this->paymentsHelper->isZeroDecimal($currency))
        {
            $cents = 1;
            $precision = 0;
        }

        $amount = $amount / $cents;

        if ($quantity > 1)
        {
            $qty = " x " . $quantity;
        }

        $this->priceCurrency->getCurrency()->setCurrencyCode(strtoupper($currency));
        $cost = $this->priceCurrency->format($amount, false, $precision);

        return "$name ($cost$qty)";
    }

    public function getSubscriptionsName($subscriptions)
    {
        $productNames = [];

        foreach ($subscriptions as $subscription)
        {
            $profile = $subscription['profile'];

            if ($profile['qty'] > 1)
                $productNames[] = $profile['qty'] . " x " . $profile['name'];
            else
                $productNames[] = $profile['name'];
        }

        $productName = implode(", ", $productNames);

        $productName = substr($productName, 0, 250);

        return $productName;
    }

    public function createSubscriptionPriceForSubscriptions(\Magento\Quote\Api\Data\CartInterface $quote, $subscriptions)
    {
        if (empty($quote->getId()))
            $quote = $this->paymentsHelper->saveQuote($quote);

        if (empty($quote->getId()))
            throw new \Exception("Cannot create subscription price from a quote with no ID.");

        if (empty($subscriptions))
            throw new \Exception("No subscriptions specified");

        $combinedProfile = $this->getCombinedProfileFromSubscriptions($subscriptions);

        $productNames = [];
        $interval = $combinedProfile['interval'];
        $intervalCount = $combinedProfile['interval_count'];
        $currency = $combinedProfile['currency'];
        $productName = $combinedProfile['name'];

        if ($this->paymentsHelper->isMultiShipping())
            throw new \Exception("Price ID for multi-shipping subscriptions is not implemented", 1);

        $priceId = $quote->getId();

        $productData = [
            "name" => $productName
        ];

        $priceData = ([
            'unit_amount' => $combinedProfile['stripe_amount'],
            'currency' => $currency,
            'recurring' => [
                'interval' => $interval,
                'interval_count' => $intervalCount
            ],
            'product_data' => $productData,
        ]);

        $key = "price_data_quote_" . $quote->getId();

        try
        {
            $oldData = $this->cache->load($key);
            if (empty($oldData))
                throw new \Exception("Not found");

            $oldData = json_decode($oldData, true);
            if (empty($oldData["price_id"]) || empty($oldData["price_data"]))
                throw new \Exception("Invalid data");

            if ($this->compare->isDifferent($oldData["price_data"], $priceData))
                throw new \Exception("Price has changed");

            return $this->config->getStripeClient()->prices->retrieve($oldData["price_id"]);
        }
        catch (\Exception $e)
        {

        }

        $stripePrice = $this->config->getStripeClient()->prices->create($priceData);

        $data = [
            "price_id" => $stripePrice->id,
            "price_data" => $priceData
        ];
        $this->cache->save(json_encode($data), $key, $tags = ["unconfirmed_subscriptions"], $lifetime = 2 * 60 * 60);

        return $stripePrice;
    }


    public function createPriceForOneTimePayment($quote, $paymentIntentParams, $stripeDiscountAdjustment = 0)
    {
        if (empty($quote->getId()))
            $quote = $this->paymentsHelper->saveQuote($quote);

        if (empty($quote->getId()))
            throw new \Exception("Cannot create price from a quote with no ID.");

        $productData = [
            "name" => __("One time payment")
        ];

        $currency = $paymentIntentParams['currency'];
        $totalAmount = $paymentIntentParams['amount'] + $stripeDiscountAdjustment;

        $priceData = ([
            'unit_amount' => $totalAmount,
            'currency' => $currency,
            'product_data' => $productData,
        ]);

        $key = "price_data_quote_once_" . $quote->getId();

        try
        {
            $oldData = $this->cache->load($key);
            if (empty($oldData))
                throw new \Exception("Not found");

            $oldData = json_decode($oldData, true);
            if (empty($oldData["price_id"]) || empty($oldData["price_data"]))
                throw new \Exception("Invalid data");

            if ($this->compare->isDifferent($oldData["price_data"], $priceData))
                throw new \Exception("Price has changed");

            return $this->config->getStripeClient()->prices->retrieve($oldData["price_id"]);
        }
        catch (\Exception $e)
        {

        }

        $stripePrice = $this->config->getStripeClient()->prices->create($priceData);

        $data = [
            "price_id" => $stripePrice->id,
            "price_data" => $priceData
        ];
        $this->cache->save(json_encode($data), $key, $tags = ["unconfirmed_subscriptions"], $lifetime = 2 * 60 * 60);

        return $stripePrice;
    }

    public function collectMetadataForSubscriptions($quote, $subscriptions, $order = null)
    {
        $subscriptionProductIds = [];

        foreach ($subscriptions as $subscription)
        {
            $product = $subscription['product'];
            $profile = $subscription['profile'];
            $subscriptionProductIds[] = $profile['product_id'];
        }

        if (empty($subscriptionProductIds))
            throw new \Exception("Could not find any subscription product IDs in cart subscriptions.");

        $metadata = [
            "Type" => "SubscriptionsTotal",
            "SubscriptionProductIDs" => implode(",", $subscriptionProductIds)
        ];

        if ($order && $order->getIncrementId())
            $metadata["Order #"] = $order->getIncrementId();
        else if ($quote->getReservedOrderId())
            $metadata["Order #"] = $quote->getReservedOrderId();

        return $metadata;
    }

    public function getTrialingSubscriptionsAmounts($quote = null)
    {
        if ($this->trialingSubscriptionsAmounts)
            return $this->trialingSubscriptionsAmounts;

        if (!$quote)
            $quote = $this->paymentsHelper->getQuote();

        $trialingSubscriptionsAmounts = [
            "subscriptions_total" => 0,
            "base_subscriptions_total" => 0,
            "shipping_total" => 0,
            "base_shipping_total" => 0,
            "discount_total" => 0,
            "base_discount_total" => 0,
            "tax_total" => 0,
            "base_tax_total" => 0
        ];

        if (!$quote)
            return $trialingSubscriptionsAmounts;

        $this->trialingSubscriptionsAmounts = $trialingSubscriptionsAmounts;

        $items = $quote->getAllItems();
        foreach ($items as $item)
        {
            $product = $this->paymentsHelper->getSubscriptionProductFromOrderItem($item);

            if (!$this->isSubscriptionProduct($product))
                continue;

            $trial = $product->getStripeSubTrial();
            if (is_numeric($trial) && $trial > 0)
            {
                $item = $this->paymentsHelper->getSubscriptionQuoteItemWithTotalsFrom($item, $quote);

                $profile = $this->getSubscriptionDetails($product, $quote, $item);

                $shipping = $profile["shipping_magento"];
                $baseShipping = $profile["base_shipping_magento"];
                if ($this->config->shippingIncludesTax())
                {
                    // $shipping -= $profile["tax_amount_shipping"];
                    // $baseShipping -= $baseProfile["tax_amount_shipping"];
                }

                $subtotal = $item->getRowTotal();
                $baseSubtotal = $item->getBaseRowTotal();
                if ($this->config->priceIncludesTax())
                {
                    $subtotal = $item->getRowTotalInclTax();
                    $baseSubtotal = $item->getBaseRowTotalInclTax();
                }

                $baseDiscountTotal = $profile["base_discount_amount_magento"];
                $baseTaxAmountItem = $profile["base_tax_amount_item"];
                $baseTaxAmountShipping = $profile["base_tax_amount_shipping"];

                $this->trialingSubscriptionsAmounts["subscriptions_total"] += $subtotal;
                $this->trialingSubscriptionsAmounts["base_subscriptions_total"] += $baseSubtotal;
                $this->trialingSubscriptionsAmounts["shipping_total"] += $shipping;
                $this->trialingSubscriptionsAmounts["base_shipping_total"] += $baseShipping;
                $this->trialingSubscriptionsAmounts["discount_total"] += $profile["discount_amount_magento"];
                $this->trialingSubscriptionsAmounts["base_discount_total"] += $baseDiscountTotal;
                $this->trialingSubscriptionsAmounts["tax_total"] += $profile["tax_amount_item"] + $profile["tax_amount_shipping"];
                $this->trialingSubscriptionsAmounts["base_tax_total"] += $baseTaxAmountItem + $baseTaxAmountShipping;

                $inclusiveTax = $baseInclusiveTax = 0;
                if ($this->config->shippingIncludesTax())
                {
                    $inclusiveTax += $profile["tax_amount_shipping"];
                    $baseInclusiveTax = $profile["base_tax_amount_shipping"];
                }

                if ($this->config->priceIncludesTax())
                {
                    $inclusiveTax += $profile["tax_amount_item"];
                    $baseInclusiveTax = $profile["base_tax_amount_item"];
                }
                $this->trialingSubscriptionsAmounts["tax_inclusive"] = $inclusiveTax;
                $this->trialingSubscriptionsAmounts["base_tax_inclusive"] = $baseInclusiveTax;
            }
        }

        return $this->trialingSubscriptionsAmounts;
    }

    public function formatInterval($stripeAmount, $currency, $intervalCount, $intervalUnit)
    {
        $amount = $this->paymentsHelper->formatStripePrice($stripeAmount, $currency);

        if ($intervalCount > 1)
            return __("%1 every %2 %3", $amount, $intervalCount, $intervalUnit . "s");
        else
            return __("%1 every %2", $amount, $intervalUnit);
    }

    public function renewTogether($subscriptions)
    {
        $startingTimes = [];
        $endingTimes = [];
        $now = time();

        foreach ($subscriptions as $subscription)
        {
            $starts = $now;
            if (!empty($subscription['profile']['trial_end']))
                $starts = $subscription['profile']['trial_end'];
            else if (!empty($subscription['profile']['trial_days']))
                $starts = strtotime("+" . $subscription['profile']['trial_days'] . " days", $now);

            $ends = $starts + strtotime("+" . $subscription['profile']['interval_count'] . " " . $subscription['profile']['interval']);

            $startingTimes[$starts] = $starts;
            $endingTimes[$ends] = $ends;
        }

        if (count($startingTimes) > 1)
            return false;

        if (count($endingTimes) > 1)
            return false;

        return true;
    }

    public function hasMultipleSubscriptionProducts(array $products)
    {
        if (!$this->paymentsHelper->isSubscriptionsEnabled())
            return false;

        $found = false;

        foreach ($products as $product)
        {
            if (!$this->isSubscriptionProduct($product))
                continue;

            if ($found)
                return true;

            $found = true;
        }

        return false;
    }

    public function checkIfAddToCartIsSupported(
        \Magento\Quote\Model\Quote $quote,
        ?\Magento\Catalog\Model\Product $product)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return;

        if (!$this->isSubscriptionProduct($product))
            return;

        $products = [ $product ];

        foreach ($quote->getAllItems() as $quoteItem)
        {
            if (is_numeric($quoteItem->getProductId()))
            {
                $product = $this->paymentsHelper->loadProductById($quoteItem->getProductId());
                if ($product && $product->getId())
                {
                    $products[] = $product;
                }
            }
        }

        if ($this->hasMultipleSubscriptionProducts($products))
        {
            throw new LocalizedException(__("Only one subscription is allowed per order."));
        }
    }

    public function getTrialSubscriptionsFrom($items)
    {
        $results = [];

        if (!$this->config->isSubscriptionsEnabled())
            return $results;

        foreach ($items as $item)
        {
            $product = $this->paymentsHelper->getSubscriptionProductFromOrderItem($item);
            if (!$product)
                continue;

            $trial = $product->getStripeSubTrial();
            if (is_numeric($trial) && $trial > 0)
            {
                $results[] = [
                    'order_item' => $item,
                    'product' => $product
                ];
            }
        }

        return $results;
    }

    public function createQuoteFromOrder($originalOrder)
    {
        $recurringOrder = $this->recurringOrderFactory->create();
        $quote = $recurringOrder->createQuoteFrom($originalOrder);
        $recurringOrder->setQuoteCustomerFrom($originalOrder, $quote);
        $recurringOrder->setQuoteAddressesFrom($originalOrder, $quote);

        $invoiceDetails = [
            'products' => []
        ];

        foreach ($originalOrder->getAllItems() as $orderItem)
        {
            $product = $this->paymentsHelper->loadProductById($orderItem->getProductId());

            if ($this->isSubscriptionProduct($product))
            {
                $invoiceDetails['products'][$orderItem->getProductId()] = [
                    'amount' => $orderItem->getPrice(),
                    'base_amount' => $orderItem->getBasePrice(),
                    'qty' => $orderItem->getQtyOrdered()
                ];
            }
        }

        if (empty($invoiceDetails['products']))
        {
            throw new \Exception("Order #" . $originalOrder->getIncrementId() . " does not include any subscriptions.");
        }

        $recurringOrder->setQuoteItemsFrom($originalOrder, $invoiceDetails, $quote);
        $recurringOrder->setQuoteShippingMethodFrom($originalOrder, $quote);
        $recurringOrder->setQuoteDiscountFrom($originalOrder, $quote);
        $recurringOrder->setQuotePaymentMethodFrom($originalOrder, $quote);

        // Collect Totals & Save Quote
        $quote->setTotalsCollectedFlag(false)->collectTotals();
        return $quote;
    }

    public function getSubscriptionProductIDs(\Stripe\Subscription $subscription)
    {
        $productIDs = [];

        if (isset($subscription->metadata->{"Product ID"}))
        {
            $productIDs = explode(",", $subscription->metadata->{"Product ID"});
        }
        else if (isset($subscription->metadata->{"SubscriptionProductIDs"}))
        {
            $productIDs = explode(",", $subscription->metadata->{"SubscriptionProductIDs"});
        }

        return $productIDs;
    }

    public function getSubscriptionOrderID(\Stripe\Subscription $subscription)
    {
        if (isset($subscription->metadata->{"Order #"}))
        {
            return $subscription->metadata->{"Order #"};
        }

        return null;
    }

    public function isSubscriptionProduct(
        ?\Magento\Catalog\Api\Data\ProductInterface $product
    )
    {
        if (!$product || !$product->getId())
            return false;

        if (!$product->getStripeSubEnabled())
            return false;

        $productType = $product->getTypeId();
        if (!in_array($productType, ['simple', 'virtual']))
            return false;

        $interval = $product->getStripeSubInterval();
        $intervalCount = $product->getStripeSubIntervalCount();

        if (!$interval)
            return false;

        if (!$intervalCount || !is_numeric($intervalCount))
            return false;

        if ($intervalCount <= 0)
            return false;

        return true;
    }

    public function getConfigurableSubscriptionProductFrom(\Stripe\Subscription $subscription)
    {
        $orderIncrementId = $this->getSubscriptionOrderID($subscription);

        $order = $this->paymentsHelper->loadOrderByIncrementId($orderIncrementId);
        if (!$order)
            return null;

        $productIds = $this->getSubscriptionProductIDs($subscription);

        foreach ($order->getAllItems() as $orderItem)
        {
            if (in_array($orderItem->getProductId(), $productIds))
            {
                if ($orderItem->getParentItem() && $orderItem->getParentItem()->getProductType() == "configurable")
                {
                    $this->configurableProductIds[$subscription->id][$orderItem->getProductId()] = $orderItem->getParentItem()->getProductId();
                }
            }
        }

        foreach ($this->configurableProductIds[$subscription->id] as $productId)
        {
            return $this->paymentsHelper->loadProductById($productId);
        }

        return null;
    }

    public function getConfigurableSubscriptionQty(\Stripe\Subscription $subscription): int
    {
        $orderIncrementId = $this->getSubscriptionOrderID($subscription);
        $order = $this->paymentsHelper->loadOrderByIncrementId($orderIncrementId);
        if (!$order)
            return 0;

        $product = $this->getConfigurableSubscriptionProductFrom($subscription);
        if (!$product)
            return 0;

        foreach ($order->getAllItems() as $orderItem)
        {
            if ($orderItem->getProductId() == $product->getId())
                return (int)$orderItem->getQtyOrdered();
        }

        return 0;
    }

    public function getConfigurableSubscriptionSuperAttribute($subscription)
    {
        $orderIncrementId = $this->getSubscriptionOrderID($subscription);
        $order = $this->paymentsHelper->loadOrderByIncrementId($orderIncrementId);
        if (!$order)
            return null;

        $configurableProduct = $this->getConfigurableSubscriptionProductFrom($subscription);
        if ($configurableProduct)
        {
            $allowAttributes = $configurableProduct->getTypeInstance()->getConfigurableAttributes($configurableProduct);
            foreach ($allowAttributes as $attribute)
            {
                return $attribute;
            }
        }

        return null;
    }

    public function getInvoiceAmount(\Stripe\Subscription $subscription)
    {
        $total = 0;
        $currency = null;

        if (empty($subscription->items->data))
            return __("Billed");

        foreach ($subscription->items->data as $item)
        {
            $amount = 0;
            $qty = $item->quantity;

            if (!empty($item->price->type) && $item->price->type != "recurring")
                continue;

            if (!empty($item->price->unit_amount))
                $amount = $qty * $item->price->unit_amount;

            if (!empty($item->price->currency))
                $currency = $item->price->currency;

            if (!empty($item->tax_rates[0]->percentage))
            {
                $rate = 1 + $item->tax_rates[0]->percentage / 100;
                $amount = $rate * $amount;
            }

            $total += $amount;
        }

        return $this->paymentsHelper->formatStripePrice($total, $currency);
    }

    public function formatDelivery(\Stripe\Subscription $subscription)
    {
        $interval = $subscription->plan->interval;
        $count = $subscription->plan->interval_count;

        if ($count > 1)
            return __("every %1 %2", $count, __($interval . "s"));
        else
            return __("every %1", __($interval));
    }

    public function formatLastBilled(\Stripe\Subscription $subscription)
    {
        $startDate = $subscription->created;

        if ($subscription->status == "trialing")
        {
            $startDate = $subscription->current_period_end;
        }

        $wasUpdated = (empty($subscription->metadata["Order #"]) && !empty($subscription->metadata["Original Order #"]));
        $date = $subscription->current_period_start;

        if ($startDate > $date)
        {
            $day = date("j", $startDate);
            $sup = date("S", $startDate);
            $month = date("F", $startDate);

            return __("trialing until %1<sup>%2</sup> %3", $day, $sup, $month);
        }
        else if ($wasUpdated)
        {
            $date = $subscription->current_period_end;
            $day = date("j", $date);
            $sup = date("S", $date);
            $month = date("F", $date);

            return __("starting on %1<sup>%2</sup>&nbsp;%3", $day, $sup, $month);
        }
        else
        {
            $day = date("j", $date);
            $sup = date("S", $date);
            $month = date("F", $date);

            return __("last billed %1<sup>%2</sup>&nbsp;%3", $day, $sup, $month);
        }
    }

    public function getUpcomingInvoice($prorationTimestamp = null)
    {
        $checkoutSession = $this->paymentsHelper->getCheckoutSession();
        $subscriptionUpdateDetails = $checkoutSession->getSubscriptionUpdateDetails();
        if (!$subscriptionUpdateDetails)
            return null;

        if (!$prorationTimestamp)
        {
            if (!empty($subscriptionUpdateDetails['_data']['proration_timestamp']))
            {
                $prorationTimestamp = $subscriptionUpdateDetails['_data']['proration_timestamp'];
            }
            else
            {
                $prorationTimestamp = $subscriptionUpdateDetails['_data']['proration_timestamp'] = time();
                $checkoutSession->setSubscriptionUpdateDetails($subscriptionUpdateDetails);
            }
        }

        $items = [];
        if ($subscriptionUpdateDetails && !empty($subscriptionUpdateDetails['_data']['subscription_id']))
        {
            $oldSubscriptionId = $subscriptionUpdateDetails['_data']['subscription_id'];
            $stripeSubscriptionModel = $this->stripeSubscriptionFactory->create()->fromSubscriptionId($oldSubscriptionId);
            $invoicePreview = $stripeSubscriptionModel->getUpcomingInvoiceAfterUpdate($prorationTimestamp);
            $oldPrice = $invoicePreview->oldPriceId;
            $newPrice = $invoicePreview->newPriceId;
            $quote = $this->paymentsHelper->getQuote();
            $remainingAmount = $unusedAmount = $subscriptionAmount = 0;
            $remainingLineItem = null;
            $labels = [
                'remaining' => null,
                'unused' => null,
                'subscription' => null
            ];

            $comments = [];

            foreach ($invoicePreview->lines->data as $invoiceItem)
            {
                $invoiceItemMagentoAmount = $this->paymentsHelper->formatStripePrice($invoiceItem->amount, $invoiceItem->currency);
                if ($invoiceItemMagentoAmount == "-")
                {
                    // Add negative amount at the end
                    $comments[] = $invoiceItemMagentoAmount . " " . lcfirst($invoiceItem->description);
                }
                else
                {
                    // Add positive amounts at the beginning
                    array_unshift($comments, $invoiceItemMagentoAmount . " " . lcfirst($invoiceItem->description));
                }

                if ($invoiceItem->type == "subscription")
                {
                    $subscriptionAmount += $invoiceItem->amount;
                    $labels['subscription'] = $this->formatInterval(
                        $subscriptionAmount,
                        $invoiceItem->currency,
                        $invoiceItem->price->recurring->interval_count,
                        $invoiceItem->price->recurring->interval
                    );
                }
                else if ($invoiceItem->amount < 0)
                {
                    $unusedAmount += $invoiceItem->amount;
                    $labels['unused'] = $this->paymentsHelper->formatStripePrice($unusedAmount, $invoiceItem->currency);
                }
                else if ($invoiceItem->amount > 0)
                {
                    $remainingAmount += $invoiceItem->amount;
                    $remainingLineItem = $invoiceItem;
                    $labels['remaining'] = $this->paymentsHelper->formatStripePrice($remainingAmount, $invoiceItem->currency);
                    if (empty($labels['subscription']))
                    {
                        $labels['subscription'] = $this->formatInterval(
                            $remainingAmount,
                            $invoiceItem->currency,
                            $invoiceItem->price->recurring->interval_count,
                            $invoiceItem->price->recurring->interval
                        );
                    }
                }
            }

            // Update the order comments
            if (empty($comments))
            {
                $subscriptionUpdateDetails['_data']['comments'] = null;
            }
            else
            {
                $subscriptionUpdateDetails['_data']['comments'] = implode(", ", $comments);
            }

            $checkoutSession->setSubscriptionUpdateDetails($subscriptionUpdateDetails);

            if ($unusedAmount < 0)
            {
                $items["unused_time"] = [
                    "amount" => $this->paymentsHelper->convertStripeAmountToQuoteAmount($unusedAmount, $invoicePreview->currency, $quote),
                    "currency" => $invoicePreview->currency,
                    "label" => $labels['unused']
                ];
            }

            if ($remainingAmount > 0)
            {
                $items["proration_fee"] = [
                    "amount" => $this->paymentsHelper->convertStripeAmountToQuoteAmount($remainingAmount, $invoicePreview->currency, $quote),
                    "currency" => $invoicePreview->currency,
                    "label" => $labels['remaining']
                ];
            }

            if ($subscriptionAmount > 0)
            {
                $items["new_price"] = [
                    "amount" => $this->paymentsHelper->convertStripeAmountToQuoteAmount($subscriptionAmount, $invoicePreview->currency, $quote),
                    "currency" => $invoicePreview->currency,
                    "label" => $labels['subscription']
                ];
            }
            else if ($remainingAmount > 0 && $remainingLineItem)
            {
                // This is the case where the customer updates the subscription at the same time they bought it. Because
                // the remaining amount equals the subscription amount, the Stripe API does not return the subscription line item
                // so we have to use the remaining amount to build it.
                $items["new_price"] = [
                    "amount" => $this->paymentsHelper->convertStripeAmountToQuoteAmount($remainingAmount, $invoicePreview->currency, $quote),
                    "currency" => $invoicePreview->currency,
                    "label" => $labels['subscription']
                ];
            }

            $stripeBalance = min($invoicePreview->amount_remaining, $invoicePreview->total);
            if (!empty($stripeBalance))
            {
                $magentoBalance = $this->paymentsHelper->convertStripeAmountToQuoteAmount($stripeBalance, $invoicePreview->currency, $quote);
                $magentoBaseBalance = $this->paymentsHelper->convertStripeAmountToBaseQuoteAmount($stripeBalance, $invoicePreview->currency, $quote);

                // These will be added to the order grand total
                $items["proration_adjustment"] = max(0, $magentoBalance) - $quote->getGrandTotal();
                $items["base_proration_adjustment"] = max(0, $magentoBaseBalance) - $quote->getBaseGrandTotal();
            }

            if (!empty($items))
            {
                return $items;
            }
        }

        return null;
    }

    public function isSubscriptionUpdate()
    {
        $checkoutSession = $this->paymentsHelper->getCheckoutSession();
        $updateDetails = $checkoutSession->getSubscriptionUpdateDetails();

        return !empty($updateDetails['_data']['subscription_id']);
    }

    public function updateSubscription(\Magento\Payment\Model\InfoInterface $payment)
    {
        try
        {
            $checkoutSession = $this->paymentsHelper->getCheckoutSession();
            $subscriptionUpdateDetails = $checkoutSession->getSubscriptionUpdateDetails();
            if (!$subscriptionUpdateDetails || empty($subscriptionUpdateDetails['_data']['subscription_id']))
                throw new \Exception("The subscription update details could not be read from the checkout session.");

            $items = [];
            $oldSubscriptionId = $subscriptionUpdateDetails['_data']['subscription_id'];
            $stripeSubscriptionModel = $this->stripeSubscriptionFactory->create()->fromSubscriptionId($oldSubscriptionId);
            $stripeSubscriptionModel->performUpdate($payment);
        }
        catch (LocalizedException $e)
        {
            $this->paymentsHelper->logError($e->getMessage(), $e->getTraceAsString());
            throw $e;
        }
        catch (\Exception $e)
        {
            $this->paymentsHelper->logError($e->getMessage(), $e->getTraceAsString());
            throw new LocalizedException(__("Sorry, the order could not be placed. Please contact us for assistance."));
        }
    }

    public function cancelSubscriptionUpdate($silent = false)
    {
        if (!$this->config->isSubscriptionsEnabled())
            return;

        $checkoutSession = $this->paymentsHelper->getCheckoutSession();
        $subscriptionUpdateDetails = $checkoutSession->getSubscriptionUpdateDetails();

        if (!$subscriptionUpdateDetails)
            return;

        $productNames = [];
        $quote = $this->paymentsHelper->getQuote();
        $quoteItems = $quote->getAllVisibleItems();
        foreach ($quoteItems as $quoteItem)
        {
            $productNames[] = $quoteItem->getName();
            $quoteItem->delete();
        }
        $this->paymentsHelper->saveQuote($quote);

        if (!$silent)
        {
            if (!empty($productNames))
            {
                $this->paymentsHelper->addWarning(__("The subscription update (%1) has been canceled.", implode(", ", $productNames)));
            }
            else
            {
                $this->paymentsHelper->addWarning(__("The subscription update has been canceled."));
            }
        }

        $checkoutSession->unsSubscriptionUpdateDetails();
    }

    public function loadSubscriptionModelBySubscriptionId($subscriptionId)
    {
        return $this->subscriptionCollectionFactory->create()->getBySubscriptionId($subscriptionId);
    }

    // Returns a minimal profile with just price data
    public function getCombinedProfileFromSubscriptions($subscriptions)
    {
        $combinedProfile = [
            "name" => $this->getSubscriptionsName($subscriptions),
            "magento_amount" => 0,
            "stripe_amount" => null,
            "interval" => null,
            "interval_count" => null,
            "currency" => null,
            "product_ids" => []
        ];

        foreach ($subscriptions as $subscription)
        {
            $profile = $subscription["profile"];

            if (empty($combinedProfile["currency"]))
            {
                $combinedProfile["currency"] = $profile["currency"];
            }
            else if ($combinedProfile["currency"] != $profile["currency"])
            {
                throw new \Exception("It is not possible to buy multiple subscriptions in different currencies.");
            }

            if (empty($combinedProfile["interval"]))
            {
                $combinedProfile["interval"] = $profile["interval"];
            }
            else if ($combinedProfile["interval"] != $profile["interval"])
            {
                throw new LocalizedException(__("Subscriptions that do not renew together must be bought separately."));
            }

            if (empty($combinedProfile["interval_count"]))
            {
                $combinedProfile["interval_count"] = $profile["interval_count"];
            }
            else if ($combinedProfile["interval_count"] != $profile["interval_count"])
            {
                throw new LocalizedException(__("Subscriptions that do not renew together must be bought separately."));
            }

            $combinedProfile["magento_amount"] += $this->getSubscriptionTotalWithDiscountAdjustmentFromProfile($profile);
            $combinedProfile["product_ids"][] = $profile["product_id"];
        }

        if (!$combinedProfile["currency"])
            throw new \Exception("No subscriptions specified.");

        $combinedProfile["stripe_amount"] = $this->paymentsHelper->convertMagentoAmountToStripeAmount($combinedProfile["magento_amount"], $combinedProfile["currency"]);

        return $combinedProfile;
    }

    // The canonical amount is the cost over a fixed 30-day period.
    // We do not use months because they have a different amount of days.
    // So do leap years, but we can live with that.
    public function getCanonicalAmount($stripeAmount, $interval, $intervalCount): float
    {
        $canonicalAmount = 0.0;
        $stripeAmount = (float)$stripeAmount;

        if (!is_numeric($stripeAmount))
            throw new \Exception("A non-numeric Stripe amount was passed as a parameter.");

        if (!is_numeric($intervalCount))
            throw new \Exception("A non-numeric interval count was passed as a parameter.");

        switch ($interval)
        {
            case "day":

                $canonicalAmount += (($stripeAmount / $intervalCount) * 30);
                break;

            case "week":

                $canonicalAmount += ((($stripeAmount / $intervalCount) / 7) * 30);
                break;

            case "month":

                $canonicalAmount += (((($stripeAmount / $intervalCount) * 12) / 365) * 30);
                break;

            case "year":

                $canonicalAmount += ((($stripeAmount / $intervalCount) / 365) * 30);
                break;

            default:
                break;
        }

        return (float)$canonicalAmount;
    }

    public function hasExpiringDiscountCoupons()
    {
        $quote = $this->paymentsHelper->getQuote();
        $subscriptions = $this->getSubscriptionsFromQuote($quote);
        foreach ($subscriptions as $subscription)
        {
            if (!empty($subscription['profile']['expiring_coupon']))
                return true;
        }
        return false;
    }

    public function isZeroAmountOrder($order)
    {
        $orderItems = $order->getAllItems();
        $trialSubscriptions = [];
        foreach ($orderItems as $orderItem)
        {
            $productModel = $this->subscriptionProductFactory->create()->fromOrderItem($orderItem);

            if ($productModel->isSubscriptionProduct() && $productModel->hasTrialPeriod())
            {
                $trialSubscriptions[] = [
                    'product' => $productModel->getProduct(),
                    'order_item' => $orderItem,
                    'profile' => $this->getSubscriptionDetails($productModel->getProduct(), $order, $orderItem),
                ];
            }
        }

        $combinedProfile = $this->getCombinedProfileFromSubscriptions($trialSubscriptions);

        $charge = $order->getGrandTotal() - $combinedProfile['magento_amount'];

        return ($charge < 0.005);
    }

}