From d6bd9b61b6f4420755fb3aa3d116b30062863d4f Mon Sep 17 00:00:00 2001 From: Jacob Brown Date: Thu, 27 Sep 2018 12:43:30 -0500 Subject: [PATCH 001/479] OMCONNECT-91: Add OPTIONS to Allowed Methods in Web Api --- ...d-Options-to-Allowed-Methods-in-Web-Api.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 design-documents/Add-Options-to-Allowed-Methods-in-Web-Api.md diff --git a/design-documents/Add-Options-to-Allowed-Methods-in-Web-Api.md b/design-documents/Add-Options-to-Allowed-Methods-in-Web-Api.md new file mode 100644 index 000000000..e2e5e9b18 --- /dev/null +++ b/design-documents/Add-Options-to-Allowed-Methods-in-Web-Api.md @@ -0,0 +1,37 @@ +# Overview + +The `Web Api` currently only allows for GET, PUT, POST, and DELETE. I would like to add OPTIONS to this list. The module I am currently working on depends on another service connecting in though the web api using the OPTIONS HTTP method. + +## Details + +Here is where the allowed methods are listed. +https://github.com/magento/magento2/blob/2.2-develop/app/code/Magento/Webapi/etc/webapi.xsd#L27 + +``` + + + + + + + + + + +``` + +I propose it be changed to this: + +``` + + + + + + + + + + + +``` From 9ae4952d531f8c3fc802ca3f569da3eb314cae12 Mon Sep 17 00:00:00 2001 From: Yevhen Sentiabov Date: Wed, 12 Jun 2019 09:54:05 -0500 Subject: [PATCH 002/479] Added a proposal to support JWT out-of-box --- .../img/jwt-api-authorization.png | Bin 0 -> 20480 bytes design-documents/img/jwt-class-diagram.png | Bin 0 -> 154150 bytes design-documents/img/jwt-data-exchange.png | Bin 0 -> 14183 bytes .../img/jwt-data-verification.png | Bin 0 -> 14032 bytes .../img/jwt-user-authorization.png | Bin 0 -> 11723 bytes design-documents/jwt-support.md | 180 ++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 design-documents/img/jwt-api-authorization.png create mode 100644 design-documents/img/jwt-class-diagram.png create mode 100644 design-documents/img/jwt-data-exchange.png create mode 100644 design-documents/img/jwt-data-verification.png create mode 100644 design-documents/img/jwt-user-authorization.png create mode 100644 design-documents/jwt-support.md diff --git a/design-documents/img/jwt-api-authorization.png b/design-documents/img/jwt-api-authorization.png new file mode 100644 index 0000000000000000000000000000000000000000..565812d3cbb2e94cf1757c5cd831e5d12ad557e5 GIT binary patch literal 20480 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 ZC)AdwI>|$^xoi}CyqLajXBh0XzX2+w4#5Bb literal 0 HcmV?d00001 diff --git a/design-documents/img/jwt-class-diagram.png b/design-documents/img/jwt-class-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..8e548f0e67fa18b8a20aa69f42157005308b9e87 GIT binary patch literal 154150 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

&LeEKl7V>6#<@zxKlM+2wp9I4!A{YxW+Q^ z*#f<|zGYN$mmHS2pFIS3j%00iM1JmKGrJCP73vWM-dybOOgKEu*535AuKXl!eGk(T zlRF;KXFzX}Zqf^EtiMGZ?q`YDk4(mo>y-gYL9CkpUm$IHPG|sI)7Q54QtlJ4lPO#PA}h zU~;$lg$K`v&UiCq+ z51)80*mUl88!}rgn||5f@T;EpRMYEw@Z3kKJH2>q*cpt-=D6R~O7&iRzX5NoDC{+b zZ1YxV2KVSr?-lWPF~j+aqsg;Y$`96a7;6O{--xZ=8EiW10gs7>Xo59@xR8`L;@vHe zVy zMj=DS#X0s;qV_GF3QjcY={S!D%jYrjzGN<1l(@1VybAPR#VomPY5PJvf^6cNT9wI9 z@aIYXHCuY~lB~MHw(dPv-qF4t$eUuu9Y|GVoQVJ`Ik{wOAge_z!%#dLAs=G(ebcuk z51w?zvuQjY!>DUU1G&s`qBk{8eq~rf63|MUckac1JKRWC)mTJ&;F4nhs-@TU(+{A1 z+Uq5d=um}veTT8a8D$Um^WD5yMXUvirtcjx|7uPxXYgzq5=0SOT9G`#R?9&@^IKXJ zzx3$N9Wja(MytaK1ZIEMs|5u+(eao-ursafFnSd3WGfw{b5+}{D`QSl2b*EYfgQilr5hP1cYA3CgW>=ocFBYGRZtn8|& zv1V?z?OG`pk20z7_+T|3sY$3MaRLHaA<4b^)@LqA=Wl#dI& zXAd^Yf>~j&RRu4e>L|k_oqzz+&&+MN_HzA(@oxmrg+bpK2KA3--gCl*bcz7jENXqT z_y8}r@~0>ng5LRHggR8A_3HVS8EwqtmqrlwsOQLcob_VI6p>EJk5@y z5(0Jm;4Fjea?&et|1FmYwC2Owa`}>bC!Z&mp40T;xycTF#Dy$|BG39A4r3F%nzUab zogxK6)^Uu{EZOh7wPhOOdZnh=a;aQl06dvTJ(_JprA)1eUGI7S6f_AV0r5lu92WTA z(`ObL(hK2-dRT$nN*fkdre=!!J7Xm#ksEi~n*Ksf(J?NoJIz}mlotJ#(pBBTE$!D1 zo0RAodSA^SPa?68`@7vI6*-lnD5Zg{Iya8cslPu;FRD`{k{FB7+>YC58m?50Z|!#{ zm@`5Hg^^9J@~A1YQK7#ug{W_FJ<6)XcPYwUPwa{0&2^>E`@==FPLVt5;)*p%ep#Vl z)h9%Huh}@a`s-h`cExNoyKdqLiI(YRm}8tFzkV$Yq}I&# zrl(ug+w?xy>qZn2Cj@8?F!OPX;Ppz{_m`6t*!FU_?k~gg;}jH(Zz#PA^(vvFL}s06 zB+yZ&p5>cGvdQ1p6z6->-w-XTw^=X*q2RZKE%nApiUnk+1wx~vM!d$zJ-YcFrd<2q zL|GzY2(@)iC}jyg3=~B&h+9|cK=BU}q)*qp8gumM=(_pMW*P-3Biw8C39?lsBdJNDBJ}|mN)~+zS&QC&R~tA^iwXotj6;A1pd9Xqs>db z$D)U$S2Gfk%<1eM(RUCH>M%{mK>M%~buMHrgz1Fw$7YIs2=A+}_$&SNHU*c5E8Wc>$RvHkb@vjf;||w)ebchjUMiDa zJC1uodVIqVHqzDtHsa3Mr6p^|Xm41~Q)<7pu+u5O-x{)rjMbj0xlkj#w97RH`L9}Z z|8q-R7%w@Nlq)qM9Vn15_`CIc~0Lo-)LotQuW+| z8>ZDOjl$0O=ULH3@jG?n26HFbhp zKv*3AK;JSPNoK7`z48x);$xVZtNkjV=>B+Sj(+vASY^(p;zQe+1?S&DRz*sK&hgJ@ zdrNl#rWywX$vhsq&B<6v+0SQkBG0(ot{Yw-7iQ;qC z_Cc^<_8XqHCcYoE^iV{{<=fu%vhPx=qplfS_QZAjZw6sfsOk$QHqal;W`w`SIrh=f zJq@sqz2g!NAystEcV8l$mW863K~yES3UK!7wb)=;d>T4Rd=HTZ zlg%p}eS`Vxq~8MMcwX8KpXcZ(X+gz^o@JEqp*t`NB}^a7{fy#fdJidzZ^Dfheu;9B z>lQD8^&;Ail<9&YBlIOTlCYC`-tUjt!KULfqwm&~fe{J@`7Ghosi*cJI8Inw0srx^ zZy0%Jopl>~7=z=w;@Yhpt*8~9O_r>fB~$b_+{jphBQy|6u~;_oCDsp%u|MtQxhmAi z3#wLAAtW50B>o|AZi2D9B(-yC{HbQTXiC|f3_)dC1KI?obVKz9W$w8&$}d-CwC%?k zGu{~HX&-_D0v)q^)H>Rt5L)|~+>YFj&CEaxMSbmPZpMf6gBmL_&8X#zxvG{+Fg2OM z_%E81FS?}{I|Gdh!{xEYekx2?80V>V{-_H@iV9*<;CA46)bdy)^SsLB+7pQM_^zq~ zWtp=&l(#qaV{9e$GyV1IvKBXWJ%%Dm3vGjk!8x97H6v|u2Wi%Vzt#EdJLc=3Oc~E_ zzY8mLq<$dci7k3K2+PAh{^l1tm#4<_W%nO{POinnul)iBd@{)I)$SzegXT3|btr!A z;Qj(Eaz3KBQ>jec5a`h?=4ElgR4V4mJ`h^9QMc6;#>sIjh_J}RxtTo=B- z8SnC_-!&|^8GMSJ@$tgCi;x0{C3^aJd-+) zh_W|?B>LjOR@HBeQ|xvQW@yd3;yerkn&`h%CPknTPT`75Hx-K5Tm_d%?)cY9=4IdA z95~3}^Pnc(Rtt07!O!*YFNQrntKD78YZ|Rc8TTFFGMa56n#IcCyVS%WH3MqQ=Jz7F z>*i27!@EjL@5cJ`Es?x6Wd12}F<)GoUP_9ocOMEuz->sT_((K}%H^|aAN}F3kw~31 z56#_GaPFik+YJ8MU}}f6V-*WCHUtL5L<5s&w*=NEKVJ|sa%A*P!;njzu7v52&=n($ z9%Azh12&~?X{0GA2J6rX^E$K06X7*I2^)9J__X%nyN#r7t?JXp50#qZ79w3tn%7&` zl>);RvL<7U7eS6*NlH}V>AF8K6KTqq>VHgD^K_X91VZ#w=yNDi$BQ&W3*wmaY9I`> z3hN_&4ys3#4WI=0x6q$DN~y5E_q9AzZ0p(Pki~Cfs6=<6|39fbYO;II`v#Pbt7{=> zh}ZtSs)8sh&3%JCcc0i0-38D~Ujq{`^dK!%Lre8C7M2X{79cu0jcM3QR z4wMF|MHw<_X#IuDvd#*WU))9EAeyWem9ez$uuxQm^txsk0vJVbwEe3L(KiE_GHb|IIG zevUhsl0$9S(wtpv5X}co0L!d(VFQOw<2lf=RKW7yl^PFuNJI@Tcg{07Oy!sI!w1c* zt`_T`wR@F)4?1Or+>2%~Hyg9Wqv@R~IeBl1=A1yPQFm*Sm=FzFlU~x}_kK6+=DU9q zPb*7nVWumsQPI^C7etLU<>9p1hnGNhI#?EAxOubvKygVziISFeF1a4=UZavJ=LnX* z+(l+kk7svip2h+4Sn-43HKtBZUmU7MUb8*Z!)dn?NZdUUMEG^hAnLJ%Rqs55-0n#16Jbym$FOU!#P-)O-=V9YH}T=;r}+PX-IXjolQPPS<^ z2C4Lrq0BC`Op*ho>FRlOGTHO!+OG$x zu?B_?0${!DU$aC=z+H}4S=Qy#yurBJg1hKY_IZGLnr9&CN)DODz-(PykDeM+0d={> zjj_i!27)e={wSozX%4aJQ~VEDqEfCd%K720{Y_${iEEDXQR6AP)|>SGtoxx9JGRbV zfx)b-0S(EqsJqR-CZ0Rwx~>GEE)-CoT%0vmgpOvp(1GmlD;Yv=!CpJ)q_93ZvZmr3-50gxX`%2pGeZR)<>8_^Q50N+B)@adTPELojL620wQ;aB z0l|im->lt5yv~?uiAwx4+MwzJFa9LUCIEdBgL|fP?V=C5U`>A6o;Vd?-z7#Y+iv2A z6}X2Zj(SvRk8fMu{BHSbfX9e))Dpd<9|XWYnCU`N)shp&-udc9 z5REr#(aYYGC}t`@ox2b-Fu(wS;v`n%z+g$mY3XyK=*y7J_f=B(To{WS#af8nBN%SQ zcUR(B8GTK2JYOE(ZBYqF@`>VWi0EjJw+A5dtNs}*%MpbJ(G$n^L{-d@GV2$N=(<3< zAlWR@bGxf#4iNnb)-X!PO?Vh~S^TU?z#^u$J=iTu`_lh_kt($!zxGqw>i~$HfxF`J*nYTE{;Dlm=97XQ59tA#F&&EGI`{ZB6g_PsI1pv(wi_-7gZlh z3QMQ#)6LPKDW?qYNK<3fkWTU&fR|1V=M8?ELs}bYKRG;3$TL=Fg{_)QWy0CG@Io9Zxp)>mk6%GFY3V)H!z&kwd<}PB$ zZIf$mhnyFen~|exdklH4V0`FaQM2Gw+{SQ5-XxNLg&;Cob;X3Ck~oe(_Ri21<=uBp z)I`6a^fu@A3=KRMI}ZP|{a*>JkW*Pg7D2-}QYB*@^n?X>EjBn@WFuB{TDr`zO_j!h zl4A4P(Go-diuI_`^Ivo%t8;lA(odV*himX{V1Sqo=a-zmAHBHjTHT6GyY)Xp7OSm! zi22;Tm9L@}SbzDl8q0XKu5Ue;lF@_9lH^>Xbj=o7gmuX1Ek zC>0r_yGL_9*lz7lsdf_aKe)a|+n*uw4`G-nDDqHm>Qhx5IG|b%3VeP(AJd*}KP%nr1?dNT7?+xtnP$zD%%^|Y^RGxbCN5S42m15oD;$kIu zDV-eJI`mobX`f-qS28&iq+HOE{Abr)W@$<8X?(@C^6ab4^8Qxiur)8*K3|sJvdehB zKRi!2R^~I=Q#Q=S1zR>T24jv+F*vu==97Wcds)4E=@><$g%a&h7=e=pRUGb2+3?{G zjiNt`gl+@K1M~Lf94lzhYhcsi^g&O~=PDXvU$Pw=@PQ^iOX`mGyS%RV!ovnhmR!<+ z@rI#!KtF=`=+S+n7YIB-=2qv&DVcP$QId@a{jC2FYySbw<@?8v<48qhXYVa!@4Yuk zg9zEmCPY?bZ<3wJC}oReg~-UPjBE+nkr{r^+vwfr`}_XS|D6B%pX0pF?e)6v>%Ok% z^_-9A^SZ7u3r)rCJ4WN+M!bj~*ZakQO|={bWqo&r@HOR)V>(jvhN>=j4{YsSyxSbM zpu~`*O7Ae3Z);e0^E5Sn;PVja#{!<00F=y@s{4bsS9Q+m%@WUV-qJO?SSH+Ef$AEY zNSoA;llUXm1dWj9VYr|D2k&x&7*kF zUrhwwOS$~!3K{1u;=Ya99#amZB7)PBow?Z|BLH!fyKQ<8*BzkHkZ=YJ-*+Ayxfjao zs?>&>CSU(J{eJj{U6TskbeSN5L-SBBtEs8OKm_`vYCRc=yMgVG?H>&F6u4_2!iiJ- zK?S5RXgU`2f^jci-fSzdsIjQrn7I&uZvTPcAT4?Ed*>%B1J<@gOMzrM9Vnu(wzk2%93R3dl5WY5 zFjS)(gQEVe<~J?>cRaqvPTafBL1mt^Jun3zwKtBgt_-OCO!ajW1r+*Wmc`(~3{&*) zH|titXk-dcN)T;A-q6AhZ@n>9<#$?>Rn{GKnBLwavE zgrDaoAPMFDp%=3h&t{sQSNSxX$b6XVN5Rixc8Rrt`H|rK=49_jOh++7{{O}Zz7lrg z9_7nHG=huud!C!+{0*&Cps; z&v_(JX|Cy~tce0)t*oEwk(%G9IDfVXnLm-) zli(H_)P}Z1Q8RcJ*u+Fuh_Rxds=~Rh-JG!R$`kG%jiNwSVRQ-x?Tv|tf~gBr$SlE_mt39xH`w39CE~h9 zB-er?pVN_44o0(cgAaXFko- zm9dtZ^just%IwG`{c34O+PiGwN#UHZJ2|;M#`9_ugK~S$^83d$s#8k8<=#iF&38HH zcD9U&xm_F9`%$NCYr^0+fR}YwGWetxV0>(4;4)vo2$6Zm1+JTFXpxN4+RZU8i(-Nb zWV^q|8H`1ca*39Gj5)%4g?Te6Q_yAc5kLp!2+$d>S;MV; z*^38yxd5$YwzV-v%>L48HqVZZbWxhPnaGx>H})HZB1Ff(X|zX6ntH_VR`QB&F8LCe zhnnteCAo{;CB~)_!qBO)G)}H44CU4vy^lrGb*pK+l!Ry45<}9f4m-KR_Te3)t1{zm z6g-WV#5n2O_)55YHlN~GedA4wdzJkf;eDwN ze@M2OHdbkD-JWu_5i7!%S{e%aVf02ZEwDU#GppKa_@&=DWgA933h;-iDen9b4ay9E zJE?G2r9K9owM{s-z^es96QguWPFMK9DUza#mm2YMLV!)C6oQCFVh_C>G9|@$(>UO< z0zY?V`Ot8a>%RY|p0ypc593JTAm zUVwfr#eN*NCC)RfBED{C@DNncb?qOsA5JMJi0g?fJ?wi+j%qpl{4Kd$)~=j(^DKU* z7bBhzGIW}|S)Keg1Kh#meMgYUXr4vyC4W4Sp=jyuaA87+Y)fh+K|n@&)`9HE$QoE| zcjIRf0>9X~Fk2LBQMbuf(8&ePMosw8jM7-HmpcEi~LYe%heMabh2oK-3|B~720C-HsUcyjGC!MjIlxRU^N_j3wANz(zOq^xjojc^0OeOl35yloAy)KNL}ng-5j$U z(5Aqvk5ej_d?CD?##B2n+bFe4QTS~L;N`2buW>OjFs2^m=H`afIC=FcxDOVao)s4t zf7Lm3lleW_9*3;<)y$4&v2~i?Di^!oJoIGApKD-QvKA(JHN4iRX@ezWBK&{ zvDytDk2VJh3w*ugGP_{W#}vlDhB2#*I;7kVdI+raH|^^I!ckytd!=7a?Inui8!S;G z6;a0iq%gx~qHSRJo91(kC)a)4{!6pm2<0vC329CV>a0`=6Pgi@A;@5Dd?by=XiNwC zM*ObKlGn6IyVIC0S36tVK-hE72valdz~x`VcR8EC>fi9NH`iM;*e$+we(7F1-&gN& z1+vX^m^DnOyM(}PJ~@$;J=(Y)ktHGpM*cRK!-1NdRku5xj9%R;)-2W72%`M_%QSYd zBIFdhekDqnQ-6JJ5aoMoo}ZAJUONZQ z&$PNydfJudTY*ViBHgI2xEq<^!antl!av)gtfu&m)y=pv(_QSkt08$;-^t#Bsti^D zm80a&MrHu2j8}P=s=(--jCQj&)~E9=*{oALGP&@f%#Iq$`zF@OTS{N*Vd#r& z-U9Kn%G)J465l?oyKK1ep_Sz3EMyPDewBMIz2Wg}QxSZ(g-ef@7;bLJ{0*$hV-Wd2GmDO*L9x4e_)nU z?z0m<=MuTH2t`)H9c@D099`1 z{J%(}G?MSEMK;s&Rh?LUZA0J{YoptJi<>IV#a}Lr>N_q{SV|wx0!E6D`kd7? z$6P@_LKXZ8ZHBepOG*;6B5m`P7HzrK3rwHy|FN-b3sd12;XH1 z0a6$&!lE|!-v+_?8?gWQ^EWP0Wl_TpBY$>@;gHX}oH9hYsP}m>1HcwG|9*n5K-M3{ zBf*HO4?aWjvzE{B4ZTj>+BChqge3Vel=+rK>R1Su zAvL7hw!H^7Na{=(N>MOx39dC{8eb`^CUAs=USKE3!8cN;xWpJJ^zqsualBuUbxJE0 zc!lx?VhNph5<-itt{;hD+ADv`Dwn`0jdmNl@3?mXxbwHEI<89nbu=P#8>Z<$-ca>L zbL!^GQ&vK6t)x^DXL|e5>Oi?BlD;@X$3FPaJxhtia89t%hLrXx5 zN>Idy>c(kgVHr*tiLR~%U7wR9eGMK~ra}`A6H=jRxwpH@F?I@x_ zPb`>$upK>Fi*Sv@0@oA4BB+>&55auu1h8o;UWpGOluTDgyQBb(-fvL(*HxUOz>G%0 z6b^)YTO6kIpoL4aa90?5tX;G;CE}nnNE4=lcZn{O8N!ICg;*{^t5aGGT`2ThECTn;p)32McOY*%H2c2jlSwEFN$zJl=alO+kbC&KUHRK#1H66P zmS-YFn9;ti7@)GzepS?ujY)-1N4{dWluEnCaPGA%dbb7}htvq;i%I zq@{|Hu5rGm81NhR4bP~bd7k%5LwHlZ+RFs0ldY(%HVD#D4?B~#UH>J$->|)MZ|{vR zaS>^2EK~X1FlgpXn+eqcM=LB*!R3c{%3o$ek)p>^D`D>C#woQjn=x!iD}El^bDTTZ z=4C(z8eaO7lhN&lj$&5*7va?JTvmC>eR?Rj&v^%^I5#tBPs$4p8kcDjnTHeO56K~O z*o@5E7Ek6QxYlGG&pRuj^w#YD3$W%qN*eDSRq8np*{!Jk$?Al@v2n({m?mx`-t5=c zDf{A%lk%~2sGkxrM-tggs}&~lJbx#K@`4W?*!7WZ_G?|~r^!GG>rlMj;u~U6C4skC z1ZnP;CT$(=gKvXFZWHQK;t#ek7OTobHVoz*rhi?C=QJP_!?A5HIn54*`yyMH(qGP8 zTKV1Bpy#ceD$MhSY9Ox;OZOe>sG%ehn>=zRYC#|4VbvczMbdQ;c_v-sa@7O2@Kj-J zOhTqIYi|bEOjgsTyl!TM%ee(XaW_ZezGgrraNtEfR5F~Qypi0 zOD!%9`@a5G`1A5wezf>~r%g0z`z$7Kr*F!yj=tC4{K<2{d}+@amuBV<(OJzP+G{`K z@tp>!h#t;OSx!#A4a>V)L+^pT5L%ksb$jXacBSXcCB{?cNeA?w1XnkbgWKy4V7rY_ zli2}3DBUFleUAQATc>Ey2W$VWC+?##qNa~3hPv-esF)+t;h>z}+XGwg;m|@3;*NFy zu>~ggo$qXtg*sFcXIy2V9_MqxnJ+1sGHX?E7GKr=gT2D}Ee<1*1(900Qd@c0Z;q3x~$r1IxSjQbq?#xbXB!>%yMZ1WkcUxk4`0 zbg|8>J?VVJ9e#gHIPYGlx+4AV55>R2&Nxm9N+-23tuz(|Gcmw7i+Y`MTC#mvIFzG> z$?pj|8X8(d#yFN}zabe!WMZ`wHQ#Ez-i7k?%BQm3C`k20&8X-BMCOkb{s+&XXL3P5 zQxkl?wWH`OB|#rPKTVz1e}_KO%ZXZVS+*aCfaCGTfp_2(ydjbNXrcGp`Xg7+%-jn#}KdU1wCfAf`^rgX!sw*;~h#tCjx# zcb(m=PZ+E7`Xnl&S~~WcB}c=s^;)sTysCujQpbH4o6&204xMR|xnwMFe|>LyTROut zdNJlI_HeRT0DLyL(&1OeD& zefg};2cur@S2-dwUW@VEopafB}VMFG%V8=kN{efuq>HB|*I1u1b;ySC(pLp<5mc#I^Cg+~Z;Z_LIr`p{RHI?zNno_T z0=vnI4HX5EnCFD2MAj7;%7w{oA0Q|?PqFbJir>0VUaT-i4CdL%#p=+4k|M#XY4fA&F({a%0ls?ePtvMgJ803Z z&tO)bL0w{-B6oaF!iYzPDkUZLO1}bUd##Sh7$k#_LqAFFuyeG+eg-orPTxE672{Wrs=4$P!gIBrP99ftCG#*O@y4RYF}S&6FGvzW zWqo9iPGAte`I+COfkbRZN>86j+`Gh9>^ikG_7U>qX%!wXZVJ0@v&>WT^(vvqYpGq3 zTYICyfSrTEMSl(st!NN!_S5lTx9&S8COh zgLw%bv}D+r<06>F%Lg~9KB!&jyK>jU4tx|!i6KqItaT#siO8@BJJ@*@L@rb^te7!{ z6Mw`_|Eksb zUS0fimAG1yU$Dwan^8-p!+`hv2UTL3Wt_cL%{LX=RS#ZpXLc;=S7N-c% zpw5rmBW=Gi2Fh`4$;%qgZjaq4R^Yw;^V;2Tr^oHa05y&1C~i1d50{{BER9C(-#UYY zJ!V~x&!_3TDnaqVgoG)7x`bCCZYfbi4xW#Mg>}htSbqQ=t9F&}bH;BiAI>qfi`nZ% z6|U=_uM<(@HcyYDHi94rC+%B)3k`$_XnJ9@J~WbbS|2E^uMjK-MTl|Rf8n@r=lLTj z2(WD3!I!Z@Lc^JGJqiV~Ll*@FD7A)oDb${w>JZ?efreMdV@y^?Ta~V-Z-f2P89Qt? zMsmv=!S`yz|FiE&-52q%r~Rdp3eK+Qi4@BU(T?l3Azp`!|p$$ZQ0hZ6Wn*!$r8@~ zx#3|u_h)+Lr`W~t%Y=sUF{ttxLvI|#-Yj5!_Ok340JwnXPFWJ-Vy3Eri|M@W>ub=QU!#g;VF5)gpb(a`v3$Pt9$vO_XSU0-FNfg?RR7beXHSQaVj?}r)w8pY1 zH3VR;Q??r4-&jam`b|*v8bF`@yF$i2$!~Y=4mSQ6AC)Uya3hTIid|jvXe0V6Vv{wu zQ^VR;=^Z{thxpk8DZu!igwyUYafjZx+M~G6Xz5pATS5dx)0NQzS@3Q?z~pQTAij&e zjDDUGRfuUEJXNI-UPX*J49W6~pFbd&;Eexpbg#Mh*e9j03(Ms1#dU^2%;Iln^+3Cf z<5v-6>x&?Xd~fRSvXP2hG`)fHyL98jnI zBXD;uq{juqU9Q+A9vtvEgZXM0!H}VP0#Qu0k}1Ou`AfZ?N+@`eO1XxMJ(;)qlylDwz9oU;3Hg;6+n9|Q zk8zsnYj`QLe_|wV2r4B~EhYxxQp$LBF%K02IuadWI@Y=)2o7~HjoV`5(DeMcgsDr} zl>BFzKPe*CH~=4Qg$7S!kFm8iT?`iwLC4mh&FnRwkZLx@$@ga-^Py4(79~xm!Z(g5#4_n9CVM75g&>j_6`Zm1ze}PIlz(eDDr*U zH@D0DU6uoDYIv}`nTA=sa|N8JtOC49HMC9!JZ)lic`kRohK~C@lO2qBRN#H};7_h@ z(Smy+$?A-A=w5IFZyT_uuLnY)*NG0r_JcJ(OA}M6PXW`7R{r4UTTa34*(N?*>3T|_ z_GKiPFF0)@+kO9!$p6K0Z$QV?etlKr!Iw%w;x4_HE%E_dCAT>vItpN?f<*!RCq4(K zz@~lZE}#G^Ic(!3OqR8Far8@N0(45b^i5kgu@J92t&hI#)leCBoRXH80xbcm1>pU# zu#NuWwZgpWNa~%zi*2xmidB}F{wl`&cD}{L^1RR8)GN6f5fNkdiU@Yq)lE$bcx~s? zx4v*(U8cs&bv!!Fru4-W{}|qD=RVsSoL%&Ma4Ia3Wn&bu8z|h_YI}TjB@S)tQ_r3PBS;*hy<;to}T|a;Mowr7C27U}xdil$jC@}|VT15akU)|cCp2hrJpo^>X zW0&^sh8xfQ4*ub4M;(Qnm9?i5UY*RIFF)r!59b)9WdBq9VKe_vv3NaK$I#+n0q&oE zo6FbxNHi3SV-=!lWzhb3PY_%rejNzlXr=)ZF7_fHnmK!djGxvoeFZW$FJk<#NQrOY zv0@f7m*3319k!FVqhP*%XN<(ha2qR&8CNMZzA($VeM#8i4(=PfE!0xTiG6=N^?M8Q zP*%;67^DfzWEY)tI(?>xc4^0U^RAbwhRc(wy=i~XCpG!>o!d3M`QBWAr=l`}_S&#+ zX8h2fSM7X1%oC6MKe&HQnyC1N>mJv*b}@Dval!NKZydF1+x++u7FtPRusOQ%+>V0o zTm^Yti~1vQyvXNeBm&u<0*KPGWsjwhM9MIQ$!M&K7U}W+t|}>iGP*k0jed^JcpjOR z^Jer4WT#I1=NxLq|Gdv=UuTTUOx9BGdnk~F-8|Rr*rzlRRw!AqZ^+9f_9!@WD z=XzuO0g3K+bMIgMvHE;Cwfe<4uuSkZl&iLJ?Au9(CU882ov$;M(_;s!d``Ww9f*YG zsivR4K2A<@JPF(GS!{L4E3X*lCpzv_@FhpYv1@6jig{8`)iD2*?B z_C0$yDb8|2?W?Jm2n<~28#aleN90Wd`%ji9Yk-6elVh}V>vJ7MXpZpAN82sc#}sBSy$d?zzF zm=D~sV>2Qi95x*FH^MiMG!*wWYmK{YwLBqwf3obcnBA_AOqHPe7j}Q5nv%9&h-~J?9ISK9b=pH)O z^)8u^-VgX5T4WFmu4Vhf0bXp7CA#iP$MUe}SH9pD?ZNKF4V5^jJ1TKZQ0$a%wZ=%` zfQwAyJoWl46J{k2X#WDj_N|qGOxWM%2PMcdU$4=b;W&z%P=mxc**XevW=Yr6z> zB#;&`u;fU`%*sp^dEd)8#}K=>)|K|o!$3Za= znd$w^h*Zu5V_*Yb-*^(uj>s*u5yBKq1qI-n4++khjePsynTSg*;SnH{wL4!43z)iN zjUI&(!CJwoq6ZUd&os~(@!hTf@R@c{nBU)Bpwe=qgA4GsG>>xJHBWu{q;m={GrNk? z^K@T72vCQHD4%I?txR5kp%*lS@jmk!J6gijT^Tx76SVGYrm3<9+1^PyKJ$e?L9vZ7 zzF3GVm3{x5-A;xUpx3?im6f7=P^$}}ebZ-Cen_0ssqyMLN~gdMDE`B5o!?a-xI`na zk}~ZN1Qt#u>p}nk2DBxPwpTZ+$@3Flz5n^CU0||gxjg_s?)Qhfc)!>ZT?8pLy+WX| zq1l?ikRQrR-(dDB=E5_Dq^Gzv{Uy5d{$FQB;GnY)FO0fqyLR7lJNHr=ymh&dm5beP zO24oCCU{{l=D%RKphYf>H)b$qx5gm(-j$lGoRshW5Sm3Lov!uP6>iru`sf2I*GD|f zJB;n2p4P(~-`u8rruRcT-h@nh`6J4v;m3T926Rf^J7igBBR`b*e%Pw=)*sQyX*z4} zB1K^>nU!O*7PY@KX0itB>f_UxbeMUG0SySmH?Q&@>AR@wPmgv|3m~n)-1faAjf(!E zva9;an_IJE_qd1L?ESw|>EExZ`3qF>-G^6}EkhwQ8^xwYg3!IE)Yh>{OnNc^lFmj6 z-(vD31+SG*&Uir+O_Ldk6+(G6abC6NK$@h)x5of+nz&Iwn9g9JX&~2^jv!al_P|A! zLaH#=P^hpn{`&oG6>4?7*SWFhO*devSGk$)hQT0rWiCm<{%US0Z|wWtx^~(Qy`}sX zvo3-`Wv|a>g~|$Kg8{Vzp4l!m$)V}f_e^*0vG+3=_ltHUiE@Qw&;zEl5FacV8>3Tr z(=HGwRsf6=IZkziUi_yGyh-E&;-lFpySQsFG#Z*H*>JyysyS9hp z!YWHO2I2lp-U7ZI)bl?K<}%HVkpEOA(2!B>xOhg@`|5SIK-*tqp>UMs%UQvoQ}=K=04mN6VxBW~|w(b6tbMx_*SxLV5d zoG8)E>En;zy`fJoo>`vDioWDk^<=tj<1{@u293Ze8?Z7J$vd`dd>uQA;`2Sbj?<1m z+nudpL0Lp?+8L`ekFs3`-+KiKWSRXMu!aiH1dAMg`)`c0V!Y;^&!;{YTqd!7S1!ZU zb*?HZ+)VY)ueHLP1g~XEs}jY${1PbiCpK*pw@po=Iv7&n+%>;bxf=m&r!8)COySOL z_X;Jw+w7*!8(U}J)3b^0Ahrg0Hiv#7NoS_Ja7}t-86=0o_4Te+{9D(?nn#z>FGRVfnE@3Vr$lGXGdc%34UTg?IUpbTqCML)^`armlSed zYy%=}ND!5n4DpptP*gKS)WXS!5w&>*lpVMJ5r*>JS{q%pPmh0@m$2;)*>Y_%#4vMx zek$RaGo5W?TSD`3V;=A}oxvb7J8&o5GSpeWGCv&iUEyU@TxCNUyS&>`OqkMUJ znIww^w*@M+i{6d=Q*)}llwy*!^(P7I=F+EYfGGZ!NRd{JsM&n)i4+(a2)k+X-8Az_ zg_<2YhZ(o3s4%`Eh(0-A-^kUr{d4a(*TPV((iBk&Dd1&_=prnbO;V_(Ug7u$0m2NJ zq+u{?rXLz_y?(|zRS=UqhR7YBC&-;iSEMCmOrm`!Fa~ts3`=>JXfOSxF(2QkFk;!I z>bKZ4&Ms%tgg4mj#;TBQ3Y7WFOnQlLP%(~(zNp$HZYx7LXUO^x=_Tr`wjuOB&yP_Qdl4Cy%1qW2hqHO|C>XhtbGeWBQ`ay8?0?7^>VH~#JNT}|^Ej_ti6MOy4g#{zILy3)H@h;@PPQ~W z!r}qzm~d>*}Pg_9n98Xd+yP;qeet%(>_KFF7I`Q{+6SPZvlPOJMSUvlMv(p07ugrPpUK5g zhp`if@;WkoTvPGQ#O?<^67?=e4x7E=b99;NPZyX-eN5 z9PKpM%akVDlmLe|j4uq9CQA88Hb+v0UCX~d@oTz0&#*>xWAR&%dJ-?4=f*d@Unz1)bOx-OaO{ROuKuoMH*JvW!= zoPTsA!V>!C2#O&8v$!v3*S^`;J#$?gT3KC{f#cJR=AH^Wn&fjrm7G$cVa@pbe12xs zfQ_{_RoHpCP~PHa&CbfS9>8yzmQB=9Z-cn(vP5(KV)%Hg@`i!>1-Cr6?eNVP1gKja zapAUMhR*d^#8NKE!0bq{I$?v$l8fj5>wAbrSva-C%~C^;_1R~GtoF6r%$cIZhVg@y zjtru%3s8R#JBMG5G5^t#^iu~(71zO5js;{~6W{z!o#lq2iyJ$+8Y#p#3Jt$-q=Tl- z)3|a*qgf5Iwec;JXmOe92D7UM1i`5=*9C{*65<|FqXO1SzaC%@DVF9JI=SIz{fNmz zrDqo|CUEgxwX(uKz`hqdlwCUl&r_6j4A%RdieXYC{semst>>jPWOl7iwZuqzd5A+R zkN3BKP0K-Ip2#P%KblTD&T?4h5%^s?jrX(oH~L2Xllk9Imo4 zZe}QPsnG6Y3adg;MhIJ;&={3=DQh<`;6LB{KNW$npR}?v#&eg%h2I3VdXXwQs_!43 zzE3T{7MWTv5nFoyM9M|n0x91+0{Mg^IhBwUFKQ{0M%cjU^@cli2`jyJVVhI6oPz-S zD-8>CeYwhyFJ4QOyYf=KGwxMVH)qLyvEZcy7AUS^5>8h{>jA-RWRe9HNh@izQgv3_ zLj^68ss8TQ;s+ug!x+dSZ`jA`_VAgBL1ZQX{|5~ft`_)M6O>FPQq8tRet&1t3enUW ziBoaDJQypd3wb7`*_ENRsM^Ao>4W#tgYc<{;O@#j8Jx&BcuHs>|G$=naa#Jo2<$)g zjhBcg#>2x449U%9Aw!zSL#-XZ_sqEWLIo}Kh_gYR1#Uzf)cR>8)$}&Hmk!47Nx-dd zdq0$CBV4kD-tPu7Z!c+4`5Oru+YC!e3GZx4%;o4cB z;G0!N-%R!DXFuc-TnKV!WP;=xhut2hM|6=g?c5nehT*T03^Ad^4pnMEL_WCJq&cic zrG5k5YjBc|-pGc$k=(uNs7HUviI;#P)jS?bi|PzfxM_iV z&rKw5L;j$O0|cDu=<$C(QEJDhd6E0a1@8WPKeTUf3h4$OC#p9O1*{@FZV2@W9n9k; zLRHa8W_0&YRVo>C%qP7f1>;hwD?Nt;Lg)qLF6PpTJgh8(h7^(MSB%85WdM#?NuCCd zIPM!a1H8;4oZJ?KCs+u`H+f!wh5q>0T*7fI!`vX>Gyn7%T5m@t`WIb}KeX%!riuB| zy8ET$ZX_c9n)8RSudxV|$gZrc&?2_r!HLFe>Krl%Eb&DZEn%dFT@f$_NyR1S0`d0p4*>9B81E^OD6mx&PDm zaSj+$szUl2Qp|KzycnX0xao*(xCKI4jHQSp3-XOPJ~O{oR)Q-;MDUN92X1P53{oMk!#;&$ ztmN;KPMBE!ouR`EKV2{-gjiPMk52k{^p5)97%QgO{b6hpCqj1gTo%H1Wx?Il|Jw{W zQN}spIz`S`gy>cl2Kj8@)LXEZaMXxZT%ji3F{L3OhFHk;*FD(^7wuzI3|y=6A(t7S~}TY8I9hX!=Mt6kVRAe<<%EO z?yDw5mke}mBW4F6Swq`DnZT)E)&-srk4Y281()x-`PW~MFE2+y8PL>xVO?{EjQ>jA z!Jbys=3L)dFyl&Zou;`>o3StW(*N|?TDe_j)gwEz496uU3)vTVWmAc|u=bh;qN`{9 z>_wabRMQ0L% z!KzI~$fFn#ulD2;8y>#CsLQM`EbTSf|Fcah=E?zwZa!*%!j%Qbs13lL-n`~*0pRA# zEsd~W%|qeh;@W!)hk@X4aNR6pHLP~`Tl`j$uS-Nq8Z2l#l;Lx*|1nwMZhVUYyDE~e zJf1}|q(AS4-`k!2l27u$ z%j?=YehMe&OH*utf20Otouq--srxXmc8CkEACRPZB6Wz;fn^383@hJT2|rnpNqCf5 z)U40;$dzi3K3LO`zsFI{TIx8Z=&~@N&1X(6`5=$Jy(5Y5OLx`1`JX=Eo*VEPHdLHC zi(@=meZO#BD@BmAz_8|}o;V@1`sI}8*va8fU@Z@8)L;~z(qE=_xB;9sEbwa`6mmT? zT-!7>X&!UkI02%5{HUe3KoXxqDDtYw@L^r3yx&cNWNNX099uBUW`+U z@+A1?a1vT(L%3Os%|MY%d+K>gVLZJOvmW&%;LV>R^gEPN-jv|nwLmL|b5FU9B(LUQ zKhLC=AYbRbce(t|HAXm5r0aq{gO~+(8Gq24n>hVaBwaE-LL8g zt#I;D0+&(b&GI|9;c)tuwY5I%l()_^wY^52gcCXPAq0g3bWGj!Vn259GlPza0zMB+ z+)0`w2Wn)S%bsOT9RG@EiY6l?;}IDYq!ivYjVQVex|PWfSf2}xjMSd0-0giHa*m(A z4NljVRttw9^k*?t!<&2vy#N3~S_0ktU}^5Yq|g4JL^!H3l$ev;+Gkg{UHh->{h#1J zOdach%i}{r)h}WeJxKb-g^I?F=LdV@!{Pw59jz^9rViNOH1z6=E007Nv)jIlw+A$ONesxr9h08;`^NZzk@uwh#?$rA^peoJNh;f$@%sKh@3cD$7! zndo+A%>T4vdWJu_Q&MwKaY|k_{CJy!-QyGPB0cjvLRl72OAE)B~*RN zdcrr@NR9im1{u>6!G4Cm^|1e$yNEF6KpIM+-aCeA-@k8Nm4*otm;QlNZ2dqcsJb|7 z`uJBo9WaL!IwlLyn}2ONd=D2Hp>73I}IIL>{1ad-dUC;NV7ZvvZ_JHM5Vog_UC7M zm_M?jp#RnJih`rx3=!q@_+Nq@UZZUBoOuGPAO1R%M-BiA63svq`tMNSnjHe8X_No| z`Oyhn&c(jHR0y4*d&oLRpvs}*4_Ei)a*v>-8d5|n1Ru5i+W*y;!?OnhHc{Nslt1PL zO%`y!HE~hT|8LiyAkNjaocdYB;=zKABP`?S9&QqFP=17xyk_!)rF4(RCnjV_|6?p; za)kE@FOsVQ?_miN1G63_gxqrz%=8Q@!W!W3zDda3>ODC9R7HfpAV>!ZfYW zW%~WsE?5s$U)h%_XVUGCKr~p`0W{}1ihg`e;o6bIDDto>6rNI~huBIn^o%W?-z+9@ z#8D)5G@Hia(5-M1!ka(@V_LKa-F%n1txhxp)Q;9wxp5ogXrZ|>LonMZEkb$1_0rXd z=fssLf_d`I(d7H~V1nnAr4+moBaP%;D(R2e3s$NeVSbe(aW7TTahPiO?PL2<7)&-`I=?{Vj`fThYG5$s0$I$}Wdr^#XUw>Q3v zFuMbtp%REhBP*zdutjej`O6Oah)?|HS?&}w0vbH`%Q}V$7{`(1YUMUs#x9Ue8{((N z3$n+|u_Onb9AWvoej>j|BS=LwQ|U)%LtsN4MD|0udz9fC`76Q#=5T%gQAU||6ayWQ z9Ru^W_>lkK9O**4etWtthK>Aoa7KD+jpXZ{%s%*mO*A#iIPm2K8a*x3`lXsp0#S^B zQP0_17z%e#FzJ=?&({bk+|0YEyx;Y9-}NA+Hf6ysr+Q6X{AeHgR55PnF_D=Q^>H19 zyyuRY6$u1fsd!ss2m3+M(b235Nkz7kO_y;%;~Y_D+y&7)k?~ZqzG|4@y5;THR972_ z;)o&Z36B3P5d)^4ya2_R(wpcF7ViT-#fPw_C~j)@{dW^EIyMU$=4=>>+WGnaiRz@_ z4Z?)TY}W@$>-I1$gi)(KajZJ_3y#%-ATUyoBBj;o6LsNh3iK6j4tos(3YA1}#PtS9 zBoZ$1y+_XoJL-E56X>lRh@fslwO05U>Etd~(@WlFvGJRC zqF2`6?WpW!pCTBSxH~-1%XdN>UT8ou@{$+$9l;k+*={g%dBAePI~^@A6mpvLFX@V&Lg#n&iXqkKi-{S} zsek=G6)CBe?D8?#0 zsE$Ma`O@Okv?DgC3`DHw`5POmXa)53o?ow-4RZZues;u0Gu_&Ft%8=|bTyejQW4M- zplJID8GX15D53fQ1jNGh*L-ph!B+|zm8Oo(W_=l=m;~2<+#9BEtA6X;Ta7m>0>eK~@oMEOia> zgeJhmkUo_D)A1C%33`rhQ$K(EevTxe|!Rp4W&{OOSnxdw-Fq0dXO(*~nNJKmu!aLQ6_(2&` zAn%GGbM@3{5anZFV){*wmd}vC`O8C)+kjy(7ckVXjvzSZw-{&X6P!VS>L_}EF?1s( zLF7jzK}Q_qfUL<0idjXg0o|3giDW^#5m6qBIL~<)0eE5*5yeWx4+NtB*NqdOX#r72 z-q&rc@sJ@9d=z$?Qd!|^iXx$DjbjR7z!H9yfM}6Q7DkA zf#^==wteOLazu9~aC!c#fBy={Qyy{! ztRFg@^sx3}L~MOE>7{%ep`~|Ylb%`(&n@y-!)1heeAz7~V=<~lFGi}xTxo(VcO#Jp9P)ug>Ll_CW4B*PO9xyqS_^4 z>Bn5KnXKi5uNkL?_+JFQ(U`wI`3g?H<=4d&Wa;`7*js*cyuSJL$3!~LUp_lbiW`do zU2RXG^N&8p2=G~Gkp`TS`V-Jqw-o+0Vm{NsPdx8(6=~=-+lRL7IShKeO?iK=oqc7* zjrkT%dRkF&UY*He5w7wgD;TZzuQs1I(JkD_yH$=>g`Ms@bgixNoebyqOM40drYiEX zva${!$h2O|v1^KzXH$zzpC$Rrew6_hB7R@elGG#fD|T6QBjxobUP>R#3V5CocEj=D zycL4{iNI;wVJtQ^q4;E zr-80^j$yu`yVGh>$<#{m)1Jz{dWX!83g?6u9q~miNqh4v;}fRtZe*DSF^$*bTVr`7 zcES@Me$jWG*baO0F^-QSUDC5)2q%pji=<$z&lks#>D(H}jCrm0 zRqH_rcdsO`w#7lr)J4zf&$Z<*jqts^-#J`3{cE}bw?#|oVw=sFv-0A-Qj1ad2UiAk zLX`Iuw=Zg2^hJ7+3f{(f7-@6eOEpKmzW8Yjufi|o7Ey=qEam+gg9MMRwU+#R+%{RW zx!;~1vu95)mK&v<+^&j1146L%3sUL;3-SNI3*p#zVb@zs}*C3mw+b<1v>|0>5C=FQ)vk*PEm&AxWF(fdoKv z5g4-Op(HpnV_la1^b)ys%H(4v(C5S5Lyl?w|JjSmmVvqBQKoIIyscl`A;_ng{Alm3 zY>nW>-5)DonYLCxi>%X3=X3<~)(a2PS8Y5Ti+IqnRfX4Fa` zqDt|3)ara=ymL==xrAPG!&9!!j#y7!kjj$pgh4SHt=91*N5)p#a@(fAzN zo6L^7gZ(flC3_(S5Pn4{Z$dcfIi8$E0vD(#f>UlQ>+?&Y2D*IUhMFAHka$FYul9=9s-&NuiD>qd;JosC#aT(S&ymO;djuC$$LMiC`gjm}G;CS~x zTmKJpZyi z_?9!^8>X0GMOHBmKa=6UMu&9}K>c;A!HfI$DPtlTTn?A=Dk&u; z<=0tBT;JWk_-ygn@Ypl^yM@L<;z&@&{Au(4T?K!G04gm*Xc^679_hUCmK=<#UQDE+ z`SSUK;6<8X*Dzw$_o4i=Qqjh@2h+CQHxE%OhF@XIU^4_*#r8_L%`rcZS1hr@{B@N_ z)rq==;$B`gi3Vl}6^g_469BVl-~R3Gy=lS1cX6T3t)R|1MKRRN@D`GFFzHEnV!a+J z`L3}(@#zZM*E(w(bW;Kji;Uh_t*{V$_T@HIAu%8o8&3z^3K>YVm;z>on9g2(4jbGj z==EaHY`Z_``U&d}z@cXdJbSjdwT1u28mh%ouL7T#jJc!HvfY3PLh{2k^O~ldbU?-uhd#3?7|*O zYeofo%vBX@%Bv^ zcRT>Y3SNr_K8q4ispzLcVCdCtbQ^P<3&v%x*l!TXOu75AzkxSWtK#R9BO6UXGkA^D z8)x+4$oTACHi$dgVAgBXu)WH1ZJPlVfO&w0N}(`}Zc$Mv-_qg;rj;-{t~SRd ztZmYF(d`&IQeTO$GRS1GU(MHKjRxZ83iRt!dS73ZJb&mcKWLM9=sag-aW1ZA{UGGp zyN6(&R-1)H2Kxr@Slvb}^O|G{9Jxzk_&Sl)&h)6eQ*YpbM71rmHM+=~&VQouc`3r) z&$HxAKO7w$!KZ3i3=>x%8)a;RDMReUtf-vYK)ve~ZrO7DaeQo@)9$L)TJ-tk_6{FP zXf%g3hSU}(x|3K6!Bn+#j^N$xB6=m(jqWt5jSC5q*s>U+!{O$eI>&=EDNn}*MwUsb znyir0jaUYa&KE*AIk!aoE+Md?^?CQfNv(p$0>aXQUXDm(u%Ll6`@th>y}$cCOo;o> zdO0?J2i>d%xd;q>!RQU+o}|)==i(_BhCkRq35t=O$JE+8ZLE}^gq!5@1pcnczrx|r zgu~*8hH1*Ko)9WXLvJg`Ez=ZFBop|t6ra)89Ygk%$)Mpw+?;=*fc5{_Bcd}_gfX~>VXXbj$aeEPk9!(+L%6=u(Weo4+4%NE-D#JbAp4=y*NlBztTsZ> zqdea$(p;=%9c2YibJ7(l8+d>4AWxPXNYV{UZKVUp`j<{&0Aqk{l!^Q$76<^0(B*hO z^%Su#031iS?@Vj|IX##ld^Pv&bUh9PaJ*2>xt^#_eiK2X!3E~wB1n4kg?b4#(?^^9 z2-=!jZ9xSM7pi%GqqOKq;T;|oOTaLxzinqQ7=!fiMQd*;zlff7I4#9md*gPk3O+5( zS&2lMjn;6@WH-VdPlY>nTGQJ?G5p}ChG#JqCcRF!V|Zf{XH_#mG{FALVu3M+cBPx= z?)C}=QHT*8s5JY{(|eAAO^B?9F*S%;upBQ)TGd!44;^&_^OIyu1u-Z2yS!Hm%}_OQ z9BLd6eWpbbSP67f5bFFP+D|Xq=P^JKoncg6fH>z5Mz{zmGjzie;4DJHoj4O~kNRrJ zP==JuL%3qj_|>k-1M7)V&0no~Q+?M~cxSF_-H4w8kx^9j2%PL@lS>I@Kd%B1*0W5{`kl`?Ll8ug@ zs^>Bw=NluDmOaCoA%yt5d@M7=wHC!x5ZgSmRpE^Z5k5@`ExYAurIcsfjq?}OM1&sr zsL@vo;GqEZvBjmDyjs~)^3G+QQOdwMc62$epzJI;FqK{E2E3*#Lm?6|{b=IgPPU;TABsp{4BBj7$v?)TiNL&69TX z9S0kyD-)?#kzpXL?U2`Y2pi-Z?{1~rLPxD|F?8|yO2_0iA&IWX^v|$!X0F!guiFY{eSP%IS)o1H zHTq-0UHNQ=D6NtNR}-^#ZbdKrMc&C|!9pH4kO=>Z7kZ&U^ypdO*wsXl_0v7En2IJl z(1OU6d=kj4iJ!6-7glK)le z>N$NrYuHm2c71>&hA8C^fn_JsIyCg+CV6*nuhg0pfX~k77LACA0KWf64F`ZqA8_~K z;3JuMAfbJ->pYwAQ zsNwLNK4XqeO0-&d+MLd(-v3aue`{ewHMSs3 zkoaMEUbIgqgvY$~v3T+uM8y-2alj{a0ciXp2DI7u#-{#L{MSLhgbQbOHUrva33t{^ zamtri;C~9ikiG<^{vs`u>BIP8)nauz4SqS!Cp;V#pd~NS&6g(#{&Nk06@!j85}F#e z|D1WD;IFbPe1?g@S6_<`PQ|wJ{W&gE9;hJ)Xm0*X6B_U#|NOCo(A5r_wwm1w2SLc)Ff|Nid3!!2CF2ho5E|3^i;|B>B|L+d_?K1lt0 zCHTOA%79fEZOivx+!hgp*E*Or0!&|c0p9oh`&c;8Bz|%6d)x?Zo&NxmzZ*k6aDmyu zfI>FM0CnW6{S+s&T8NhOf&b|hw;Tj3j<4_le2yw~gh!8*ncUsnwr79|6?z?RvUj5X zD0Jlv>LveXF(Q2Qq19x)YVjrn(f-AM7UyCbTM9Y&`pl?W1x)*a99|yQLH6%{8KKC* z8d>sRmv#j{SVcId4nB0?!`+WAi}2|Le>I{hYyk{^JKSBYUYo!a6+tOGh?{Mwi6q~IPBfTvFBVq!q#i@v+7YvQo`m;dmIkm&Fi0q+d4G5EnKBn%g9Xky?*LCQw} ziwO`;FF-z5npd>&EC%F+0@fKuU#nT|wvoTkT5vG83W8tVU1?W;0NFEyftQ{#gBAQ0 z2M$^i2>D#(g6g)}2O)6a(}4}E>PL!iWa>XVP94Ze#pj0Guy+92uX3|pJ~$CRO5n4w zTA6G?gt`-S>VLvgc8Xkg#Tdo`l6?z?C!hbLEB(KDb3lds$U_3~O+G|fVukGfvk+Sc zz;{@O&*L8q86=klAAANd{hugc*%t7qXHopAxg>Ed(`aC9p^doo?Jxnj-+%u6-#m;< zu;u^H?+!REK9OgTUhfgOKHcZBfAhJZv4k|%8klBxNzj4gNS67W@+BnqMgm?3@VqaQ zLYaHr>~XcVweK~oiUAT+jg6`;3XCR;0))h5;N$%DDTc|xD$)*lWcwQ8_L(~R2BllS zgWHn8ckPr0FDDPJ$BTodrA%v-|I^Wpq+JmCw-(_4_812q!kor3&5A6p}VCf{p-UC@D)~j3Gi0w~dNJvP4=(Lr*ptF>dC>5TZ zMzQ|2=`tD27&?#rqR*B`UfcghuZ^N(3`6`^(qlwqfa=*}Lff6)w+=(_ou`QTOxr5J z_Soqvv{s@l9lhhZhiDPt6R`da{6}(dIUm{EoS^%|wCa=qjzq8@{7`g+vW!Ea!x<9| zEu$NdoR%Rh1E60G2)M^?%ESCi1iV(ot2U3O1UY_b(oXmyj@^PXkO5-s*EkFhy&7q^ z6DtR}q!e^;6Uv}R=?_coGlBSLC)G~31da$1fa>+(XX3)@`VY(ISmd)KXwC=I0ckb$ z6$?H1b%{{Mo1mC`Bp-Qr!R6{gczkhpr2)9QQp`WQYPH~)1qpb44I;~ zDMgA{SFFU!>rf0qotxi{@Utnw@%`V9BO5~&*f@W(_x0j$z~|x+gJP3oIIqd2M89Uz zfjh=Twz#p8Um4ld)TB4s(!jR{Tizh$NQ7aJ0s!B^rEYy)C|i05-w}QfI@nV+hcWg* zNu516HOz(!eMa(6yO2W!Jp4#BaA5D7hIC>K@c-t+5=`*8^k^mVFy zwy?HA{Mp1)1Mk{|551^p%$6}NtOwq*AJ}m0vB67z31Ym08UsEMu9Io8i{cr}^MRuK@Ld94ffj9Dmvo2@J3d z1UdNWdl~fcDt5=h~xE{B0|Gn8dM-q0Nvd&yV#dJX!M&@e3 zc1q;I!)9DU-Y>rfw7`DA6HIwEf(%n7fV4%;!Vv?HBUe|>0W`6p8QKyM^vl;EP8D+5HgMV zV5t>VXz93|Kihfb3#~3GDt!#oMf|7}dSL*hCDo zGx%7{eL(tZ`U`~Y0Hnf)={-$`OYG54JH^NKvfk)Rnrzr6uZjT~_HxEoZL0q6;pelm z@ON`Fq&jgSN@TphF^< z_QnBnhLvUX3UbP~GNOtmp}>Yxz`Vy;Y|DQRCtC});ViHhf?#qRP?Ro0iLJW#VgVlR zqcuTx>w5fvux3wft;=rV3JyZgQ{Q;11u+&m8j2^nmAWnUbhOdq_=n$wbf!M|h-nH| zx14F7pTOX*fj^*t+8X-MoI{CfvgG1Iwnog`u}bDE9}gKcilwDY102tB+-1{GtImGy z0T)veQPGcPK@|$(ofVJzSof5TuH=KY z&|WK`R2Fx>j1Sj3K6bvHz38I_(Aj0L>F7`W?Kj+Z3o4vy4Yk%m_`&LuJgcKyf)?`` zE(MNbxMdRQkt*L0qG)7;mOS|~G#p$9G>=zUkle2;XeXbL3iIQs6-jg- zMWe)W0D@NpcXC-B`GmOX|*H z4WSETNdi8Tt>dx(@xeyb^T;$ubEH`ToQ2>y`IMEd{~(|~6sV%!H#gxj2E+mxvLQGY zd#tMMXs-8qk{NVz2F{*bDvc|GjI^{1KX8g!(%SW9Pu%@Bw>OGHeGl9a zF)2TVQD{*m2CH@yHdzmQ|8fc;{H`WIdw*ZEjH?CHLBo9^=Cs7{6N{s8%Em>XGz$n`Vrh8IhV@O_ zX&o?EyqWo?7U6&Mb?yhBjUb62u|(%%u!kXuKwcf!<*ltkz;OHxF8)_n;zqvM&F7bP z{W`9+X06YL-8kp&laNNiq=e~0x4)B68lV4d_N`7W?f<8y;w00eYu&)`XcoK9(G8U? z=SukIu9bC6Eq$CJz75b55p25C z1`O)Z!vd%APyPX1dspYJ-UsM!DPMea8E96m7BvkyKz(5J-xWlWclQRPPPy{SwWRRl zjZWNjjn;u5XKGx&ChbUxEQI7K5OLaeT5lGi+hCPNP|*wN`cCy9L`zU|0p;D9gu2mGy}*~NHz{cNd?T7^@t z2Jm^}@7Se;lWKwirBN4-TQl+}!@Q2O!q~?35|z&&ZjTkN`Q1>=ef4I*uD!1q!!_I} z(pp+ys976ZYEu;hmI08n*;_NdW&D~`DC3wCpKX)E#`08#=mc=C!c={Klu%h8ZXT+HZ2pkeqwwuJ-e4YSP&o#{b%hU8sx3HoMC&QM6Azch z(6w!V9^)k?go#6ojWUA-lGkppFa>86k@Q&>P9yMCq8M^18knB=tO5w?W0AZcbMj$D|0N+Ey_uRp=*6bwNksdWQiQ-88vw$hk`y zMzxsF=67!-0xu=(KJze%sNLMy0P5v{sV#O>!8F}xzFiVC}39jZ3-mng6cCXX7=;|fc`e-Fl z1U>jff^8u^tr%b>K} z(fP`Up&nnAnk;d+}A^Rq+#cm{UfgE{()~S!SaC) z4I1;jH{p;L3fL4tyFq&<&H%cP@@sr9%P{R{>1;zU#?4}Pk$NcW+|8Ir^W|;#+MXy$ z?9SED+}m>t|8|VaJZ|Q#6L-1XeW)Ts6ftlzb4q|%k?DC{{@u4_cdx=>#Dr=vpqmn( ziVyKQbpbk!}QRCQ=JBf6P52 zNP1W$bB)LVe1Agd7;BHSUrAoqY77UR{BYR6#x6!CPQ4pydPHRfw!lB3ox@>BzGxbK-9nf@37d7>n)|uEsBh z_Wq;Y4+A}bMzh?h@)rN=qF&_)Q8warIiiq?F&snC3oZquPPd>WzuE1o)_G%yu4W@1 zH!V9^XyXqQ&Wfh3Vtx_3ptrR1?~AF>U9di{wL;g1&j8xQpL6^7JjQEY)Kzqp*(9=O zIS-+u+u4z*#~da;Lw)0Yhe;PjBmNEkz<4*quBb#iHBLaShoz^Er}~v`fx|A~iE#sH z$xLfxCavBbOb$K=!@^3kLJ*jtBKkmKUbET~SJ{!gdME-xD6lI28&C_7TIV5cvKu{8e>I&8|1^;zD(~sCD3~L; z4<|X|xhZCy7qWL9T|z%C)P0KPAbyDi0x8k631a&)Cx7-#@S4_}i^W8`lU80Wg8t0m za9bX$K+(XoPLoZG(Pj4L21cylk-LXdwJ-gSxqARDg{`Tv1h0)Xia)mL$B^{E{IASF zd=Yu%0rERqVEjA3`VBl5nk! z3~Ul*>zR@K#}5*gApqV9z3;bPmj)HcBY|v{xE~SNqvR`hT9v3$koHG#64`T9@L_RA zL{7KS3^RnAbrV>O1AAXbFe33Xg1EQ|1BQ1-d)I*l%9&Vc4}>&w(!{n{OJ`5wqQ!xg19%_ z)f>`z-_(HG@t#KneXd$eBqKK6+WK4uPLNj$swN`fO26 zCg_!`XNDlS*1Pld{1YW=dV^jpW}>A=TD_7C@Cu{mBxZvd?M%FWsWvXR+=W#|zyO>g(SSX81ZIvpg;XZy?uD&mdw82RW?kLZtg zG(J*W%@4f=ea+`}ft)4)uO}~HZKnoE^(Y-nEn++A-8X(#s6@^`tn?1sRiaF02s(f3 zwxEsGe;0Ye>;v{*x_7L*PzOh=lVx9E03?t(W5UHOCJepZM7Zd^REn>+1RF( z?+adEM1d1OQPi6bR5N6{Hh|Uw#%aJd0mA1UHI*JKgkR(#RFS1PnGkiCm(NbH%R1C>alJbYSc|5zdebXX7lcl zAe#D&cF;mX65DL!zaDCpE#Afq3AU}$QKtR8KzoMTJM6`lBsoXIf!hO$g*FbcN~DC7lNK}L zd#KU;UH4)7EgMUiSOT?j=go;LWrW0&$>0} zZu`N(c@(uB%s z5Sw9Fg%cW(H*xO=*z034uUcZ8s=u1g<*GE`F|DIlqHg}NPlp*vTV=O%_IHPCW1|VF z-$B`n8;o}RV@dw)`6eRdH=$T~`h0$tSFv2}#r5r?pAcFz@mnG5E zjsHGIfL%^FI;srhF&${B_)ViJ@KvH@s`=mDoTbj(sTFZe?Ww|4Fk?4OS zB;`ppS`#0xy;<}5#o2TE9k}49_S0y$`C}WyjXR3Z-kxg;;34mg>j&b#4tU94=dR@Ba*Sp9GSn!Q7f)?I7(j#g6L`czU3 z(-1)1=Vq-u&N7`b82ZGGEz0hZKnk~rQL5MrQN9l)&+9-Jxu6Jvha+CG+Ol0 z8)dDjW9~nhR*8eW=T$qdPeNBLMr>(;2D!LDHY6LY^_Y7YF1I%)XF?VWU6Q8cJxV|` z99tR<8uSiE^BTr&7+!jbC^afAX0{q#W}$^^*Z5w%;7vi=BM1G}yI-m6H%^4DyDuI=dOo=(y3pg+Fh$^=l_`D`)L{E)Kl9y@(3*=UZ!2hgSzI*!xF zR_F6ugZ`7^G%z)iSecFg2Myd9RQ3#QP5Otye&y zYX#$VaOpa+Po+X3UTG3BFgfQCV4+m4M6;{KtzpZ??2n+>Bq>*8_HD|z>ln9OH7;ao zZql4S>3eZmuN&{xCt@0#1@sFeOU&uUh~u5+t0(xQxk@6bL?fp7(_su$oyvJqPne3u zjF^lc0pZ`ibYWRQWh42K=(@GsSi7kWx(1~R!!COCRqHfsjQew&s*U1O%f}i^fZBrU zWd8wxZnt&6g#irUbA$mGYfylwsIz!TG59n-dU$sP$fkAiVz1?1)4XXEo*O9G7 z=@|oFQjlOU{SmPjoe5sNEeFa*wZYj)hqAdw%-(e%Usoil==G0R9uc{YKqY#5a18+x z01+AIy-4+l&;**r$S{1|L+6PEZ~_9cxv>d!0ut$6;*h~)KSS!^Xsi_0uXJN)OyYy> zc(fL7$z9Q`z~vg>WVjkJsw1efUfdtVc}&eSTYJu7d`R%iV)DD~x2V#{n&v6%#+ZYs z{w%KAQ~9mGwE*(l%lkJE&jl9~oLh+8uU_(RU`E`eAH@^~dlGIG%TY<9f~HO;WDjcU zL?FFE{qhpJe#&HYI6z(jE&5wYzv=IzG(k4swN~!&kYMRY79UIUL)V4Ixezmab)UTJz1`*gjOL1PhFw3YkT5M%<)RGFVkUFV18o# zot{oM=$x^hCZK{wyrkDnb{uQkdP@0LcNYyzOhZy7S+>&=B+j+w3?Jsc>EHhuchsag z1O^rFL#)1D4sVzl0^XV-Etl{R(2ItrJ#&8B5;7HnKVGdYvwL)+v;DO{ig%@`;`8@| zi@wS6HcNn$EUA^%(c^bg*dfl=d7{z`Jpf8^dR8_+)O6Nwoxu={`8#sRb!ltsIz;Ww zGRXc`m;ocbj9x%yf9K`3HSDXwDLBMBkT*L)236L9RdIzk+Br@QF_lkdsssQ{;El)W z45L2yr1;c3_GmTgx#PjEes@E809#Zpxf;LN?8V79?Bbu~rahQmny}b?I9<2M$A_}R zR6D%r%tTunm9H5S*>IF3*c@HTl5%BiWyevbi}U_V2~SKwLkXX0<8^q1?S=RyL=Jy+ z`KVgQV%jqSqdX_%a(P^5q+-D$F~MT1kUwKCc8)M-as5Y_|Z>5KNA86J)1Dc6f zzEhVvjELFGS;3PC%Ku4F&A<^`!?$h^K+pMYA^Y2&smO?kXG=`}Vw$hc<+v;>ipHD^ zo_HE^cxK(v8px51kzmWuKk>BPOZKM0MJD@7PqcfI17XNIArVs8t6;z4vcr-7T5A1$pn^RUUUt7$WIo^ z{uXxb2WLl!crgW$ z1Q(;td`3+Z^{3Qm#GgxJJ;Kw(;CQfu6pn^;lyh>tAXN&A5L19}LSy;H=l666!VcPN z9mmVXcF{&wzB6x(r_tvIVT*03Bahhr6g%`w1=M&tarNOo5v3XYy3bifVS5SC zyBr2Z#5Y5`;+LN@chY6F-Wk()D<#i&#O0cFn=mP3=bx56zZ)52wNumpwq)N{;=aRu zJ*gb7DFUXeDUUe^;h1mgPI5LwFLdf2k^5;iUsh~rU%aN(D4dbuNp3Ns@MY3#n=ozO zR$jQz!0P$cic$)VBp$Z5aG`YW3n5%rzoG$|v`ZLKsXL&>Y_wQP&b*VUsb!P!AmYjV zZH)so8-U5D7eq|MJ3WV#%4abn9ZuPilrI4;5EuQN8J^g4^agG&vto@-R*WW z%1}KL%eQiIW1sJZAcYC9)-R&8?e7w&;o`u)_@09^AaUv~^nndW(pg zmq!~tfuiqd)GTX#FND5jsp$*@s|`aka35v%#E(-DtDqi}vJqbBmuzf~xV(WxNlc?e zlI+k|=XQEcrMyw> zWkP5*DiPw&!&L8hUL@43n(}pjb4#_tRx2v{<-OKv>qN})TmWlzAeDGb@=V1*u6t-< zP%Dh)>yX3MzGpukzoKalyp4X2pN}D?xpDNJSAeEU-di%p6gJ%xb9B=u0$xF&nozp} zOg`Pa&eMeLv4@q1sRzDl3H27p5ce;?GwJi}S`+7-;k)(wumQr1X$?*?Beg3*!JiB6 z@K1W8Gxe&N$xyKjK5xF8dl*J;UeiBSVTH3@vsrkl2cY=4^UI90fS##(H=J9*dtjPB z74LMi{PcQE!DJxjNwB9ReWY|d;h}j?(m@2ZvrIMmOW(7Fu3C!QKAgxl00Uk!YZzor zJrW0wr`nlMZ*DwHcT#e9J{i3idcD#8$RNG>{IjYO=t-g~$P*>^lE)q$ zH>U!ewQOQ z2hGi#a-0FO@6T_fy4GbGXnDW=`7^MRQ@`;nroMuBV`(M-oHQ0-1O%sOWE+QLFLQ24 z)e2N9h{qCBQb%wXf>WPIdg4KY<+CLdZa+}G9BPZWrf^%C&;zbB0yP2`Rk~cdC@S$& zRCa&2Pg6iD^!b4O5x2s=>PokfS1lQ)s#zP}!iVk0kv$ad*{UPqBRONFRQenx$GneJ z3&Qn^&5N6x_oqlwwYfyjYUC9GO@?)JjnKr;sUgx?a{#Am@X+gAI`?ypoi6T&$E!-V z1aF86Dxc+q#2;M8YSz9azrEN%o;iIINPS|J;XSFY4t#F$T9qQcO!{gfABf0*5%Ha} zE9~h7w+Y>b*GeLSWHp9)Eb|R0{eJg`@`3P6^|pi!sF&v8;q)P8F3bj4;en4l25M6X+q z9%iQ#v~cHi$>!@x>%LP`(EZ{D2iG+y{L#sBk-zAbUsg!R+lnTM zF(#g)IgA)OeN99L<*M&dk(EX_E-C!p;(=sQ9ZH{+Mb#dwH9tXD$~z07OW?H2z|hfe z^aw?=6lw+5x-z~q ztB@jsK?g_mvZx6obks9KMvtMnXWc7$gR$#SkAaM&2Dp^rkY`TX+A{8iq*vgfcc@l+ ztO6q$Do}EWAqt;DVx}5Ke5L(hb&CT%g9;)pOB97&{SS1<*QEGb>|MyJi9Cj~ZAvi} z{-3XAK$!tN0vMposDbtc^=B5yhzXF-p-N2q&TFO$CDWUE3kDHH4gi)5@!u?${i-4D zmm^aE_tMRc|HkM0*LG?@d30KcD{_5{PLG!-$*t6(43=nzpEibEf4WT$lD4N$P7~N2 zJ{1koyXOU1kontgmG0E;XLSurLt404PV%+(B6Y@4%j_$0`d+ET&luxLbW315=EtHJ zJ)`J`-KP=C)@*qu_p0}KzwtH4=oreD9ZRUHu5#o!keK?6g^`V$)KD?x=UX5$JX zO}^-F_i^q2GFX3cy3EPypotorN#So3XO8Yqo)kpB9D@Hdv^NYLroiIswL0M`(W7>s zkulLdLKOfMsM2c?t;u4mWQBZ4+#MtL)i6jeOsILOR+kB>$4VPeitr?`RO*F$gF#Bc zJr#+M;l^S$fKTwC9%`Fgxbs8MM#O9^-`tMAoqo}78GcsYI=yCr2 z_x+CWx%!O&un_~N(LUk#`xFlECV}7=%xQ$&M!4ThCl#n5Wr)f{GjmF`hd}E}^l+Fr zXCL~;^tvtVZ6MJG7MLG<*X=CXNk^iKE4#%jB4*lx=HzQV7k&^N%lHn@e_i8UH+J zxijjVf&mN!Sid6m%-UIR4PP)Ot{ZpInSRR=`;oJBw>L*lHT&fH0s+WnpUb3S7tJCt zmy@Wt_s?Ffyr?D#GjZDGvsX2(hX@ecWG{S$n0mbP)Lt3HWV*4{J25=`KJ}Q$*Ln~~ z0FTHk<^)tv*NTa3m0e*b+eGtU;Hw)O3I$Bru*-r#R8od=eUv&O@IgLLt=&bWBt-R! zSa=L19}??GTQO4(Cv`3K{M1wv9+z)Aw+9+PTA%oJ#4bUP1bqZEF7<$R-sue@p-SHn zMsV+%Y?k?zS)_u>KXi#^{Vl!|TH!P0Mc{{*c^NJ0M~ptnI4hPDWs~&Dy$i$<>@6S+ zHl?Aa|CP}2OBBKq@OOZBfh3#aQ_R0?UIQ5RCd49D3d1ZIpQ7(A1ni8ag%0#p9G zXZ4kZp$DeNygiR1l??$!r+oEQYNBs9cZcPFASr-42a>y?*l7mC0AH^)bGVh!A#mLf3h8M#&mVu=%jR69O_ zQQyVJjwRDyWKF9ANXU7#PNyRoDe7MCb+^7TVyWC9NVXx(1+&SEPC zu~lva2UshY$;&LpGHAgA)y5-ETa%%b#Nf%7bH37=O|CR%PqoakPxPB^Wv7|C#$+WU z7%eeQ?v!!lPjRC>1H=j{1b|Oq=YUnv!|IRwr_~c0X#f2sm}vL=$yADQ_-zJfq68CQ zGThFNj$SX(?j`~JqczewU@E-xi4>_Fq^-BRaCGv7901`wBQi7?_UmnmQc?jJ$Xqse z9n=x3bMVZd~R&M2*Sd~IeDv0J1_4qWOaABk|lBvG@x|6 z_YWt_Z8L$pc%{v$jE^_C=K>-mcm$ZqkWdo%h6*Z`S5ciY}vh3>T^;p4#OYK9w!7Kc_=KB?r&U~Jb*~a)Hk3Mq*;OH zx-Hb)`u$JWpFa^tb1TvdSrkQNw_{u`SZ$Z+fmD|RXpwC++)Y zjQXpOP{f#K<~8e1dsy))&~C;mSdZl}?azDr9KeMwb)|Y0*=UP(brf?`2$*--D_D|W z4X)Cu;9hYJO78D!rH<0@kNnHv;RDYIulXp@@g4jx$Q#tWY-(m*k7BsKnLPBS88?}E z{nwNOxC|omP6j6hA5>9R)Wt*t=H-#-e5)ZWjVBw-23?uPLIAqY`AM%at1U7RYuuT4 z0HQ0FR;Nih3iPPB=6-6!p#c;f##ag#l%jI-KJLH0WcvP)7M{g|nlo_+jT*U8dT!ml zLK^NxEinX(0{Yel9#~!2z|=H2M2l#!D60RAHb+Zp1tIzs?q_y63aJF9eR0UnY(V-jThyz%37izwi5&Bz@aORq6;1R0Y0rQ$_EkpqMk==kt1#<_ymyj z#XWsrUe2tXBLx}juo%?kT(L&#TW&99ZPS{-EQaxQe-f|O1Xkmi42q4J8rxtn6{D@K z4G#RP>yu&Icdxgvy;^->>b!3F=rzhhcpcYiVQ8~X!N4_%or%)%^Cq_w5s+yNq(us; zeArvO8Xw+0ZY(43^OA1mgc1`$B7n0XjQd%z^KJKTB>Xb4-6qx<4@Fi(zNS-SvB#;9 zGd22MB|q?3L4&nYkN_&GO=*9;E+0b3jq(_W(Z?f`cG*9G>PQZ2oEP0#Osd}Vs^+z9 zdmKoyS==5k)~qx^q*h1?IN6z88BFCb&}-ldBM}O7+8QO33?rgn$&rbr2O|MvF6+TW z2J{+ctx8HL@o%veh6-nIssawoC}~~%`abfhP7Uo)y)P&3TRq6J=(7W5br9>E?&`wE zIR|(WuJhL#pGp9)u41mCq$##IKJo*9YRiwHx*hQI{a6RN(%(d`^H9Eo%$@zXj&5?W zZlt8stWlvy1KbOXU^1UL7-v^vCh5$?r<7+tltvFt@dIN{NJT#)fp6dewS1Bvn2JU} zvH_+r78tY$<|(8GLCk?Q$RIfY)4nc_sKAspe=uT#eniryhDa&Z(G01sb?0~TGH{BW zl|W9h4YWl*KS!QQTu(Hh8WLO2e@d8WXt7Jf%5zJD>@CpO zlEjX%0e4aC$!PuylGxBFR$~bz}SNALk^$yuREi zKVN*0`V-7}wBG~EDgcao$dQ*EG%fsd~&C5i0+z$V1!>Jxp<6EG?-!nhU-Y0mIctVTTd{m zneBqvm__Me_+$pyHMUeQRbFfG$BkfsN3jVrLL7#-E;DnULi!3IlYTzesnpCv%9nCs z>jTLQ@4$pVFsrc;!xWdXn6E`+E_R)YklJ>VJti2weG55n=|=J;S8`x*7=hCT7`+%ro>P{Ga#Q ze`ZoQQQ}|u7&A0cN`W(0ZrY1a_os*TKf(wK>C!+m>y?ozehOel$gu3LYc2XWMh`gk z+COiNn*L)!2N^2Z2+)C(gT{b=AS<{~{T&ddV)h1VoSA@24jhU|0dC+AiuId_fc_6T zGtU2T4uh>s1uvcjvGsweeehBD(X!EI01LrK@)7udeE9zWSi!&kg#7>=9R_ZSec)za zrsc`TW9rn{z=7q;OZx8qhl@mP6yhQPsTG*~E>!{q0uzf_2ej0H_S#o`qM#R`28j5Q zNFn%uv2PebB7}GnEWprzZ?uo zs@_3Km1Ws~mIboOL1ys$Nf;Oyj{hygV?bmgi~m$jj3ppKi_6o!?!}G(s_k5l^TT}Q zoZwx{)oy?6GBD8!K7ri(%io~BOh{PR2_xVi)3!VUYGA(GSy?d!jNo6>pxpZ3pe(Qt z7IA&s`_LWn*z^Jcz+b%HfFSEPkYf6tChYDzF;i=Q7f{}{jpQlr?0&)khYy?uMC&?A zZC>aQmE0{tEE#3lrw}l{Yoe%GYfBd)?i6jUhtOduccS#5 z0M$nfNUl=k^nwe8C`Y2n{ZyT%ulaIU(1cHc4f=5KLmu~cgcj#68yK;-dk$8`=6?U8 zF>q;ep+g5RNTI3Yj?Y7{Y5+salJhdqtV4)}pa2!AKp>dAnEv6Wytfer4QBzkVZ_nx zq~N1<`iR{yQG+3Tw9=fx7g3cjKiASC?BidcDbLKNex?w6up7O=+Q!0%7aYUm_uKD< zZ23!S#NvnD{rMf*DeB}`$DZl@d25&L{eq5bE@pJe?@s2DSySQHQ~yQBP2ug$5xX-$~97^8~YBte(iGvIYb>|{2Hj+0xBx~btA@s#~7*)sHzYFoJj{X8aU69Bzrph0)@c{I{N9~>+%jzS7h&9 z1b(;B)jXVpzpvQ_;rJpDyb<*C*$i15+tjtKXicxLs(})z*-UT9@}e>^GgRcdbV5xf zW~gYbZg&#+BV1zbZ7@0k$r5xG@9Os7`^P`j2~-Rc`NWo}z^5HRyajU)_`Onr#g^lm zD=guG*W>deHuwDmZTvE3CZs(UP6H?x3n)#WVlQcm+ByF{21skPkDoq$P-k}tvRE4l zlP}>w!E#50s7wOcV_Vo<)5m6!j?&|95@EzB-t>N9Q1L!vkmC)mqSehqbKo5wdidn!9K89VM#U);!5@;nklZ8!CAW3k!s~%H01^UW5D}%pH8Rg*Wr3HUD ziNb_B_7pq@kt2OPpa3z{LoQ}8(fh9|5IA+3Dw4_XR|*lWQ)v4Y^%d@cC(yk6qW&N* z_$#+Vo(Up|nB=!i_y8ac0pw7(gCl;)b^p{{yj^hy0nLIUxJP*!g|Cj1`p_dkV9mQCEn6$<~(y@aBLUt@Vd4B^V%|fu2!NP~;zJ9T>L(_I6g{ ze!Z<#s!9z0oq0!uo2H+5jPy#}3#{H1A}56f`*qP^z<%|T4~P5B=pa8KYdXpeApG!e z@P?dCCPiy6q0N7_I0OG4IQ{*uZOapKBs0p^)8gmve5zqmt>itCIB4rogd+Sd*)og; zG7*}t8(;|aAo-Hi*j+XLw=loKVUZ(gJKzA`*cB`SE}MtPWH<_XX5U5!)W zetqSoc_Sv;zHpI5Uc-8{mP!|#RH|YL=EeR0qaAxMEErb30m3_^rD?LxG{Eq5f9O6NwHcLqKgmQWDEsw75&9Y+lHI}5 zApMEft-ZA>S`0dyc0TKIVsa0ga^_=Ri5}@7WOXTbpRkn3H+G-CI1`q;mAp+G-(kaG z1&07A@Bl+ve+_JO0?MVUu5!#Q8G6>IE-!QhN8&dI)^zRv=?OmV145aHu{n#B@NYG>;R58*k4LTC#T-wTt%`4i{t%+cxxBBKXtaG9F5&hrx2Zc8F zG~H{uad+78^~RmJpp)#4;_58$SR{`YiGcj`6D5DM)JIl4NeE=fqu2@7K131@;bd#q zr`%qV%?9Nqyj=ii2zV-LO##NV7hWc(goSXYN+@+Daq-9fRpco&t1G2P+mEoe`73iC zD2$YrT5i@whY(Zzdu@EwP;7{^976_vzG?yPS==_1*@u|tKij&wD@P{ySh|9sgDrpu zuorGGF6{BUBJ14`>#*&vhHb?-iMvMuFeqqMeWzS=B4WqqW{Q^xlQI2y^5JOJKrW&A zfu8mUVrlBAiOjDPnLnuB9;UY-1BiPV~KzXHC=Y6avI@0elXJ%tSSN5rS){ zou(Id12YY`kGUJ`7YB3UXY3Zy=G}s>S5jO%e1nWfJ+Mp6bXc-}mfg~y6W6kinL9ut z@?)&eo4;cX{4u>jiC{5J{$0@2kE8AkDD0wvQ@jp2)*BFB*`a_{4+rwWOE1WIGUX-Iv5#$5}FM%Bc=b;3Ia~o9)(}&)|E1L5J6q&O1EWfJq_&y1~YHrVlmVd^BqM zm8F0DkVE0a#SGii&F7B5yt22!;>=aVEuC+0*~_ft?E)_z`5>|Q z9@A1^TI{;$G}+R9CqIpN3chMf*g$84iXXj`}59E4Y}=KZCANux;~KQbSPE=XWUx%V1N59>j%Kg zWXFRHq_`s=(E-*nv*#@8C;K0H)oUjcQAiX($Pv)sbzc(!UcT?shcpWLIfvR?OLpZP zIc$sjK?(WV8C=>tr8)XK6{fsAR=)z&96j0T3`ui;8hFo_&ceJs(oHUEy}`@`IXJLj zx?ica0=v2_!JB02tKspg-cvy*q*wjX@P`ZC=`UjFsa~|PrzdKXRbpk_3aOR-Ocx@N zov`|5m#+0ak`?1sF{GSeJLe#0@CBz#zopCNxX1{=N&xb{{A-Es?D+a%BSjK-J|=eZ z^f95|XW@XwI7h^68SAB0IsSCzuwXAQb{Qk(A0 zV?Ap#>oR`TABovJZYu@aj(}~NIvD>7nQ|_$DMZ~yvYxA8L93n~hfW4%9=j>@3cOPJ z=bWb^_Rf46`LB<5C5cap6N{`;t6aH}$L~{2dOge|;ra=Hal@#d77bQHYb!NfvS?$>>iRW6Gp@uV!FZG8f%~ z;$!@zZn@Qg-FpJ1+_}>xrMyB^o;`HQMdWk#55sofwgHgN_{S!TW~Bpe>Fo__bIB0~ zK&1Ich{y#4v{I5DDxC1qeC@U*G7qH9=WoYf>4>xNEz|MBPaUEd;|1rue}_z43OSq> zihbnl9334i84i-t2^tUDy954RbeZIaLG+HdizYK?*H}WHH^{L>POZCR6;Yh;IG4|; zi*Sh0t%tYopzQGbE?_(w6;-nxf3hNrFTUdNO{qAEV7xCm-xme@8SFb>y^}PbQdcDA zHYl6XYaZyx5sqo-?YfY1AqC;msb+>|pC2YSpck#3FN_sUn2)$qCykf*hnEm#cz|ub zx~`GIi|+LdTF?}+Op!qycbtShSv;~%Wt5pj+-ON(y?>pDLx*2BjXs|!TmEJjWv1C3 zt4ilj>wz`W8JQOjx3=MJ{kyZxULizsjZiS&)r&BLCS}59W_6Vuoy(&+PgSeQXC=ww zYLtNkp){^%e5&>dEmn6W&-%Np(kb5jB)yCf&(e-9cj))Edu9P}4(c{D1y*`3b~r*Z zPuft)JnjUob4L?EeNmNE_JN&7w9Y3?{Y{uTR$vi--3C9hg`pCambrf%vuy}_m> zjlyiBCqbReX#4kOQl)Y$j?k0j2*nE1Z+&2Hx64*UQX$Dl8X!tsGuVbIm&j=lVA?=o ziuOfcZQE?j;J9rJSrTudH}Y;>#}rFu=r)>`%EY)l`}hZ>7wJN3&3a!vs4m`J8ck2$ zAQFZ6-Oy9>J2?m|jI%IbPuMR`^oGVPTELMpp*CI{=E9#7y4Dc*<@2>ra7&dcf}3vZ zGLnfLNe2fA2q|?mG&KBjAWDvqSL#{~c#v_u1-W?Q)A0NR2)NU|`(x+~i$ zPtELtc)25g4IiSe4=wgv;W&3-+W=NT!AX$sE8ERe^!8PYUQ>bZeQY#*=6SEys$Z(X zj3Wp_ANmUGJtJnVDlIhhVVKYZ%?7m~&KBLq;F`TT%K5eQ#hM~scDrq^S3QxKVCG$g z@*{<3;sd51A{T>Cqj0{wW@5c^1K@|28)uAjVK6+lmlsLihAI=YA#4l;=!x??}G?i_4GZzqcyb` zzLoU=xAhDgo;Xo9Z`iwQSh=$k_90C~;yBp!i+y=|@O{qgm+oilWC zUpgz0$1b!5C!D%RO8a`;^JVk$gv_1bLPA(#vgoeZxE!I~)t`4UB_rDB)uyM5cDGk5 zPoB_NAEt=OrIC7hTCPdfVG{am^cG*UuDMb_76g%*ad$}5zXSZAY#S7}9vjLN+JLJL zN$T-Jyd((__U!WWqR9+3nn@Urmllb2$^_xUw)L>9r(I* z@aaR0xe-7oaG4pg84!I#lBO8<%#Xk^rN)SZ=~~UZz~Hfn`f|{X&c}P53l8&(N9Q}* zeALl#va2y~_2*AXjEa2@DYt1Z!c#uhd2gwH2)amEdH!^7jv|_#DvINs!|Mp@p1{e& z^%)+x8hS$wbh4JZNTVNeU+9e78T{)0#sMg!Kf z4zHn`#myR3n;Go1oRH5}2kkO39PDWPjp8|ITY_&M=JxA6vnX`DR5uw3zgz2Z?ZTYO zqus>1@q2lV=4NhljrDFfmBnypg3wpR7#${K`9!k;gVvoRPQE5fKb+59=$poPyWuH9 zk0LIt%k6p)fP3NRwg9*#qJiqf+;?^wsKrLD4=a_8P>vEz)4N|K!) zK?r5{X3d?$=w!HRKbaUntb@GXUG9>41f0H@)8!w`n16b8xz{4o$^j<*uqflOK!uaj0f0 zN5*>xtT>@H-{}$jxIQCy0Cb?Dc+yc{U!tokF;?lEbdG%z5BAB{Bpz$QRkxbVpfv5@ z*2`IqUndIUxuCRFkV`2?(Xg4}3}(lM6B_AdSaJNZnqcnX^`lUx-Co5)B(Nj(>(>T8 z#Q3!|H*l#`ZDXm*tTI~bjNR0A$<_{HFr`u|>rGXF%yL4puTp^a0aw6}b}H_UXzi~! zHi(#XkQQ8Y(|?#W;K<{YP{3ReSzJZMhlw^Ba7Q$KGT^yckf`@$ASata)5n27-`3}k z-D7imbwB;lh_04P%0M(`lRcVte;UBU;tNwo=;29c(L~X#QiA1<&u*PAH_pYuBC6U6 zXdx23u?8=0RG@2H+Gz3PRD~ceHfYn*IR2$96u`bb+~MGaY>VSVLiGz_J-FU zcvqZk%4-Zo!{y&3W3ZK8?bmKFMz1NYRt%j64%Vn4}uZL7R5$x+LIfb5>bUUxbTA}EmZ`PYc1C$>CPR;#+*h)6`pbo(r}NhOCpLmyALJ~Z zkI2ZJ;mB7DyTSTdKfxUoPTN{BGW2ZWih5xN%0Ry4hC1#&2XP8E$dh`77a38Esd_Z) zX{B{&2#$Wm54?;(kP-$xj-r1kT{f2S?yg)C>1S9nzwE^CftEd!u3B++@OBxnHyM9U zPgDlt+0;;KZfc8Qgwm$t^%ICEQ^p7YKL~J)e5*(0xn|fmdj9nJ7bdrATYqOyNu!mn z)Hmn@!|?LA7v?}jME_bB2w9ZxPiyj2rm*jud%{nUEPW#RB?JI#{IjXAuec=h zo+k9`R6k2a1)zh6RMPLp=Zd^9rVM>x!$KY;!K-=!gmHWCiQQDslG?JuzE`u2oB(28GtSE#c$Kljf{)gvCVtao9L~_tdY-SF5FK`n)+f z-r{*Sn`}I7tPH`a{c7kW4*QdPGmcB?c>_barwFUbYDHn?suvc;3Ea<=)9K6yC@Qul zUCyYd{}AQ88J^7iA&`CV-aS9!X34PIH&j!``r*>ZrI1<|)4PyrZ9kB0lzfbW)D+i4 zL1y*0*5<7V6{0#SSmVOYTyX5zL282Od?&9a!HWf!m`@vzXAE%kFD*)$Ehx8r{hE~H z&vJJ_2ab_65!MW|qC`QkJYjHPlSd(t=Ek=Vg){Low7nik;_sq>tw+%(1$xrd(@jZ{ zX&XCai$wRZsQAB?L5ZUM9Wy#g$b04~@O*@}S|%)Ew#ZB>X<+oe)@{>eL?ngYo2OR< zpX}Sk&{!;#zm2Lji8$RjATSb~9?@!z8VJlb+`c@g&XiY77uGPLRyx<{i#|-tLrGBTNZFax;l5I5vG$P31dTV{77GTN4r$*5mfW~ zNN`+!-yLY5!c<|wfQ8Qp1PXwZFa4+7FUNgfnbh%m3(_n5(`U~2$hw1eycw;N>YO(= zf6^ONBt~K1$LC;n`5`dbV8iOL^8Iz0#SSMHoUp-gm6{bQG=vC7u6CzESuIpufGW5k zr-k|i|8F_1SXpN17lG3w8X?y|nqzY1rkBTuP9#D%RLZo9_albkp$wx|{AM0q&GuWE z3h7wl9Z7qZcP$D4dH&00bUr6VSDNlt&M-ED| z`NQUbT7uu3+n(2(pwZr}n@+kF8!Unoic>!!0kRvu>F@TCfm$A52&Frs+O}jMVA<|S ztl+~&(q|TkdJd_1d-XnEnA+eh&CH-)c#NEpk?}^KmzOu7&$dMRU7ICKG)cg}CPfS= zz3ie2L+v3tTgMW6h-t5M`7EUuN_Bec!~H?YnGaeJ)b_Pez`ln9I`J{FySuxrhSsCB z$}#p9r$er!I1ex})r)`*{ovwe-75{KI0O|zmq7DQ&I9f6oiNl~LuGRe1Mca6{}@U@ z^;(Hq5{M8~1{qJej{pCo6a;dvzkBodzg}TzPzi{G76_G78=8UzC`OOzn1X5)$r&rH z|8tS?Lse>pH5{T>mdJ8#CYJ3a>3`cGuFh|Uiga<&G$X)YY?Ut~z*EKHF0A49YL(~x zQ&a!YZ({&0Y;ST!9WAuf8@$@2SisLd!*cZilHg)Bz+5-Sk9GObG@%0wPnZOaj@BNO zrwk^hpGo>5uG>j;7F58B4r#e`4CS*WO?pkWg1T@6twq46>M4(nh-n>)O8-3=@>1$meh9E|Qw!a-p#U~<( zEI?tX2x6oN8re9Yy@17_k_$65B!5W2T8v0`XJKYWH=YMdYij9KQsIU`o=avhYGS(5 z!ly*1rv2wnjGgJK4uCfyk2gEK@Itz4cYWccn*S21;`L8uG}yxdH8SL_s^7PCr2Ko>uxETZuHL6VUu za-(J?nPoqqnEy-)ihos0vZcN#R^qMCGW&ie^K#hC@bxA0Vu6D7O9*b_F!W^BlCmo> z(qU_yrRpuVPW{`Bk3||~#zUEKKq}q_-~)C*Tx_&h3ztqY4d#zdxWAE?{-VDbz)jkI z{n7zfletpGb;uJYf0w;EquZNn8D;>mA)cBC>E4$V@EVZqyoZ*&Y|p8|0WA4n(mh&;VS91wmhmFN$uQn7MiWS3PB~sY1{O zmVsA>l8NfWn?Ng03ZC_oXkmmMY`g-oko&TNiz!F1XB-HP`NY>r z;(WnCYD)Q3ow0V24F89lMmI@VI_Q@Yf;RiNWL{g! z4suU0iuFu&DTYY{ z69A)BZ$V=dlS!i#YU8sTwFAN;l>7(D_a5QHHrWP(>{JR>3lPqThn4yIOYcGVEszXJ z@~|s{EWMfx$;ZoFU zrDDx;`b!<-N0;Cp(2R#1_u;bn360^|tjWCA@$+O@mrMzt#gAy3M;c4=qIfBmIl%Gu zl2vw<16Rx!Cu#;Y${mp18ms_ZXqh;AHOdPYXYd#QhA*q#!%rz%F&uZ?uFg0BYfj-! z$RxGj)Ye2<6sKvA9h-C^=R1YW*K#_l*QxDvK@8*oVz16&MVUb+2LN}_ z0Ej{yIEyH9apYfeeM%;vOi3#V+y4BQvSHbZ6`T$}L&m?XR!`3?=0SW-9T0oQ1FRxOd{?HWo+^(Ybph$t_g2cY{zzMm3@k`mbp z-bATLodqB)XnOEWqo<33lb)=1VscpR+wd*=EDbi-4fNwA+0s@8Kz->qOw2Yu40NC? z<Ozffa#W=59?o6S{k+LJ5oZ@4DbjUw=N-3wd=#*6KKh$OB8 zEgT~wBlD45jA%N=aDX%1Ou&x^4Cf2x;C?09LF%`Cf~@#VRQ&7mk4SPONpUgE`zQJk6CSv@xf(47Qd3&6CHA1efY>}{(dL==4P;ngU51KI|>4Ta3D1V$4hHqEVzT~32+y}Nu z^&pgp`yrr2_Yr^Tn=QK$tZhx2*}B)Y=k*mqs-A}ASNd@r%2uK2i0L)3h2Sjf> z!R^cO>igrjo1;H6&7fe@3#Ay6UFWuNC^kofHELRESKJ%UmGvQ%&Ha6q_-K9Jl)Z4G z;NvqudDBt&GhwUc&Qfl8`wyzTFD~68SMS!?@R;=Qngaf`((!*?+_ar|*(680%=U-I zBR2xx4;aHJ&8SHgW0PW|IMrmfXUoZ&=iWaa%CYhrekx)Vm~@o)oM5o5*D&UQK8Q5g zCBrbP%nsy2TsCuetOaV6w=|p0X?RNKK3c|?EggaNGc#A@ZYiK2*>7-LpKMHa+npLQ zo2<4sZ6|9}zhH%xMe2#JXXq_wDBzt=$og|KHWFR%; zu>=m3-@^L6HlgA_K#{4G1L^-hSLAPy0ghN41aMIzo^Brd;%97YA|7Wig%bc^*Y0kk z=OsRH*t`$~nbS1$)|hn)!qs@O02f_;Pc7UcPOLNJoA<20@zUncv&9-JP0}xzpStDX z;VJxIw$9YwzL~?gbh&48K`Hvya`w@2@+jP2ZiB;Y(<3(1>ZWJvO?Vu}^zY8aPn);0 zzH&{pPJ{Vcp0zm+9kH?+N?BHVH0?2r2FN-Sw!9*j9YZSS`ejvatU4zVN||yT#d^)+ z@RmQsbB|h5u*9!>=6{DEpTGQUhg7WlmFV4!qlxVPg9!tRW?!e9Rq8e^aN^Yk_sETe9XB@L0f#ha6>CCCV! zokGCjsjqLbGHv;bp(-E4T&$%wBnAkeX@4du8YB(M$N2dEj|f;7R(j*3`c4iGlx}$p zTfGovG}6&1=SOQ&!`ae$7Il^2vJs;KH-dR*ROy${K!Om#nr+F5YEO11Ln3un{jd(y z@Iq6R^x3-+FJfc8luH!U&^JZ!a(d%oKksu#GM$=z-$^3|$OI+N+j@7b5!(LF`W52P z2e1^aCQm~A0kl^1`KbhzF}D=4J9y{rW-}p@e60Rm+ecyl<-_p{KwR9nD&YRiA*nhHzM;hU{ncaQncGVwPSYRx%7L*z4BF^@ zeMK^%Q6||s&(UQa?;(&@(9RyKT@Bs){JDM+hn8*)XP0pgE{K* zudcgTJmQ|CMtdZnh^7gJpmliKGc5D=>51SrRPVO=t0hw|PJMB~NV`5#4dNtquU3g- z$YuBe)5TVj^o{*#S>x|6LaVg;C+NAkdIL=~jiSFis7i8K`qX1pOk$dy5La~%`E=XgtTO0c9CH$BTw_KbS`KfY zQe;ya21ZiID@Pw&O=yh0j;h}KqmV7L-Tt&q!<3BOFeEWO!*1Xxu2;2c$0 zd$7{eHXcr54KyEFA!n|-xw0|AfBKB$9aN_=*(K;sgT z*>|-O!fsJXR{v)wP@Po!v(wVaX5u<5an-s-h1;gH+0Sw{GgOM9DjZ%Br)?GRsV-vm z&VOQ>$rG>~E~q@0DY4-s>CH0h0A8c|J?7YtQkiBlfb;pF!xQ`D_q~(!IX@uNnl(!BOa`Suw}}#m zPCC?@cs?L|R|MNP?^Z~~$>`yGy*u+&HXoJlb`@5dQJy@7C5lM6XL_FUM8OiLR91~Q zeF@iR8t!tw{t<}n*)WDsv_^r1~fD`od-izFZfL5AJuFTJ#!Vhs)iAsyaSRp%h z*0z=-ZvQT^a8Hu?ijkKDy))-@BCAsyB=5(ApSTIqRsH6*ETaOTd530Dz|wLzZwT*x zq~Gv+^%eDFTi{ZmknEz9E6!Jc|y7&wgD%FC2_S7|NUAx7X6HM+)aG;wir z!^A)OqK$%Tv%dIGhvOJ?D!&4ZV@-k64XnWMHY3`ps^I)-GaR`S$BnW@eF@PV3ym(`8D?mj5b`(BZ^r6Y$a*)sXY|+< z#$32F)T*hnx(hh4WqWteR`LwS5-4#}h1?$Me4J=8=)OHInf>B_aJx5MPh4X&hq)HR z_HenQezu6Yy5n59Q1sEU^Atjz<%ttqn#nt{0jVUhG;A=hVmrnm2?cf zQgM1L>J8x81)Zkrgu0X8=Z_lSA5U*@9d3j1lSl#iP0fMp^+l>|nN%6iqvy>mBF>#I zHX>v${{m**(IT`@|R`$!ZbZr)0ICy&Taoy3_O9P z>fRUg13<6WBao0mK8Ro@n+w2c3w8SAo&J{ zL+qEoV^bscMfB<`je(FYsOHono!|7gz!#=L;$5quAI<5_g^_mlAaat_L}itbF9P+J(@1J zc3e1vcafS>Ke_E&=#J+ousO zae~@G-H>vf5R2v)@|&w8#S}poOvS;-|o{i40b9ss9Q zNyHb1kq!EE^Ys&xUCRz!iU5-Ivtjc0JsDUh0DCJny4nNyde@om@6MnoE(>K)NVpG@ z2-nM#k|~ZU#bjL^TDklyHWo&41^rL&e|)0Txb(fgaSw?MIj8!brt9J15lJnLGY zbz_73$bLK3`gPF9s9S2r_Q!;aRy@Pc{+=urZ!pqPB&D1q^W>M*)I6k3V()-DBm{oh z{U5vQ-sSCAKrTf$QMGtXcN0b*Pv^AsNvP+en5Lk8ZF&kSMpYR?t z7H|n#LgIpmR!-m`wY#LTniSH6K=DN(KZz6Y3Ji8cgUS{;66xdp(^HMV#X+Yp+MX;R zL2bVJIR+JvqW@WEr-RQIL2y{1Y-nW_%@<^l1T+873=*V|m>|aoIMf(610YHIOn|m6 z2gJVSL3g%D-~32koXBKQl5DW~-2-kj5_uq4L7##K%Byyj(maXt@a~Q_-6}b zH-0+qolaL~R%&XP@Xqe;vj`=UTY5NQ_azWk6tjBpou-jXzrEW(iRV%|W+>O7juFU{ zft{b8MFK0drBfOTRwx+dj|1d4f|LCvqP+SatgobuChD;0vs(6Oh6a;@{1^ZO;?>lB zH-1ZiK`DFGVW?nUfpX@q&_FDqlG&HzPEfhU@b|MQQ2xZl1prv^*Pnb0Xti9>8D-sJ zCMGHa3MBr4d|be{U_99W+H?P<-dn^!T)RBjR2HP0w2|HWKrT&h-oX@DvHE|kSbdU< z1k`yXAw%e&u{?07qU>RR68(+UObW;DA#!7}!$V#^KWtE8YR3QtA!xtP-1~dPQqB25 ze!7gF1c@VhKsJz1r$h@bEPcNwSdS_L>DPnZ+v_`ySF0KBx2(?Pi=b8Kdh&-bIuBuU zd_3*WqHb^p6Hm$7A$Z=o#B;9@VMbn%%QYw zEy@Nb{Plbln#E+5Yqgu=GwuwuD`LMwT5p$qZdsoNF8=pNcGo=^aw+_*GTGgjsjZ)P zCG7WS^fiY?Y3I@R1Rti&yS5%BA9j_xKtvIXwss4BHgB!R?v8D#N&I4+FPdQUyYioY zhA~+0D2x7dO5^$$SvlxpmE?8;bDi39CBXbV5c#t9MD zA8&>330AdYNhxNSH8k_#JFwFpjcq{*LOuG%%WT-9`6XLY6`t(cz2E6{icl)OK3#_U zkbuc|gs%Uy`<6aOKf$b!QV9*C4|D1Wl$pKa(dmmmI|)XJUuq z4XT$C&4yYvHhNNW1N=ZeJTnwy(ZL-2b^_G&Y`v0kP4$~Sf*4Hlpll3ZZ?Nmz7Nvh9 zx_gkK<4WM+0aaT!d-j?gO%K=t0YTQ3Yr^+Sxkf|N?-uK4>pW4@{EG5rH=l%PEP>RV zUC^$6v%=N+PFx>`SSBp!%jqcQv8_09b;pGJLuXQ~7n+(07d1$F0puW)jNPZ$$$CXb z0R}z0-cc83{~>rb>0wAp8=evh1%fW!NSY0f_x5*5OZA#Ed~s!y0%4p3yxffNDdFYz zvV4tlj*pnuoo4AD?qe&dGwbJAy}!A-S87lfj_h|NC8(!*1gnXQv0>;7XK0V4an$~L z6X=P#{jke7>QnjF*?6sx%}L@jpxm^iC?eUiSWaO2s_b=7^w67XP4~|PKyAq|+<5YK ziB-47t>f$_`dwTfT5AY_(fVkSXXwc0|FISK$`p9n<9EJ7txv59ty#NsX4YF8`6Sir zZqN$=L&xO%X9p%#p6Tv@_^3cj)E zb;vhMc>%co{podHKeDdb-3}kTwVArRIkP;|5~MxnvLlex+YBM(kD)mg?9o)me$m@2+(QFUy&L^t z1=-~1d&p}8=^9a-pa(PB?ofLl7X%Bnq~NASkP|^+9M==mWt`KKpQ8I$Q;wDS^Y>-P z*~CDzEaoYQ@ZV5X9W(J5&;5b#8?d8&m^*(&n&hOUL}oFPr&iFkOj)aXj+4M|5$@)^ z+-a1sEBNTShZ)l%rDZj9?YkXXPzwnfQjz@ps`=1-tehVd1%s`LT@1%#>i9fY`1;e& zf1JtRfXuM)Mxq8I{|nQ!i)|6>@zMJz;u67{_7i!*a@+3rPZlYS z#l5ZIc4v8>GUS}rxtVgoF$_x78q!vRjc8gT{h=??@w

1`O3KiK!N^7Wih$05)B2 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¿upF$pbwl3Upx3w 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 P0lt*v)MZi9CISBk)}6bJ literal 0 HcmV?d00001 diff --git a/design-documents/img/jwt-user-authorization.png b/design-documents/img/jwt-user-authorization.png new file mode 100644 index 0000000000000000000000000000000000000000..040ca0ba48d3f7df8b58e2ec7f5db048f28032b5 GIT binary patch literal 11723 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 NTI%|0RVoj|{}*el7Bc_< literal 0 HcmV?d00001 diff --git a/design-documents/jwt-support.md b/design-documents/jwt-support.md new file mode 100644 index 000000000..a361b838d --- /dev/null +++ b/design-documents/jwt-support.md @@ -0,0 +1,180 @@ +# Support of JWT authentication out-of-box + +## Overview + +JSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed (also, can be encrypted). JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. + +The main use cases of JWT usage: + +#### User authorization + +![User authorization](img/jwt-user-authorization.png) + +JWT contains user's "session" representation. After successful login, all user session data (like ID, permissions, etc.) is stored in JWT token and user's permissions and allowed operations can be verified just based on information from JWT. + +#### API authorization + +![API authorization](img/jwt-api-authorization.png) + +Allows for client application to make API calls to needed resource with JWT as authorization token. The resource application can verify needed permissions from JWT claims. Authorization server can be separate application or the as resource server. In first case, the resource application should retrieve the secret key from the authorization server or send JWT to the authorization server for the verification. + +#### Data exchange + +![Data exchange](img/jwt-data-exchange.png) + +Allows to built communication between multiple servers. An authorization server shares secret keys between servers or servers can send JWT to the authorization server for the future verification. + +#### Data verification + +![Data verification](img/jwt-data-verification.png) + +Allows to verify if data is received from trusted source. The diagram shows RSA keys usage for content encryption but different types of keys can be used for the data verification like octet strings, key files, X.509 Certificates, etc. + +The JWT structure has three main parts: + + - `header` - contains information about signature verification algorithms + - `payload` - contains data (JWT-claims), the RFC defines a list of standard optional claims (https://tools.ietf.org/html/rfc7519#section-4.1) + - `signature` - is used for data verification and represented as a hash of encoded `header` and encoded payload and secret key + +And in general JWT looks like this: `header`.`payload`.`signature`. + +The JWT usage became more and more popular for authentication and data verification purposes and Magento has a lot of integrations with different 3rd party systens, we should support JWT creation/validation/parsing out-of-box. Another benefit, that JWT is language agnostic and token generaten with one programming language can be parsed with another, the only key should be shared between applications. + +## Solution + +We do not need to implement own solution for key generation, parsing tokens, encryption/decryption, etc. [There are](https://jwt.io/#libraries) multiple PHP libraries which support all needed operations and algorithms and we need to create own wrappers. + +[PHP JWT Framework](https://github.com/web-token/jwt-framework) - is proposed as a library because: + - implements all algorithms from RFC + - provides implementation for JWS, JWT, JWE, JWA, JWK, JSON Web Key Thumbprint, Unencoded Payload Option + - supports different serialization modes + - supports multiple compression methods + - full support of JSON Web Key Set + - has a good documentation + - supports detached payload, multiple signatures, nested tokens + +## Implementation + +The following diagram represents needed interfaces to unify the workflow for JWT usage. + +![Class diagram](img/jwt-class-diagram.png) + +The `\Magento\Framework\Jwt\KeyGeneratorInterface` will provide a possibility to use different types of key generators like: Magento deployment secret key, X.509 certificates or RSA keys. +```php +interface KeyGeneratorInterface +{ + public function create(): JWK; +} + +class CryptKeyGenerator implements KeyGeneratorInterface +{ + public function __construct(AlgorithmFactory $algorithmFactory, DeploymentConfig $deploymentConfig) + { + $this->algorithmFactory = $algorithmFactory; + $this->deploymentConfig = $deploymentConfig; + } + + public function create(): JWK + { + $secret = (string) $this->deploymentConfig->get('crypt/key'); + return JWKFactory::createFromSecret( + $secret, + ['alg' => $this->algorithmFactory->getAlgorithmName(), 'use' => 'sig'] + ); + } +} +``` + +The `\Magento\Framework\Jwt\GeneratorInterface` might be used to generate JWT based on claims: +```php +interface GeneratorInterface +{ + public function generate(array $claims = []): string; +} + +class JwsGenerator implements GeneratorInterface +{ + public function __construct( + AlgorithmFactory $algorithmFactory, + CryptKeyGenerator $keyGenerator, + SerializerInterface $serializer + ) { + $this->algorithmFactory = $algorithmFactory; + $this->keyGenerator = $keyGenerator; + $this->serializer = $serializer; + } + + public function generate(array $claims = []): string + { + $timestamp = time(); + $baseClaims = [ + 'iat' => $timestamp, + 'nbf' => $timestamp, + 'exp' => $timestamp + 36000, + 'use' => 'sig' + ]; + + $payload = json_encode(array_merge($claims, $baseClaims)); + + $jwsBuilder = new JWSBuilder(null, $this->algorithmFactory->getAlgorithmManager()); + $jws = $jwsBuilder->create() + ->withPayload($payload) + ->addSignature($this->keyGenerator->create(), ['alg' => $this->algorithmFactory->getAlgorithmName()]) + ->build(); + + return $this->serializer->serialize($jws); + } +} +``` +It will have implementations for JWS and JWE (the default preference will be for JWS implementation). + +The `\Magento\Framework\Jwt\VerifierInterface` provides a possibility to validate received JWT. The implementation might use claims validation for more advanced payload verification. +```php +interface VerifierInterface +{ + public function validate(string $token): bool; +} + +class JwsVerifier implements VerifierInterface +{ + public function __construct( + AlgorithmFactory $algorithmFactory, + KeyGeneratorInterface $keyGenerator, + SerializerInterface $serializer, + ClaimCheckerManagerFactory $checkerManagerFactory + ) { + $this->algorithmFactory = $algorithmFactory; + $this->keyGenerator = $keyGenerator; + $this->serializer = $serializer; + $this->checkerManagerFactory = $checkerManagerFactory; + } + + public function validate(string $token): bool + { + $verifier = $this->getVerifier(); + $jws = $this->serializer->unserialize($token); + + if (!$verifier->verifyWithKey($jws, $this->keyGenerator->create(), 0)) { + return false; + }; + + $checkers = ['iat', 'nbf', 'exp']; + $claimChecker = $this->checkerManagerFactory->add('iat', new IssuedAtChecker()) + ->add('nbf', new NotBeforeChecker()) + ->add('exp', new ExpirationTimeChecker()) + ->create($checkers); + + $payload = json_decode($jws->getPayload(), true); + + try { + $claimChecker->check($payload, $checkers); + } catch (InvalidClaimException | MissingMandatoryClaimException $e) { + throw new \InvalidArgumentException($e->getMessage()); + } + + return true; + } +} +``` + +The [POC](https://github.com/joni-jones/magento2/tree/jwt-auth) replaces the usage of standard WEB API access tokens by JWT and shows how the wrappers and their usage might look like. From 89343997f96f94138cb3975fc250e88da7258764 Mon Sep 17 00:00:00 2001 From: Yevhen Sentiabov Date: Thu, 27 Jun 2019 15:50:42 -0500 Subject: [PATCH 003/479] Updated proposal according to implementation changes. --- design-documents/img/jwt-class-diagram.png | Bin 154150 -> 162296 bytes design-documents/jwt-support.md | 188 +++++++++++++-------- 2 files changed, 119 insertions(+), 69 deletions(-) diff --git a/design-documents/img/jwt-class-diagram.png b/design-documents/img/jwt-class-diagram.png index 8e548f0e67fa18b8a20aa69f42157005308b9e87..3d064deffb7fbb6d883fb7a76c4576ef46bdaedd 100644 GIT binary patch literal 162296 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 z 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%0Aaz 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 L>gAU*M*jZ=ya@eA}e(+<`%9*}+Hx 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

&LeEKl7V>6#<@zxKlM+2wp9I4!A{YxW+Q^ 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

1`O3KiK!N^7Wih$05)B2 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 = $algorithmFactory; $this->deploymentConfig = $deploymentConfig; + $this->keyFactory = $keyFactory; } - public function create(): JWK + public function create(): Jwk { $secret = (string) $this->deploymentConfig->get('crypt/key'); - return JWKFactory::createFromSecret( - $secret, - ['alg' => $this->algorithmFactory->getAlgorithmName(), 'use' => 'sig'] - ); + return $this->keyFactory->create($secret); } } ``` -The `\Magento\Framework\Jwt\GeneratorInterface` might be used to generate JWT based on claims: +The `\Magento\Framework\Jwt\ManagementInterface` is a base abstraction for JWT encoding/decoding/verification: ```php -interface GeneratorInterface +interface ManagementInterface { - public function generate(array $claims = []): string; + /** + * Generates JWT in header.payload.signature format. + * + * @param array $claims + * @return string + * @throws \Exception + */ + public function encode(array $claims): string; + + /** + * Parses JWT and returns payload. + * + * @param string $token + * @return array + */ + public function decode(string $token): array; + + /** + * Verifies JWT signature and claims. + * Throws \InvalidArgumentException if claims verification fails. + * + * @param string $token + * @return bool + * @throws \InvalidArgumentException + */ + public function verify(string $token): bool; } +``` -class JwsGenerator implements GeneratorInterface +It has implementations for JWS and JWE. The default preference is JWS implementation and looks like this: +```php +class Management implements ManagementInterface { public function __construct( + KeyGeneratorInterface $keyGenerator, + SerializerInterface $serializer, AlgorithmFactory $algorithmFactory, - CryptKeyGenerator $keyGenerator, - SerializerInterface $serializer + Json $json, + ClaimCheckerManager $claimCheckerManager ) { - $this->algorithmFactory = $algorithmFactory; $this->keyGenerator = $keyGenerator; $this->serializer = $serializer; + $this->algorithmFactory = $algorithmFactory; + $this->json = $json; + $this->claimCheckerManager = $claimCheckerManager; } - public function generate(array $claims = []): string + public function encode(array $claims): string { - $timestamp = time(); - $baseClaims = [ - 'iat' => $timestamp, - 'nbf' => $timestamp, - 'exp' => $timestamp + 36000, - 'use' => 'sig' - ]; + // as payload represented by url encode64 on json string, + // the same claims structure with different key's order will get different payload hash + ksort($claims); + $payload = $this->json->serialize($claims); - $payload = json_encode(array_merge($claims, $baseClaims)); - - $jwsBuilder = new JWSBuilder(null, $this->algorithmFactory->getAlgorithmManager()); + $jwsBuilder = new JWSBuilder($this->algorithmFactory->getAlgorithmManager()); $jws = $jwsBuilder->create() ->withPayload($payload) - ->addSignature($this->keyGenerator->create(), ['alg' => $this->algorithmFactory->getAlgorithmName()]) + ->addSignature( + $this->keyGenerator->create()->getKey(), + [ + 'alg' => $this->algorithmFactory->getAlgorithmName(), + 'typ' => 'JWT' + ] + ) ->build(); - return $this->serializer->serialize($jws); + return $this->serializer->serialize(new Jwt($jws)); } -} -``` -It will have implementations for JWS and JWE (the default preference will be for JWS implementation). - -The `\Magento\Framework\Jwt\VerifierInterface` provides a possibility to validate received JWT. The implementation might use claims validation for more advanced payload verification. -```php -interface VerifierInterface -{ - public function validate(string $token): bool; -} -class JwsVerifier implements VerifierInterface -{ - public function __construct( - AlgorithmFactory $algorithmFactory, - KeyGeneratorInterface $keyGenerator, - SerializerInterface $serializer, - ClaimCheckerManagerFactory $checkerManagerFactory - ) { - $this->algorithmFactory = $algorithmFactory; - $this->keyGenerator = $keyGenerator; - $this->serializer = $serializer; - $this->checkerManagerFactory = $checkerManagerFactory; + public function decode(string $token): array + { + $decoded = $this->serializer->unserialize($token); + return $this->json->unserialize($decoded->getToken()->getPayload()); } - public function validate(string $token): bool + public function verify(string $token, array $mandatoryClaims = []): bool { $verifier = $this->getVerifier(); - $jws = $this->serializer->unserialize($token); + $jws = $this->serializer->unserialize($token) + ->getToken(); - if (!$verifier->verifyWithKey($jws, $this->keyGenerator->create(), 0)) { + if (!$verifier->verifyWithKey($jws, $this->keyGenerator->create()->getKey(), 0)) { return false; }; - $checkers = ['iat', 'nbf', 'exp']; - $claimChecker = $this->checkerManagerFactory->add('iat', new IssuedAtChecker()) - ->add('nbf', new NotBeforeChecker()) - ->add('exp', new ExpirationTimeChecker()) - ->create($checkers); - - $payload = json_decode($jws->getPayload(), true); - - try { - $claimChecker->check($payload, $checkers); - } catch (InvalidClaimException | MissingMandatoryClaimException $e) { - throw new \InvalidArgumentException($e->getMessage()); - } + $payload = $this->json->unserialize($jws->getPayload()); + $this->claimCheckerManager->check($payload); return true; } } ``` -The [POC](https://github.com/joni-jones/magento2/tree/jwt-auth) replaces the usage of standard WEB API access tokens by JWT and shows how the wrappers and their usage might look like. +### Claims validation + +The `\Magento\Framework\Jwt\ClaimCheckerManager` provides a possibility to validate different set of claims like issuer, token, expiration time, audience. The list of claim checkers can be provided via `di.xml` and each checker should implement `\Magento\Framework\Jwt\ClaimCheckerInterface`. +The following test shows how different types of claim checkers can be used for the validation: +```php +public function testCheck(): void +{ + $claims = [ + 'iss' => 'dev', + 'iat' => 1561564372, + 'exp' => 1593100372, + 'aud' => 'dev', + 'sub' => 'test', + 'key' => 'value' + ]; + + /** @var ClaimCheckerManager $claimCheckerManager */ + $claimCheckerManager = $objectManager->create( + ClaimCheckerManager::class, + [ + 'checkers' => [ + IssuerChecker::class, + ExpirationTimeChecker::class, + IssuedAtChecker::class + ] + ] + ); + + $checked = $claimCheckerManager->check($claims, ['iss', 'iat', 'exp']); + self::assertEquals( + [ + 'iss' => 'dev', + 'iat' => 1561564372, + 'exp' => 1593100372, + ], + $checked + ); +} +``` + +### Custom key generators + +The `\Magento\Framework\Jwt\KeyGeneratorInterface` provides a possibility to create custom key generators like based on random string, API keys, etc. The default implementation provides key generators based on env `crypt/key` (`\Magento\Framework\Jwt\KeyGenerator\CryptKey`) and simple string (`\Magento\Framework\Jwt\KeyGenerator\StringKey`). + +## Summary + +The proposed functionality can be added in a patch release. The introduced interfaces can be marked as @api in the next minor release. + +The [POC](https://github.com/joni-jones/magento2/tree/jwt-auth) provides a possibility to use JWT wrappers instead of own implementation for Cardinal Commerce integration. From ad50b49e996dc7c40c3b76693b06b50f85321794 Mon Sep 17 00:00:00 2001 From: Yevhen Sentiabov Date: Mon, 15 Jul 2019 08:39:25 -0500 Subject: [PATCH 004/479] Updated checking claims mechanism --- design-documents/jwt-support.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/design-documents/jwt-support.md b/design-documents/jwt-support.md index af7e8e895..89404cc58 100644 --- a/design-documents/jwt-support.md +++ b/design-documents/jwt-support.md @@ -161,7 +161,7 @@ class Management implements ManagementInterface return $this->json->unserialize($decoded->getToken()->getPayload()); } - public function verify(string $token, array $mandatoryClaims = []): bool + public function verify(string $token): bool { $verifier = $this->getVerifier(); $jws = $this->serializer->unserialize($token) @@ -182,6 +182,22 @@ class Management implements ManagementInterface ### Claims validation The `\Magento\Framework\Jwt\ClaimCheckerManager` provides a possibility to validate different set of claims like issuer, token, expiration time, audience. The list of claim checkers can be provided via `di.xml` and each checker should implement `\Magento\Framework\Jwt\ClaimCheckerInterface`. +The list of claims can be configured via `di.xml`. +```xml + + + + Magento\Framework\Jwt\ClaimChecker\ExpirationTime + + + exp + + + +``` + +If `mandatoryClaims` argument is not specified and needed claim is not presented in the payload the check for this claim will be skipped. + The following test shows how different types of claim checkers can be used for the validation: ```php public function testCheck(): void From 0ced814f55338945a0fd2a7f383ff34363f38a45 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Tue, 13 Aug 2019 11:42:34 -0500 Subject: [PATCH 005/479] add proposal for layered navigation schema fields change --- .../layered-navigation-filter-names-change.md | 160 ++++++++++++++++++ .../layered_navigation.png | Bin 0 -> 92283 bytes 2 files changed, 160 insertions(+) create mode 100644 design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md create mode 100644 design-documents/graph-ql/coverage/layered-navigation-filter-names-change/layered_navigation.png diff --git a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md new file mode 100644 index 000000000..32f35df4d --- /dev/null +++ b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md @@ -0,0 +1,160 @@ +**Overview** + +As a Magento developer, I need to render layered navigation via GraphQL and have an understandable schema and field names. + +Currently the schema for layered navigation is very specific to how you would render it in Luma and magento internal attributes. It doesn't make a lot of sense for GraphQl. + +![Layered Navigation](layered-navigation-filter-names-change/layered_navigation.png) + +**Use cases:** +- Reading relevant filters after a product search and displaying them +- The UI logic has to be able to loop through attributes and list them as sections +- Each attribute has multiple and at least one value to be rendered. A value of an attribute can then be used to be filtered by in a product search, by using it's ID value where it's label is only used for display purposes. + +**Current schema:** + +- Query and return value: +```json +filters { + filter_items_count + name + request_var + filter_items { + items_count + label + value_string + } + } +``` + +**Problems in the current schema:** + +- `filters->name` it's actually the filter label intended for display and rendering +- `filters->request_var` it's actually the filter name used in product filtering. this is not a HTTP request anymore, it's graphql. +- `filters->filter_items->value_string` it's actually the comparison ID value that we use in product filtering. Indeed is a string type for now because all attributes are. We don't make that distinction and when we will the 'value_string' won't make any sense. + + It is used as: + ```json + products( + filter: { + request_var: {eq: "value_string"} + } + } + ``` + +**Proposed schema:** +```json +filters { + filter_items_count @deprecated + filter_options_count + + name @deprecated + filter_label + + request_var @deprecated + filter_field_name + + filter_items @deprecated { + items_count + label + value_string + } + + filter_options { + filter_option_results_count + field_option_label + field_option_value + } + } +``` + +Response: + +```json +"filters": [ + { + "filter_options_count": 2, + "filter_label": "Price", + "filter_field_name": "price", + "filter_options": [ + { + "filter_option_results_count": 3, + "field_option_label": "*-100", + "field_option_value": "*_100" + }, + { + "filter_option_results_count": 2, + "field_option_label": "100-*", + "field_option_value": "100_*" + } + ] + }, + { + "filter_options_count": 6, + "filter_label": "Category", + "filter_field_name": "category_id", + "filter_options": [ + { + "filter_option_results_count": 5, + "field_option_label": "Category 1", + "field_option_value": "3" + }, + { + "filter_option_results_count": 1, + "field_option_label": "Category 1.1", + "field_option_value": "4" + }, + { + "filter_option_results_count": 1, + "field_option_label": "Category 1.1.2", + "field_option_value": "6" + }, + ] + }, + ], +``` + +**ProductInterface filters field area is impacted:** + +Example Query: +```json +{ + products( + filter: { + category_id: {eq:"3" } + mysize: {eq:"17" } + } + pageSize:10 + currentPage:1 + sort: { + name :ASC + } + ) { + items { + sku + name + } + filters { + filter_items_count + name + request_var + filter_items { + items_count + label + value_string + } + } + page_info { + current_page + page_size + total_pages + } + total_count + items { + sku + url_key + } + } +} + +``` diff --git a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change/layered_navigation.png b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change/layered_navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..08c199080fc9f76860e206146139898e4577489f GIT binary patch literal 92283 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

~$I!p)J)WWP-TmXKZ=m`E?uHpgGCs_5@tA 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 T1y!p*fc_*zWQ9uw^?&|f$DVb+ literal 0 HcmV?d00001 From 718d52932cf179ec338d1a3695ac93dab7d60670 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 20 Aug 2019 17:37:13 -0500 Subject: [PATCH 006/479] Change output names to avoid redundant naming that is obvious from container --- .../layered-navigation-filter-names-change.md | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md index 32f35df4d..d9bc9a5dc 100644 --- a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md +++ b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md @@ -14,7 +14,7 @@ Currently the schema for layered navigation is very specific to how you would re **Current schema:** - Query and return value: -```json +```graphql filters { filter_items_count name @@ -34,7 +34,7 @@ filters { - `filters->filter_items->value_string` it's actually the comparison ID value that we use in product filtering. Indeed is a string type for now because all attributes are. We don't make that distinction and when we will the 'value_string' won't make any sense. It is used as: - ```json + ```graphql products( filter: { request_var: {eq: "value_string"} @@ -43,16 +43,16 @@ filters { ``` **Proposed schema:** -```json +```graphql filters { filter_items_count @deprecated - filter_options_count + options_count name @deprecated - filter_label + label request_var @deprecated - filter_field_name + field_name filter_items @deprecated { items_count @@ -60,54 +60,54 @@ filters { value_string } - filter_options { - filter_option_results_count - field_option_label - field_option_value + options { + results_count + label + value } } ``` Response: -```json +```graphql "filters": [ { - "filter_options_count": 2, - "filter_label": "Price", - "filter_field_name": "price", - "filter_options": [ + "options_count": 2, + "label": "Price", + "field_name": "price", + "options": [ { - "filter_option_results_count": 3, - "field_option_label": "*-100", - "field_option_value": "*_100" + "results_count": 3, + "label": "*-100", + "value": "*_100" }, { - "filter_option_results_count": 2, - "field_option_label": "100-*", - "field_option_value": "100_*" + "results_count": 2, + "label": "100-*", + "value": "100_*" } ] }, { - "filter_options_count": 6, - "filter_label": "Category", - "filter_field_name": "category_id", - "filter_options": [ + "options_count": 6, + "label": "Category", + "field_name": "category_id", + "options": [ { - "filter_option_results_count": 5, - "field_option_label": "Category 1", - "field_option_value": "3" + "results_count": 5, + "label": "Category 1", + "value": "3" }, { - "filter_option_results_count": 1, - "field_option_label": "Category 1.1", - "field_option_value": "4" + "results_count": 1, + "label": "Category 1.1", + "value": "4" }, { - "filter_option_results_count": 1, - "field_option_label": "Category 1.1.2", - "field_option_value": "6" + "results_count": 1, + "label": "Category 1.1.2", + "value": "6" }, ] }, @@ -117,7 +117,7 @@ Response: **ProductInterface filters field area is impacted:** Example Query: -```json +```graphql { products( filter: { From 6bf279132ce54c46d4cd95bad423bb258ad70da4 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 21 Aug 2019 13:12:39 -0500 Subject: [PATCH 007/479] combine documents for input and output changes --- .../layered-navigation-filter-names-change.md | 160 ---------- .../product_filter_and_search_changes.md | 283 ++++++++++++++++++ 2 files changed, 283 insertions(+), 160 deletions(-) delete mode 100644 design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md create mode 100644 design-documents/graph-ql/coverage/product_filter_and_search_changes.md diff --git a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md b/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md deleted file mode 100644 index d9bc9a5dc..000000000 --- a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change.md +++ /dev/null @@ -1,160 +0,0 @@ -**Overview** - -As a Magento developer, I need to render layered navigation via GraphQL and have an understandable schema and field names. - -Currently the schema for layered navigation is very specific to how you would render it in Luma and magento internal attributes. It doesn't make a lot of sense for GraphQl. - -![Layered Navigation](layered-navigation-filter-names-change/layered_navigation.png) - -**Use cases:** -- Reading relevant filters after a product search and displaying them -- The UI logic has to be able to loop through attributes and list them as sections -- Each attribute has multiple and at least one value to be rendered. A value of an attribute can then be used to be filtered by in a product search, by using it's ID value where it's label is only used for display purposes. - -**Current schema:** - -- Query and return value: -```graphql -filters { - filter_items_count - name - request_var - filter_items { - items_count - label - value_string - } - } -``` - -**Problems in the current schema:** - -- `filters->name` it's actually the filter label intended for display and rendering -- `filters->request_var` it's actually the filter name used in product filtering. this is not a HTTP request anymore, it's graphql. -- `filters->filter_items->value_string` it's actually the comparison ID value that we use in product filtering. Indeed is a string type for now because all attributes are. We don't make that distinction and when we will the 'value_string' won't make any sense. - - It is used as: - ```graphql - products( - filter: { - request_var: {eq: "value_string"} - } - } - ``` - -**Proposed schema:** -```graphql -filters { - filter_items_count @deprecated - options_count - - name @deprecated - label - - request_var @deprecated - field_name - - filter_items @deprecated { - items_count - label - value_string - } - - options { - results_count - label - value - } - } -``` - -Response: - -```graphql -"filters": [ - { - "options_count": 2, - "label": "Price", - "field_name": "price", - "options": [ - { - "results_count": 3, - "label": "*-100", - "value": "*_100" - }, - { - "results_count": 2, - "label": "100-*", - "value": "100_*" - } - ] - }, - { - "options_count": 6, - "label": "Category", - "field_name": "category_id", - "options": [ - { - "results_count": 5, - "label": "Category 1", - "value": "3" - }, - { - "results_count": 1, - "label": "Category 1.1", - "value": "4" - }, - { - "results_count": 1, - "label": "Category 1.1.2", - "value": "6" - }, - ] - }, - ], -``` - -**ProductInterface filters field area is impacted:** - -Example Query: -```graphql -{ - products( - filter: { - category_id: {eq:"3" } - mysize: {eq:"17" } - } - pageSize:10 - currentPage:1 - sort: { - name :ASC - } - ) { - items { - sku - name - } - filters { - filter_items_count - name - request_var - filter_items { - items_count - label - value_string - } - } - page_info { - current_page - page_size - total_pages - } - total_count - items { - sku - url_key - } - } -} - -``` diff --git a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md new file mode 100644 index 000000000..c1134f6ff --- /dev/null +++ b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md @@ -0,0 +1,283 @@ +# Proposed changes for Product search and filtering + +### Changes to Product filter input +**Current list of available filtering and sorting** + +This list is currently hardcoded in the GrahpQl schema, we will be removing this list and using a dynamic list of attributes. + +*Notable: We will remove the ability to combine conditions using "or".* + +``` +name: FilterTypeInput +sku: FilterTypeInput +description: FilterTypeInput +short_description: FilterTypeInput +price: FilterTypeInput +special_price: FilterTypeInput +special_from_date: FilterTypeInput +special_to_date: FilterTypeInput +weight: FilterTypeInput +manufacturer: FilterTypeInput +meta_title: FilterTypeInput +meta_keyword: FilterTypeInput +meta_description: FilterTypeInput +image: FilterTypeInput +small_image: FilterTypeInput +thumbnail: FilterTypeInput +tier_price: FilterTypeInput +news_from_date: FilterTypeInput +news_to_date: FilterTypeInput +custom_layout_update: FilterTypeInput +min_price: FilterTypeInput +max_price: FilterTypeInput +category_id: FilterTypeInput +options_container: FilterTypeInput +required_options: FilterTypeInput +has_options: FilterTypeInput +image_label: FilterTypeInput +small_image_label: FilterTypeInput +thumbnail_label: FilterTypeInput +created_at: FilterTypeInput +updated_at: FilterTypeInput +country_of_manufacture: FilterTypeInput +custom_layout: FilterTypeInput +gift_message_available: FilterTypeInput +or: ProductFilterInput +``` + +**New available filter options (On fresh Magento installation)** +``` +category_id: FilterEqualTypeInput +description: FilterLikeTypeInput +name: FilterLikeTypeInput +price: FilterRangeTypeInput +short_description: FilterLikeTypeInput +sku: FilterLikeTypeInput +(Additional custom attributes): (filter type determined by attribute type) +``` + +Additionally FilterTypeInput will be replaced with more specific filter types that limit the types of comparisons that can be done based on the attribute type. +**Existing filter type** +``` +FilterTypeInput: +eq | String +finset | [String] +from | String +gt | String +gteq | String +in | [String] +like | String +lt | String +lteq | String +moreq | String +neq | String +notnull | String +null | String +to | String +nin | [String] +``` +**New filter types** +``` +FilterEqualTypeInput (eq: String | in: [String]) +FilterLikeTypeInput (eq: String | like: String) +FilterRangeTypeInput (from: String | to: String) +``` + +## Changes to Product sort input + +**Current sort options** + +Similar to filtering this hardcoded list will be replaced with a dynamic list of attributes that can be used for sorting. +``` +name: SortEnum +sku: SortEnum +description: SortEnum +short_description: SortEnum +price: SortEnum +special_price: SortEnum +special_from_date: SortEnum +special_to_date: SortEnum +weight: SortEnum +manufacturer: SortEnum +meta_title: SortEnum +meta_keyword: SortEnum +meta_description: SortEnum +image: SortEnum +small_image: SortEnum +thumbnail: SortEnum +tier_price: SortEnum +news_from_date: SortEnum +news_to_date: SortEnum +custom_layout_update: SortEnum +options_container: SortEnum +required_options: SortEnum +has_options: SortEnum +image_label: SortEnum +small_image_label: SortEnum +thumbnail_label: SortEnum +created_at: SortEnum +updated_at: SortEnum +country_of_manufacture: SortEnum +custom_layout: SortEnum +gift_message_available: SortEnum +``` + +#### New available sort options (on fresh Magento installation) +``` +name: SortEnum +position: SortEnum +price: SortEnum +(addition attributes that are availabled to use for sorting) +``` + +## Changes to Layered Navigation Output + +Currently the schema for layered navigation is very specific to how you would render it in Luma and magento internal attributes. It doesn't make a lot of sense for GraphQl. + +![Layered Navigation](layered-navigation-filter-names-change/layered_navigation.png) + +**Use cases:** +- Reading relevant filters after a product search and displaying them +- The UI logic has to be able to loop through attributes and list them as sections +- Each attribute has multiple and at least one value to be rendered. A value of an attribute can then be used to be filtered by in a product search, by using it's ID value where it's label is only used for display purposes. + + + +**Current schema:** + +- Query and return value: +```graphql +filters { + filter_items_count + name + request_var + filter_items { + items_count + label + value_string + } + } +``` + + +**Problems in the current schema:** + +- `filters->name` it's actually the filter label intended for display and rendering +- `filters->request_var` it's actually the filter name used in product filtering. this is not a HTTP request anymore, it's graphql. +- `filters->filter_items->value_string` it's actually the comparison ID value that we use in product filtering. Indeed is a string type for now because all attributes are. We don't make that distinction and when we will the 'value_string' won't make any sense. + +It is used as: +```graphql + products( + filter: { + request_var: {eq: "value_string"} + } + } +``` + +**Proposed schema:** + +We will deprecate the `filters` return object and replace it with `aggregations` + +```graphql +aggregations { + count + label + attribute_code + options { + count + label + value + } + } +``` + + +**Example output** + +```graphql +"aggregations": [ + { + "count": 2, + "label": "Price", + "attribute_code": "price", + "options": [ + { + "count": 3, + "label": "*-100", + "value": "*_100" + }, + { + "count": 2, + "label": "100-*", + "value": "100_*" + } + ] + }, + { + "count": 6, + "label": "Category", + "attribute_code": "category_id", + "options": [ + { + "count": 5, + "label": "Category 1", + "value": "3" + }, + { + "count": 1, + "label": "Category 1.1", + "value": "4" + }, + { + "count": 1, + "label": "Category 1.1.2", + "value": "6" + }, + ] + }, + ], +``` + +Example Query: +```graphql +{ + products( + filter: { + category_id: {eq:"3" } + mysize: {eq:"17" } + } + pageSize:10 + currentPage:1 + sort: { + name :ASC + } + ) { + items { + sku + name + } + aggregations { + count + label + attribute_code + options { + count + label + value + } + } + page_info { + current_page + page_size + total_pages + } + total_count + items { + sku + url_key + } + } +} + +``` From c389fc4a6c145283d90e73e2e4c79a8f2a7dbf52 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 21 Aug 2019 14:28:56 -0500 Subject: [PATCH 008/479] add section about updating customAttributeMetadata query --- .../product_filter_and_search_changes.md | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md index c1134f6ff..c185dbc41 100644 --- a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md +++ b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md @@ -215,7 +215,7 @@ aggregations { ] }, { - "count": 6, + "count": 3, "label": "Category", "attribute_code": "category_id", "options": [ @@ -281,3 +281,25 @@ Example Query: } ``` + +## Changes to customAttributeMetadata query output + +customAttributeMetadata returns an array of attributes `[Attribute]` + +```graphql +Attribute: { + attribute_code: String + + attribute_options: [AttributeOption] + + attribute_type: String + + entity_type: String +} +``` + +`attribute_type` only tells us the value type of the attribute (e.g. int, float, string, etc) + +We propose to add an additional field (`input_type`), that will explain which UI type should be used for the attribute. (e.g. multiselect, price, checkbox, etc) +This information can then be used to determine what type of filter applies to a particular aggregation option so the client knows how to filter it. + From 62fadd673199d96a926e5c2abc7e7cdc78fb2d6a Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Thu, 22 Aug 2019 08:40:32 -0500 Subject: [PATCH 009/479] Add 'relevance' to sort options --- .../graph-ql/coverage/product_filter_and_search_changes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md index c185dbc41..f13c467af 100644 --- a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md +++ b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md @@ -124,11 +124,14 @@ gift_message_available: SortEnum #### New available sort options (on fresh Magento installation) ``` +relevance: SortEnum name: SortEnum position: SortEnum price: SortEnum -(addition attributes that are availabled to use for sorting) +(addition attributes that are available to use for sorting) ``` +If no sort order is requested, results will be sorted by `relevance DESC` by default. + ## Changes to Layered Navigation Output From 9353f4b6617aae082fe94844456d2716bb461adb Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 28 Aug 2019 16:04:24 -0500 Subject: [PATCH 010/479] replace FilterLikeTypeInput with FilterMatchTypeInput --- .../coverage/product_filter_and_search_changes.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md index f13c467af..abff54aa4 100644 --- a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md +++ b/design-documents/graph-ql/coverage/product_filter_and_search_changes.md @@ -48,11 +48,11 @@ or: ProductFilterInput **New available filter options (On fresh Magento installation)** ``` category_id: FilterEqualTypeInput -description: FilterLikeTypeInput -name: FilterLikeTypeInput +description: FilterMatchTypeInput +name: FilterMatchTypeInput price: FilterRangeTypeInput -short_description: FilterLikeTypeInput -sku: FilterLikeTypeInput +short_description: FilterMatchTypeInput +sku: FilterEqualTypeInput (Additional custom attributes): (filter type determined by attribute type) ``` @@ -79,7 +79,7 @@ nin | [String] **New filter types** ``` FilterEqualTypeInput (eq: String | in: [String]) -FilterLikeTypeInput (eq: String | like: String) +FilterMatchTypeInput (match: String) FilterRangeTypeInput (from: String | to: String) ``` From 0c83d81cc8a4e518f0c5eb51012f28fdfe50ad39 Mon Sep 17 00:00:00 2001 From: Yevhen Sentiabov Date: Mon, 7 Oct 2019 11:23:02 -0500 Subject: [PATCH 011/479] - Updated proposal according to the latest changes in the prototype --- design-documents/img/jwt-class-diagram.png | Bin 162296 -> 159429 bytes design-documents/jwt-support.md | 57 ++++++++++----------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/design-documents/img/jwt-class-diagram.png b/design-documents/img/jwt-class-diagram.png index 3d064deffb7fbb6d883fb7a76c4576ef46bdaedd..c1f7a3a7b056e8884f4981b1a2759e2f84f9b5b0 100644 GIT binary patch literal 159429 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�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

x>64&RBtla zUcjJM5hTcLu_}3KWC8=t$Kae&)X6j|ypKbxiw7kA`UnL%pL>vjWlMFDO`72T`{=f7 zz@!mPHnmU~6!kr8_F?{Uc}gy;nSCE`h@jA$6_?ARX%gE`_-) z*!4Tay3h+Y$P28ZYn;h4#zgY+~ozsNd_Gyc& zMt5t#AV&#E3ADZ~G*6OD)<9U6uUm8HB()*fiI8*UFi%Au962*WT7AzeuOxy>!ET46(`$l>LK3sZ3*$f(;4AS~IU5_NI;QGpG^{VAM ztTs;CA^JPS&_*sogaSH_!NE?xr0E{Vn{Ri32 z3x0yp;wTa<>XK%&6?e0_+qcH9djkj4l*+DO^aQ3z@Sb_fK1+Vw?v;8T6IdtsiUY!e zwZv3JYGNG<=5u9uLv2y}YF4|io`Z@p8#f7tk!DnA;Ci|- zFAw$F5^+Vw?SC5B^=SvFlFxo0LY#+GY0U!?onMSe%tkK=)Zaa*(5EJ6yR4&B=swb_ z9=$ukoXjwTHTER>V1L}=N;fC9x#%ZSY0sQU9@&sd90mumE0tu4c2Yb+~=vgXy_S6?1&5L%6TC5BjbUzziJtuRHn z7hxO}Nv;kv>jR_E_1gq1Iq|nW1eb6u2c#iHHxzTe{**5OXQp#me0CF~^_{et3AX#d zx_`d&CdgC3=O{^08bPcD(pvRWawb%6}P$k`84a2L4Z9C9!O;Z(^+j4iFfJD=S%55>`A5$6V#s^ljZT z<6+40WygSGb;a``BSyWVrvVX_FOjpY4|U+}-45MNFS^2t<|>uEXcJd4NUoaliSvhCqPeUjpF-KwNh=d<-^uyI z3B&UkKg)BuoAZ@6^!ubA)xj-ox7I1dd@->upJX9uh z!k6T>T&OhR|i6lm1`KFMW5V*7cE=)X^B?NOG!R0f`VRWgHUxWzi;5cd#wh{ zYSlP`WL}s9UO%Y2-B83_`au>Yu`~x57!>({&paReUqQ-10%rSO<;vv7h&$oIbJwnU zIeEJvhO@2R(wnIk3^##d%O3N^mckR^r{S*Aj#dFD8~IEYtL9g+4-MoGdh;iXgtA3q zgVN;9Z{Nbgx3qP*(*)mY>(kqQlKj%^m8=0%eMD(HpUso3{-pQWE|rgRTFYl!*5k98 zhl$^8O9|vBB2w`y-DX(0OLRp#zsVBwyNxzm{#HD8GlCp$)o0W=Cwb!4A@&!t0$tvO z-7h|?kYttFemUz315#Zk$VAC0!-R`+zyoy6o`O2hZ6Dy9wvtqwUOZ=Z3~=gAWo*V) zrV^BFmuoC!vn49L`V+O2c=N1ej&bCVRd0)&1jSgZDe!jkR_o zR?a8UUBcO5?Ju%5Wm$@B@W20lC`QZ2%(nv{3b$PP{3Tg;wjA`Rdyn_Xn$lnr)U;3I z4H9349`7_l%$kbbcg%uDwni#^;T+cAETg_U|E!?>!0}iQEJ$T6k#>*S)Esz4{_`a4 z4MnQKjC+*NqU+2M>G1P9!t zH6v`fyyIw4eT%LK(iJR#{xF(cEw^&7@0qs>5IkK48ImyaS>i?!-U1CX&a9Q%SlLaTmrE|;C!0dF7!?R+(xm?q+;4qg8^oMAARtT>5w zRSmEBJz)Seb(@ox-;hNOgdpABNYxv+@FzUGbA*xPi<)DM*_0DPyjiG)k!BH;rnVz+ ziBm%#GWOI*fs8-Cl`>Bi6-th7r%ZiI5>y2vV8>S)3&7Mt&)+I4`HyUQHxW@Tb{o54 z@LQAFtDo{tmMF1PzjfiCr44B2SRF=pURIVSt`ekMM1UNh4MGn=T-l!ZZ%nm4e$+39c@J=Es4qDe9EH=L`p&dI7oDGuK?{^4R_s z4=`7lai5JD3|482OnhgqkXd&2&d}wbv>ZomoD#S{SJ-*7ttGTplj z*{7uHdXpdyWG}QN(tXt#V^9m(&P9}0nB!O|p-%o)kxLnYW#7-P9W8|t1kD|en^4F& zOFOByH%+W0{knc1YL3($8qEuDkbOU7X2;L?;##0Hac{A3Rmg6hAFiY;8K~~mH>09j zOa~fG$fa;cn>5_-Qj}yobjJ=7MUG~L(Kwt}u5WK@%BJz^*YO>QP>2U37ra0_S-%P ze0#_<9XC1R`kh>*{a<~KTeCh-dt8b__Joa#JBF#1#o?1glO^!t^x!iLtQHAsx}0_y zn*9FUwAbn81xzkZk3y1y9M?z@l^LI(4@hcmQ5GK_?MpblQhx+w_2>z`!LN$wBS>1& zWl;4RJ;`=N@zr@0U;QZARa*U;BKEmo^8DB#KC%iDd+3@S!q^}qDIopUQ!f(b2Go2H zEMc!+|0{0@Pz0ihs7h-TkE}$b><#0>Jidk$-q4z`(8QUV%R4$eCE*}sm7HsUG>=TA zynVg=%Q9Ow{wWVrieIBDL+%gW)nloHLH!GbgKL9xoQcbLuG}Wfw9Y`4j2AQ6Pebs& z5^uzSQrk4FAr)jFd`B6QBxc_BR~;`!1wyujCpYx3QpzX@krIIvN>|Z?KU1kmU7@tF?sa1?Hx~BT zM^{_t?{JW`&sI*Nl9q_kTJ?-ed{v2;MZEs~B|>>5zwh9Hea4KSUHl{Y{*ve*e(ucZ zUsoOV&JKL)DR`9pjxvbQvg;#*g@E44%+n1w+J6)WYqMH@z#?E@9?$V164Sbx{J(5^ zj^}rAQ#I<1=U-GBsgVkLYoyg4oL!~Mx_QIcUHntysHxp5vJ*L)@PXTXV9fQ8zCv9a zXJ%h9wf@0Og<04OkGjjqbSjnlfI2S|A&_)^N|!Kl7U0IkKB55b3Fss5+tPr${kxVp zO1uhG1P5!(zxyP!}4E{ga`Z^X1{^vtgT3=roWd6|tmOZQ3EXU#xJwu;tcV9-+g&;b|RNOkZX4>|HCfaB4w>u)9??HIqc3Ow(0P83D|NHQ= zgv`u^42FRZ!5k8>Bb{>vm~^`y+J_0{B|^H3h)2??sbGkf2HgJew%rYHMD;#_|L~3B zi`)ptCSEpGC{2dwWkA8WnG}K5$E1!WBtIWST6J)ci;?zdis%`o|^~tHAogh-3c-b5i9778* zKRX}3IJjG_1cac~U! zdZ3|UcXsT3NgHicV+QGM)q1K4VHl~-Cz(r_pnxAf3jeEI2<|A(=+fU2_V+C~LYLAtxUW78lYozfuPAs~%_h=6oS zN=ZmJN+Td48xYu(bhjWWErR&3TYaAQ{l4>`GtL+c$CkKbtu_0a*W_Rt?y06M*O?Rk z4+>!PXraX-0m)zAoxK^z|F;%Mvor^M~9{v(>~6$NI~Cx44@QX0d(mAJmvfISdN`9F%V5*m^seVs;~*sxqM* z-~tTCFlB?dY(sOxyH~gnh0m z9PWInw@AR_x?tlEn~0Do(XFRon&Fn;-!%Zts`eDZPssqp!EG&~rJ1|nJ9XwlDB;fh zgz1w?4>)MW(rA2a=IJ;^z!*&=6-jq7TGbd@rjhQ{PNMe&-NH@)W8)X=?J$1f^3G>kv^D)iyU>3YQTD_QFDdUX2TEECv!x&5kN z3MJ(eo(Y9#cjco6CPL0h81YI$G5Q&$Xb`>~cNd_7b_LxwymVjJAtvB~=@`cDH>E(x3-xB2(@WYKOWpz;;eY^+Wg zLFq*}0!nF@JNSCNjFPWoP*0JqN5$u*)v|~jV5*pXN9nD;hpLINH{Ftac7)OGSf@v4 z4x)#&di(lBl3wqso?kzyU4KrRfP5a_bNw@`uNj=Kb8Y%Y>3QtWXYv8HRs-O71G3@?(n!IqffwFhxUIY-OF^|EH1#an1;G z*M`@rj)6>JilvC!w$;l)mO2L=qe@0X0lKNDX@W95G?ln@K?}pG}02+tj$uW%LFo`GoWe+aT?5 zLh3MYR^{qP!|0qHW=LcaM~g4C!CO@*EOdQl5x4Udl5Pi^XVN3_Nt+;gva zI$FhMg_yOPA29`mX(Iq#6jMYl_>h`;9{}putYs^JwsIHja~()`U1-%ti$eBwUj<-J zhpdiD1V5d^ekzd@9izM5FfxckahT{U>^h0iZuopp(I z3m4|T9}TBn^XBz99`dh9gDLsjuDwG!^2%}`G{5JO+rXzV`<}5+msXZHAFVF?QGhDd zgt~5DMAMIAAB)Pgi*H}?@9H^}eMy?MjvRkcD%K zebCKp@2y-pT0VrWbInQZ85|K!f|RE0T_u~C^(N`y2WgK4Fv#z>wfQJ+d^m5z1^>AAP7L6Mh z$D7RjcsvimX5D^mxHkRJI*L?8n@O&%-_XJxaF7v{&8yD!BTjxrlh6VxAL9>hm?IVmHIC2I(MJ#5kGYylNu#LR=L zhqbP3{mC3JF!%j<-1GcPerMCY8lrW26}w|GrqslAyAh;s>$e>v_l^Y22so382d3Uz zEle(qWI6(wD1C}%%!{9qhO0-Gn~MtwCp~2j$tvsYei%2o-z?loyNwU)9X5vZ45XJe z7g+QLl_fH(^ogd4*$@EGS1}=s&aj|9vyQ9$+{LDzyHBYsxo@=1*Ps6@pRCf`&-HVq zL~a0bBD29`wdfu~&}d7YA?RSXpQAG&M2jiH%>+6sNk$uCRmSIXkpXk|>>=whR3gEO zY$XMQufn-KTiB3T^y&K06{ewK zGeZ3U-)S8j&oKWn}R&=2qFZ ztTu1H%I^|D{dOXek^8r20QKYQbg3+!`oy%W;*}yxyil=dyoCSk_(Nf<%m=)+o&G}N ztymxJqG1Iuil1L1m0_mqY=`LZ)}AlBVUURd$jr0%9wnSga~!1~`#|#1t1UL-MgmT` z;L~N0eiZipN@jRQl3V$Fl(4-HH|fkS)qmb~?+c}@4kBVXzAT~W)S;yIQ9-lK*x83p zI06vNC+y`krwMuS&x^P6)OjoLe-$SIIaMiuA5G>+IyL~X0C;;PBA!WCxqIY%|K>zL z!s4^ydS8NW7U+lN1b9KM^8o`vQdq%QpGDDHKNu&{T-h={!$TJ%>ktV~_<%wct?%;D zcvIlvCjZT<*H6qBKYPuVl^u?4T5V?*D)(dX?!2)c^Jv%K_U+GRq-XFZYct?05?Vd5 zT$=%D?8Eg8u?b0V+JB_Np7KY^Af-{NgF2s5PLAyx}ep+p5^^agh5r+a+&0drsn9iB*=$TcCdj%&R~#Yroau z)Kp)VmjJnR6=`Bhk)hVUYz{{5x14R5J}S!g%|uV>7*#E18GamcV}$tULfHkEK`sgL zn$OUWDYlftC*agJzOqk*oi1!WleBa(K{LaTsha_mzz=P)oWSbS_?FMyyH-!wG+xGZ97l zzF6*j<(uEB;X;12jJ?|cg;nuTcdz=j^3}9hWzQ-okw({bto3i~26xOdpp{^G}ySZ)YexCICdd3Ueey?;QgS?$NY zTcD0%39L!_bb6{}Y!qnC=sT1E{SNQIri92v5ef!{&hrnZPrGk z8?}y`_Z`rBc~vQWM7dw?Rv?%0DV3lvDa=m-aN+Iz+8cOkpdXpWU;3hA!DhsV0FjDr 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⋘25(8}GFQX(+o?LKRq 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 z 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%0Aaz zvC?aPL|Se4!Qh%vhSsWM^gpAms%`| zDpHCG2hri8;w28ALm7`uWCUr^d(-%zadGGB-ycq}ZYdz*;Pk$@-=^>pyWV4rTD$4# zx`-)VM=g3wn%?-}X9K>RMs(CU7{E>;DR+IT`-e075@%pS3jJ=P!Ji9ZuKh9xj^mE-< zB3@zkeY;A@>#aN9kD~$U;!L~xF1)k62*vLDr!)fNqmA!LTzv?>8-_<>HIRBs28<2l zE1`cf(3VL6)O7TuZCEL1Lh;EIQZ^9128lkAkLNyZm?t}h7?`>4W++1pnTuT|XR zAw9b80&R{`i`JI}e!_FD6BNKvTX)E`?_IHS^b@&1Z?7Q98EJ`M~`4SVO=5 z`P_$~XdZ<%f|(pKB(e65D0@Vg5u`CVjcVPh%Kjza97{bHMRvohPf%Z=!X?OfOcD1*uNku*52 z9Oqr->s&z>hcymUCnP-@zKuFA`nkYUTe70shaXAsWIyk5Vl6YN+*dYu!J;~v55pQ^ zGsLFu!fT0$;=<- zJlCnT=V)Mz#4N3TYoi;9&oulgqsOA_#7Q&MtTmU((Z0)tkM0I_wg-Jorehe0J6yuc zszls>>P*)=OLq5vPQqhB8ga=DjHX68vDO|cR_hg9pUO;n!SQ<7{AuH(2XVjgYWY8@ zv;6vI85mFTZhh$^+ZbWrZ**Mklj(DnqPKo3!6YOF9=cqU9 zKiAVZVkB?9c^g9F7`4p*9~c1sJB9$Mqy?6e0sv;KlqE5v>Y##6f`y50xRe<|?y_xZ z8OfELNE|$dbR%@4o3|dVKid-pJ08?OpV6Qo8H%F0OD!LpyZQAZs^IzPo3*{E1wWiV zk&!VokxAaC@WiqK^Yte@5vgMKOFyNLKWn`t-0F*VJ3YaO_&*XtM-JfFVx>hV|i)wYFedGViuI7w#K!ay(;j6P9m z8SGVGd}yxxSu^d?uXPtfJ-$Np(yF3g;YM51!o0N z0HHovVYoL0k8LtTX6X^D8L+?)CSO^MIoUL^luwnpIfNQj;gU_-(pB;@pxlqi-$z!E(;5BJ^> z{a6HdGFR^ES>G+%D#(y_2vXVi?(W%Nz%$RZD!AZx-1GG5|I#WhXtvy5{xEX1K7v}| zqqg;3HvgWmsz>7GM@q3} z^N&%tP_wXJkv5q89mrBfo*eQ-fPD3*=z@1;SN6!HJ47 z>{(4PhG#>||E%rUj!1BrZaM5WeJ7EBTGyK)V#=r3vVeDjMy0OuOOI?5BS2MwQ*B9I z$+QVx;ll0*4;#rzQFNiN3Px1=FPjx{pj8TMntFShg)gV(1uH-BoYi}us>83?B}AY9 zA-jxv0V*u4t>gVj)}L7BO9Pe0EyYEY;p886a~DSYfbwH%Cs58Q-7aeXH??QzaY+O{ zc&b+|y#+P>vdA)(-A%$qgDLfHThNW-S@c(xsi$m7Oa3x!6spvQ$W;^xsVVzs-$h1| zsM9dG3|oIn+i!QEIm`4ePNKq@2)6eMn-3tu^I9jJO;-2)F6~k{C3{&+I&S-i zx@iqqJp&`2f-1uN?g)mM_m#$}Sd9w`cs>aiM(tF=ci6*(C0M1reHo&@_PfeDrEe3W z6`=99QeWREjhyp3keI}>!^}K8!^&y{!;?tG;b-QSitsM9^Af|RC-zQubC@;9d+*=Q zqzz;{a%62y;MR7ZKgnKpKOS<+Lin}b90uq-aTpIy3Sgrc%zpXmdg2*?04h_X!HE+a z6@BZjH|=3^-7pl-GSi9@QoXu#Qcb1+8(DVa*8Fy)v3L)5=b#Ku2ycmQesvw5?^}yG zWpGgCofNDqKkg0a<`(VLP@~sA`ODC;t@bdd8DhODU-18(t_v;(vlrES05b08?&W## z<%HQwOxNt+E8KccDP}Sk(JL{gxamYf#Rb15h)#^!)67KQFB|E^0&{6$MN5+l9bTMBt;sRuDgcyD_Nb;IG{$XY9z%4<_jAau50 zTh$!R!_#TIf1_LdB&HAPyvPej)Miuj>BLW-QC%vBs(d{h8Y3W{cv^(qd$-i}{ECU0 z=;wP0S`q5?4S_6@sj-3t*?%> zmX#KE7CIOJ^!YV}o$A&rjxtGaRw6Bald{!jPlYngRWL@77S%!^P`frM4VGkrBEcgm z^}s&tZW_20+*-f(7f`^nC8rM49Djs{2=W$%KXcUnEIt_hn_NK0xw)&_tAfGpJ`VK2 zI>*erAR?|vmYu2WiIwD+>4LR>&K6H{wJ-916e(jSL%GyCa}3GaX8R9FF5{wFKCsm+skFQJxdv9c~#UGw=vZu zz;4K2K&RYFxKnDkN>_FKE1CZA_wu0U^_Y5vu*lsj))}vNtK3@p=F6n|@%bo=U(*uT)7CsnXm&uz8UiZ41#A<`%rCQPWS;B!oT1|8JBmut#TKYR#b1NC?!=H3{bmw9DQwJR zru;#T_i6Ryq7u77XPDDK*IQWM%;X|Syeu((#ybi{7`8`J&V@%X(tLduuC|#J^X(<9 zc-$49c-Gvvn#9a?CO+4f|PT+)iUX zRr9DK{&M|I!p`7gM`$GI)=lR6aFY&B56UYVMek+fSoj(vGcEzMFzU{e9Db2l1!wG% z*rbL$vfWjo-}mMVHM+RDcE8gES_(nMnKorr^+FdZ;U-9vT_5OUrcIMF9q*P62Z{pr_t-1{ykGG z?c8f4dKMX-H1yz`*{Qu$I4Rpxb*vrItC{&HW?l(Y8Gk9!QZvB8jp-YZb%g`|I0TOW ztVJG?Dn6r53edzY<%Q5n*E#H`Zk97?)xDpvVla4s&`GD?m3~kD&i16c&_dkdvLrDl zlCq=7DnYeTnYJyMtUo0Gs#(KSh<74DrMRtIZGNSKF^E72^QTs^?{Ft(-eDfdjqn%T zWRFO{BnJP+b$t3aj9?&Y>ndf$mMUOa`UGGy|BLH10z)L+>O%CQvm$iBuSWd_0gd{f zA00w4xjp$PS*Y~j#XQh~o*f*s;C5g&&+&R(ToDLo!~GSSBNhZBZx4E!e(i~QAoR$R zwao9EMYtRwR(Hu+@AwAk+^#-VhQQdP$pinl92D@y2+H=~A0R~m!=p%MFnVnwgW25p z_K=-W@h#s^=YC%pkn9BrF@DQ$b&#wR`*PR$Eotxz&7VJt9Z0T=P9KX(WPg5J&W2pfop2pWh=+CR?j_)QDBop2q2JHl^-ZBu^{kA1uM&cy-=bdL^H4vPy5eq(9d zUkTt5t&gI44PXZ0!OK%TbGGlX6Ft9Au2Jw=5>Z+&oP5adSj`!W2OU1(i!9R*qqqD0 zizH^18&&*M%6r)3`2A&)JlPJXJcy-=OH3g4c^s!Z$+SonMxHty^t?8xX@7W)BPii3 zzU||~@8MIYM|0E*7UQ3VPTnL20?}v+{@CfB80Wi9VJBw`aw+bg=8z$x_V(+ok0Orm zVmhG53?U@y&-O)egIvM%;Pj_L0m-0)P)To}g7UbjJRdZ66`SWuE7icfNt9 zTjHe9Q8kXT;hbkACZwYECorE&I?!ar-qJW^5H09UkUCpMelOs8$CEW%i$P~j%s09Z zH!N`S=oZ^z3yfA6h$nOIZH^WaZ||6M=u0nH_S)I@GcR`~`Z%1rt2>6 zNFS^{I&p&?-cL|??`n?`!DJ%mfc{!p{Q%1&60&McqHwAh6QL}R=!`Qy;1mQQ6QcBU znL&(bkk9Mth-AMaz9T#<#r}FsbHPGQr)0K4vRt3w_wQ|Xy64GH zTA<~{k*QZG4-S?&FX!+;Fo_G~ixU3=ifzI8+o`djQ1=tm4|LpolOF#u!aM;CE z<9Un29Ls!{BhnYNzc6KU==dvWzsWauScAWx)a+T@z3$A1NeIG_Bh!(i7`vnCev~#* z4CZM{g5qukVTtOgHnTeZ3`#hv4^%yX`?s#}1HRp9S-!PF{uW!-L02fujD4` zFxP*TQv*RB$-M5ZNjA^}hmnnu(T6U#j)7Yc?b=MqXX=Q{Ogem~&XUd!`^6y6yAGo~ z>cytU+qxnOV)4pmlJ9>o@lEu^K0p0OUz!nbR* zainUQx~Wu81FO?WY&1<<sKOw+nw(8FW?(hI8u{7gr(;IF`-_hF)W)bm9a)YZ1m;`bTN+Gd`?z>`3s8z zig6hKMhdjFj0l3JN^W|I11*dDfo9GNJT4Dzu1_$4P&LygMv&PnShLQ%c&08C2)#`< zbA9t84#FbNALg*zrOX$txv!t;hNTLl=q0>+pbJYORDPdxs-~D$q2D`N=!L;!cqDfF z!5ab#iy9GMPFSWt=gHGk8kJ!*LoYgx?T-GWy!Emxfh7euB{E%9wrF)mb9a zjl&Q&keerQ^Ww?NCmiG{qocRy|DIi7I{jZK7u-)AZy<0z-^9?ra8^4Fb#lACp}laj ze60Z&_By%y8cn0s4=NNzqiYhBO13sc_BDi%^DM1+CZDl-PvnE*hB~ZAjbr5XC_?ey zlCLBL=^w`M)bD_8adF6rb}VvNKQ?^{BWhPPz2!h&V?$DKRqKGf*d82?zM)n?v|VR)6B16$XG5(8 zCk;o5%vjjd5{a{xOXjBreeD>aKQLCDWLJQUWH`H{ay#M>z8kzb2%(I~X!>+(bE_J&mU}5ObGdLb6UdD3rfXR6 z3abWdJq*cUDxc`g?e=H>OU0hhB_$yb`<>qmL5e{!dG~kIW{c^fZhM*&ac~Ynr$@ft z*R_vjiM-yx#?r#M+RAnc&@ zzCxO7WuzWjyV;_+@Nqgi9Sn0(qNxwrna!c>Qajp5X? zqvgwH&p-!f3TloMJ>E*me@+{yz)7!I5cFc`s6UY?yIVV08B>A#8qtfPZ-A0@NZiw| zv(xNxo}Zzrw(LO!T29Hx= zG=2D%1{#o}L!zDOc=FYX`CstlKNv@?n0Gn*f(tDVPbjNmKaSt)58hE2brJ95nJhQW z$4#hH5LE4WZb-bm_xY-}qF~Q=H`?YmGtc&&z-;HZQj(>rJ0Naq-i5!~Mh~*se=Mdk z!){=U=&bo=+!z)@Qv2LA%mb@Ho7Epk?=2>}FmrBdt$z!#;ShbZ@^7?ih~jX0OV_80 z2|7a6tSZb_hPw1C<0pS^gR>Z9mVnj9_mkTk?U{s-q+De%F%%(0x};3_tzhpOzz1fk z6c_saui;98bT;WBa^D7Ap#>jO;k2Vwxx3$-&~Q`oKz(}fvv1l>ME)8U# zZ~D8YKMMbSJc}w7d?4#HeHBU)4BIU~W|ncIrsT22=PoeB!?4tR+BbCkdtnS`d)9ko zUdxOtx2^JZ4#(uNedi?>rD}!R>%xq^;Cr3QED}_o+seKbOmY=@_8?ng3T(l8Q}^Wu zf)u0su++bC4b#(5pvLV8Er3HIfzL81r`BSN!ymf1MR6o_G2%QPa>kh!?r_kpd{^;( zl2YV2(o1>ciYbvX@a8JM&gNZ#u(OF-Kj|ApH!V#}^o=HxHr5-Bj21k~;WbGLCY>Q0pwA~UTD=Zx26$YR)Q+)xK*Vwzv1UPs zt%@johH$Te(q1V&Fq~=)n^mA?`lkj?hy6#-!xjQ0Z!$!7fXeKi;Jnsn%vVOP{X%AP zktjhd@EViBjRPb46m&>{{1*~4MKsMr^wq3zXb`%6)|boA5l2~^&%gVk6nEFtfWc_u!baY* zThR4zw=rMW&P1kqViRH-)&%RgC5^$%R~Y|)82ieos=BUiMHCREySqa;NSAagp_DWn zK)OL%I;BJD5&=O#I;G=CN=Y{Yk_saAuC2G;&+~rYk9Q1)u6a$n zo6Pm7ge0(csob4KvR&$%qBc#wbDJ$hplT+AOmDm1+3dskEBCk(8=vtnx!aZpRJ|Qc zL)7i8`;QtGb*huHc5%7sPMTX93@=75K~SAPCJ|@?f7heg5&~E`Y5x{JcpB`?a3|6{jsfT4D^eROL3mldL|h1KHnMO9JYTA3u=mh6Z6wxH zbV}=Fpu96xFaQI!X`a&y4-SyE%WG{(lpB=z1>-k1y z=;!#{I2uY|57pO7p1;XUj}Mx>;)@C{ z<0X-QItSgHqL3AVHlkODw_ykX#UIwxa~;G2tGNvBzA!-a@>zaSf88d(vCE18Y9};KPexAfl1e+Gv5zO}BxgK$NVi$^N8T$JO*p z4bM?)w;B4q=?MFscUNfG)5Uw!M=3wpyw<5ry0PDcpOs+ z+Pb_Q@D%#kh|!*Qq?KqUQso7rZUpI~@EIr(W5By%NHSLN*n<2ECk{;I>Op63)+rIJ z5_G%%lK;xZv@48KRGOPELz-X!U#9%{FC}6Vv zEO!)>Kq9;kkO12LHfj(6m4e#ds3gK*pLKu4xPHJ)v~3BrH1-lYUoSsc(+B zv-R|k|na$aU$+Au7#mtQ56R0)VOOvvH(Oz z0r$CAhVwC5`F8xzEZnFS+L5=+o5-K~&b!~C6nRD}jo1Y3xD3@7%HqY|ccQK2$$wuj zdRX~xOCUCI*P3@b;^^!#ol>`p;;;)O z;e-1%h%e)Wo}+5?v~&(VQ8e5z*HwvpHcbj2;c99A9!BEjrQo#431)6Qm@Gz~wSJ9W z$=V1b6#V{w=eu0NWat510-E5$krg?^~o+d^9)qPQNwQ{(Ot5Ji?X)m8P z&&Iieog#f7gLeerz|tp!Svx7pb@*E6f|Ph~R8DkT$c|A~`DHZDjYeamlbYDT zVW8d=2C`KW_Gp`0#_vBa_Y=iX<{raGY3TGo7t7T`v7-fUJP*pNKr$%sM-0#DzD%&ypjRAb}^ko8Bl#ID$CI9OJFrf8-1;| zv54GbxsZcbd?vHSF*Gpb{=ima&NyV^+2!o?xTiGj`cHSOXf6AB8s`dK2$BFbd^n?8 zY`q(o!e*;dSK{J6m$&b|bo(HlQqBfX>c^Kkqz@lY6Qb4l7oqVqs0meyuWDL;39_~7 zn#V(wp-?UPg=%CQ-a)O`IJ;)eUXgaB{I}Mm=`jngZ+fLSW5%YQ4ZAv$Jg0T+GymFp zk96zi2Nd}>NeExCG$tkltn77&tJo5x{Dwy=qE+V(~0gQwo%Z?k5kdEe^k+a8$j z4qS;8uHveHT{}u5bElT6q*v_iV`!HdjKV>~9c7q_W&rYi^6m;o>y<5LGn`Ts`XPt0 z_eeZ4LhvJZozwJ?BVEjIwzOAHsANojIx}llvhh2|ITXqSpbS(-2c!d*m2KA@Q40jM4VF1lGCfZO0L?-wXO*j{`9*n&MA2ojqn^o`E~hG zKhDA!COyx-8e>pfzPPkvF}36+?dKcw#&loo2rm|vRhd*2IA;xi6H^e@&|XOi!p&l# zbO2NWer>l1x!`SRM?nTu-wvoK>37^Ifdpwu1Qeo8HKfKZXEZwya2d9$Sy)SdV$jRe z2T>c8r8)Q9m+pEx=*el=dPk6o!61E#*|6Bb|HuO&h}FZB(Ux)c;!}+`sFLQppn=Vi zeEtqx4&y#~$RBnQz+=Pwv$Ci_N#f%6w)K5AZv4N9t`fw1`$Ayi{QSoxs!s1;KA~ru|RqzHJk~ z;npD~g3+!&j003<#(4C$FMq#5o|+s0*+DlnG5?fU@&Tn)fN|S=WYG;wjguN!+liOk za3kRu2QCwvONnJxWIgZv4lizXabmFk8qY2J6lWvZ2N|Jdn9+e@xToy0 zKXdEq{4y?QNi^1Z-G-oGdD8XyPl10?+ki!reeq-#43+4;=}4`2)*}>W{0J!o@ra5K z#{a^Amz{ZDoGe!pk2d;*ogCiU233Zp6s~53f9H>WG0XpL7zrp&?7cbw;=0rU`?S1Pd;c-qbHsE#5Cj^bG$??NiPF%=hAFFdfs?{Qwa`-&X}L{h$K+=4*G3 z9{;5T?*4i*T^EX|4n4x<4WOK1x=$6G?m1SMr}55-jh^vRJPQIkL+${e^UxI1h*&qN zP83nfVl)pMIt6+F*LM>fzO*%9@2%aLfPy2TtzP!u^ZpAX62Gp83o9!tV;6%gI*9E0 zsc?wN`c;)Xq0u9ATQXnr#>SpFoa@5eYS#%G^>#W5cW4Q4ZeVj?yCEz=(MaIBv-@}l z=!RveU0QQoIg4RqlK)Jv%JR`Sz)WwRe70~lw=)E2vDOSE2AHJ~<=N#OGlqf}k|Pex ze!)kg4A*u4T$dR%c_lTEH+{|3=K9!Q_c6JkC*^Jxk;$2&IJ-9C^G0JV$J1^K7JByL z$89F|M5_Y{=ws{b0ekI_u$7FeEiJ>RHM)Bb>_bI6V-Ln0mUkFZ3HaN0`tsD?s_UG7 zkMdMH*!n0e;(dry z`se`)ixn-UK^fghTk)-?8;?x}+sA+W+-rSjJ4U>_5=!mU;Bxp$={JEzTyi->;5s&y z3WzPZ3RgB{=cLW9@#V@rsQ_$8Tg`Lf(zdtkrg+ zU*zDuURoddn7T}Xnv#Zocg!5Q_z`>H;EisjfRPrN8;NCOw}c;mOfq$SPZoGxRLDp> zYmnJuj&e;g&8XAgP91JDPr%yq`D#OfcT)hn8K~KujusxKtGcb2)rM-14$S!7oVew& zyglhRViANcRQoiFTk2VHSCn?Y@;HUlcVLoioit`cK#|VDZHSay$oG~cW@$SABhEYU zJpp~U+pad#4>Pk0wC{)rG-}+AV6f&h?w&2sUJ<_okYuUI-3~{a8J>Kkhs{>*J6LEE zr#$dp{dU}S0>wPJ&~;1oR>G(fz67PTi|;#vTh-ey<}>sZaF)Li1*McP$UEZKncM)? z4L#mN;p&SR#IT!RU(FbOe{L1GHEr7~T{>T<(cR!XqO3j6+r!m*jx;iz7m?H8Daczx z@w}ypX%<@4m6dAG6+z80tM zP=qZ+;&;zlt=yZ?tbU&w%dNqLA(PiYNc?px+V1PC+$Ss=(c>^d@rCRhkWkmGJN7%8u;{R5G9jqpv3@IVF|PT$khH z`e1m~6l=2n^($RvAgxJlPtvygzUaBI;!FIFkHW^p8@rK6r-yAbwwmuv7;`nB+6WVe z@^i@EIXYA?GaF1)n@g*-nInAC;HGyw&Kg{z)YZ$|)8Y0TjVszN1nP#){wpEX+bJhh zVmT7?vmPf)dCy2HY;<6BC5|dPy08as(bBV~vJCL~1Uu+ zx>B0ejS2rjpvazW!b9*k0>S;^hZKPa+euw&1~V7ii_9CIX1~xvnq% zerG0|lJ^rwrX8Hm$0Q^Sd7hbg4`++QW~F4p&>g`o0||~~3PO`pwh>X1lSyM(z@%?AMXkAg6M6pYtkm;YJatU6r!FuRHze%*C7Ij8}eqfzc) z-p0dA2)Z*^q973pl8sFVM`GO_WLyozj{DlhR3cTBp_3p9-lYOfl0r!SlpIOm2b$8S=U8eP5;`f?;vZE)&OGQ*``51i~f z)Qc;GFU!6o6TP|2kL*3yBg+P#Z@xl}To*MmXu&5ut1Au&XXxm3vK`Ct-C3hecJ?|R zLQmuYH365UPv$UxIP9g)I?lkX*|*dgmu-Vt92al6$OmGda^^q)zCW)H;QjlZIC zT%iBMXngKbeF?nX$O0(jx&|U8AQcw8kSoH)GQTk)ga(v8S zH!JtEDPu#*kL7}9Pbt0hxhhfyW#7=i6=f0cLn1z|SJAA|4Q&mDg6XnA3;s(awoqUCf^yb)KqbE4%;QFxC+0j#NIKa3VUxg z*4E%^=(0VG;Wqb~M$Zwwo?kJThaq;Xly8|89z={&skrmzaO^-@u2lGL8wgCfCMi`k*ZDqFB!7ef@0MlO{BSD<^J39x@=nRs6iuV9G^M1_o8vj0 zIoai(02ib2YZ2)+EG(O~mP5s%F-;D&=?cF1kb2KgC(5MzJsh27s&mxVL{KEMn7~Lg z|6%@&>`o5MV&rM*oz$2|WHlZ32fEc$&fn6}u@<`2`@_zpdkZaw443EMd}UT=7T2WR z@6Y+}R8B=10;!knTO%<3?p%qWs#VtW-mfanaKJ`lbo6xMy?~sZI4H&(!48W|4ZmaL z;ZcW=@ITDHFvvz*K46!ZNV zrRrw7c4)O+WcXU>@y8R1GVta){ii9^VZk4`QzVc z^Qqn?@Y)MAAEo6)VJgEp>k#ofRdibl^MQBQc@9+cC-DZHtd2PCrKUdp`SYvrDvMPx zNFMVfT4u4iJ_X*ekT<}=qM4QsjVpQ;rpIo|gd8g%t!(01y)u*B*KGf6MtJ30a7mp9 zwYD#(P!fV{s{d@|=pw<1ij*=0l9GJ4J+SjP0|G_7CbDU+784*~4F{9Rc%bb>e*uYB z8Gbi;FODOx%$-N7+mtvIE9UcF|K7{n)Y1`(gwi5%y_tRnP4RQA#oDH9xplJaqKst< zn>6{6vCPwz@EG1t%j16XR#g^rNZ(InFmqq6Z#av4p9dk*c)HmVs{z0+(ITQA6eh)P z4%aO$n#J1Kkm_VTj+$~P?vNUhS=l{925J8=w+L%mCWYQbcG)%^aBz$?H(NO&~%FXgd-RN(yg zG1XWBhEni&UgA*oHn%CYrHQgsSVVDjgHcWjj5UcZ&5UJF(Blbb-bACbk_gZ-Z32%?1S@~S-?HR~sNlW9__|nQ__XF)n`-rbHXtI5 z;|cqr!vZs%ZE~i1#F`kxJpjV=ejhuktcAdHPiHHY)tTBo`71qwAUo|a-Er2ZJ{f#W zl)^sOY0E+$-^<&XAsw^$9QDyT9ul8AtXNlWkO5CuyY<0x89XLqOq+)g*w!P!bSiLv zOHch54~mZhABfLcYdfH)mhKvLeZ?(olr$^l_GZsalU+|ThH%beq1CTPUWzK;!eItDM~9+p}HF3`q{?b=;XGQqVg>Wu`l&;27} zCaqqe6^RQ+=tTeob!lnkH56F`42z@u2nrNlWVZ7=5ZKK(zi?tuxofc{pso3qdIfJF z=fN>%8_<>W_!YB^0oX%Xm?IlZZ9 zBOn{jH{h5$*`o~(_prwuvj}~WB_r2Z;lu)~!0>E#yBKM7X8TrQd-Fjnb(8mntm3P& zjq>bgW4lvSxc<*?9_E_8op2zzckTOAtuC`YiF1SeyX&fCFE0Etr2}JbjFf07C34wD z2#vGl#H(1?81Rwl@K{LC)m#9NEfjHOfUG*Uv64kp?qH^by*ulNT=kM;X>%Csqs^Al zevp;r<5s?g8taJV@mF%fDb%EtlrcBtR6j5oG1Tmx;axP;d;soR?c3yYZr|khJLTSRLT^dVVM66Y>Kfi>3ckxTrMxDd=aHkvZH^H z+J+qHYE^$CTiOFzHf z4Ps9xv6@mVGa%hy)yS<&!P2dYlc$JZBJM;IPp3s0^q^*GekLQ$}k|Z!E{B zNXIUozY{TIu5jHmCjV)Q(>ZDyZ5!HaJ}ezNSi)}~SDSc`kK=m) z$WAu}XVJQR*`g|>yy*PW8b$^gR-`|Gbs99gQuKv0wDciEs?GGB?lF-#Td zc)r6B<#?2A_D?{fUPLb7bX)(HN|+l9j3)f^cZy)=lrlQ&WW>Ae2}0NgR|n8cRiiev zJ6%{QL?oN?6%j5d)~17gxRz!dh~hONO3JkuAu^#CaX z3OS0LU^;(u>0m%dRfELeUixpk^rf@po&RjTYUvX-0iYlblOzDjl9dt*6cU8`T}>Xm z1nebRi({KOvB6ePCPa$m;ZFcb<;D=9dC`OKkNx`1YS>!kA&n$*3)+^?P^cd+#^Hn8^vV5QIQ*{@tyjDRyAjde0bDk)AL#w@nB0uv+_&S zNZ_+J)8)I-rI`R4Zd==iAavgMWh6|66P8{7sf+Apr2h2$oYLHMQi>Q~$iK^cCv&xRiltUsJ3H^e}x?C0Fu$EAAI zM_1=-Y#}P&q=R|jo0A2100Edx0f9i*4K@Fu=eXiVte8fapwF9pk9DOxg92LsLC}03g&VIU5R-|3?3sS#1Ao;K?A8l|~AMZ_-tjhw(t}$uY zAKOjQq$uE3<^zmayV>zI`O2rkq+!Nek9wkr%dHy=v2$fnGa#U@@u^H=wH=k!j;t%k zJJ))y%V)<(t&eS}SFkJgL>%x*VEHNP`3fmC4x}Sgp%2nKK!&@jEjjw6(Nk;1L$cuSnN|9_ z=lQoZd=tqXRBm27`EpC^3ZL;=OXn6xGdxPsQ>B&kbC&R5$TIf7$Z`b94NxCXef=in zdL`o0{#7SmhPLjqRb3_#VFv=)21NS~i>XNQcrNVWHMx_27ik=(Q^SRBwrw8;cRL3B}!Doex zrD*KyTs;92T&Tai%UAeicca^OCO)hfGY<$uNV$v@3Bff@WC>nli}6w1>TPzsb-d%h zP{OOmWllKh+1?TGt@TlWxr*}+`Y}X9&3hYsh0$Z_Y&VIxb$vjJcfD}y?&!~TwW)Bh zPqGP&J%aR9K4wpX_4uv98zU1LAIedn{p_zn-G(raSjir`k9TH{bEK}g)ycyc`ZPnQ zoMm0!6BAwBi+8{=f%gVkY2#FKjM}{HL||B;2z=X%00^Ve&sl2nX^GXN2|M&S zlU8_viU^E7i%5WiI{xEfk1rY*$G2-igsYUeBf~$quAc)PP(&*=4L1c8{3IWzgcaMT zYB5Kf>ZM3FwSHMy%yn$4-4mDRhWTpSvv_slIKCiBN+lU>#Peo+@wf`l5p!>nAwD&H zTLcK{y`Y8XmIUWXwOTq8R&?{eF5<_6>Zv{4nUjEHQfITL_S8TS%hROYLSu$@RwSs7 z>(5C#KIDHL87766W{k1a+H;d;A6_Cd$9pqi!cdzrCs*NK?)f>v2tD_`l9SZFG`d%b z?8|Fc`fe(j5UBh z8I{ejJP;oie0#M6Y!K7mHC{hsnDzUH4?&MdsK#{10|%)B7iWo<%}fni%~#P^!umf1yPF<` z)Aspf$)(fu<%DOsHgnF}yC`(hA=soiDLex&8Sv5zhk81feAMb5ieZonKQcI3`@r&~ z-cgUkLDLIiNeA!zUeT2e#5GkBQ#E$%C%QNp0yn&BdWO+5ov70EdiYi@JDAjY!hLjP zr(C-ZW8c$ zoKJK+xddy>L|KBW{~`t49|+{oYDLAM;hkUrHi~ za(*Xma>~e^23#?j&P#aj)H4}7@1L#nrEU%rfxO}o7a61U$7};orhbAt{gw8Zp2bQR z1LZ|#cYU+ija8YVN3T!_x!rP| z`JOU&cO*QmMgMc}U}jcVRG4!^g6PxgO$M15Bkxlq`Sf))@U)kFSXt+9D+rke4PxS} zre@@J!twCfF7quRj!dJ}8?c5Fbvfz0t2t0Sm{UK8aFxx(24UlU3z8v-h+hbumo{)w2(d5SVEc|BM#{sIEnwrBGTX*l> z3l0wt7kk}3r9dqU+3$Ufn6?_*IR1WE9dg>kcwCReRkZFGR9qYT`}-0(lELvVVwkE( z;B-s$&*?Sld|D#ykY~bg0O{Ul02p3z0YSkWjn}Gi$vk>h@H?}!v$2dSq=AE3eFXr_OG||M z8T@f~^3*wC%5W89C&3vMbH=q49*v(A*|X;$tp2A7Nf^dv_ltBxWrx#GX()wFBJzRRRFM-K&wvJmwL}VEB z@siK8TWHaD8wLdH8^N~;6!S?0G5$?wpwkt98nBOZ2eI;?fRrbZzAYv#F@LCDBOV%n z(G4gF61s0R_0G7?`;_N+Z-L5I1W1O;r16*oW>pE^0T1fGS;?>TWDXF9^2l6`@c(nx z@%}loxd*P7iC5>agha|~=@0eV>Fs6yY}4M}{_w5ty`A}HvbBMf$)hNeUql<|_J03J zm_U#oJ5&WLFYyLUcbYoeP!kK>0}H%?`K83s`bThp7ok%H@9H&P?TxGRJYuY|na;H| zP|o~=BmAC_ML#eb9{kYvg5$r32*BYgpD+Y={Zmg^7BDhmC6M%L?PNf^qyx~e_N{Dx zDmUHpz`&7G9oXB!4TZnf^7}=IiUf;E_vg=~IXO8Y`pR(7e={~0xj&hM?d0+i#d3}zWfQ9uaM6D$XMF8jgcL-0rSmRnKa$*6Cl0`X^BYU*o&x9&uY z?SKK10-bp7n)fFP==UW2YmL7~0po?gdS_Hg2^&O^<88-Or8^@tph?5|g`F;{!&yQy z@m@pKMEIQ7UIU7uJPDGqvGKdw+8!WOn1}4J)PD1{8T68pJ7t%PA>ThZ2#AYQ>qj2c z`Cr#@f%25W#ODEExHnmV-xuW|?@)!l7@2PM8hEe}S&7mt_Lv(en8dnI5nyuvt{Ui> zaG(dJRmKF%AX^^kWvBANa+fh2UMSC~tV;r(JyFd94!CsKC}8>={kt^&>*^98z&r4t zco#yG%8aZDT88xBPEJnV;^tsyH!?L%V0L!E#vPUcpKZ`4=mrB0X3pEk2M2(SOC6!F zEy>f<|NDLoP$X9NIK&ZbqMH!P;{zH5C_{Z> z!&MSjpg;cJhiUNsf(pOfiWzhb_EbSM=-MEXT3Wix!xO<< zHug+r_Vvb47BD?Q+VHfXD3T2SMUSS}re`E>uC7T#s+7MM=kI?TptCFf?e-!tDBmE7 z-^))Cag3mQR$Av;-$*V^_Erc^2KO=pJ=SkW_7yc>F)0G*!)l8BRjUIV0swH7BaAlk z!I$0wcu%)~M4#p|q#~(W=kd24^WTU3_2L|A&>$QD8PWfHWc|Oq=C5A^fY0Pkc)l+P zxyk#GTMpyl3HJam!clk)aQTs*^t0PFdHjO1i%=a{VDsqzK56z07)qB$v8Wi}B2Uoj zKHm0!l_G9qYrFP`+<-Utz#k`GLIfIrJaVovaEKFiTv=Pd;PrzU0#9z zwVMnbflLKD_n8B|yOc?R{U~%K<;g;iHuLN4%g}kC@2m&GzjI7abX^J?209RU9If98 z!J-25ilP#=tZyl5W${5w6rYR3BjokqZ7I> z#hvi&1lNN4`ai)$8b3h20_Xu}IAJwR5NaY207wVi!|gmAe`hd|#t79!_EA(S^2eR& zs`|vV53|+ELC7wUB!nH1grKJa#$lx5TbIW(K`L#5C7MT>_%j1xu_{6*>es|8nd0RU)QKW8r0}&YDS;E2X5(G^YbY2 z7Vu(ODQ5CNqxZi<4sHKo3cX?Gh)$ZI=*UR)6rQmoU6}X3jyRDVd_F#~vV8+=M7LY7 zr-D#Gq6y7`rOG33CHOyYM;F`U;^)1ns3@?TzLKZlVWr6HWSCFe$qnsDLGdvM2Zyn_xeV>^+tG!1D7g3!KpUOE&Qx9Vws&^YzbRa)qmNw%L!;h=96|*QbZIh`1fYoYpjl*E{!+i%zXs`#ybMKcza5Um zEaWvZNXmyH3Gko#;>}^1CbC@poz&n1E|6$9CSfqmzf4;K2bi34zs{ zr!^sjJ9s-@{vXu1M9M>%@EW*S?eD^({ZW5?!`+B=EMrGv~zD?Z7 z&zX!n{pWlA#h`sJl$Bc+clZHx8#?^584gGXgfQS&(6Tb`xg=ng*OCmMM zNHD-lOG}NctlnS_wznhk@$oefu;-j zV(SS|q_thS@QhPVa3SNPHqTL>vmu;m!ywfg+@Wy zDI0RyzkdLpzLOeGfb#$>hcG5!Ssu?<%%4F_);*uhQYdHtt8i)gjV9SU}7pX<|zMB0|9je^Uf`c2KrdkUzbNV zBY*z)@&U*go-9>>Qc4Rb=<=`lRFGJ#{7ykt#s294(JCTS#lU6bk`DRpG;$BlMI5KC z?pGAWRhVB4TU_Xd13fkel0 z6Z28Y=V9Zs0*|#l9@FcWzL!WxqDQR_GA& zk%}{_;N1~ZOhBY~4%qN3y?sZU{G%MQ+;||i1_cbcCeyf%{c2&^3YkamoWDOI2Lun> zv#9oP3bwn(#avHQQ+hzLQN^)vBJb!wN_#Bg8hf%D!HrMqsIWRyxEVPcFonBF! zoZkCCxWZ!6vl!A85vXX#;0Wda_jqqUL^EGj984y(#<4w6&3{1kou6^O#Wxb6!$ar# z^3$ypVLV0O-*cRBexmL4vDd=FDem#Sh@zJ&Yk52350S*>g~XFC#Ed&tY>705NGo9k z`E;f1tKrPR9`0%3Yqfi#(yth8EY!vRZ~j0w7c$08eU7tL~F_qJH)vRK8)F;Ij!}^EOK~3oKv`fVB5S^$PO;ad^vhI<`orY?ulp2+Hj%U8#+#Eif`dAnA>h5lo#mJ!+gENZZJSpeXX4hBlrX4jt+ADkE z;QV8etnN0rs9b$KBRHrd2el~v0CUUaxs_xqWNIHK4k{|xWFkFp=+cbFevUhu@n*mbB&G4$$vUCHg8QQ_{5~=LH|YilJw> z)=aJ-U`eF99FTIjTGCLyxDtBd^1XdJa&j1`0wI@8Q8N&CMeSbfFv$VtbKm_m=S`^F zf_|wYZP&NMF1An1Iw<4TS{w3wU_b)bOVq#n~#`m)2svJu~15{U+XH?J$UsurA zwgYWJ`{E~u1M`jzJH1vj@5NZBU7o*jREwe*MUQ-J+xdOQc&c&GFd|I}<@wS&fj@xW zEBL3V0G2P;GCCp9Y@LKJHbNmu`bG#2M=5(=dv&|?&VJ`)`Bm-^Dv^ubIDnt?bXmWa zTMU`cN`8B9Zx^^LMRBnCWkGv$js0kE*bn0puzE>gW)jIJ_x2ZeR9Pe;2=b+hmplor z^k}ZmAhUkEb_lQYB@Y0zQFF+An})1L%~9E{Q#%S(?`hi35^N(CDmkw4`n2Fn$<-jg z>58hIVK(D`6Fk#X+g2n`GmS%^jNa(}RJg_GECdjPI?O*^14coR(0t>-E3Q3N*tUr@ zIF?&78ysbQ9w(do%r8@~Ubr=MuWRDHFNbE)q*@GzP+YnNXB%MFIJhWb7k<=<0Pcdp1e&&&GK5S`AI2U?dAX< zr|Ph#oBl1j2lKNIC#MyJn+b9cjP>7Kk9b9rr)Q|w(z&s<{ye4){{S-d|PnPu&rg6UEnJGjbfB6FM~Y$tE(9QPf@|s7m)OZ4sQM- zPpR90^Ewk;SZHGgG*C>v8qp}xQIb-f2K4W=m8DaaCN*L$8Ta_6b)V?(j zUPW1VydjHhKhM#HSTvwe%d>LV8T0mZqWQLI#d0vkop30=!9oB|-WB6Enpi;O&RD<} z8+0V+UC->>LGPaVWFP(TLvU5fUHCpSuhlemN6(!PA@^P_ihI$Bz#DU(dwR0(V|LC{ zoUI<3KjJ7h>g-&as`uo7%xMrExN*`Swl~`p$ylwMYtzNEpT2@=I%a}R!E2&Z74p$0 zmP3wjXRd+@HrrGn@kW0VNp*~|pO`bVIcOx1DLeVmot>%oN`syo6QVAg6NEmuE-b$i zINLsHByqQ&3k*-=mQ}4xF_i^X9(JL|616#p4On~PMl;G8eo-6^!f*RieC}7kb44(-Cj6nQQb2a6 zR)7P_QB0;eY2+md2cr_}wIh_-&5yUbX-<7ACY7q}I#wr5rr&{LB3U8BrYXba6;}Sz ztG$CX`K*hR1%4J)X?M`Rl*++)L1;RfcK(GKZ*hpe^}391rqUDdZ=Y@^vZ#@R^AUBZ z-eol8!ONJhXP1el=0h)K+`JGc>BI^M=_oyU!iua2ZFOaG*>#n7Q#~?+a|81_is50Q zv9J!tXSskrPrM|8S8h)!n~X?Xt+X)dt8%d#Gg<17u4I$lS%8gtCO_FySv3c&;+-ZO z)2Fzw;%8woY1090;9!ZTpMoh_1Ft?(Dy#s5WYjo$Kt9?Xw0OQl43lxA4=1PJrBsy3#$uUGXUiqvd z;Cb|*RG;vPmPgLZO&9H8@|OJWltL3#&G$O#QQIwZlB&>?Lx|usF&8c zGWHp%Hh)0gQilcpm=N&Ya)zh}D=YSjL&#j${b~5I{%2~hn4LPG70j0tb__DgDg#?n zmA;MSOb*|@(?y(a&*`2dPZF^FWGkL5j#x}0`;>84i}Q&L*{c|4{W5y6r-hE+w>Ey< zuc??2X2NKKqB8ho+R^Cbe!gl3DN{^(ylG^?h_0(;jw`u}9)CzjkTF`|U&gZKZ!;^-owbWI5)st5TEcj!Gmqrf^_G- zXWcW8KyoD)L=d1akHWjLy-JDl*0$W*LcMxc=LJj_ zk_IO2I&62SS>R9Pu;R}aijsx!c;H_P^+7@ry>ks$slXGZv~9=#5k%Ta8c31cUl%3* zx=x{h_@PY6(HLAt{`negCD)0h7MJktsyZBOJ4qBUi<7cA@-AP|TsC=mHX8p<8mBV7 zM%JA(*AeaQO%uF!+r6a(ffn1eCe$+p82&SQyFKU1lEp8^Kr~F4e-~s%pU>8}3al;l%L8>0pz|z_ zWWy>~D%HtukTD|rz=`UW3W_8zs55(?CJ@x=NCK1T#%XdUApD3vDuh`Pm;D2U^hF{= z_Isijd>D&Ys|>%FcV>n&91(e}Q?-ZS{8TaB(UqR0>bfaLxG*kRT^ zh?H`}B|!_cZj3L%phQq6hgwMbFuAh3ut3bIuQanGy3bubLbLz9A zrfM1Nche!x2@vyf+h;w%ESQ zd%2$EJD{ue4ds`7S}`YbvHXA^{J6ZCf;EXH;EU=p*m*ghR__GE`Kl{AdpHJ73f-y| zw^D=dPS^Q}e?B4OaMlIH<5`%j%bhpL5_n!cVGM}QKjE%M(~W;Po^go}A&Tc(ue4TN zu_>cbA>Al`89T!S#3SSN$=r#@0hL`iDXq%-^;!=~x+hY1iGtQZ`H->SyC;k)LTNDs_wRR<@F7ocfU(UZiGWUy3`Nf7zv*@?Kw)yB=+`2Acd z*d>o@%J8XMBH=m3Rva$7)7MJ^Cvs}4gA-w*C@ylt&&?e&mcmp;g%-g9lBL}=p|N}W zr@R&bU8M2S|rz~l%v7bjH^5Hm8R%O6TLaQm$6{4#({%f8)j?a9vPN))n* zbDZGakml|Er8zGa_^qjv1PWI1Jqo(cs93gF&ugU{#Q3hxW8b&{WE=$khz%F4>$__$O8G!UDz3V1(vqQ2;? zhMy~GU>KFw z8aPh!J@m>wdJU=ox9Vh2B8*r%KRz$`0Qh|y+IM|$*KfW}&=*+79?1D0+Ws=C%C_wq zhNY#ur3FM9q*F?e5D@9^25F=uq#Kb%H%du&cP&b~L8PR+;oUj)ysqcIpJ#mkzA^YA zj-hKE$6j-rb8d4-Zlms$ixInAS!;m>-5P6VehHx4NI5!0oGI+f*PM7wc;q(RC%od_ z*T;vB);}GoBDGCfqQ8ckOaf3K3OYRGQkxV*6EkLPdF_AU)uSbS1T00r(8tcH^Wh!Q zm3zHR$>i&x4I&IMojUSqIYUSROH_OK}0W<&wwM@@$UH zUq2Iw>4IJ$2Gb?G&-2NWPp%dAJ4;&dyRTjM-(RomT-K=PSx>}P9i@!bMD>{rMw0O( zQmOf=74_s`vsl(qcORtvW*mNh?$R~Kv%kE>fg4GVtC%ZgBItf0v4{P(z*tb17rZe;Aczz5e&){+nzsMEF;<9Q=?8H?sdl%^_|iEh{i z^8`OeD(iPKSe04SS3oMME&Dr>cpPCs8K`9HA0h_$Jb#8KxIpvxha(oEi^emQTE>zM zY`VX|2A-s`q@kWS>A52hzS|tD&gVp|x=k@Utnc|28$F`rh&g)K+Fy_XVa#ExvNn=z z!A-3Fab2a^0;W6F18ipNpJ41y6JTGX`q>x5%=jZmrugs$RfVw6CH3(2B^Fw4Kwmt~ zk5|IJj1OM;_n~VWWs*8e&G#SY`6_yPPu8|;*wb(m_>Z*z<0e3U;2YVz`CU)@y9!sh zZJ+n6GWnP2HTPqDL5}Om2-4xHE1Ggj(BLqo(xi*9?;aX#SS#0?!??fLMVoLC%!30!D=edM=6rymHXKW=Rx?5vAgHU~&rf%SSf6-{-IV ze&0enOzkqO-E1!z`gD|etTZt~uthP(@7rAK>up}A?+_Gp>3?c-r&Xypn-y=XcvPK+ z8?%EVk7x2pO}HV?0N3>b($0?kx5yeXf|a2)Jt2~56ekqRvCL)xb#D0Uwb%XcJI~w6 zg}VD*MwyIA3!qU5NTl6g9uvwD?FEkr1?4zl zvl1gkmioublb!xBmlcCms!(utADT63bJ!<(|7$1?)wUvKEsf(R&%1RGgkB-@P6L1{ z`zlhDs`B_0$b}+oi$A=iH4PB7G?-9r>It8g@UV|E09TGBJ$W0PjSE_8Tw@3WPl+>y zS${e=RPS_mTt`utu>x2q=29}sABo5OgkB9-@$aO*{AW@pe$`Zs1-s+7Dyn~WN2>8~ zzJGVe<_D2v9tHT3;K1zt;!(T#gNol(L9aH{Wzc)as>2^}y`YqKP+Wgf{~$RL7REki zN6x3NSO1YGPg*kCe0werP{NIR=jvexeQzQo5{Jo9&fVG9sPu#VDWB{VMg^?ZMz3oE zKF6-&cy;aT-)oG&dK2ODm2*SYFtAx)OFuzQOM&YQ`BOx9ZkCZ@VLMf_p@qStJvbF%@M0J`fp90+Sb7L9diBkr{)0+noax_d z8gFU zC$po@OAcs#@X%Vyg2N-gc|xQqnTUzJ$p_Oye7vUn>VMd+(##0CJb|m9kpY()sE>ee zHjaf``|tg!WjKm{5cjhiTG%foJx_zY%Z`HG{IWN+8@-P)z`coaOS%mfgdmaS=gB2w zZxStM(#&|^@i!QU;V)i6Z!)6%8QT@^)xCAc3_$mNeWi_G8ktrn!xvwp1;9&V6sSCl z$kPe>3A#IC*u}4SBQp6lc04$3h78g67T80VvC~bSY2vG4*nCWUKk61S940La9Ik=q ze#(HNe^zw_O@?>+{PBl3Zi=DQD?5omNmzbREey+$u^@>5E0J|!fphc{IMeCmk2`*q z`L8k~#**$$3I(x&URltK6FfFfJ*-x;WUyCx_*lJ++`n6)3&6S30*grC>o&~ld6L5K zgV0mM+5_4JM(uL6iVW9}PUm{#80k@d4HP>wiNVVsr39rG`s|!;4FE>wb%%hD#e2egd1A9P0W8)8e8A-nCMula4O#r}jpI>6!CM7tZ$e2*X zsbOgUt^?YoU1%!&&sX4y`Yx1yzWcALBqPfFPb!DRy?T)M^&Xr9NXOck5{seHntZl_ z`hLtVhp~E(FC4V@W!NQ5Ld=&4L4bI`cG06MZ7k{zuQM@gy{!ktWxvvouHE~d@F$!Y=e_95N?(4WstG|}F6qX$+=;`5bkDq}A#{pCYbnI%!ydKxpFAGn`z zz@95R#x-9sz}gQ=WV z^+$gl9<-fN-PDR`PO<5S1_-t9OG;tHRPvp?p!2aK*>zv3qwxu>8DA#Q6gF3v{qjz& zvZYsG4^FiFwE4s29s)JktcX7Bs67shP)10p2pN8Hv z_qdOdIDfV@vL@?=O%~=jIH~XD@rd z&~k=*Gol?D>N+!NR3LM%vviT%TezK^5NkS7L>ht4YT7nxm(P#+IGldIc;LwZnqT2? zgb}2}5Rd3z8}S)8p02id2*{z1Q*!>yXhFT!3pz?y8qB$DpInNEenC0qZwI{QOxsUE zM;Kwy3Z1Rx_sh*z7Y4#_@C}a=q?p98?`jHAY>nkgg1n|R=l`}}mOk+rEfC33Ox)+g ze&{eUz!l2HN))s$-C^Tvvcd;l@?mF-(~bwV{4F!Sh0D7wuvTz_ho{>uo1EbZ-4^qJKtaGsXQKsWQn2PgALx>KvRT1(El&og?%X#+$auC|{* zdA0l}w1?8ELkzI@k2Z*pX&I^P5yKOykJX#>lMKCGAm%$@w>C8Egymuj>7 z&3hSp4}$&5ub-Z5ZV`1s%a@sze*||7_#pU!2Ff#P znfj`CyJ+@A(&DWV+U^{&3DaDEokW7nL$k#HlU0R>I)zi4E-niD$q{qvAMj#o*L%y7 zIVx1}PuF&$ULOAv)7U<{I9kSgXde}FPF|!jNfDYdC>Ps$b8(L?Rh*i)mk*K=BZP(Y`PNk zUQ4Kip z_+->7VP7VyPZm1I)hmDVygt)m!KTBAujDHgR@C)5Z!-ejK5^-ic%0eVP0N71d+jy6 zuNB7K^r{sEEICki#^0ej5V%hX!aD zteFZ2)E|Ny5)T2Z$v0oiP(d|4n8qRHZDYI$ALgZPtlPAcnu`|V_BbYvN}M0+K%9T5 zJE0PGU@w8qnI+y0tu+VL1ADi4&rrtL$M+lr*y&$a?HQo|7_yU6@je4Kz_W~8RJ;ty*df#+i3RVViw!?in znZ5t@Y5WwUEbD^CwcN+}y>#l^nf8(B4@K^{?%hKfP0Yf0 z9M&l(xEl}~C*ynw{W27Ha{R??6G1Edo!4sx<&0c759Y;G-Z^1If1CEhycek~FI0n#hW>*?wlY-aRVsAd;Fc09ZRBC8!fA2X+fS z%cJu{5q{H$Zd1@-3!ExzL2qC-fJGpqSC0K5>AaC0OiHKFzv9aHZjZjSC@XLw@Z}Rq z8a&*R{qUo|3=aZVKx`q(FrO@&sWfX{w^E4(sF9~E*@v~Ag_y!@lHVVv@*d1 z@(@w=7^CbwbXqUhWg-+@^&sH_t+Z05I2NQgyq#N*ttAiio^MKa`=r1&) zAN~Tnet5Y6*}=z>`6p%QbtYrbq-82)8(ER!k!n;XhUtb zyOp62DS%&!uUp4P$;q_1TZ%>%~mN!ET%s*pTKY_#kz}`vOetj zX9nLC(<*E=b{hDpfP6<|!V%DQY#?AScn)DdFq8EBkqrsKPW+v6>W6yI#YByPjy0 zNQjE&0eMadWx*!1-I&`EOFoQfR6c(#9N_Oh2-NltQB81|iv8~2`0%e6U`Jc1@g=jV zglnj!1{-?<2o>*`;?GSf-4#nkb@3QJ>os|5Tc0C~s38Ib zYcigwAqDI#9Sy)EBTA3_Z0*5)qp;|W77Bmj_fa`5?t>B<;=i$^hzejw2}enY#xZnf zEvxhF6qkOWRbTzC#`artnHFbCSqp#Yq<&Rhbc9m|gj0-D<5~b}ihW&^Bb&@U%f9rN-H4==4}$Dp4osD2 z(`EL6Tp0b+?$qguy`X@jKanotz3qvb=*9U26vEzO>vF_ZB&!>70gcb2S)=eYe`3id zBJ*)Ow@2j;xd`8yCE;3vYtI!I=x0NnChXr3ML(<0ka zgFIG5pxh73z_HSg{ac_kQAVp6(y{avc7|^ThPVL>0bR>IK>`)}Z*8iW0`A{_pLBDFn8m2S>nX zL3o^Jrig~Un=kut8O$C`u<(BlQl(Ct!KX5mO8+>uz8&xq{YOA)e7xxgsRLPJi#`{@ zBjc%tT-BObFSMBfW_7z@JOxZ4U0FO>I z$cI$dfMf&6&p?K=F~ttP48JN?V4O8TRI%t>^dQJ}B=BP%VPsbrVNfD4{fKW)@V&<6 zOc(KKn;O;w+GO?9!m(o?U438IM}ZN;Y?+twM!(^})b#p>WAqdT0UIR5R{mou z&*gVSMMa&So+f${@)?iwxCzdF--Re|IJF>VdsMuK-Fy1!6Tue3*%S&b-xR63_Ih9Z zBGQ|Z>Nn&7)6ozhs98R1Bi+i=P3h$*weSng!WPbQva(35T^pb72M!_jWy0zG*A49A4gf z@CKf`1UolF)<-{w<$YA35GkBUDWk(1L}q7?8g~0hq7|dY!aHEXqD`fL4dHcU6z*pIkKI%zlJ6A_*sEzYpD{Kbp ze!9!uc!^lldcr`0v zYZ>aT<}+0YlFKKa-y(I$?eG@ z?t?~k$Jt=NuK>9+la?c?)F&49x(fHjxLqal2a@9L|) zsRCx+>texUI&dguR5szhaE3PeoRh~#SS^niX9w>|He&IJL3Wm$u{0`7UIu;NWi!tq zQ_g+;spA@$GkNR;KO7JN&(No^jij%{@nLWWeHYX@0P(!e>yP!$A+rKWkn5|l!i#&1!HhHr3uxmdE?=5*enqq%ze+LB! z_L3|vFH&tFdT6jd+i@7feDu3UjulNPagcX5`=m48^LND$tRP#*AnLYj$z{vg9vpwq zwHMsQJFViY{Nh~6qS=V)Y*VY@Sv*=p(AG#y{S=$pg__zF2&?kXNRlTfNphgxZ2KJO`w0DpiU3D==r;fA6|f*{Pg8?)^H{15qc)U_r#zsn3or~M-;rS z>w;pKWRmWM^EQm-G>ESUeN%#+H`7DY>RX1@xDmasWx2n0=R-xfh#=UD99bBh!q>Pad52R_N{yPS1RV@Jtj2MUm1Lfe6}JOdll$gK|UP zsvr2qzYc{B!afzh;q{g>1O+mO+H-)3s<2oW8%{EoHo z{e&N3ai=wfkK48T&*5#J(L!;GVzj<5LZrgX}z?^N@H zG^Ve$#fPccZ?)ssx<$OqHRKwZsK;%#ntcT9Or1_88oZv~B9WiSdG4z3(MmMxTbj1* zZ{DvJ=!R9BT27I|1)WO{-wB=M`i$ZbY?_YYld9UZS4Yepj%7wfvt^E(=@|_l+K}KR z+UB)|H^dR5;4=g_S2wM27(O9@YJ?*c!s%<|3tE28hnwnSAtAZAg1&5ZXn8y#T-_98 zU~_pq$e>?15>h_(ouduMFT@CdQoO&9X875o}NxVY-IEchO6Z?QRSR6OOtX6)(!vKLr#^kuBn z`rsQ3uhTLix`r4ldFvXg+DO*kIppX5ryD&_jQzv6rxjt7h=zxaxl+egY9&GWk7Dd4 z{P;CC4u&B`6}7Slv{oE0Imv;v=*d2$bdZq#ZyxF2zv<;;7fcp~n=bKuin@Mr)3_gv z$XTM>6ndnQ0Io4h#}{Glstn;gxY=^NU`fnu%Qf(@zlZVkxL*p(THkoM^X*%Tz(nqyebe~LPcCwYG~?(QD!TOPAo+LHa`DS6oGWmUZ_YVqs4mK# zCxt}oAeFl5WRUk@Rq}f9FSjoGUmaZ^uJ&Br-!`B}js*6VwiwV24iK`A7R$dedQ3e* zT})c}YNY#9Z@d7#Zi{z>)I>6k`6{euK-5U7CTB|Xdo+f(d5;}^^4g%jO+lsBFH3c^ z1L%OHFpiRp7_ptD+a<+&*JBW{Gf!c`$jSFjd#qZ1j@|GhoH4n~VEqKU&L4xhAUr#`hh0Bw zXRj`XrU`om9%AgNKPgpN%OPo>>3|t)G_?@OJ(!!-VJvlDP`!D7`5dh+)MA7>oz&Aq+Gia&CAFl~9cXTyofAt?}Oc zp4GrFae%PI{2BG!Nk|2y&ll(5KKJ&euuAtZ>s{IdHkGa5l@WIFN)AbG4S~!0OhYM8 zlj`UI5!$0K6VEfXe`Hxz3QRKTm1KSqM2aF89Ov@TVyp0&)8)myFEQ*sA&z6)yRjo| zXR3o|a*!KSHC1NEO;n2DpP(F0T&ZKPyol46(#(-OEs`x1MouJAu`P;c2@E9|L#=aP zCHUa_n{>s!=v@ol)yxdhy{;eew!&>?fpoP$RSS)KSQB^1}hV#p$ zN{iFW$U#$E5qcw|&f&jH{TWsbESl-h$+%b9j}Y!~_1QYUwrL~aa6w|0P?oAKf|?)2 z643t$)195@?BX{1VeQ3W_SZuawI3{LNQo3*3-UU4OF%4HGQnf ze`;O=6Iin(kxD_s`Xx6vK}AX0ja-9`CLRx`QhJ$&watVRo(ai&!N$7bY;LQfHmq+=I91e%Jpue_wLAy7s)~Iz>Ttr?9aQVYoa~tpbx6( zRY#JhO}(;b-~5OZtyWD%V8SK`d!lTLoe^KJ{p4~|=%?2ZVMpXtE>_N0cPbt_VKreMkf=Y)9Z29t zj*@5ksu`*kp6~MDxxX(KRp@}_ePs9b z$uO2mk$O4F82!+TY7S1X?wn7W-Ezb)zD3Jxp7!0Ov`eB$J~pN1j15h!Mi(G9W9~bC zYDIHzOEK%i+i#}hJ!)iMlF93Q6CN0TbW6$UD92+#_{_lYf_Z7f-MKr)08FUaVd8eG zF@32tBjhZ!%w!d#T=@jLS$(PJA;v69YTH9)SM+TU00kw4BI{p(qVNDv80awpK!H5w zeUpN2R-$3P)ZBDuHqFGuq*!~Xpru6(=lh+fyW1Y$+S-IsFJVv`bz0|jd8P1ylm6~WMdR&Ag^#OEI-YkN z3L!D1S^rcUH1U#CknTdD8=K z*CBlCh+LgCY59qEdt-H=U9Yv?NTgTPqa&Pn%*=XYa)F3X|Ai;v@Q6=TuB7gywFX!3 zV?B0Uu+%a&tCXt%YGvfVoqlOn+uJ0|7RTZ>`z-y`OF&$Itgb~j&J#0*-vwoWn^~$~ zg<8Tn8gC2QMr)xt+T$Y)oE3c8Ug*Qc;9)-(AnO*#!PnHOFKZug_o z@&z>p#1TBd4V=x;nc6oXfifl5sGKJ(0sf~uZ*BwOXiJvX-Qw2^r=mO6s>bkg3LY_} zX9MZCo;S|RCGdh_lS5%_^RwC%Y~YMreD`;zN^G2+^u6^eV`gnODOAqJp}YJ*IY6(m zEaF8@$9qTWARGS?faO$rjtNIZ&|{Lxx^2R#0u4VW4r>-c>#U9tsQjR$yDa-IG`q*# z82b6JrC+Q>gFlE?>+F7uuifql_FjDxc^U-nQc8L+o|RQe1=;R8sUh1GA)QJz1v;Nd zHmh7plQQvb1mu6$#HL9OAR$Y3={gz=Q!3w4yRz#F$)@L8j*&0;l;kz0IsE?WYLtz2 zE&-mUgJ{lY|4pR8`1|0Y*qE88;IIw!NEh@jI6TI=dto~v-9b6iNG~_>#mFBtw3WaJ zDL#EA1AUz0XGby~>SU3#kW|!TSEE3*--Z?2`#$GLI>m&u7=r9WKIZ?*nN_K{Nkxiy zh@ovUfIUr}xDs*3(exE?AcICNmhj}A8@uiD^eLKq!t|hgh)gP7m1?_me8_aw_76nw zv2{aG8}YOD>U;m5#h#u;n+heF0sp@k1M!}n}zz7nXWzq$g@PT~_BaU5#h8#3%i#p*FQ98IgEL>g}e zofmy?x{u4{!=jlzxNSs9{jpx!u+n?n8k{v z)7YJ%Tt>2Kxq!UHW%KbABpKCDG2Np7Hp=XwxuL6VY`t0Sf7gWJj(lR&ut>z)$ok=I z-*>-&OZ-n^_AUs+Zmw2GPhxlJn6tim$5(YXIeSx0(plIyKzja}sIMy_TMc1k8M-N2 z`H!KUwJ<`G42W%sZgYD_5M~5(mti>$M+?on-{bu7cV5*zc8i<#9*I?kR~7b`?fB{q zhH&c$*Tk5t6HkaaOajoXd=lh`sNTgW58`;oEoaGw?LB^cHr94rE;I7WQlw2C8#m5T zCV9|lYizr!j?XRQeb|0wmzuU+-bP4YlSIwgk@IDZ!tA^ZC4DkH3j)geln+hzT+V{} zIy8|+B_V((!KWwJM$H&sCuX5%Ac2)e@aw2f*vhtpEwXyfbH*EjWHX_>jfgGpqeh=t zE>z{5;5)uAb-Y2C0lyn_rfnbKZMDM9lmEb5P;JxgF}XhlMR?J}HiW(Y9IK=){HA+v zTdib*Te2UIU#-rPTf(wWmt3CIdsB~LP`#ckR z;F{8b=CBRct${r84KcO_D>^Y3vJnENa$;}X`I#{oq%(4fiKO_$i$kMZAB)`b+l0Yd zrxl0K;>9ME4mI3Dr#$*!2af}xn9-CB=Br}A`cvQfc&H-^A=zkuoU7S0XLH=?&y?1~ zN!sq{y3P}|hKB<8{WT(I8M{K!a4+h~y7R0#mR^?Ap<5GLB0F7ig>hVg44_6d4X~44 z5*-$fO<6?~bW<1kL_@=pUo<5^$lp8{TU+9{coEL1!|4nw<_xpUKUjZ!R5IR`rYJNT*wE$vD2-2hjc&^ z$Vlo(d|3fCjGjSJ_IjJ44?_me6wmS_iK#qu(!moH1!YD9EJf2KIUV0jL`$Ka$Sr*C z{F?*uR84^-NN~y99i8B%Xg`)aghgYShahqZY`>^lKg=V@K7DbWw zFBBpGPj2Yj9R=VsJB~gZE)Ct@XEcPL4|w^g$!EX0T;>|P!;W+E<=KgsU5bc4;*=IuQPJt)= z0oK~&%gJPb+GjS{LlS7?E+BlXx6?PX-e2GO{kc8?n>4pZN91n0B8K+S0!3C z#L%bEDX|(aWht*wG2vwqJeVN>_iQ z*DAP7mJn+uM*R1bchU{7l#HVqi6qaiY8n95(zNpl{p#IJ`Td(=Y;V*iw?{ocIf!MK)k$UY~$KC*s!oamF>B;0| z#1Utsleg^7qdAAk5dlth$arx8+4w z89e7Pa#-v=OXRTk-rnVDl6t9C{B)Y+WOIb zVpQvWif9>M7(|w-6-}?VXD&j&p@CDXa=)?k2Ff$5YZDvDKlCHsm+oyaeFz=JuajOm2LA=(1L#p3P zn(LPH;bPb2D3_^MnD^a)XI3oG?t6Cbbu^6onL!a|&SxeZbcerc@NuG#Pka*BE$@3@ zKBKgEwR{JsQu-~_>#NJpv7MRr9eR8c%W=7U(GH$g4K zq;_8mn4j8*9ap~#+A{nxpl=OTwLR03syBu9Z2*$EA0pV;yGws2q>Hn6SaIL@nOZVR zCuTbDLEUW{)E5hwy02?3bC;OxM5Mcd;FqdM$J@G3mL?X&jW%h%D#kMCTJP65Cw!ZM(YfJ;?6Iq}OG()7eKZ9*_yMv9;;%djbhLMh=i`bj{u4GoZJ&P!> z$>j&H%0X%k9R9tZ`DkXHtPzr_nno&`&|U22!iJAFI#MzcH)k?g3nmJ}(F?h^ni^)` ze7ygmQF}Z^X5*hIWad(&TTM`RyzZA=6NV^{vn>7SX?zM0cqF?D@T2+VEzw-dIdvJ9lIh@6JAE!X;< zOZoVkuGS$!bLZ_H?l|A?ywULs-+f$#_xRNtzglc{H8t3xSoMo%+SLJxHoR!$UoU|0 z;2}fOxOK?NkjppiCd1DnigNymFGXezAaTX2MXz%Uf+`68YM5UBQDc(6`xiy14645? zlK>~{UB+u;uqlN!J`xoAgGgG@K%os;$vXbR0CI(jATpHZX>@(aPd~85{gQBd38qZe zKWf1JbJIH;6PNuxA?+py^n1ZOq*N`ZSE8j99H!8Zo|ngg`8e|poSnQhvIKIvn*!*L zU2lT84U;zHyfTH`XD|Rg(cVg7!_|86T?6jLw|OGRFGD|!WG1ApDQ2@|ha)e(YjWZ- z8~ECC>u`b8(f1AHR`+S?pm!0o(sJpEWXto=tS;ut3FF=^9g>5KVF=PbpDxKaX{+9D zX3{LP`#n(n`XPxs_tq^9tzM38TzOt9oc6SN1(lkbl@1Y-j*ls91j$&HmeDL!Dt|LL zw&MzBIGvfAcJxd}#A^dN`hT$U;l^q5=0s+cYN0fOQ6adKz-K}mk5xlOoY^8~Cd=mS zc#)R)oQn*&{hOViYacODgzjbU4uiLw{k|{atCzk5D?*D%L%2Q5d%F!pLY*>mdJ1Q^ zHC}lJ6}v)cR{lJjRSL-)#Rih0cnp;BLV;G#tTZ$?qGejgs?_MgG*jrnM}EViQ=3J% zeg0i+E-=;L-I^7zJFoe{sr=;?e@&i4L}-0v`CjB+cDD9gaX|kVS--**JDq*!{6({Z zTVAnJB8CWob5B9{nMw}j)$Cc|K=6D14V$*#kJMG4N0FHPZgnn4WznlGs+foUa@{2( zP%Cju6^Eu^3;4OXw%9|0f3%^GuoR7t{1Y*~k=^0&q{|-;uALtT?LRd@{VWJ~e1MnH z+JFr40HWmDw!RrBHEhy#<$Lql`m)3QY}9`k>C%_nnDneRZO?o|;5m%O#c!gDO*A zuT>)DJh{xaaFrI``nhb=)ZcD57Zk?BhYaK*-qzMMbi1swK6hN}Eawvi*khn1)nC7$ z2#WbjQ^TKN^L%tde4T6?=C&Y+(e}nBGib@^_o9WY2{GX>vtxy1dZyJ_I z5jd$DFwMC>QR%Cf8FPUbY+-Fe2NMj>UQP_yU`!f=*DKVYC^;?YB2&ZV5;@!4v)};u ziTVd$%UM# zg>wnd{NP|R%0mOHTcHpA?!57rFzv6EB33HJdljXA!JwWN~&_7nL0L;>3~+*0!e z&ztEr)jIV}^Q|tt+W@@@2gdKNA_U8L!YNe*hY2?~7^FvzgR}noC zRz8N4?H1-rU(t&SR7FJuNCU|E3yQlZ_ccDX*e*nJK(^)^ldJDyMJ1o>Hs61zY0lS! zW5Fe(f#2Uy(>*_D zMsEG^)Ud^%0ENIh0cD$~>3TJ0xqzf<(%ak?L28QyPma95&_j)AIGtd<2=)7)ur8Q2FHswn@>tB{K~r`L9YdbZ$CPdj z?KVui8#6j=3?hhQ)iM}I3|*~!sRcuVGnH=k^-oa+$p)l}qqg%RCjyQtIFZ^v99xI5 zf)PBN`ltrhA_UZ*Av3EF8M){f4>|`$^d)B|Qj|Fx4L-+@G^k>{;cP#&!##G0J!ZG! z=8~;zN!B0D{m|A*NKUU?mXsYCkKf5E2+qU1d-<7WqRe&W%B_?ILRe zJNq-Zdeb-s9M=0IwcQ1ek7!8f%tYXhB2Y6T~XLvfUXI48$<4A`3 z=;B`wHd_=A_iSyyis5+I4-xt{&!`KwyiQa`%-8?!kOC+v0F!JNVhIVXdSaR}iM9iT-&<$#N!eJZhcBiaFj@5)gG=}0e9|ay zd$x?68Gx~5yZ|3r%s5Wf6mWY%yPGeuBk8ARb0uBj;jblm)3>;Sqo{U5^Yh_lFe!vz zRL6(ynHsYUuE1sTqtekOtH7D`e#QnGe{kPkx7RP-G$F6NWBXd#@8BzUo^H%A{H?4m z)PPi-4iB~v;RoUBD@#-=9(Ws1KfPP#0*@^PK({ARJF2weXrlt@14%2f_45$1($y@u zS}88^Igj0ZXWH&HR-;KswPKicgVUYEXT7kRFf>>FL~$A$C~#3d(Af0j7Vc>h1o~`o zy{N5YZ~szbz%DTi76;ZoSSdeKA@tJ5zUl~^RhAR&L#YC|mp|M;fUm`e`k6@s$JW-i zTTAZyJtgS}uJelRh-Me=K6ja=?Zv-@)h1f}#XL(&Q!r3;D{*=XLQP`Rzd8VJiue7m zrz9`(3DcPbU2s6r9oI<^kRbkNoY~&Os6ruI?Z?#*B}Ct;kRt5i;R}k74#YYyK(ZeK9qbCkoZ16Jw9%MSgMps%p`pRBa2-&ZhZQ z)40adcs+fV#|{b>UtV0uIv<=cIp@fC9Ra7!kv6yaUsuUEMUaq+S3yWwA7{YwXHU*u zk5K;ma{g2Ka^MHQW9uLc={vDlwo#%)er(?@hocU!3kwd0oQGq3Yi*_LU$*hzfAt6< z3m-6U%-F7gZ@^KeEYQzW%oKa3oFgr!``Rxh?4_$8+woI8X;tu0u@eqQrvKxq!0Y)x z%d5`_Xl-(XNn9YA9(^@eHD4J4grUp#Cy*Cf8L{Cqa(f5EnliWQcNZn3^St@Hg#K)L zU9Jg8`(tfIT__ zyn!1bcq99rf-wJ5+JCtiFt)l6bDxnFQvqnKg%67wfAtQ?ZFR_CP-^fuZvU@AaR5{4 zn4IzIWQ$YK=L#)Rr0<6OmHYK8yL)FOPWnNq`%JH&r-FHEhhRGwyt`)SFzqM$dLB=7 zj`NkL)&CQAaLGnGu!y{P*jnts_v)Yz_{UB3e-6We0qWCaeberonOdhij`^<(h5jf#NRzM36i&}ULJdr{&PEYEo))P z=SmQoEJP7Seis2~kIWJsJ5c!`0>e@0nWvq=8ERt+TDG%|OodfRU=_hZ5 zX|`kbj~h$Lgr0Q=MIRK-C*YS|Pjdq_Mbn)wyGfP~4i#|9m(e(Jvgq9`TnIgl6sfN2 z&T>AEVbPi8ZD2y=d@t1p2PSmigYe0rqrOP+zXuEwA?gRuIh-cspRkRGMlvI!ptqd# zJry%=&BiCgA=Kg)Lg+F2$ZhpX&Ek3+41LMPU>FOjtn8jx^d7aQBUP!qU#L;5Cj zOl8s4e{&TbPcFOxg!y{aoc_XI{6+aeLNuGn=sc}@vpceP3%D#kp7C;W_?2^7&4TPY zQzErc9=v!0$fG+;VXwPCxRx*>Pc|YJ=FI59`gJWxvGZh62okfwu9lXv?|9{Qy8}*x z3^`wv>AL|_?Rxte@T%z1A;iJ-Z_={PGt%C>po4`G9*YEoh1h&BX^rbk8=3#W$N#K8 zJT_8acw`Wpal+G2SQG%17Oa^tH)PwVY%|U7grLtnn43(i4K)3Do{YYluJB?NOrc-- zy+5T`ris9-Q2GjVE|iY=un*mSfDx$P36Lyn|F)3vWse3QRhf8o?Ke8|n{ux!krPr& ze;1vWAPcq%<3;6#7Kez}z)(tJ+9tvpjoBP#w%nh)wDQ796u0@00wu^Wu$%gOS9Y+`YdtW^0yTSq|AP(s|C2X2NNZ0Gw(#{tkyAQ|vdc7S zs#dS16BYmV$!gu;y(ktKL!_ZpzNiuLS4w|aLc^__HOS67?v^_+uqYMZ<{N!oSzrFX z@p;nH;SN|FDXprDzNuHif~*7dhEe}RohnoFg))ToYnD!+b-ie{oN2DpZPB)BEkdaE zT;T}Ct7(NGXFHJrN2uwGgwl~7ulmvR|I(CFZr!HLDS>sIaL$9jwEr@N^^tjPaw<~5 zpXnXpdeGWu^mX)GS4};4pgvu>nUKfOPU+q>p`O0>DubJDM8qO(jB|xugAaMnmR?`+hZkY2^98=HDA%H+0iqwEGi`QwPJV zJ|lJi02nHxZZr}C<6Aw|Y`24~c*yQ5rk%)}8~0nf56+v}@l-SAW*t0h+uLD3`?;P! zuyMCV+1KV5ZgN34-QVEZ-9-lcfe(H8$0tVnLlXbjU6vVz&h(yBl`xYf^TAuX74{c5 z-cL}4Y;SDtiFGTSd_1OBE*_*`0-M3{?>vYY`(bIf9-FfFS+q(5zJp8T zUL2ez`hQ&W#xC%(_Q33zT}Genww@KVL$?FRt#3L|(=yUrrX3BDm{8TaPuv&3sSl!@ zGIC9>l3m{dQ=9Xv-RdXmL3>MD*+%Cs=A+db;Tz+EjrUl)B(8yt({_`2$cRo9Ma58K z^rtE$E7n0j)?_VES&@ItI$0tk7iRJQvk}jVc%03O*;$4}` zJ6&O4K-XJi{%SA-GF|L9TUklU19SyH0sxvJxvr>x#G2g;auLQ$8mk+I^vul{WPgs_nG~SWsL6;@rzUP)>!;M!u7uHc~QKnHT)*v2i zwnL#fx43X$qCz*-ZTf(}R>88(yd=Wttr=>8Yqkuy%>x|{aevv4Gax2hT-^}v5Z^Vs z^%*|!s;k~V5KG=3+nP^genRA~_m5j#?9fr}etoI4 zxYrDq!bQ5*Eg73$-YH3QsITbh>A^)HS?M}+VH~gdEa(RM=l<$i z;H>?7@1eB z9+@25|9{<`c|29!zsF04$eei&QHBgB%8)Tr4$2hD%t0vgJYh*gDqT(MF7tv$~PN|;7d0Kn0b zeG8}MiDFsdtvNa(X}|!(wXe1A1(Lv^VE;tIbU~}Kn56NYw$nLZM&OR|pHToi3qdM! z}{S_c9M0Fz1~o>t3upVu*-5RG~8pH$n$!0#Txun{6h;q+u@>j`Q~xB8V)Le z#IUJY4c0POcAD!ytLo!~84MtSK)?IKLcz~{6nxX1k24*=rEh{1p#PgDDfPJvGt`oe zze#$Lib8>ZSKE7Bu1u8{S85xrROD@bKx@(XQ}(9FacQIx$YtLlpx{Z-Vpl(UDzAbN z0s}DO%hV>6@;v2@fTHZ_jPQ}S8SQtE&&IkKxvzX|dR3FUYVNYxaUj##5a{Oc*2`BA zHog$LaW%u(_VZYc+ObFRNU?`TGF%Y7{yV|t?cNUs;xMK$iVqJ+~;H_*Ro4IG<^1*kg;C=U3!`IjlFxuW;y<9SP(n9(x4~jHq zC9sy-YK3ZOSb5SovxltVnQ4%vd8kc+3))$>F>JHV<`ll~m1*7_Et&E(xj{?UT-_Q2 zSzdQyrb}*_*6Srkunn+NFw`{%L;PbsBOjetYvpGW86aOI&o)mGM?OTUhyWpbqs@#^b zEV>edm!CR z6mxP(#uY~k)@lw#m-#M91c{A2d#WXy=k-vR4;_6e+em(9xNJzR4rIK`rZ=pU{M5_c zh>~K^N&{+PK={wmiYQM)ulX}7qv<~1<|rubdM8IqI8ma6A> zb^Y`@&{}ZvcV!-hoi6yy$QE%{k*B9Je`vh0?J%Vi#bIzk%d4jnisoasMD9JH2>qLz zhuG4)2zKg4&ngYb`@>~-NEEhmGr)?O0iHySSsyIUn>|{>gTqZLlq#k0Pd41ylIY5T zL=YxmLwbBVNr~aO5Hy1_srm7-G&*kEit}#)IjqF)7=lquW0K(@n+<%32b@09`Iv0B zs;o-rw`}4BCYyaK=^LRt?(=D=wz5QxpjPSL145|bE01#^P=*t$RHSTesJyMRJ*h*P z^#XEfUN|>Tw*C6gl*Q`^)efJ!@slG$S7{}iWP_h5?_|&q?gMWCjX#4$mq6x3S;TS= z`!3K!!%JRAhaRJKny6Oc%bh4Bz^OqZ9@7S0A+n00{8V{FW5!K-xU_YPkZ;@EvN}*v zFhoV{4Y4%kWYrrUTA4N>;#&?1+fjyl zdvr1+Om9o93mVU3n8Jqo2^T)WyAlP(ar})}1qy*oEw0Z|?_5w=!W|=g`vn9B*1_H1eS%}{uYKP`TL_T%jEMPdE7bIrt}4RVY6{%`=Q$#3N@jnHBx zbndM^nf9FQ%h%8;u@X>!ab>GhGHsj+$+K#h3kjUwQ67NGq;cfY$h*s_~rNLf!`i@9&B=Uu*oBOE+%8!11(kH5BmOV>2TIL~ZIM5EWR1qGx;MxAlPdXyJ*zGO=N_^Ss4435D zOkaf2yqz#G45VN0irw?|i1?CFRVPX1fmJfla9;6YtkyT&WK2tj9wmM08p4>-`yro} zL=UcVr-*TOj`o`tj%V`CFZA=hR!ko8>`(@66gK~VLrcMYyVvLr@SBa5nh0^bLFqI6 zo&dNMCgu_K&o=H|+M;kNekvOi_dSc)3C{0DjSUS{g`fW;{lidhoLPABGGR2fQOpU&Xn= zfY91~q|wQ94Nb`;;bm!amMW$=dW#Y8Gy|u;EEQI}ugC|FwC6sKl-O=@%LG$8qlHkS zcG$a&l~tj}5mE4kAV~Z5P?2VpOo=uy9takCTsaEHnvP zbTT}vV6Q62ChRHUE(m?uviJe)3`q90_tVS9FU}&R*gWcwDSaMv6f*U$G&xDSk4v^b z!8xmbd7q*EW?}=LjI)9IO{G<73W9AbEm~CqY6C-_?$C zJtzf!*9m6MMWy+}Nvs5>aXeN;Z6{|I{K?ZFG^5hnCta76RQR$9#z$1vPHJ>MOl^ZA2`-+CM*(@PxhMH)s|65!PR=2 zcbw4mQ#7^`PnA4Fs80xpF7O+Ylb@?DmsO4 zb*C95j{BE`gfO%v04PeZQ{6S*o-MvX9bjjgdnc%cA9&8H{F)pd!I8kACd}yh<>9kY z7|MjbLGB|jIDMHFlxqxh+^_!nT1xq*U%tQ38Ko-2YJ!}vhuEH~2i0^Xo~Pdn zURUa~zH;lFl8HU?3oDWrR3l}lN*!lAr@*FEu(ItcbAqdr`Z&C3*()-z^K zj6}B9eCkW}4otaRqb;sR3$jn^L@~e>%NKW5>8=ReE~@kq`AH5Od0D@zBOnkqw>DPp zP5qND7wR%kS|^UW&iDCF4kbgRZHQy-*>xzcA%6x6X1Q@R*&aBnoC zLrh@Yo>h3skba?N^wbp2#XdSvM)=aj*rn(PP!cOYE#T1zbD_^_u63Amy-39PAhnjEIqqKV-MuohGa5CIz7BOYGlQa7 zAJ>WCWwte%6NAVA%_g_-1pf}}QYowihgkDT(IMSPi{r4xv2wf%c&=l$t{FaMZeJbZ zJ!yqpXtD>g!+^#_cG2^wfw-?rv59sJV-RC7yv~CMa1Q8>D|6VASg{N$f;Zn{$H48P+Ux!a#50p(0=q<`NQQEU zDrc4biY@Bw*j?Nm)x|nsQ;>^!!YeR3MEp4CdKh}=?fH1x)l|kHk)h`|?-c-&^@4a} zH7IJ~gBTuscCpuq(abjOU|E2KhK(&e=g8c6WBwKCORuVQ2-r`7fT3h5;CA+VN#Q_h z@GCw3#+u*pQ2dM)wO>HTXT8)(N_57BaZhPq^?wUO;|DTx-g*>ZfSSbJx|VyS%rE)!|nXPkTS zSOy^BInvMrnJneFK@oYO_wLlfY=|Uc;xdBRyGT!NyRkHTf_UY6hum_soKPQ^?m9Ox zn^Op(2D(f1d)lBfVX7UK&$NQ6=mL>X-v{GuFj?ABP?P~3>Q_TKMj@E7e|0E|MmILH z+H*I<8lkFFZIcwlP5u<9<#H+mOZxYK@vkAfZO{&F*FA|!K(9I9R{2Or>C9+82HgD$ zx3c&5MMXoQBz zBFA+WsW}%WH&1f>v@$__WL?g6M3kWUm6dYi@w30h^2uWu2f%mzsmXizA`qZ1Hq}F0 z7We3cTV|%F?^U`kS{bnd^F&=-oirD#pxGu3u-rxt5Dfz#H^%O85@A>?pE}<`R%Czl z_C!T$Ju;s=*vYr|7RgrP=Nm20SE#u0dTtHvbGr8?Ium=Prh&coCvSMSk$kq{FH%?}v!rkdS$q6hHb7+l-%@B_duQwa zJ%%>Y_OCIt#H4p^JocmR+6R&Q*Y_2RF*dL)2#m@&Np(O3K z%&?)-+WjJ`9KAwxt!?*L-=;9D#;`;wy%$1=G2D_=yj#T)HsnT%N5D+l=#+7YCKeG& ztj*2Gs$O`!p7Fu4*!e|rei6@$lSJbHI!lw8c_4$siVYb^+CH`V{^(wnCW>%(pMWKc zm{t^aUwghqxWPJBfF<5w!?@UqKz7MBo!XJ!E1v|V^f`L4nz;ia*eHP?>V-|lFE9&^jEniD{%q&Ta7%RJ1j zICoGxGT*9$s?SP?9;{~G(08|WG7L_W&oQhDIGFIKoCmg$gUI;3Y)dZ1qJ;h5hRp(q z{UyJkZktvXO{gZzfmLVh=HI~21kh$VJq)FV*FSf(!WDbJmNO_Cj4@WcP7$7Pk!lq* zG_LB>`Jk@U{t5^jM)9e6Uz3U)V|33sB=vmFJ&<&q9Z+32M41(`&0O}1@YwJ~>tJ?j zw#u;Ma)gu+u<$_l{3Ec&NY8KwX*^&%d!F+E>oSdrMmqxlO$#@BL5VV+%YuX67`q5I0yoTJ%6pr0qEu*J)=u<%WQs{iEi>9d5{ zSN6gn)AY93H+!BN&Jrq@a)R{mH8(1Uhu6dFp;)_S?vWb&@$MwzK97FQS`Nm(A{q;y z{J>>gv=?7AN>ID!F@NEFUjLO5KczD!WBJLQY68gkkk6-muA@}-@(Mt~l%i;p9B82* znu;X_tf$TwW%lA~YjLL}ClP_gZThuMQ5(6h4NGq;&?22~kE`79RZ3x{sL=p7Q?f0B7L`n!$=AugKlo zZwUo^smvs|l-MMd{N)mEp`rI^{Sp9vQvqH7CEx zDfVG%FIaSgV>b*wQhGbJ?Rs(1#vtg7mOE|g&k+?=+w~7E~T7playpWNZj^Q~TMcN)3*Ad@48V zYzI}ZZSySg<#^#vnj?yqW|uNKL4iCVL+z;v@CFS_sFZLCaT!M=@Cv-5aTe&DwFBxX z9Wlkg;wVo-4FN|~Nxn1OYrM!r{`D(-Q}KL3OQM2VWJF?l0o(PbH=DR98;bexZw3jJ zbZ#qH0mqm9aj;`up0DuD0Hn3M-O|yZRi>A=AKo}ULfP5Iz=nxcCD>MsLAHy47?^b; zu@%%VX+L~VC5p(9p=>XGIoE&}X{1{wxSlln4B`_yeFi_dJf^SRDP~fYE6i+cx`Gqx z+#gCaH2oy4dKj36o!2|Q_^e;FUrh^gEKo{UFTA6NS^;L^YiZC%>DIU2ksQ;586ME! z$Y#Q#I}--`5F08gsxvps>|_iGy5zn`x?j7FrSmUYqKx|oK~~|2uTE&}Hzz^9K3eTa zvfm!772MTq7SlCa>1imrQj-vPj?b_ru!t2h77LbwDh7n3!K>tD#78EB8aFcI3C?|S zY4Boio>vEz&`d3E^1v33JR|`Hl4Rs<04syq%i_O%rVs2SdaX&BW#3V1Xy8ZxrG#)( zk9D-%e1g#tsGv~cEZjDrb^simEM_cxr=?ola~nE4^)CmadOdV>T(3v`2{vU1s>m;) z^u(1%=a}wLm;X<|F?N9ZaD?z4=L99hnWZM-?u28w^Zko&~^LFj={@A_(=I85BppEKu z5`W>?1ijgScKa>W|JKb?(WGF0$R6uQ4OVQR0W94Y-CrNo?*k&b5GdW#RTZFa?sXR! z3)hdHqvL}g)|&emU7SY4UICQcPhlXvK*26>jUQ#rpMPbK1qZ*0o^=Q0lOhl?zSRHA zRuIA@>f}D0K7jOo5M|#+uK>XKPMUHpDgs3*5L_eh>Ws**1N-0|@An4yK4Wyy;(P$) z%Bs_hr2VoSAF;kLg8Ht(yrBCnJ^p?+i5?Jv@T$G4+%V#mHj=$o=p!Q#V*Y&V_W`Xn z)eG9oeikzwyCvc#@aF@8H;4hlo0(o^iol|9lf?A#Y_r;--&pAe0FinOm<5vAp_+jD;>3Owp zsO&HI;73pZfra+ae}20pKIl~rzZCax-iBZM|NHIu#smNO@E=Y7etQ&Iz|)G4-O+nU z!@u7oz%Aqc;ryT5Duo3i2F5oSbl3y>?Pw5JApfyd;5c%{$^9b{k)is-s+?6vz>kug L>gAU*M*jZ=ykeyFactory = $keyFactory; } - public function create(): Jwk + public function generate(): Jwk { $secret = (string) $this->deploymentConfig->get('crypt/key'); return $this->keyFactory->create($secret); @@ -102,16 +102,6 @@ interface ManagementInterface * @return array */ public function decode(string $token): array; - - /** - * Verifies JWT signature and claims. - * Throws \InvalidArgumentException if claims verification fails. - * - * @param string $token - * @return bool - * @throws \InvalidArgumentException - */ - public function verify(string $token): bool; } ``` @@ -124,56 +114,61 @@ class Management implements ManagementInterface SerializerInterface $serializer, AlgorithmFactory $algorithmFactory, Json $json, - ClaimCheckerManager $claimCheckerManager + Manager $claimCheckerManager, + BuilderFactory $builderFactory ) { $this->keyGenerator = $keyGenerator; $this->serializer = $serializer; $this->algorithmFactory = $algorithmFactory; $this->json = $json; $this->claimCheckerManager = $claimCheckerManager; + $this->builderFactory = $builderFactory; } - + public function encode(array $claims): string { // as payload represented by url encode64 on json string, // the same claims structure with different key's order will get different payload hash ksort($claims); $payload = $this->json->serialize($claims); - - $jwsBuilder = new JWSBuilder($this->algorithmFactory->getAlgorithmManager()); + + $jwsBuilder = $this->builderFactory->create($this->algorithmFactory->getAlgorithmManager()); $jws = $jwsBuilder->create() ->withPayload($payload) ->addSignature( - $this->keyGenerator->create()->getKey(), + $this->keyGenerator->generate()->getKey(), [ 'alg' => $this->algorithmFactory->getAlgorithmName(), 'typ' => 'JWT' ] ) ->build(); - + return $this->serializer->serialize(new Jwt($jws)); } - + public function decode(string $token): array { - $decoded = $this->serializer->unserialize($token); - return $this->json->unserialize($decoded->getToken()->getPayload()); + $jws = $this->serializer->unserialize($token) + ->getToken(); + + if (!$this->verify($jws)) { + throw new \InvalidArgumentException('JWT signature verification failed'); + } + + return $this->json->unserialize($jws->getPayload()); } - - public function verify(string $token): bool + + private function verify(CoreJwt $jws): bool { $verifier = $this->getVerifier(); - $jws = $this->serializer->unserialize($token) - ->getToken(); - - if (!$verifier->verifyWithKey($jws, $this->keyGenerator->create()->getKey(), 0)) { + if (!$verifier->verifyWithKey($jws, $this->keyGenerator->generate()->getKey(), 0)) { return false; }; - + $payload = $this->json->unserialize($jws->getPayload()); $this->claimCheckerManager->check($payload); - + return true; } } From 0db232bfd00c910b6ecf3062774fa1fcab2947ec Mon Sep 17 00:00:00 2001 From: Lena Orobei Date: Mon, 4 Nov 2019 16:20:14 -0600 Subject: [PATCH 012/479] magento/graphql-ce#196: Swatches: expose Hex color code and swatch images --- .../graph-ql/coverage/Swatches.graphqls | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 design-documents/graph-ql/coverage/Swatches.graphqls diff --git a/design-documents/graph-ql/coverage/Swatches.graphqls b/design-documents/graph-ql/coverage/Swatches.graphqls new file mode 100644 index 000000000..f81a2b4be --- /dev/null +++ b/design-documents/graph-ql/coverage/Swatches.graphqls @@ -0,0 +1,19 @@ +type ConfigurableProductOptionsValues { + swatch_data: SwatchDataInterface @doc(description: "Swatch data for configurable product option") +} + +interface SwatchDataInterface { + value: String @doc(description: "Value of swatch item (HEX color code, image link or textual value)") +} + +type ImageSwatchData implements SwatchDataInterface { + thumbnail: String @doc(description: "Thumbnail swatch image URL") +} + +type TextSwatchData implements SwatchDataInterface { + +} + +type ColorSwatchData implements SwatchDataInterface { + +} From fc3c8d19946a73fa7aa5fa576312b5fd83e1048e Mon Sep 17 00:00:00 2001 From: Max Lesechko Date: Sun, 1 Dec 2019 19:53:47 -0600 Subject: [PATCH 013/479] Sequence functinality in Magento --- .../sequence-functionality-in-magento.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 design-documents/database/sequence-functionality-in-magento.md diff --git a/design-documents/database/sequence-functionality-in-magento.md b/design-documents/database/sequence-functionality-in-magento.md new file mode 100644 index 000000000..05f36073c --- /dev/null +++ b/design-documents/database/sequence-functionality-in-magento.md @@ -0,0 +1,132 @@ +## Sequence functionality in Magento + +### Terms + +**Entity UUID** - universally unique identifier, user-defined OR auto-generated by system + +### Overview + +#### Sequence mechanism +In Magento there is a sequence mechanism, which provides system-generated entity identifier. We already have this mechanism for the following entities: + +**Magento CE (SalesSequence):** +* Order +* Invoice +* Shipment +* Credit memo + +**Magento EE (Staging):** +* Product +* Category +* CMS page +* CMS block +* Catalog rule +* Sales rule +* Product bundle option +* Product bundle section + +As you can see in Magento EE we have much more entities that use sequence mechanism. +This causes bugs when CE specific functionality are not fully compatible with the EE, due to the fact that some entities use sequence mechanism in EE. +This affects the contributions of community members. + +#### Staging specific database changes +The following entities in EE even use the new primary key for main entity tables - **_row_id_**: +* Product +* Category +* CMS page +* CMS block +* Catalog rule +* Sales rule + +These changes were introduced by the Staging functionality. +The part of functionality that is responsible for updating of foreign keys is hidden in schema Recurring scripts not even in Staging modules: +* app/code/Magento/AdvancedSalesRule/Setup/Recurring.php +* app/code/Magento/Banner/Setup/Recurring.php +* app/code/Magento/CatalogEvent/Setup/Recurring.php +* app/code/Magento/CatalogPermissions/Setup/Recurring.php +* app/code/Magento/GiftRegistry/Setup/Recurring.php +* app/code/Magento/Reminder/Setup/Recurring.php +* app/code/Magento/TargetRule/Setup/Recurring.php +* app/code/Magento/VersionsCms/Setup/Recurring.php +* app/code/Magento/VisualMerchandiser/Setup/Recurring.php + +The solution is implemented declaratively using DI, but it still causes some issues with upgrade in EE. + +#### Declarative Schema issue +Due to specific install/upgrade flow, when we use Declarative Schema, most of Staging foreign keys recreates on each upgrade. + +We have 3 ways to explicitly declare new Staging sequence foreign keys: + +1. Add 10 new dependencies into 4 Staging modules +2. Introduce about 10 new modules to solve all new dependencies +3. Move database changes from Staging modules into corresponding CE modules + +## Solution + +The simplest way to do without backward-incompatible changes which can be implemented in a patch release is the option one - add 10 new dependencies into 4 Staging modules. +This may be a temporary solution of the existing issue. + +* CatalogRuleStaging/composer.json +```json +{ + "magento/module-banner": "*" +} +``` + +* CatalogStaging/composer.json +```json +{ + "magento/module-catalog-event": "*", + "magento/module-catalog-permissions": "*", + "magento/module-gift-registry": "*", + "magento/module-target-rule": "*", + "magento/module-visual-merchandiser": "*" +} +``` + +* CmsStaging/composer.json +```json +{ + "magento/module-versions-cms": "*" +} +``` + +* SalesRuleStaging/composer.json +```json +{ + "magento/module-banner": "*", + "magento/module-reminder": "*", + "magento/module-advanced-sales-rule": "*" +} +``` +Only 9 of them are unique, but now 7 of them are dependencies for Enterprise module, which is de facto mandatory for EE edition. +This means that these 7 modules cannot be disabled any way and only 2 modules will be locked to disable by Staging. + +* Enterprise/composer.json +```json +{ + "require": { + "magento/module-banner": "*", + "magento/module-catalog-event": "*", + "magento/module-catalog-permissions": "*", + "magento/module-gift-registry": "*", + "magento/module-reminder": "*", + "magento/module-target-rule": "*", + "magento/module-versions-cms": "*" + } +} +``` +## Alternative way + +Let's look at pros and cons of moving all the sequence functionality into CE: + +**PROS** +* All significant changes for the entity tables will be in CE, in the corresponding modules +* We collect all the sequence functionality in one place - CE. + We may even think of moving it into the Framework and replace it with an UUID mechanism +* This will simplify contributions of community members and increase compatibility of their modules + +**CONS** +* For some entity tables, we will have unnecessary changes in CE + + From 85c4f890fd7517e29208fc05c941ba5ed88a3fdf Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 31 Mar 2020 12:08:03 -0500 Subject: [PATCH 014/479] review changes --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index ce856dd26..cf07890e5 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -36,7 +36,7 @@ type Mutation { id: ID! items: [ID!] ): CompareList - assignCompareListToCustomer(customerId: ID!, listId: ID!): Boolean + assignCompareListToActiveCustomer(listId: ID!): Boolean } schema { From 22c3327edb76cce935391a5d620bb34086bee3b7 Mon Sep 17 00:00:00 2001 From: Alex Taranovsky Date: Sat, 4 Apr 2020 13:59:28 +0300 Subject: [PATCH 015/479] magento/architecture#: GraphQl. Add a mutation for subscribe feature --- .../coverage/SubscribeEmailToNewsletter.graphqls | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls diff --git a/design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls b/design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls new file mode 100644 index 000000000..c8bb2de5b --- /dev/null +++ b/design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls @@ -0,0 +1,14 @@ +type Mutation { + subscribeEmailToNewsletter(email: String!): SubscribeEmailToNewsletterOutput @doc(description:"Adds an email into a newsletter subscription") +} + +type SubscribeEmailToNewsletterOutput { + status: SubscriptionStatusesEnum @doc(description: "Returns a status of subscription") +} + +enum SubscriptionStatusesEnum { + NOT_ACTIVE + SUBSCRIBED + UNSUBSCRIBED + UNCONFIRMED +} From 840ae8bc59d2a0b1ad3eea83ea8d1c4d452f328c Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Thu, 9 Apr 2020 16:36:21 -0500 Subject: [PATCH 016/479] Initial commit --- .../cache/non-blocking-stale-cache.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 design-documents/cache/non-blocking-stale-cache.md diff --git a/design-documents/cache/non-blocking-stale-cache.md b/design-documents/cache/non-blocking-stale-cache.md new file mode 100644 index 000000000..76a50ee07 --- /dev/null +++ b/design-documents/cache/non-blocking-stale-cache.md @@ -0,0 +1,59 @@ +# Non blocking cache writing mechanism + +### Terms + +* Stale cache - the previous version of cache that customer will receive until a new version will be written in cache. +* Block cache - Magento cache type, typically consist of cached html output. +* Config cache - Magento cache type, usually consist of cached config data. +* Cache revalidation - process of cache invalidation and writing fresh one in cache storage. +* Lookup for lock, lookup timeout - time that takes to recheck if new version of cache already written. +* stale-while-revalidate - mechanism that send old cache while new one is on the generation phase. + +### Overview + +Currently, we have two cache types(Block and Config) that uses lock mechanism to avoid parallel cache generation and excessive resource utilization. +So every time we want to generate and write a new cache, we acquire a lock and parallel process wait until lock released. +When the lock is released, customer get fresh data from storage. + +We usually think that trade-off with lock waiting is acceptable from the performance side. +But the larger amount of Blocks or Cache merchant has, the more time he will wait in locks. +In some scenarios, we could wait **numbers of keys** * **lookup timeout** amount of time in parallel process. +We noticed that in some rare cases merchant can have hundreds keys in Block/Config cache, so even small lookup timeout for lock may cost seconds. + +We have couple public issue that shows possible pointed limitations of this approach - https://github.com/magento/magento2/issues/22824 https://github.com/magento/magento2/issues/22504 + +[IvanChepurnyi](https://github.com/IvanChepurnyi) proposed to use non-locking way of cache generation a.k.a. *stale-while-revalidate*. +The purpose of this PR is to wrap all things up, determine all A.C., discuss approach with community and deliver solid solution. + +### Design + +This approach is well known and already used in some popular libraries, i.e. [Varnish](https://info.varnish-software.com/blog/grace-varnish-4-stale-while-revalidate-semantics-varnish). +Basically, we will send stale cache while we generating a new one. That will free us from needs to wait until any locks will be released, except the case when we have completely empty cache storage. + +To do that, we need to extend current Magento\Framework\Cache\LockGuardedCacheLoader that can revalidate the cache only in blocking way. +Also we should not rewrite business logic of Config and Block cache, but rather implement solid separate API/SPI. + +To keep efficiency, we still should use locking mechanism when we have empty cache storage to write a very first version of cache. + +#### Acceptance Criteria Fulfillment + +1. New code should be compatible with current DOD https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing_dod.html + 1. New functionality should be separated from business logic. + 1. Functional Backward Compatibility. + Feature should be optional and disabled by default. Since cache freshness of blocks and config data may be critical for our merchants, we should introduce a new config variable that will enable/disable the feature. It should be off by default. +1. Stale data should have TTL. +Since we don't want to have a constant copy of the stale cache and it is not designed to exist for a long time, I propose to have up to 10 minutes of TTL. +TTL should be added when we save stale data. +1. Efficiency - means no parallel cache generating/writing process either with stale cache or a fresh one. +Basically, only one process should generate or write cache. +1. We keep only one(last) copy of the stale cache over time. + +### Prototype or Proof of Concept + +PR with working functionality for config cache - https://github.com/magento/magento2/pull/22829 +PR needs to be reworked to fulfill the acceptance criteria. + +### Data size and Performance Requirements + +1. At least the same efficiency when we don't have any cache. +1. Better efficiency with stale cache in comparison to lock waiting. From aa4e675a07f1af09c4b45c1731a4b1e6fba4e81e Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Thu, 9 Apr 2020 16:38:09 -0500 Subject: [PATCH 017/479] initial PR linked --- design-documents/cache/non-blocking-stale-cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/cache/non-blocking-stale-cache.md b/design-documents/cache/non-blocking-stale-cache.md index 76a50ee07..febfd32e4 100644 --- a/design-documents/cache/non-blocking-stale-cache.md +++ b/design-documents/cache/non-blocking-stale-cache.md @@ -22,7 +22,7 @@ We noticed that in some rare cases merchant can have hundreds keys in Block/Conf We have couple public issue that shows possible pointed limitations of this approach - https://github.com/magento/magento2/issues/22824 https://github.com/magento/magento2/issues/22504 -[IvanChepurnyi](https://github.com/IvanChepurnyi) proposed to use non-locking way of cache generation a.k.a. *stale-while-revalidate*. +[IvanChepurnyi](https://github.com/IvanChepurnyi) in his [PR](https://github.com/magento/magento2/pull/22829) - proposed to use non-locking way of cache generation a.k.a. *stale-while-revalidate*. The purpose of this PR is to wrap all things up, determine all A.C., discuss approach with community and deliver solid solution. ### Design From 78ce834bf803cd5ab0b4f2d8d734232150a7b73e Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Thu, 9 Apr 2020 16:45:28 -0500 Subject: [PATCH 018/479] AC updates --- design-documents/cache/non-blocking-stale-cache.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/design-documents/cache/non-blocking-stale-cache.md b/design-documents/cache/non-blocking-stale-cache.md index febfd32e4..282d9e999 100644 --- a/design-documents/cache/non-blocking-stale-cache.md +++ b/design-documents/cache/non-blocking-stale-cache.md @@ -8,6 +8,7 @@ * Cache revalidation - process of cache invalidation and writing fresh one in cache storage. * Lookup for lock, lookup timeout - time that takes to recheck if new version of cache already written. * stale-while-revalidate - mechanism that send old cache while new one is on the generation phase. +* FPC - full page cache. ### Overview @@ -38,15 +39,18 @@ To keep efficiency, we still should use locking mechanism when we have empty ca #### Acceptance Criteria Fulfillment 1. New code should be compatible with current DOD https://devdocs.magento.com/guides/v2.3/contributor-guide/contributing_dod.html - 1. New functionality should be separated from business logic. + 1. Orchestration of cache manipulation(load/save logic) should be separated from business logic of specific class, i.e. Magento\Config\App\Config\Type\System. 1. Functional Backward Compatibility. - Feature should be optional and disabled by default. Since cache freshness of blocks and config data may be critical for our merchants, we should introduce a new config variable that will enable/disable the feature. It should be off by default. + Feature should be optional and disabled by default. Since cache freshness of blocks and config data may be critical for our merchants, we should introduce a new config variable that will enable/disable the feature. + It should be disabled by default. 1. Stale data should have TTL. Since we don't want to have a constant copy of the stale cache and it is not designed to exist for a long time, I propose to have up to 10 minutes of TTL. TTL should be added when we save stale data. 1. Efficiency - means no parallel cache generating/writing process either with stale cache or a fresh one. Basically, only one process should generate or write cache. -1. We keep only one(last) copy of the stale cache over time. +1. We keep only one(last) copy of the stale cache over time. +1. If customer decide to enable stale cache functionality it should be used for all cache types that uses locking mechanism. +1. While we send stale data, Block and FPC should disabled to prevent stale data to be cached as main. ### Prototype or Proof of Concept From ac246a95f03be05c42ff5fa597b46509f66f5e9c Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 9 Apr 2020 16:51:18 -0500 Subject: [PATCH 019/479] ECP-469: Finalize new search schema --- design-documents/graph-ql/coverage/search.md | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 design-documents/graph-ql/coverage/search.md diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md new file mode 100644 index 000000000..0cd039061 --- /dev/null +++ b/design-documents/graph-ql/coverage/search.md @@ -0,0 +1,62 @@ +# Queries + +```graphql +type Query { + multiSearch(phrase: String!, productSize: Int = 10): MultiSearchResponse! + #Filter supports multiple clauses which will be wrapped in logical AND operator + productSearch(phrase: String!, filter: [Clause], sort: ProductSearchSortInput): ProductSearchResponse! +} + +input ProductSearchSortInput +{ + relevance: SortEnum + position: SortEnum + name: SortEnum +} + +input Clause { + attribute: String! + in: [String] + eq: String +} + +type ProductSearchResponse { + items: [ProductSearchItem] + hits: Int! + facets: [Aggregation] + facetsValues: [Aggregation] +} + +type MultiSearchResponse { + products: ProductSearchResponse +} + +type ProductSearchItem { + product: ProductInterface! + highlights: [Highlight] +} + +type Highlight { + attribute: String! + value: String! + matchedWords: [String]! +} + +interface Aggregation { + name: String! + buckets: [Bucket]! +} + +interface Bucket { + count: Int! +} + +type ScalarBucket implements Bucket { + title: String! +} + +type RangeBucket implements Bucket { + from: Float! + to: Float! +} +``` \ No newline at end of file From ed8109c5416f0075aa9084e0df1a621f2ea06e9d Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 9 Apr 2020 18:11:18 -0500 Subject: [PATCH 020/479] ECP-469: Finalize new search schema - Added stats bucket & range filter --- design-documents/graph-ql/coverage/search.md | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 0cd039061..352c74633 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -4,7 +4,7 @@ type Query { multiSearch(phrase: String!, productSize: Int = 10): MultiSearchResponse! #Filter supports multiple clauses which will be wrapped in logical AND operator - productSearch(phrase: String!, filter: [Clause], sort: ProductSearchSortInput): ProductSearchResponse! + productSearch(phrase: String!, filter: [SearchClauseInput], sort: ProductSearchSortInput): ProductSearchResponse! } input ProductSearchSortInput @@ -14,10 +14,17 @@ input ProductSearchSortInput name: SortEnum } -input Clause { +# If from or to fields are omitted, $gte or $lte filter will be applied +type SearchRangeInput { + from: Int + to: Int +} + +input SearchClauseInput { attribute: String! in: [String] eq: String + range: SearchRangeInput } type ProductSearchResponse { @@ -42,21 +49,31 @@ type Highlight { matchedWords: [String]! } -interface Aggregation { - name: String! - buckets: [Bucket]! +interface Bucket { + #Human readable bucket title + title: String! } -interface Bucket { - count: Int! +type StatsBucket implements Bucket { + #Could be used for filtering and may contain non-human readable data + id: ID! + min: Int! + max: Int! } type ScalarBucket implements Bucket { - title: String! + id: ID! + count: Int! } type RangeBucket implements Bucket { from: Float! to: Float! + count: Int! +} + +interface Aggregation { + name: String! + buckets: [Bucket]! } ``` \ No newline at end of file From c1171ba37d6b23765772b56db33695597abd1bbf Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Fri, 10 Apr 2020 10:43:13 -0500 Subject: [PATCH 021/479] ECP-469: Finalize new search schema - fixed range type to input - added support for custom sorting --- design-documents/graph-ql/coverage/search.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 352c74633..ed6920e57 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -4,18 +4,17 @@ type Query { multiSearch(phrase: String!, productSize: Int = 10): MultiSearchResponse! #Filter supports multiple clauses which will be wrapped in logical AND operator - productSearch(phrase: String!, filter: [SearchClauseInput], sort: ProductSearchSortInput): ProductSearchResponse! + productSearch(phrase: String!, filter: [SearchClauseInput], sort: [ProductSearchSortInput]): ProductSearchResponse! } input ProductSearchSortInput { - relevance: SortEnum - position: SortEnum - name: SortEnum + attribute: String! + direction: SortEnum } # If from or to fields are omitted, $gte or $lte filter will be applied -type SearchRangeInput { +input SearchRangeInput { from: Int to: Int } From 3aa85676db2d8713c5ecb85925b3ba852102df68 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Fri, 10 Apr 2020 10:45:13 -0500 Subject: [PATCH 022/479] ECP-469: Finalize new search schema - search direction is mandatory --- design-documents/graph-ql/coverage/search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index ed6920e57..64806e0d5 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -10,7 +10,7 @@ type Query { input ProductSearchSortInput { attribute: String! - direction: SortEnum + direction: SortEnum! } # If from or to fields are omitted, $gte or $lte filter will be applied From 1ca6590ce403bea4f1808058705b06410bf1b126 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Fri, 10 Apr 2020 11:09:49 -0500 Subject: [PATCH 023/479] ECP-469: Finalize new search schema - Added suggestions --- design-documents/graph-ql/coverage/search.md | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 64806e0d5..4f15016fc 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -35,6 +35,7 @@ type ProductSearchResponse { type MultiSearchResponse { products: ProductSearchResponse + suggestions: [String] } type ProductSearchItem { From 8456844289feb86153315578818b3f16e895e6c9 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Mon, 13 Apr 2020 10:03:46 -0500 Subject: [PATCH 024/479] ECP-469: Finalize new search schema - Added attribute name to aggregations --- design-documents/graph-ql/coverage/search.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 4f15016fc..c7d86e7fb 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -52,16 +52,16 @@ type Highlight { interface Bucket { #Human readable bucket title title: String! + attribute: String! } type StatsBucket implements Bucket { - #Could be used for filtering and may contain non-human readable data - id: ID! min: Int! max: Int! } type ScalarBucket implements Bucket { + #Could be used for filtering and may contain non-human readable data id: ID! count: Int! } From dc48ac301e412a44811a57aa6ac8e79d93e92a91 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Mon, 13 Apr 2020 13:57:41 -0500 Subject: [PATCH 025/479] ECP-469: Finalize new search schema - Fixed underscores --- design-documents/graph-ql/coverage/search.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index c7d86e7fb..5f5cc6111 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -30,7 +30,7 @@ type ProductSearchResponse { items: [ProductSearchItem] hits: Int! facets: [Aggregation] - facetsValues: [Aggregation] + facets_values: [Aggregation] } type MultiSearchResponse { @@ -46,7 +46,7 @@ type ProductSearchItem { type Highlight { attribute: String! value: String! - matchedWords: [String]! + matched_words: [String]! } interface Bucket { From 113b8e251daf6490ccdc0aa47f8a25bb19a69d46 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 15 Apr 2020 09:39:18 -0500 Subject: [PATCH 026/479] ECP-469: Finalize new search schema - Removed attribute field duplicate from aggregations --- design-documents/graph-ql/coverage/search.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 5f5cc6111..fc1f0f890 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -52,7 +52,6 @@ type Highlight { interface Bucket { #Human readable bucket title title: String! - attribute: String! } type StatsBucket implements Bucket { @@ -73,7 +72,7 @@ type RangeBucket implements Bucket { } interface Aggregation { - name: String! + attribute_code: String! buckets: [Bucket]! } ``` \ No newline at end of file From 89f26e8da10dbeee39b6e23b76b8531deb8c34b6 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 15 Apr 2020 13:22:51 -0500 Subject: [PATCH 027/479] ECP-469: Finalize new search schema - Various fixes --- design-documents/graph-ql/coverage/search.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index fc1f0f890..d031956d6 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -4,7 +4,14 @@ type Query { multiSearch(phrase: String!, productSize: Int = 10): MultiSearchResponse! #Filter supports multiple clauses which will be wrapped in logical AND operator - productSearch(phrase: String!, filter: [SearchClauseInput], sort: [ProductSearchSortInput]): ProductSearchResponse! + productSearch( + phrase: String!, + "Desired size of the search result page" + pageSize: Int = 20, + currentPage: Int = 1, + filter: [SearchClauseInput], + sort: [ProductSearchSortInput] + ): ProductSearchResponse! } input ProductSearchSortInput @@ -15,12 +22,12 @@ input ProductSearchSortInput # If from or to fields are omitted, $gte or $lte filter will be applied input SearchRangeInput { - from: Int - to: Int + from: Float + to: Float } input SearchClauseInput { - attribute: String! + attribute_code: String! in: [String] eq: String range: SearchRangeInput From e08596113e28e740a949ea109a9ed563e33bf929 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 15 Apr 2020 14:16:11 -0500 Subject: [PATCH 028/479] ECP-469: Finalize new search schema - Added suggestions --- design-documents/graph-ql/coverage/search.md | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index d031956d6..42b090608 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -38,6 +38,7 @@ type ProductSearchResponse { hits: Int! facets: [Aggregation] facets_values: [Aggregation] + suggestions: [String] } type MultiSearchResponse { From 5d77acfeb7a3387c6858e2058fe766a6e0c2b5f6 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 15 Apr 2020 14:29:29 -0500 Subject: [PATCH 029/479] ECP-469: Finalize new search schema - Various fixes --- design-documents/graph-ql/coverage/search.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 42b090608..28316b0b5 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -35,10 +35,12 @@ input SearchClauseInput { type ProductSearchResponse { items: [ProductSearchItem] - hits: Int! facets: [Aggregation] facets_values: [Aggregation] suggestions: [String] + related_terms: [String] + page_info: SearchResultPageInfo + total_count: Int } type MultiSearchResponse { @@ -63,8 +65,8 @@ interface Bucket { } type StatsBucket implements Bucket { - min: Int! - max: Int! + min: Float! + max: Float! } type ScalarBucket implements Bucket { From 0eaeb27c0defc0665dc46d773c0b0b96ee812f96 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 24 Apr 2020 13:37:29 -0500 Subject: [PATCH 030/479] Remove non-nullable in many types in graph-ql/coverage/company.md --- design-documents/graph-ql/coverage/company.md | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index b349a8fd9..af4cd1389 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -15,13 +15,13 @@ type Query { type Company @doc(description: "Company entity output data schema.") { id: ID! @doc(description: "Company id.") - name: String! @doc(description: "Company name.") - email: String! @doc(description: "Company email address.") + name: String @doc(description: "Company name.") + email: String @doc(description: "Company email address.") legal_name: String @doc(description: "Company legal name.") vat_id: String @doc(description: "Company VAT/TAX id.") reseller_id: String @doc(description: "Company re-seller id.") - legal_address: CompanyLegalAddress! @doc(description: "Company legal address.") - company_admin: Customer! @doc(description: "An object containing information about Company Administrator.") + legal_address: CompanyLegalAddress @doc(description: "Company legal address.") + company_admin: Customer @doc(description: "An object containing information about Company Administrator.") sales_representative: CompanySalesRepresentative @doc(description: "Company sales representative.") payment_methods: [String] @doc(description: "List of payment methods available for a Company.") users( @@ -35,33 +35,33 @@ type Company @doc(description: "Company entity output data schema.") { currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): CompanyRoles! @doc(description: "Returns the list of defined roles at Company.") role(id: ID!): CompanyRole @doc(description: "Returns company role by id.") - acl_resources: [CompanyAclResource]! @doc(description: "Returns the list of all permission resources.") - hierarchy: CompanyHierarchyOutput! @doc(description: "Returns the complete data about company structure.") + acl_resources: [CompanyAclResource] @doc(description: "Returns the list of all permission resources.") + hierarchy: CompanyHierarchyOutput @doc(description: "Returns the complete data about company structure.") team(id: ID!): CompanyTeam @doc(description: "Returns company team data by id.") } type CompanyLegalAddress @doc(description: "Company legal address output data schema.") { - street: [String]! @doc(description: "An array of strings that defines the Company's street address.") - city: String! @doc(description: "City name.") - region: CustomerAddressRegion! @doc(description: "An object containing region data for the Company.") - country_code: CountryCodeEnum! @doc(description: "Country code.") - postcode: String! @doc(description: "ZIP/postal code.") - telephone: String! @doc(description: "Company's phone number.") + street: [String] @doc(description: "An array of strings that defines the Company's street address.") + city: String @doc(description: "City name.") + region: CustomerAddressRegion @doc(description: "An object containing region data for the Company.") + country_code: CountryCodeEnum @doc(description: "Country code.") + postcode: String @doc(description: "ZIP/postal code.") + telephone: String @doc(description: "Company's phone number.") } type CompanyAdmin @doc(description: "Company Administrator (Customer with corresponding privileges) output data schema.") { id: ID! @doc(description: "Company Administrator's id.") - email: String! @doc(description: "Company Administrator email address.") - firstname: String! @doc(description: "Company Administrator first name.") - lastname: String! @doc(description: "Company Administrator last name.") + email: String @doc(description: "Company Administrator email address.") + firstname: String @doc(description: "Company Administrator first name.") + lastname: String @doc(description: "Company Administrator last name.") job_title: String @doc(description: "Company Administrator job title.") gender: Int @doc(description: "Company Administrator gender.") } type CompanySalesRepresentative @doc(description: "Company sales representative information output data schema.") { - email: String! @doc(description: "Sales representative email address.") - firstname: String! @doc(description: "Sales representative first name.") - lastname: String! @doc(description: "Sales representative last name.") + email: String @doc(description: "Sales representative email address.") + firstname: String @doc(description: "Sales representative first name.") + lastname: String @doc(description: "Sales representative last name.") } type CompanyUsers @doc(description: "Output data schema for an object returned by a Company users search query.") { @@ -78,15 +78,15 @@ type CompanyRoles @doc(description: "Output data schema for an object returned b type CompanyRole @doc(description: "Company role output data schema returned in response to a query by Role id.") { id: ID! @doc(description: "Role id.") - name: String! @doc(description: "Role name.") + name: String @doc(description: "Role name.") users_count: Int @doc(description: "Total number of Users with such Role within Company Hierarchy.") permissions: [String] @doc(description: "A list of permission resources defined for a Role.") } type CompanyAclResource @doc(description: "Output data schema for an object with Role permission resource information.") { id: ID! @doc(description: "ACL resource id.") - text: String! @doc(description: "ACL resource label.") - sortOrder: Int! @doc(description: "ACL resource sort order.") + text: String @doc(description: "ACL resource label.") + sortOrder: Int @doc(description: "ACL resource sort order.") children: [CompanyAclResource!] @doc(description: "An array of sub-resources.") } @@ -114,16 +114,16 @@ type CompanyHierarchyOutput @doc(description: "Response object schema for a Comp type CompanyHierarchyElement @doc(description: "Company Hierarchy element output data schema.") { id: ID! @doc(description: "Hierarchy element id.") - tree_id: ID! @doc(description: "The hierarchical id of the element within a structure. Used for changing element's position in hierarchy.") - type: String! @doc(description: "Hierarchy element type: 'customer' or a 'team'.") - text: String! @doc(description: "Hierarchy element name.") + tree_id: ID @doc(description: "The hierarchical id of the element within a structure. Used for changing element's position in hierarchy.") + type: String @doc(description: "Hierarchy element type: 'customer' or a 'team'.") + text: String @doc(description: "Hierarchy element name.") description: String @doc(description: "Hierarchy element description.") children: [CompanyHierarchyElement!] @doc(description: "An array of child elements.") } type CompanyTeam @doc(description: "Company Team entity output data schema.") { id: ID! @doc(description: "Team id.") - name: String! @doc(description: "Team name.") + name: String @doc(description: "Team name.") description: String @doc(description: "Team description.") } @@ -304,10 +304,10 @@ input CompanyTeamUpdateInput @doc(description: "Defines the input data schema fo ```graphql type Customer { - job_title: String! @doc(description: "Company User job title.") - role: CompanyRole! @doc(description: "Company User role data (includes permissions).") - team: CompanyTeam! @doc(description: "Company User team data.") - telephone: String! @doc(description: "Company User phone number.") - status: CompanyUserStatusEnum! @doc(description: "Company User status.") + job_title: String @doc(description: "Company User job title.") + role: CompanyRole @doc(description: "Company User role data (includes permissions).") + team: CompanyTeam @doc(description: "Company User team data.") + telephone: String @doc(description: "Company User phone number.") + status: CompanyUserStatusEnum @doc(description: "Company User status.") } ``` \ No newline at end of file From 9d3fa57f8473fb6ed0b1d27c7e741558a8087424 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Fri, 24 Apr 2020 16:41:21 -0500 Subject: [PATCH 031/479] ECP-469: Finalize new search schema - Removed multi-search --- design-documents/graph-ql/coverage/search.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md index 28316b0b5..8b0e6c325 100644 --- a/design-documents/graph-ql/coverage/search.md +++ b/design-documents/graph-ql/coverage/search.md @@ -2,7 +2,6 @@ ```graphql type Query { - multiSearch(phrase: String!, productSize: Int = 10): MultiSearchResponse! #Filter supports multiple clauses which will be wrapped in logical AND operator productSearch( phrase: String!, @@ -43,11 +42,6 @@ type ProductSearchResponse { total_count: Int } -type MultiSearchResponse { - products: ProductSearchResponse - suggestions: [String] -} - type ProductSearchItem { product: ProductInterface! highlights: [Highlight] From 4a6d693ff9c68f2c54119f22e549b7e47c5c1b9c Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Tue, 28 Apr 2020 17:45:23 -0500 Subject: [PATCH 032/479] AC updates --- .../cache/jmeter-test/clean-cache-test.jmx | 952 ++++++++++++++++++ .../cache/non-blocking-stale-cache.md | 7 +- 2 files changed, 958 insertions(+), 1 deletion(-) create mode 100644 design-documents/cache/jmeter-test/clean-cache-test.jmx diff --git a/design-documents/cache/jmeter-test/clean-cache-test.jmx b/design-documents/cache/jmeter-test/clean-cache-test.jmx new file mode 100644 index 000000000..83ab4eb25 --- /dev/null +++ b/design-documents/cache/jmeter-test/clean-cache-test.jmx @@ -0,0 +1,952 @@ + + + + + + false + true + false + + + + host + ${__P(host,magento.test)} + = + + + request_protocol + ${__P(request_protocol,http)} + = + + + admin_path + ${__P(admin_path,admin)} + = + + + admin_password + ${__P(admin_password,123123q)} + = + + + admin_user + ${__P(admin_user,admin)} + = + + + base_path + ${__P(base_path,/)} + = + + + form_key + ${__P(form_key,uVEW54r8kKday8Wk)} + = + + + admin_users_distribution_per_admin_pool + ${__P(admin_users_distribution_per_admin_pool,1)} + = + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 10000 + false + + + + + false + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + + + + + false + false + + + + + + + ${host} + + 60000 + 200000 + ${request_protocol} + utf-8 + + Java + mpaf/tool/fragments/ce/http_request_default.jmx + 4 + + + + + + Accept-Language + en-US,en;q=0.5 + + + Accept + application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + + + User-Agent + Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 + + + Accept-Encoding + gzip, deflate + + + mpaf/tool/fragments/ce/http_header_manager.jmx + + + + stoptest + + false + 1 + + 1 + 1 + 1384333221000 + 1384333221000 + false + + + + + + + + 30 + ${host} + / + false + 0 + true + true + + + ${form_key} + ${host} + ${base_path} + false + 0 + true + true + + + true + + + + +props.remove("category_url_key"); +props.remove("category_url_keys_list"); +props.remove("category_name"); +props.remove("category_names_list"); +props.remove("simple_products_list"); +props.remove("simple_products_list_for_edit"); +props.remove("configurable_products_list"); +props.remove("configurable_products_list_for_edit"); +props.remove("users"); +props.remove("customer_emails_list"); +props.remove("categories"); +props.remove("cms_pages"); +props.remove("cms_blocks"); +props.remove("coupon_codes"); + +/* This is only used when admin is enabled. */ +props.put("activeAdminThread", ""); + +/* Set the environment - at this time '01' or '02' */ +String path = "${host}"; +String environment = path.substring(4, 6); +props.put("environment", environment); + + + false + + + + Boolean stopTestOnError (String error) { + log.error(error); + System.out.println(error); + SampleResult.setStopTest(true); + return false; +} + +if ("${host}" == "1") { + return stopTestOnError("\"host\" parameter is not defined. Please define host parameter as: \"-Jhost=example.com\""); +} + +String path = "${base_path}"; +String slash = "/"; +if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substring(0, 1))) { + return stopTestOnError("\"base_path\" parameter is invalid. It must start and end with \"/\""); +} + + + + false + + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path} + GET + true + false + true + false + false + + + + + + Welcome + <title>Magento Admin</title> + + Assertion.response_data + false + 2 + + + + false + admin_form_key + <input name="form_key" type="hidden" value="([^'"]+)" /> + $1$ + + 1 + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_form_key + + + + + + + + true + + = + true + dummy + + + true + ${admin_form_key} + = + true + form_key + + + true + ${admin_password} + = + true + login[password] + + + true + ${admin_user} + = + true + login[username] + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/dashboard/ + POST + true + false + true + false + Java + false + + + + + + <title>Dashboard / Magento Admin</title> + + Assertion.response_data + false + 2 + + + + false + admin_form_key + <input name="form_key" type="hidden" value="([^'"]+)" /> + $1$ + + 1 + + + + + + + + + + + false + ${admin_form_key} + = + true + form_key + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/user/roleGrid/limit/200/?ajax=true&isAjax=true + POST + true + false + true + false + false + + + + + + + false + import java.util.regex.Pattern; + import java.util.regex.Matcher; + import java.util.LinkedList; + + LinkedList adminUserList = new LinkedList(); + String response = new String(data); + Pattern pattern = Pattern.compile("<td\\W*?data-column=.username[^>]*?>\\W*?(\\w+)\\W*?<"); + Matcher matcher = pattern.matcher(response); + + while (matcher.find()) { + adminUserList.add(matcher.group(1)); + } + +// adminUserList.poll(); + props.put("adminUserList", adminUserList); + props.put("adminUserListIterator", adminUserList.descendingIterator()); + + + + + + + + + continue + 100 + 20 + 20 + 2 + + + M + + + + + + + ${host} + + + + ${request_protocol} + + /contact/ + GET + true + false + true + false + false + + + + + true + + + + false + {"query":"query resolveUrl($urlKey: String!) {\n urlResolver(url: $urlKey) {\n type\n id\n }\n}","variables":{"urlKey":"/"},"operationName":"resolveUrl"} + = + + + + ${host} + + 60000 + 200000 + ${request_protocol} + + /graphql + POST + true + false + true + false + false + + + + + + + Content-Type + application/json + + + Accept + */* + + + + + + + + continue + + false + -1 + + 1 + 1 + 1505803944000 + 1505803944000 + false + + + + + + true + -1 + + + + 10000 + + + + + function getFormKeyFromResponse() + { + var url = prev.getUrlAsString(), + responseCode = prev.getResponseCode(), + formKey = null; + searchPattern = /var FORM_KEY = '(.+)'/; + if (responseCode == "200" && url) { + response = prev.getResponseDataAsString(); + formKey = response && response.match(searchPattern) ? response.match(searchPattern)[1] : null; + } + return formKey; + } + + formKey = vars.get("form_key_storage"); + + currentFormKey = getFormKeyFromResponse(); + + if (currentFormKey != null && currentFormKey != formKey) { + vars.put("form_key_storage", currentFormKey); + } + + javascript + + + + + + + + formKey = vars.get("form_key_storage"); + if (formKey + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' + && sampler.getMethod() == "POST") + { + arguments = sampler.getArguments(); + for (i=0; i<arguments.getArgumentCount(); i++) + { + argument = arguments.getArgument(i); + if (argument.getName() == 'form_key' && argument.getValue() != formKey) { + log.info("admin form key updated: " + argument.getValue() + " => " + formKey); + argument.setValue(formKey); + } + } + } + + javascript + + + + + + + + false + + + + + + get-admin-email + + + + +adminUserList = props.get("adminUserList"); +adminUserListIterator = props.get("adminUserListIterator"); +adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); + +if (adminUsersDistribution == 1) { + adminUser = adminUserList.poll(); +} else { + if (!adminUserListIterator.hasNext()) { + adminUserListIterator = adminUserList.descendingIterator(); + } + + adminUser = adminUserListIterator.next(); +} + +if (adminUser == null) { + SampleResult.setResponseMessage("adminUser list is empty"); + SampleResult.setResponseData("adminUser list is empty","UTF-8"); + IsSuccess=false; + SampleResult.setSuccessful(false); + SampleResult.setStopThread(true); +} +vars.put("admin_user", adminUser); + + + + true + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/ + GET + true + false + true + false + false + + + + + + Welcome + <title>Magento Admin</title> + + Assertion.response_data + false + 2 + + + + false + admin_form_key + <input name="form_key" type="hidden" value="([^'"]+)" /> + $1$ + + 1 + + + + + ^.+$ + + Assertion.response_data + false + 1 + variable + admin_form_key + + + + + + + + true + + = + true + dummy + + + true + ${admin_form_key} + = + true + form_key + + + true + ${admin_password} + = + true + login[password] + + + true + ${admin_user} + = + true + login[username] + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/dashboard/ + POST + true + false + true + false + Java + false + + + + + false + admin_form_key + <input name="form_key" type="hidden" value="([^'"]+)" /> + $1$ + + 1 + + + + + + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/cache/flushSystem + GET + true + false + true + false + false + + + + + + The Magento cache storage has been flushed + + Assertion.response_data + false + 2 + + + + + + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/auth/logout/ + GET + true + false + true + false + false + + + + + false + + + + adminUsersDistribution = Integer.parseInt(vars.get("admin_users_distribution_per_admin_pool")); + if (adminUsersDistribution == 1) { + adminUserList = props.get("adminUserList"); + adminUserList.add(vars.get("admin_user")); + } + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 20 + true + + + + + false + false + + + + + diff --git a/design-documents/cache/non-blocking-stale-cache.md b/design-documents/cache/non-blocking-stale-cache.md index 282d9e999..1d534e1e2 100644 --- a/design-documents/cache/non-blocking-stale-cache.md +++ b/design-documents/cache/non-blocking-stale-cache.md @@ -42,7 +42,6 @@ To keep efficiency, we still should use locking mechanism when we have empty ca 1. Orchestration of cache manipulation(load/save logic) should be separated from business logic of specific class, i.e. Magento\Config\App\Config\Type\System. 1. Functional Backward Compatibility. Feature should be optional and disabled by default. Since cache freshness of blocks and config data may be critical for our merchants, we should introduce a new config variable that will enable/disable the feature. - It should be disabled by default. 1. Stale data should have TTL. Since we don't want to have a constant copy of the stale cache and it is not designed to exist for a long time, I propose to have up to 10 minutes of TTL. TTL should be added when we save stale data. @@ -61,3 +60,9 @@ PR needs to be reworked to fulfill the acceptance criteria. 1. At least the same efficiency when we don't have any cache. 1. Better efficiency with stale cache in comparison to lock waiting. + +Performance test is added - [jmeter-test](jmeter-test/clean-cache-test.jmx). +To let this work please: +1. Enable account sharing +2. Disable secret keys in url +3. Use web server url rewrite From 44549404074198f293ec24549b6fb6d62f923640 Mon Sep 17 00:00:00 2001 From: Vitalii Zabaznov Date: Thu, 30 Apr 2020 18:31:35 -0500 Subject: [PATCH 033/479] perf test updates --- .../cache/jmeter-test/clean-cache-test.jmx | 105 +++++++++++------- 1 file changed, 62 insertions(+), 43 deletions(-) diff --git a/design-documents/cache/jmeter-test/clean-cache-test.jmx b/design-documents/cache/jmeter-test/clean-cache-test.jmx index 83ab4eb25..3231c4ecf 100644 --- a/design-documents/cache/jmeter-test/clean-cache-test.jmx +++ b/design-documents/cache/jmeter-test/clean-cache-test.jmx @@ -48,12 +48,37 @@ ${__P(admin_users_distribution_per_admin_pool,1)} = + + flush_cache_timer_milliseconds + ${__P(flush_cache_timer_milliseconds,20000)} + = + + + target_concurrency + ${__P(target_concurrency,100)} + = + + + ramp_up_time_min + ${__P(ramp_up_time_min,10)} + = + + + rump_up_steps + ${__P(rump_up_steps,20)} + = + + + hold_target_time_minutes + ${__P(hold_target_time_minutes,10)} + = + - + false saveConfig @@ -528,10 +553,10 @@ if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substr continue - 100 - 20 - 20 - 2 + ${target_concurrency} + ${ramp_up_time_min} + ${rump_up_steps} + ${hold_target_time_minutes} M @@ -620,7 +645,7 @@ if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substr - 10000 + ${flush_cache_timer_milliseconds} @@ -681,7 +706,7 @@ if (!slash.equals(path.substring(path.length() -1)) || !slash.equals(path.substr false - + get-admin-email @@ -718,7 +743,7 @@ vars.put("admin_user", adminUser); - + @@ -769,7 +794,7 @@ vars.put("admin_user", adminUser); - + @@ -829,41 +854,35 @@ vars.put("admin_user", adminUser); - - - - + + + + + + + 60000 + 200000 + ${request_protocol} + + ${base_path}${admin_path}/admin/cache/flushSystem + GET + true + false + true + false + false + + - - - - - - - 60000 - 200000 - ${request_protocol} - - ${base_path}${admin_path}/admin/cache/flushSystem - GET - true - false - true - false - false - - - - - - The Magento cache storage has been flushed - - Assertion.response_data - false - 2 - - - + + + The Magento cache storage has been flushed + + Assertion.response_data + false + 2 + + From 967e2b86084f6931dc9282e85b2524297f814191 Mon Sep 17 00:00:00 2001 From: Yevhen Sentiabov Date: Mon, 4 May 2020 11:45:52 -0500 Subject: [PATCH 034/479] ECP-425: Customer Stored Payments schema - Extended input types to support Vault for PayPal Payflow Pro --- .../graph-ql/coverage/payflowpro-vault.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 design-documents/graph-ql/coverage/payflowpro-vault.md diff --git a/design-documents/graph-ql/coverage/payflowpro-vault.md b/design-documents/graph-ql/coverage/payflowpro-vault.md new file mode 100644 index 000000000..cefbda45f --- /dev/null +++ b/design-documents/graph-ql/coverage/payflowpro-vault.md @@ -0,0 +1,32 @@ +# Overview + +In the scope of extending the GraphQL coverage, we need to add Vault support to PayPal Payflow Pro payment integration. + +## Mutations + +We need to extend existing `PayflowProInput` by adding a possibility to store a Vault token: + +```graphql +input PayflowProInput @doc(description:"Required input for Payflow Pro and Payments Pro payment methods.") { + cc_details: CreditCardDetailsInput! @doc(description: "Required input for credit card related information") + is_active_payment_token_enabler: Boolean! @doc(description:"States whether an entered by a customer credit/debit card should be tokenized for later usage. Required only if Vault is enabled for PayPal Payflow Pro payment integration.") +} +``` + +The `is_active_payment_token_enabler` would specify that credit card details should be tokenized by a payment gateway, and a payment token can be used for further purchases. + +The next needed modification is extend of the existing `PaymentMethodInput` by adding Vault payment method for PayPal Payflow Pro. + +```graphql +input PaymentMethodInput { + payflowpro_cc_vault: VaultTokenInput +} +``` + +The `VaultInput` provides a generic type for all payment integrations with the Vault support. + +```graphql +input VaultTokenInput @doc(description:"Required input for payment methods with Vault support.") { + public_hash: String! @doc(description: "The public hash of the payment token") +} +``` \ No newline at end of file From bc764ea0806809350485685a9cb96ab645c07f82 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 4 May 2020 17:08:15 -0500 Subject: [PATCH 035/479] Make `id` a required field in B2B GraphQL Company.user query --- design-documents/graph-ql/coverage/company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index af4cd1389..0bacb9a06 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -29,7 +29,7 @@ type Company @doc(description: "Company entity output data schema.") { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): CompanyUsers @doc(description: "Information about the company users.") - user(id: ID): Customer @doc(description: "Returns company user for current authenticated Customer or, if id provided, for specific one.") + user(id: ID!): Customer @doc(description: "Returns company user by id.") roles( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Optional. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), From 34490576d8b8ae23485ed8052d79a06d9d0a237a Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 4 May 2020 17:10:28 -0500 Subject: [PATCH 036/479] Make "Check" types in B2B GraphQL Company schema nullable --- design-documents/graph-ql/coverage/company.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index 0bacb9a06..37c6c8103 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -91,19 +91,19 @@ type CompanyAclResource @doc(description: "Output data schema for an object with } type CompanyRoleNameCheckResponse @doc(description: "Response object schema for a role name validation query.") { - isNameValid: Boolean! @doc(description: "Role name validation result") + isNameValid: Boolean @doc(description: "Role name validation result") } type CompanyUserEmailCheckResponse @doc(description: "Response object schema for a Company User email validation query.") { - isEmailValid: Boolean! @doc(description: "Email validation result") + isEmailValid: Boolean @doc(description: "Email validation result") } type CompanyAdminEmailCheckResponse @doc(description: "Response object schema for a Company Admin email validation query.") { - isEmailValid: Boolean! @doc(description: "Email validation result") + isEmailValid: Boolean @doc(description: "Email validation result") } type CompanyEmailCheckResponse @doc(description: "Response object schema for a Company email validation query.") { - isEmailValid: Boolean! @doc(description: "Email validation result") + isEmailValid: Boolean @doc(description: "Email validation result") } type CompanyHierarchyOutput @doc(description: "Response object schema for a Company Hierarchy query.") { From 9e3e1f4deb285b221ffbfd69f2709fedfb52fa14 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 4 May 2020 17:35:40 -0500 Subject: [PATCH 037/479] update descriptions of root query types in b2b company schema --- design-documents/graph-ql/coverage/company.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index 37c6c8103..1f82712d3 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -6,11 +6,11 @@ ```graphql type Query { - company: Company @doc(description: "Returns all information about the current Company.") - checkCompanyEmail(email: String!): CompanyEmailCheckResponse @doc(description: "Returns result of validation whether provided email address is valid for a new Company registration or not.") - checkCompanyAdminEmail(email: String!): CompanyAdminEmailCheckResponse @doc(description: "Returns result of validation whether provided email address is valid for a Company Administrator registration or not.") - checkCompanyUserEmail(email: String!): CompanyUserEmailCheckResponse @doc(description: "Returns an object with result of validation whether provided email address is valid for a new Customer - Company User - registration or not.") - checkCompanyRoleName(name: String!): CompanyRoleNameCheckResponse @doc(description: "Returns result of validation whether provided Role name is available.") + company: Company @doc(description: "Company assigned to the currently authenticated user") + checkCompanyEmail(email: String!): CompanyEmailCheckResponse @doc(description: "Check if an email is valid for company registration") + checkCompanyAdminEmail(email: String!): CompanyAdminEmailCheckResponse @doc(description: "Check if an email is valid for company admin registration") + checkCompanyUserEmail(email: String!): CompanyUserEmailCheckResponse @doc(description: "Check if an email is valid for company user registration") + checkCompanyRoleName(name: String!): CompanyRoleNameCheckResponse @doc(description: "Check if a role name is valid for company") } type Company @doc(description: "Company entity output data schema.") { From 86435ecd72eef289bafd12fa4665f6f1e2977ee9 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Tue, 5 May 2020 13:47:12 -0500 Subject: [PATCH 038/479] Changes to company querying for company structure --- design-documents/graph-ql/coverage/company.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index af4cd1389..2b93c13ab 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -36,7 +36,10 @@ type Company @doc(description: "Company entity output data schema.") { ): CompanyRoles! @doc(description: "Returns the list of defined roles at Company.") role(id: ID!): CompanyRole @doc(description: "Returns company role by id.") acl_resources: [CompanyAclResource] @doc(description: "Returns the list of all permission resources.") - hierarchy: CompanyHierarchyOutput @doc(description: "Returns the complete data about company structure.") + structure( + rootID: ID = 0 @doc(description: "Tree depth to begin query") + depth: Int = 10 @doc(description: "Specifies how deeply results are fetched") + ): CompanyStructure @doc(description: "Company structure of teams and customers in depth-first order") team(id: ID!): CompanyTeam @doc(description: "Returns company team data by id.") } @@ -106,19 +109,16 @@ type CompanyEmailCheckResponse @doc(description: "Response object schema for a C isEmailValid: Boolean! @doc(description: "Email validation result") } -type CompanyHierarchyOutput @doc(description: "Response object schema for a Company Hierarchy query.") { - structure: CompanyHierarchyElement @doc(description: "An array of Company structure elements.") - isEditable: Boolean @doc(description: "Flag that defines whether Company Hierarchy can be changed by current User or not.") - max_nesting: Int @doc(description: "Indicator of maximun nesting of elements within a whole Company Hierarchy.") +union CompanyStructureEntity = CompanyTeam | Customer + +type CompanyStructureItem @doc(description: "Company Team and Customer structure") { + id: ID! @doc(description: "ID of the item in the hierarchy") + parentID: ID @doc(description: "ID of the parent item in the hierarchy") + entity: CompanyStructureEntity } -type CompanyHierarchyElement @doc(description: "Company Hierarchy element output data schema.") { - id: ID! @doc(description: "Hierarchy element id.") - tree_id: ID @doc(description: "The hierarchical id of the element within a structure. Used for changing element's position in hierarchy.") - type: String @doc(description: "Hierarchy element type: 'customer' or a 'team'.") - text: String @doc(description: "Hierarchy element name.") - description: String @doc(description: "Hierarchy element description.") - children: [CompanyHierarchyElement!] @doc(description: "An array of child elements.") +type CompanyStructure { + items: CompanyStructureItem[] } type CompanyTeam @doc(description: "Company Team entity output data schema.") { From 224ff5dd5b454fca172bdfd8da4fd9c4727c08ba Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Tue, 5 May 2020 15:19:23 -0500 Subject: [PATCH 039/479] In B2B Company GraphQL schema, change CompanyRole.permissions from [String] to [CompanyAclResource] --- design-documents/graph-ql/coverage/company.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index af4cd1389..33c7b04f7 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -80,7 +80,7 @@ type CompanyRole @doc(description: "Company role output data schema returned in id: ID! @doc(description: "Role id.") name: String @doc(description: "Role name.") users_count: Int @doc(description: "Total number of Users with such Role within Company Hierarchy.") - permissions: [String] @doc(description: "A list of permission resources defined for a Role.") + permissions: [CompanyAclResource] @doc(description: "A list of permission resources defined for a Role.") } type CompanyAclResource @doc(description: "Output data schema for an object with Role permission resource information.") { From 98a9a1c186b1ab83dc9e841164b685292b2c975b Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 13 May 2020 13:37:41 +0530 Subject: [PATCH 040/479] b2b requisition list schema fixed rename mutation --- .../graph-ql/coverage/requisitionList.graphql | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 design-documents/graph-ql/coverage/requisitionList.graphql diff --git a/design-documents/graph-ql/coverage/requisitionList.graphql b/design-documents/graph-ql/coverage/requisitionList.graphql new file mode 100644 index 000000000..288ff1556 --- /dev/null +++ b/design-documents/graph-ql/coverage/requisitionList.graphql @@ -0,0 +1,248 @@ +## Gift Card product type support will be added after the Product Implementation is developed +type Query { + "The requisitionList query returns the contents of customer's Requisition List" + requisitionList( + id: ID! + ): RequisitionList +} + +type Customer { + "Get Requisition Lists of customer" + requisitionLists( + pageSize: Int = 20, + currentPage:Int = 1 + ): RequisitionLists +} + +"Provides Requisition Lists of customer" +type RequisitionLists { + "List of Requisition Lists" + items: [RequisitionList] + "Page Information for pagination" + page_info: PageInfo + "Total count of Requisition Lists" + total_count: Int +} + +"Requisition List Type" +type RequisitionList { + "Unique Identifier of Requisition List" + id: ID! + "Name of the list" + name: String! + "Description of the list" + description: String + "Items in the list" + items: [RequisitionListItemInterface] + "Number of items in list" + items_count: Int! + "Latest Activity" + updated_at: String +} + +"Interface type for Requisition List Item" +interface RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! +} + +type SimpleRequisitionListItem implements RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! + "custom Option selected" + customizable_options: [SelectedCustomizableOption] +} + +type VirtualRequisitionListItem implements RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! + "custom Option selected" + customizable_options: [SelectedCustomizableOption] +} + +type DownloadableRequisitionListItem implements RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! + "custom Option selected" + customizable_options: [SelectedCustomizableOption] + "DownloadableProductLinks defines characteristics of a downloadable product" + links: [DownloadableProductLinks] + "DownloadableProductSamples defines characteristics of a downloadable product" + samples: [DownloadableProductSamples] +} + +type BundleRequisitionListItem implements RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! + "custom Option selected" + customizable_options: [SelectedCustomizableOption] + "selected bundle options" + bundle_options: [SelectedBundleOption]! +} + +type ConfigurableRequisitionListItem implements RequisitionListItemInterface { + "Unique Identifier of Requisition List Item" + id: ID! + "Product" + product: ProductInterface! + "Quantity added" + qty: Float! + "custom Option selected" + customizable_options: [SelectedCustomizableOption] + "Configurable options selected" + configurable_options: [SelectedConfigurableOption] +} + +type Mutation { + "Create Empty Requisition List" + createRequisitionList( + "name for the list" + name: String! + "description For the list" + description: String + ): RequisitionList + + "Rename a requisition list" + renameRequisitionList( + "unique Id of requisition list" + id: ID!, + "new name for list" + name: String, + "new description For the List" + description: String + ): RequisitionList + + "Add items to requisition list" + addProductsToRequisitionList( + "unique Id of requisition list" + id: ID!, + "Products to be added to requisition list" + items: [RequisitionListItemsInput!]! + ): AddProductsToRequisitionListOutput + + "Remove Items in requisition list" + removeRequisitionListItems( + "unique Id of requisition list" + id: ID!, + "unique Ids of Items to be removed from requisition list" + items: [ID!]! + ): RemoveRequisitionListItemsOutput + + "Update Items in requisition list" + updateRequisitionListItems( + "unique Id of requisition list" + id: ID!, + "Items to be updated from requisition list" + items: [UpdateRequisitionListItemsInput!]! + ): UpdateRequisitionListItemsOutput + + "Add Requisition List Items To Cart" + addRequisitionListItemToCart( + "Cart Id to which requisition list needs to copied" + cart_id: String! + "unique Id of requisition list" + requisition_list_id: ID! + "selected requisition list items that are to be added" + item_ids: [ID!]! + ): AddRequisitionListItemToCartOutput + + "Copy Items from Requisition List" + copyItemsFromRequisitionList( + "unique Id of source requisition list" + source_id: ID! + "unique Id of destination requisition list" + destination_id: ID # If null new requisition list will be created + "selected requisition list items that are to be copied from source" + item_ids: [ID!]! + ): CopyItemsFromRequisitionListOutput + + "Move Items from Requisition List" + moveItemsFromRequisitionList( + "unique Id of source requisition list" + source_id: ID! + "unique Id of destination requisition list" + destination_id: ID # If null new requisition list will be created + "selected requisition list items that are to be moved from source" + item_ids: [ID!]! + ): MoveItemsFromRequisitionListOutput +} + +type RemoveRequisitionListItemsOutput { + requisition_list : RequisitionList +} + +type UpdateRequisitionListItemsOutput { + requisition_list : RequisitionList +} + +type AddProductsToRequisitionListOutput { + requisition_list : RequisitionList +} + +input RequisitionListItemsInput { + sku: String! + quantity: Float + parent_sku: String + "selected option ID" + selected_options: [String!] + "entered Options ID" + entered_options: [EnteredOptionInput!] +} + +input UpdateRequisitionListItemsInput { + "unique ID of Requisition List Item" + item_id: ID! + customizable_options: [CustomizableOptionInput] + quantity: Float +} + +type AddRequisitionListItemToCartOutput { + cart: Cart # since requisition list is not mutated it is not part of the output +} + +type CopyItemsFromRequisitionListOutput { + "Destination Requisition List" + list : RequisitionList # since source requisition list is not mutated it is not part of the output +} + +type MoveItemsFromRequisitionListOutput { + "Source Requisition List" + source : RequisitionList + "Destination Requisition List" + destination : RequisitionList +} + +input EnteredOptionInput { + id: String! + value: String! +} + +"PageInfo provides navigation for the query response" +type PageInfo { + "Specifies which page of results to return" + current_page: Int + "Specifies the maximum number of items to return" + page_size: Int + "Total pages" + total_pages: Int +} From 30a5812eb644cde2514cf6b03ccd99b62ce810a8 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 13 May 2020 19:37:33 +0530 Subject: [PATCH 041/479] arguments fixed --- .../graph-ql/coverage/requisitionList.graphql | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphql b/design-documents/graph-ql/coverage/requisitionList.graphql index 288ff1556..c90a49344 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphql +++ b/design-documents/graph-ql/coverage/requisitionList.graphql @@ -10,7 +10,7 @@ type Customer { "Get Requisition Lists of customer" requisitionLists( pageSize: Int = 20, - currentPage:Int = 1 + currentPage: Int = 1 ): RequisitionLists } @@ -117,7 +117,7 @@ type Mutation { "Create Empty Requisition List" createRequisitionList( "name for the list" - name: String! + name: String!, "description For the list" description: String ): RequisitionList @@ -159,9 +159,9 @@ type Mutation { "Add Requisition List Items To Cart" addRequisitionListItemToCart( "Cart Id to which requisition list needs to copied" - cart_id: String! + cart_id: String!, "unique Id of requisition list" - requisition_list_id: ID! + requisition_list_id: ID!, "selected requisition list items that are to be added" item_ids: [ID!]! ): AddRequisitionListItemToCartOutput @@ -169,9 +169,9 @@ type Mutation { "Copy Items from Requisition List" copyItemsFromRequisitionList( "unique Id of source requisition list" - source_id: ID! + source_id: ID!, "unique Id of destination requisition list" - destination_id: ID # If null new requisition list will be created + destination_id: ID, # If null new requisition list will be created "selected requisition list items that are to be copied from source" item_ids: [ID!]! ): CopyItemsFromRequisitionListOutput @@ -179,9 +179,9 @@ type Mutation { "Move Items from Requisition List" moveItemsFromRequisitionList( "unique Id of source requisition list" - source_id: ID! + source_id: ID!, "unique Id of destination requisition list" - destination_id: ID # If null new requisition list will be created + destination_id: ID, # If null new requisition list will be created "selected requisition list items that are to be moved from source" item_ids: [ID!]! ): MoveItemsFromRequisitionListOutput From 9e19c312dee1b5be407c0ccce456feae86fa34d8 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 20 May 2020 12:14:09 +0530 Subject: [PATCH 042/479] export functionality added --- ...nList.graphql => requisitionList.graphqls} | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) rename design-documents/graph-ql/coverage/{requisitionList.graphql => requisitionList.graphqls} (94%) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphql b/design-documents/graph-ql/coverage/requisitionList.graphqls similarity index 94% rename from design-documents/graph-ql/coverage/requisitionList.graphql rename to design-documents/graph-ql/coverage/requisitionList.graphqls index c90a49344..94f407976 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphql +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -4,6 +4,12 @@ type Query { requisitionList( id: ID! ): RequisitionList + + "Export Requisition list" + exportRequisitionList( + "unqiue Id of Requisition list" + id: ID! + ): CsvOutput } type Customer { @@ -127,9 +133,14 @@ type Mutation { "unique Id of requisition list" id: ID!, "new name for list" - name: String, + name: String!, "new description For the List" - description: String + description: String! + ): RequisitionList + + DeleteRequisitionList( + "unique Id of requisition list" + id: ID! ): RequisitionList "Add items to requisition list" @@ -199,6 +210,13 @@ type AddProductsToRequisitionListOutput { requisition_list : RequisitionList } +type CsvOutput { + "file name" + file_name: String + "data for csv file with headers and records" + data: String +} + input RequisitionListItemsInput { sku: String! quantity: Float @@ -232,11 +250,6 @@ type MoveItemsFromRequisitionListOutput { destination : RequisitionList } -input EnteredOptionInput { - id: String! - value: String! -} - "PageInfo provides navigation for the query response" type PageInfo { "Specifies which page of results to return" From 797f6f57babf9ef85c03151bfef22d08fe7171d9 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 20 May 2020 12:15:04 +0530 Subject: [PATCH 043/479] export functionality added --- design-documents/graph-ql/coverage/requisitionList.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 94f407976..9b8b9140d 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -214,7 +214,7 @@ type CsvOutput { "file name" file_name: String "data for csv file with headers and records" - data: String + csv_data: String! } input RequisitionListItemsInput { From a764f79fb9e28c12169f7f627e13886a29ba4cd3 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 23 May 2020 13:32:02 +0530 Subject: [PATCH 044/479] review comments fixed --- .../coverage/requisitionList.graphqls | 154 ++++++++++++------ 1 file changed, 103 insertions(+), 51 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 9b8b9140d..4ecfafeb3 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -1,11 +1,34 @@ -## Gift Card product type support will be added after the Product Implementation is developed -type Query { - "The requisitionList query returns the contents of customer's Requisition List" - requisitionList( - id: ID! - ): RequisitionList +# Requistion List Flow User Flow +# +# 1.1. Create a requistion List at Customer's My Account section using `createRequistionList` Mutation +# +# 1.2. From catalog pages search for products and add products to already available list with `addProductsToRequisitionList` +# mutation or create a list with `createRequistionList` and on success add products with `addProductsToRequisitionList` +# +# 1.3. Select a requistion list from My Account and customer query `requisitionLists` with id filter will fetch requistion +# list details +# +# 1.4 Select a list of items from the requisition List and perform `addRequisitionListItemToCart`, so the items are copied +# to cart +# +# 1.5 perform regular checkout flow +# +# 2. In requistion list view, `exportRequisitionList` query will generate a CSV file with particular requistion list data +# +# 3. In requistion list view, `renameRequistionList` mutatation can be used to rename the list +# +# 4. In requistion list view, one can edit items quantity, options etc or remove items with `removeRequisitionListItems` and +# `updateRequisitionListItems` mutation +# +# 5. In requistion list view, select requistion list items and move them , copy them to different requistion list with +# `moveItemsFromRequisitionList` and `copyItemsFromRequisitionList` mutations respectively +# - "Export Requisition list" + +type Query { + """ + Export Requisition list in Csv format + """ exportRequisitionList( "unqiue Id of Requisition list" id: ID! @@ -16,21 +39,26 @@ type Customer { "Get Requisition Lists of customer" requisitionLists( pageSize: Int = 20, - currentPage: Int = 1 + currentPage: Int = 1, + filter: RequisitionListFilterInput ): RequisitionLists } -"Provides Requisition Lists of customer" +""" +Provides Customer's Requisition Lists +""" type RequisitionLists { "List of Requisition Lists" items: [RequisitionList] "Page Information for pagination" - page_info: PageInfo + page_info: SearchResultPageInfo "Total count of Requisition Lists" total_count: Int } -"Requisition List Type" +""" +Requisition List Type +""" type RequisitionList { "Unique Identifier of Requisition List" id: ID! @@ -46,28 +74,21 @@ type RequisitionList { updated_at: String } -"Interface type for Requisition List Item" +""" +Interface type for Requisition List Item +""" interface RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! - "Product" - product: ProductInterface! - "Quantity added" - qty: Float! -} - -type SimpleRequisitionListItem implements RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! - "Product" product: ProductInterface! "Quantity added" qty: Float! - "custom Option selected" - customizable_options: [SelectedCustomizableOption] } -type VirtualRequisitionListItem implements RequisitionListItemInterface { +""" +Requisition List Item Implementation that for Simple and Virtual Products +""" +type DefaultRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! "Product" @@ -78,10 +99,12 @@ type VirtualRequisitionListItem implements RequisitionListItemInterface { customizable_options: [SelectedCustomizableOption] } +""" +Requisition List Item Implementation that for Downloadable Products +""" type DownloadableRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! - "Product" product: ProductInterface! "Quantity added" qty: Float! @@ -93,6 +116,9 @@ type DownloadableRequisitionListItem implements RequisitionListItemInterface { samples: [DownloadableProductSamples] } +""" +Requisition List Item Implementation that for Bundle Products +""" type BundleRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! @@ -106,6 +132,9 @@ type BundleRequisitionListItem implements RequisitionListItemInterface { bundle_options: [SelectedBundleOption]! } +""" +Requisition List Item Implementation that for Configurable Products +""" type ConfigurableRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! @@ -120,30 +149,40 @@ type ConfigurableRequisitionListItem implements RequisitionListItemInterface { } type Mutation { - "Create Empty Requisition List" + + """ + Create Empty Requisition List + """ createRequisitionList( "name for the list" name: String!, "description For the list" description: String - ): RequisitionList + ): CreateRequisitionListOutput - "Rename a requisition list" + """ + Rename a requisition list and change description" + """ renameRequisitionList( "unique Id of requisition list" id: ID!, "new name for list" name: String!, "new description For the List" - description: String! - ): RequisitionList + description: String + ): RenameRequisitionListOutput - DeleteRequisitionList( + """ + Delete a requisition list with Id + """ + deleteRequisitionList( "unique Id of requisition list" id: ID! - ): RequisitionList + ): Boolean - "Add items to requisition list" + """ + Add items to requisition list + """ addProductsToRequisitionList( "unique Id of requisition list" id: ID!, @@ -151,7 +190,9 @@ type Mutation { items: [RequisitionListItemsInput!]! ): AddProductsToRequisitionListOutput - "Remove Items in requisition list" + """ + Remove Items in requisition list + """ removeRequisitionListItems( "unique Id of requisition list" id: ID!, @@ -159,7 +200,9 @@ type Mutation { items: [ID!]! ): RemoveRequisitionListItemsOutput - "Update Items in requisition list" + """ + Update Items in requisition list" + """ updateRequisitionListItems( "unique Id of requisition list" id: ID!, @@ -167,17 +210,19 @@ type Mutation { items: [UpdateRequisitionListItemsInput!]! ): UpdateRequisitionListItemsOutput - "Add Requisition List Items To Cart" + """ + Add Requisition List Items To Customer Cart" + """ addRequisitionListItemToCart( - "Cart Id to which requisition list needs to copied" - cart_id: String!, "unique Id of requisition list" requisition_list_id: ID!, "selected requisition list items that are to be added" item_ids: [ID!]! ): AddRequisitionListItemToCartOutput - "Copy Items from Requisition List" + """ + Copy Items from Requisition List to another requisition list" + """ copyItemsFromRequisitionList( "unique Id of source requisition list" source_id: ID!, @@ -187,7 +232,9 @@ type Mutation { item_ids: [ID!]! ): CopyItemsFromRequisitionListOutput - "Move Items from Requisition List" + """ + Move Items from Requisition List to another requisition List + """ moveItemsFromRequisitionList( "unique Id of source requisition list" source_id: ID!, @@ -217,6 +264,13 @@ type CsvOutput { csv_data: String! } +input RequisitionListFilterInput { + "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs" + ids: FilterEqualTypeInput, + "Filter by display name of the Requisition list" + name: FilterMatchTypeInput +} + input RequisitionListItemsInput { sku: String! quantity: Float @@ -234,6 +288,14 @@ input UpdateRequisitionListItemsInput { quantity: Float } +type CreateRequisitionListOutput { + list: RequisitionList +} + +type RenameRequisitionListOutput { + list: RequisitionList +} + type AddRequisitionListItemToCartOutput { cart: Cart # since requisition list is not mutated it is not part of the output } @@ -249,13 +311,3 @@ type MoveItemsFromRequisitionListOutput { "Destination Requisition List" destination : RequisitionList } - -"PageInfo provides navigation for the query response" -type PageInfo { - "Specifies which page of results to return" - current_page: Int - "Specifies the maximum number of items to return" - page_size: Int - "Total pages" - total_pages: Int -} From 12fe25016c379eeff3b9b81965179530b50accdb Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 23 May 2020 13:34:40 +0530 Subject: [PATCH 045/479] review comments fixed --- design-documents/graph-ql/coverage/requisitionList.graphqls | 3 --- 1 file changed, 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 4ecfafeb3..c57cc8bbd 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -91,7 +91,6 @@ Requisition List Item Implementation that for Simple and Virtual Products type DefaultRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! - "Product" product: ProductInterface! "Quantity added" qty: Float! @@ -122,7 +121,6 @@ Requisition List Item Implementation that for Bundle Products type BundleRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! - "Product" product: ProductInterface! "Quantity added" qty: Float! @@ -138,7 +136,6 @@ Requisition List Item Implementation that for Configurable Products type ConfigurableRequisitionListItem implements RequisitionListItemInterface { "Unique Identifier of Requisition List Item" id: ID! - "Product" product: ProductInterface! "Quantity added" qty: Float! From 46f56bb14a128d5e180a91db43892157ab344216 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 23 May 2020 13:38:41 +0530 Subject: [PATCH 046/479] review comments fixed --- design-documents/graph-ql/coverage/requisitionList.graphqls | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index c57cc8bbd..1ffdb5bdf 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -175,7 +175,7 @@ type Mutation { deleteRequisitionList( "unique Id of requisition list" id: ID! - ): Boolean + ): DeleteRequisitionListOutput """ Add items to requisition list @@ -293,6 +293,10 @@ type RenameRequisitionListOutput { list: RequisitionList } +type DeleteRequisitionListOutput { + result: Boolean +} + type AddRequisitionListItemToCartOutput { cart: Cart # since requisition list is not mutated it is not part of the output } From f245a33e88a086201495b0d0463cbb4ee8b2a467 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Mon, 25 May 2020 17:57:47 +0530 Subject: [PATCH 047/479] better names for mutations --- design-documents/graph-ql/coverage/requisitionList.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 1ffdb5bdf..55bdccfc2 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -220,7 +220,7 @@ type Mutation { """ Copy Items from Requisition List to another requisition list" """ - copyItemsFromRequisitionList( + copyItemsBetweenRequisitionList( "unique Id of source requisition list" source_id: ID!, "unique Id of destination requisition list" @@ -232,7 +232,7 @@ type Mutation { """ Move Items from Requisition List to another requisition List """ - moveItemsFromRequisitionList( + moveItemsBetweenRequisitionList( "unique Id of source requisition list" source_id: ID!, "unique Id of destination requisition list" From 39c95162586cb278859d302f8c07c80ec0658336 Mon Sep 17 00:00:00 2001 From: Arvind Date: Wed, 27 May 2020 15:58:15 +0530 Subject: [PATCH 048/479] added nq graphql schema --- .../coverage/negotiableQuotes.graphqls | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 design-documents/graph-ql/coverage/negotiableQuotes.graphqls diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls new file mode 100644 index 000000000..c62fd7d06 --- /dev/null +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -0,0 +1,361 @@ +type Query { + + getNegotiableQuote( + id: ID! + ): NegotiableQuote @doc(description:"Get negotibale quote") + + getAllNegotiableQuotes: [NegotiableQuote]! @doc(description: "Get all negotiable quotes") +} + +type Mutation { + + updatedPrices( + updatedQuotes: UpdatedPricesInput! + ): UpdatedPricesOutput! @doc(description:" This call refreshes item prices, taxes, discounts, cart rules in the negotiable quote. Quotes that are locked for the seller will not be updated.") + + requestQuote( + negotiableQuote: NegotiableQuoteRequestInput! + ): RequestQuoteOutput! @doc(description:"Generates negotiable quote request") + + assignBillingAddressToNegotiableQuote( + billingAddress: NegotiableQuoteBillingAddressInput! + ): AssignBillingAddressToNegotiableQuoteOutput @doc(description:"Assigns new billing address to negotiable quote") + + estimateShippingCostForNegotiableQuote( + shippingAddress: NegotiableQuoteEstimateShippinAddressInput! + ): EstimateShippingCostForNegotiableQuoteOutput @doc(description:"Estimate shipping costs for a negotiable quote") + + estimateShippingCostByAddressId( + shippingAddressId: NegotiableQuoteEstimateShippinAddressIdInput! + ): EstimateShippingCostByAddressIdOutput @doc(description:"Estimate shipping costs for a negotiable quote by Address ID") + + estimateShippingInformationForNegotiableQuote( + addressInformation: NegotiableQuoteShippingInformationInput! + ): EstimateShippingInformationForNegotiableQuoteOutput @doc(description:"Get list of payment options and the order totals by address and shipping info") + + setShippingMethodToNegotiableQuote( + negotiableQuoteShippingMethod: NegotiableQuoteShippingMethodInput! + ): setShippingMethodToNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") + + setCouponCodeToNegotiableQuote( + negotiableQuoteCouponCode: NegotiableQuoteCouponCodeInput! + ): SetCouponCodeToNegotiableQuoteOutput @doc(description:"Set negotiable quote coupon code") + + removeCouponCodeFromNegotiableQuote( + cartId: String! + ): RemoveCouponCodeFromNegotiableQuoteOutput @doc(description:"Remove negotiable quote coupon code") + + setPaymentInformationForNegotiableQuote( + paymentInformation: NegotiableQuotePaymentInformationInput! + ): SetPaymentInformationForNegotiableQuoteOutput! @doc(description:"Sets payment information and the billing address for the negotiable quote") + + createOrderWithPaymentInfoNegotiableQuote( + paymentInformation: NegotiableQuotePaymentInformationInput! + ): CreateOrderWithPaymentInfoNegotiableQuoteOutput! @doc(description:"Assigns payment information and the billing address for the negotiable quote to create order") + + createOrderWithGiftCardForNegotiableQuote( + giftCardAccountData: NegotiableQuoteGiftCardAccountDataInput! + ): CreateOrderWithGiftCardForNegotiableQuoteOutput! @doc(description:"Assigns GiftCard information and cart ID for the negotiable quote to create order") + + removeGiftCardFromNegotiableQuote( + giftCardsData: NegotiableQuotegiftCardsInput! + ): RemoveGiftCardFromNegotiableQuoteOutput! @doc(description:"Removes a gift card that has been applied to a negotiable quote") +} + +type Customer { + negotiable_quotes( + page_size: Int = 20, + current_page: Int = 1, + filter: NegotiableQuoteFilterInput + ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") +} + +type NegotiableQuote { + billing_address: BillingAddress! @doc(description: "Billing address of negotiable quote") + attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") + comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") + totals: TotalInfo @doc(description: "Negotiable quote totals info.") + payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") +} + +input NegotiableQuoteFilterInput { + ids: FilterEqualTypeInput @doc(description: "Filter Customer Negotibale quotes with an negotiable quote ID or list of negotiable quote IDs") + name: FilterMatchTypeInput @doc(description: "Filter by display name of the negotiable quote") +} + +type BillingAddress { + id: ID! @doc(description: "The ID of the billing address") + firstname: String @doc(description: "The first name of the person associated with the billing address") + lastname: String @doc(description: "The family name of the person associated with the billing address") + customer_id: String @doc(description: "The customer ID") + email: String @doc(description: "The customer email id") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + street: [String] @doc(description: "An array of strings that define the street number and name") + city: String @doc(description: "The city or town") + region: String @doc(description: "The region name") + region_id: String @doc(description: "The region ID") + region_code: String @doc(description: "The region code") + postcode: String @doc(description: "The customer's ZIP or postal code") + country_id: String @doc(description: "Deprecated: use `country_code` instead.") + same_as_billing: Int @doc(description: "Billing address confirmation") + save_in_address_book: Int @doc(description: "Save billing address in address book") +} + +type AttachmentContent @doc(description: "Negotiable quote attachment file") { + base64_encoded_data: String + type: String + name: String! +} + +type NegotiableQuoteComment { + entity_id: ID! + parent_id: ID! + creator_type: Int + is_decline: Int + is_draft: Int + creator_id: ID! + comment: String + created_at: String + attachments: [NegotiableQuoteCommentAttachments] +} + +type NegotiableQuoteCommentAttachments { + attachment_id: ID! + comment_id: ID + file_name: String + file_path: String + file_type: String +} + +type NegotiableQuotePaymentInfo { + payment_methods: [PaymentMethodsOutput]! @doc(description: "All list of payment options and totals") + totals: TotalsOutput! @doc(description: "List of totals") +} + +type PaymentMethodsOutput { + code: String @doc(description: "Payment method code") + title: String @doc(description: "Payment method title") +} + +type TotalsOutput { + grand_total: Float @doc(description: "Grand total") + base_grand_total: Float @doc(description: "Base grand total") + subtotal: Float @doc(description: "Sub total") + base_subtotal: Float @doc(description: "Base sub total") + discount_amount: Float @doc(description: "Discount amount") + subtotal_with_discount: Float @doc(description: "Subtotal with discount") + shipping_amount: Float @doc(description: "Shipping amount") + base_shipping_amount: Float @doc(description: "Base shipping amount") + shipping_discount_amount: Float @doc(description: "Shipping discount amount") + tax_amount: Float @doc(description: "Tax amount") + base_tax_amount: Float @doc(description: "base_tax_amount") + shipping_tax_amount: Float @doc(description: "shipping_tax_amount") + subtotal_incl_tax: Float @doc(description: "subtotal_incl_tax") + shipping_incl_tax: Float @doc(description: "shipping_incl_tax") + base_shipping_incl_tax: Float @doc(description: "base_shipping_incl_tax") + base_currency_code: String @doc(description: "base_currency_code") + quote_currency_code: String @doc(description: "quote_currency_code") + items_qty: Int @doc(description: "items_qty") + items: [ItemsOutput] @doc(description: "Items details") + total_segments: [TotalSegmentOutput] @doc(description: "Total segments") +} + +type ItemsOutput { + item_id: ID @doc(description: "Item ID") + price: Float @doc(description: "Price") + base_price: Float @doc(description: "Base price") + qty: Int @doc(description: "Quantity") + row_total: Float @doc(description: "Row total") + base_row_total: Float @doc(description: "Base row total") + row_total_with_discount: Float @doc(description: "Row total with discount") + tax_amount: Float @doc(description: "Tax amount") + base_tax_amount: Float @doc(description: "Base tax amount") + tax_percent: Float @doc(description: "Tax percent") + discount_amount: Float @doc(description: "Discount amount") + base_discount_amount: Float @doc(description: "Base discount amount") + discount_percent: Float @doc(description: "Discount percent") + price_incl_tax: Float @doc(description: "Price incl. tax") + base_price_incl_tax: Float @doc(description: "Base price incl. tax") + row_total_incl_tax: Float @doc(description: "Row total incl. tax") + base_row_total_incl_tax: Float @doc(description: "Base row total incl. tax") + name: String @doc(description: "Name") +} + +type TotalSegmentOutput { + code: String @doc(description: "Segment code") + title: String @doc(description: "Segment title") + value: Float @doc(description: "Segment value") +} + +type TotalInfo { + totals: TotalsOutput! @doc(description: "List of order otals for negotiable quote") +} + +input UpdatedPricesInput { + quote_ids: [String]! @doc(description: "IDs of the updated quotes") +} + +input NegotiableQuoteRequestInput { + quote_id: String! @doc(description: "Quote hash") + quote_name: String @doc(description: "Quote name") + comment: String @doc(description: "Comment") + files: [FileInput] @doc(description: "Attached files") +} + +type RequestQuoteOutput { + negotiable_quote:NegotiableQuote! +} + +input FileInput @doc(description: "The list of file attachment codes") { + base64_encoded_data: String + type: String + name: String! + extension_attributes: [FileAttributeInput] @doc(description: "File extension attributes") +} + +input FileAttributeInput { + attribute_code: String! @doc(description: "Attribute code") + value: String! @doc(description: "Attribute value") +} + +type UpdatedPricesOutput{ + negotiable_quote:NegotiableQuote + price_updated: Boolean! +} + +input NegotiableQuoteBillingAddressInput { + cart_id: String! @doc(description: "Cart hash") + address: BillingAddress @doc(description: "Address assigned to negotiable quote") + use_for_shipping: Boolean @doc(description: "Use for shipping") +} + +type AssignBillingAddressToNegotiableQuoteOutput { + id: ID @doc(description: "Negotiable quote billing address id") +} + +input NegotiableQuoteEstimateShippinAddressInput { + cart_id: String! @doc(description: "Cart hash") + address: NegotiableQuoteShipAddressInput @doc(description: "Shipping address assigned to negotiable quote") +} + +input NegotiableQuoteShipAddressInput { + firstname: String! @doc(description: "The first name of the person associated with the billing address") + lastname: String! @doc(description: "The family name of the person associated with the billing address") + telephone: String! @doc(description: "The telephone number") + street: [String]! @doc(description: "An array of strings that define the street number and name") + city: String! @doc(description: "The city or town") + region: String! @doc(description: "The region name") + region_id: Int @doc(description: "The region ID") + postcode: String! @doc(description: "The customer's ZIP or postal code") + country_id: String! @doc(description: "Deprecated: use `country_code` instead.") +} + +type EstimateShippingCostForNegotiableQuoteOutput { + shipping_methods: [ShippingMethodsOutput]! @doc(description: "All shipping methods for the shipping address") +} + +type EstimateShippingCostByAddressIdOutput { + shipping_methods: [ShippingMethodsOutput]! @doc(description: "All shipping methods for the shipping address") +} + +type ShippingMethodsOutput { + carrier_code: String @doc(description: "Shipping carrier code") + method_code: String @doc(description: "Shipping method code") + carrier_title: String @doc(description: "Shiping carrier title") + method_title: String @doc(description: "Shipping method title") + amount: String @doc(description: "Shipping amount") + base_amount: String @doc(description: "Shipping base amount") + available: String @doc(description: "Shipping method availble") + error_message: String @doc(description: "Shipping method error message") + price_excl_tax: String @doc(description: "Shipping method price with exclusive tax") + price_incl_tax: String @doc(description: "Shipping method price with inclusive tax") +} + +input NegotiableQuoteEstimateShippinAddressIdInput { + cart_id: String! @doc(description: "Cart hash") + address_id: String! @doc(description: "Masked shipping address ID assigned to negotiable quote") +} + +input NegotiableQuoteShippingInformationInput { + cart_id: String! @doc(description: "Cart hash") + address_info: NegotiableQuoteAddressInformationInput @doc(description: "Input for address and shipping info") +} + +input NegotiableQuoteAddressInformationInput { + shipping_address: NegotiableQuoteShipAddressInput @doc(description: "Shipping address assigned to negotiable quote") + billing_address: NegotiableQuoteBillAddressInput @doc(description: "Billing address assigned to negotiable quote") + shipping_carrier_code: String! @doc(description: "Shipping carrier code") + shipping_method_code: String! @doc(description: "Shipping method code") +} + +input NegotiableQuoteBillAddressInput { + firstname: String! @doc(description: "The first name of the person associated with the billing address") + lastname: String! @doc(description: "The family name of the person associated with the billing address") + telephone: String! @doc(description: "The telephone number") + street: [String]! @doc(description: "An array of strings that define the street number and name") + city: String! @doc(description: "The city or town") + region: String! @doc(description: "The region name") + region_id: Int @doc(description: "The region ID") + postcode: String! @doc(description: "The customer's ZIP or postal code") + country_id: String! @doc(description: "Deprecated: use `country_code` instead.") +} + +type EstimateShippingInformationForNegotiableQuoteOutput { + payment_information: NegotiableQuotePaymentInfo! @doc(description: "All list of payment options and totals") +} + +input NegotiableQuoteShippingMethodInput { + quote_id: String! @doc(description: "Quote hash") + shipping_method: String @doc(description: "Input for shipping method") +} + +type setShippingMethodToNegotiableQuoteOutput { + shipping_method_set: Boolean! +} + +input NegotiableQuoteCouponCodeInput { + cart_id: String! @doc(description: "Cart hash") + coupon_code: String @doc(description: "Input for coupon code") +} + +type SetCouponCodeToNegotiableQuoteOutput { + negotiable_quote: NegotiableQuote + coupon_code_set: Boolean +} + +input NegotiableQuotePaymentInformationInput { + cart_id: String! @doc(description: "Cart hash") + payment_method: NegotiableQuotePaymentMethodInput @doc(description: "Payment method assigned to negotiable quote") + billing_address: NegotiableQuoteBillAddressInput @doc(description: "Billing address assigned to negotiable quote") +} + +type SetPaymentInformationForNegotiableQuoteOutput { + payment_information_set: Boolean! +} + +type CreateOrderWithPaymentInfoNegotiableQuoteOutput { + order_id: Int! @doc(description: "Negotiable quote order ID") +} + +input NegotiableQuoteGiftCardAccountDataInput { + cart_id: String! @doc(description: "Cart hash") + gift_cards: [String]! @doc(description: "Gift cards to be assigned") +} + +type CreateOrderWithGiftCardForNegotiableQuoteOutput { + gift_card_assigned: Boolean! +} + +input NegotiableQuotegiftCardsInput { + cart_id: String! @doc(description: "Cart hash") + gift_card_code: String! @doc(description: "Gift cards code to be assigned") +} + +type RemoveCouponCodeFromNegotiableQuoteOutput { + cart: Cart! @doc(description: "Cart") +} + +type RemoveGiftCardFromNegotiableQuoteOutput { + cart: Cart! @doc(description: "Cart") +} From 0486d5bab1e40fff9c1770011b8808c72f89079d Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Thu, 28 May 2020 10:44:07 +0530 Subject: [PATCH 049/479] removing export list query --- .../graph-ql/coverage/requisitionList.graphqls | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 55bdccfc2..a21b4b176 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -24,17 +24,6 @@ # `moveItemsFromRequisitionList` and `copyItemsFromRequisitionList` mutations respectively # - -type Query { - """ - Export Requisition list in Csv format - """ - exportRequisitionList( - "unqiue Id of Requisition list" - id: ID! - ): CsvOutput -} - type Customer { "Get Requisition Lists of customer" requisitionLists( @@ -254,13 +243,6 @@ type AddProductsToRequisitionListOutput { requisition_list : RequisitionList } -type CsvOutput { - "file name" - file_name: String - "data for csv file with headers and records" - csv_data: String! -} - input RequisitionListFilterInput { "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs" ids: FilterEqualTypeInput, From 39e0340982b5508c451d942cf97abac5b5314a92 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Sun, 31 May 2020 11:13:56 -0500 Subject: [PATCH 050/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 174 ++++++++++++++++++ .../graph-ql/coverage/gift-registry.md | 31 ++++ 2 files changed, 205 insertions(+) create mode 100644 design-documents/graph-ql/coverage/gift-registry.graphqls create mode 100644 design-documents/graph-ql/coverage/gift-registry.md diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls new file mode 100644 index 000000000..ac095f66e --- /dev/null +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -0,0 +1,174 @@ +type Customer { + gitft_registry_list: [GiftRegistry] + gitft_registry(id: ID!): GiftRegistry +} + +type Query { + giftRegistryTypes: [GiftRegistryType] @doc(description: "Get a list of available gift registry types") +} + +type Mutation { + createGiftRegistry(gift_registry: CreateGiftRegistryInput): CreateGiftRegistryOutput +} + +input CreateGiftRegistryInput { + event_name: String! + type_id: String! + message: String! + privacy_settings: GiftRegistryPrivacySettings! + status: GiftRegistryStatus! + registrants: [CreateGiftRegistryRegistrantInput!]! + shipping_address: CreateGiftRegistryShippingAddressInput + dynamic_attributes: [GiftRegistryDynamicAttributeInput] +} + +type CreateGiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { + address_data: CustomerAddressInput + address_id: Int +} + +type CreateGiftRegistryRegistrantInput { + first_name: String! + last_name: String! + email: String! + dynamic_attributes: [GiftRegistryDynamicAttributeInput] +} + +type GiftRegistryDynamicAttributeInput { + code: ID! + value: String! +} + +type CreateGiftRegistryOutput { + gift_registry: GiftRegistry +} + +type GiftRegistryType { + id: ID! + label: String! + dynamic_attributes_metadata: [GiftRegistryDynamicAttributeMetadataInterface] +} + +interface GiftRegistryDynamicAttributeMetadataInterface { + code: ID! + input_type: String! + attribute_group: String! + label: String! + is_required: Boolean! + sort_order: Int +} + +type GiftRegistryTextAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface { + +} + +type GiftRegistrySelectAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { +} + +type GiftRegistryDateAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface { + format: GiftRegistryDateAttributeFormat! +} + +enum GiftRegistryDateAttributeFormat { + SHORT + MEDIUM + LONG + FULL +} + +type GiftRegistryCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface { + show_region: Boolean! +} + +type GiftRegistryEventCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface { + show_region: Boolean! +} + +type GiftRegistryEventDateAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface { + format: GiftRegistryDateAttributeFormat! +} + +type GiftRegistryEventLocationAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface { +} + +type GiftRegistryRoleAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { +} + +interface GiftRegistrySearcheableAttributeMetadataInterface { + is_searcheable: Boolean! +} + +interface GiftRegistrySelectAttributeMetadataInterface { + options: [GiftRegistrySelectAttributeOptionMetadata] +} + +type GiftRegistrySelectAttributeOptionMetadata { + code: ID! + label: String! + is_default: Boolean +} + +type GiftRegistry { + id: ID! + event_name: String! + type: GiftRegistryType + message: String! + created_on: String! @doc(description: "Creation date") + privacy_settings: GiftRegistryPrivacySettings! + status: GiftRegistryStatus! + registrants: [GiftRegistryRegistrant] + shipping_address: CustomerAddress + dynamic_attributes: [GiftRegistryDynamicAttribute] +} + +interface GiftRegistryDynamicAttributeInterface { + code: ID! + label: String! + value: String! +} + +type GiftRegistryDynamicAttribute implements GiftRegistryDynamicAttributeInterface { + group: GiftRegistryDynamicAttributeGroup! +} + +type GiftRegistryRegistrantDynamicAttribute implements GiftRegistryDynamicAttributeInterface { + +} + +enum GiftRegistryDynamicAttributeGroup { + GENERAL_INFORMATION + EVENT_INFORMATION + PRIVACY_SETTINGS + DETAILED_INFORMATION + SHIPPING_ADDRESS +} + +type GiftRegistryRegistrant { + first_name: String! + last_name: String! + email: String! + dynamic_attributes: [GiftRegistryRegistrantDynamicAttribute] +} + +enum GiftRegistryStatus { + ACTIVE + INACTIVE +} + +enum GiftRegistryPrivacySettings { + PRIVATE + PUBLIC +} + +type Mutation { + createProductReview(input: CreateProductReviewInput!): CreateProductReviewOutput! +} + +type Query { + productReviewRatingsMetadata(): ProductReviewRatingsMetadata! @doc(description: "Metadata required by clients to render ratings & reviews section.") +} + +type StoreConfig { + magento_giftregistry_general_enabled : String + magento_giftregistry_general_max_registrant : String +} diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md new file mode 100644 index 000000000..7a55b7b7c --- /dev/null +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -0,0 +1,31 @@ +## Use Cases + +### Registered customer creates a new gift registry + +### Gift registry owner modifies an existing gift registry + +### Gift registry owner removes items from an existing gift registry + +### Gift registry owner removes an existing gift registry + +### Gift registry owner adds items to the gift registry from cart + +### Gift registry owner adds items to the gift registry from wish list + +### Gift registry owner shares a gift registry with friends + +### Gift registry visitor adds items from the gift registry to the cart + +### Gift registry visitor searches a gift registry by the recipient name or email + +### Gift registry visitor opens a gift registry using the link from email + +### Storefront application retrieves gift registry edit form metadata + +### Storefront application retrieves gift registry search form metadata +Search by: + - Registrant name + - Registrant email + - Gift registry ID + + From fc64f25d1274facd703ba2bc712987f063aefc46 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 1 Jun 2020 09:34:32 -0500 Subject: [PATCH 051/479] Updates to customer order schema - remove SalesItemInterface and use OrderItemInterface instead (for order items only) - add only relevant fields to other item type (invoiceitem, shippingitem, etc) - changes to shipment tracking --- .../graph-ql/coverage/customer-orders.md | 89 ++++++++++--------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index e6c4e4adf..722725a34 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -60,7 +60,7 @@ type CustomerOrder { order_date: String! @doc("date when the order was placed") status: String! @doc("current status of the order") number: String! @doc("sequential order number") - items: [OrderItem]! @doc("collection of all the items purchased") + items: [OrderItemInterface]! @doc("collection of all the items purchased") total: OrderTotal! @doc("total amount details for the order") invoices: [Invoice]! @doc("invoice list for the order") credit_memos: [CreditMemo]! @doc("credit memo list for the order") @@ -79,52 +79,38 @@ The `id` will be a `base64_encode(increment_id)` which in future can be replaced ### Order Item -The order items will be presented as separate interface which will have multiple implementations for invoice, shipment and credit memo types. - ```graphql -@doc("Interface to reprent order/invoice/shipment/credit memo items") -interface SalesItemInterface { +@doc("Order item") +type OrderItemInterface { + id: ID! @doc("Order item unique identifier") #base64encode(orderItemId) product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") - product_url: String @doc("URL of the base product") + product_url_key: String @doc("URL key of the base product") + product_type: String @doc("Type of product (e.g. simple, configurable, bundle)") + status: String @doc("the status of order item") product_sale_price: Money! @doc("sale price for the base product including selected options") discounts: [Discount] @doc("final discount information for the base product including discounts on options") - parent_product_name: String @doc("name of parent product like configurable or bundle") - parent_product_sku: String @doc("SKU of parent product like configurable or bundle") - parent_product_url: String @doc("URL of parent product in the catalog") - selected_options: [SalesItemSelectedOption] @doc("selected options for the base product. for e.g color, size etc.") - entered_options: [SalesItemEnteredOption] @doc("entered option for the base product. for e.g logo image etc.") -} - -@doc("Represents sales item selected options") -type SalesItemSelectedOption { - id: ID! @doc("ID of the option") - label: String! @doc("name of the option") - value_labels: [String]! @doc("list of option value labels") -} - -@doc("Represents sales item entered options") -type SalesItemEnteredOption { - id: ID! @doc("ID of the option") - label: String! @doc("name of the option") - value: String! @doc("value of the option") -} -``` - -The `id` will be a `base64_encode(option_id)` which in future can be replaced by UUID. - -The `SalesItemInterface` will be implemented by the following types: - -```graphql -@doc("Order Product implementation of OrderProductInterface") -type OrderItem implements SalesItemInterface { + selected_options: [OrderItemOption] @doc("selected options for the base product. for e.g color, size etc.") + entered_options: [OrderItemOption] @doc("entered option for the base product. for e.g logo image etc.") quantity_ordered: Float @doc("number of items") quantity_shipped: Float @doc("number of shipped items") quantity_refunded: Float @doc("number of refunded items") quantity_invoiced: Float @doc("number of invoiced items") quantity_canceled: Float @doc("number of cancelled items") quantity_returned: Float @doc("number of returned items") - status: String @doc("the status of order item") +} + +type OrderItem implements OrderItemInterface { +} + +type BundleOrderItem implements OrderItemInterface { + child_items: [OrderItemInterface] +} + +@doc("Represents order item options like selected or entered") +type OrderItemOption { + id: String! @doc("name of the option") + value: String! @doc("value of the option") } ``` @@ -194,8 +180,15 @@ type Invoice { } @doc("Invoice item details") -type InvoiceItem implements SalesItemInterface{ - quantity_invoiced: Float! @doc("number of invoiced items") +type InvoiceItem { + id: ID! @doc("invoice item unique identifier") #base64encode(invoiceItemId) + order_item_id: String @doc("link to order item") + product_name: String @doc("name of the base product") + product_sku: String! @doc("SKU of the base product") + product_type: String @doc("Type of product (e.g. simple, configurable, bundle)") + product_sale_price: Money! @doc("sale price for the base product including selected options") + discounts: [Discount] @doc("final discount information for the base product including discounts on options") + quantity_invoiced: Float @doc("number of invoiced items") } @doc("Invoice total amount details") @@ -221,8 +214,14 @@ type CreditMemo { } @doc("Credit memo item details") -type CreditMemoItem implements SalesItemInterface{ - quantity_refunded: Float! @doc("number of refunded items") +type CreditMemoItem { + id: ID! @doc("Credit memo item unique identifier") #base64encode(creditMemoItemId) + order_item_id: String @doc("link to order item") + product_name: String @doc("name of the base product") + product_sku: String! @doc("SKU of the base product") + product_sale_price: Money! @doc("sale price for the base product including selected options") + discounts: [Discount] @doc("final discount information for the base product including discounts on options") + quantity_invoiced: Float @doc("number of invoiced items") } @doc("Credit memo price details") @@ -245,16 +244,20 @@ type OrderShipment { } @doc("Order shipment item details") -type ShipmentItem implements SalesItemInterface{ +type ShipmentItem{ + id: ID! @doc("Shipment item unique identifier") #base64encode(shipmentItemId) + order_item_id: String @doc("link to order item") + product_name: String @doc("name of the base product") + product_sku: String! @doc("SKU of the base product") + product_sale_price: Money! @doc("sale price for the base product") quantity_shipped: Float! @doc("number of shipped items") } @doc("Order shipment tracking details") type ShipmentTracking { - method: String! @doc("shipping method for the order") + title: String! @doc("shipment tracking title") carrier: String! @doc("shipping carrier for the order delivery") number: String @doc("tracking number of the order shipment") - link: String @doc("tracking link of the order shipment") } ``` From 4bcacac188cccf60a9f4295f4edb11962bc63607 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 1 Jun 2020 11:56:34 -0500 Subject: [PATCH 052/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index ac095f66e..6b26792f0 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -9,6 +9,18 @@ type Query { type Mutation { createGiftRegistry(gift_registry: CreateGiftRegistryInput): CreateGiftRegistryOutput + updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput): UpdateGiftRegistryOutput +} + +input UpdateGiftRegistryInput { + event_name: String + type_id: String + message: String + privacy_settings: GiftRegistryPrivacySettings + status: GiftRegistryStatus + registrants: [UpdateGiftRegistryRegistrantInput!] @doc(description: "As a result of the update, only the records of provided registrants will be affected. If the registrant is missing in the request, its attributes will not be changed") + shipping_address: GiftRegistryShippingAddressInput + dynamic_attributes: [GiftRegistryDynamicAttributeInput!] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") } input CreateGiftRegistryInput { @@ -18,15 +30,23 @@ input CreateGiftRegistryInput { privacy_settings: GiftRegistryPrivacySettings! status: GiftRegistryStatus! registrants: [CreateGiftRegistryRegistrantInput!]! - shipping_address: CreateGiftRegistryShippingAddressInput + shipping_address: GiftRegistryShippingAddressInput dynamic_attributes: [GiftRegistryDynamicAttributeInput] } -type CreateGiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { +type GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { address_data: CustomerAddressInput address_id: Int } +type UpdateGiftRegistryRegistrantInput { + id: ID! + first_name: String + last_name: String + email: String + dynamic_attributes: [GiftRegistryDynamicAttributeInput] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") +} + type CreateGiftRegistryRegistrantInput { first_name: String! last_name: String! @@ -43,6 +63,10 @@ type CreateGiftRegistryOutput { gift_registry: GiftRegistry } +type UpdateGiftRegistryOutput { + gift_registry: GiftRegistry +} + type GiftRegistryType { id: ID! label: String! @@ -59,7 +83,6 @@ interface GiftRegistryDynamicAttributeMetadataInterface { } type GiftRegistryTextAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface { - } type GiftRegistrySelectAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { @@ -144,6 +167,7 @@ enum GiftRegistryDynamicAttributeGroup { } type GiftRegistryRegistrant { + id: ID! first_name: String! last_name: String! email: String! @@ -160,15 +184,11 @@ enum GiftRegistryPrivacySettings { PUBLIC } -type Mutation { - createProductReview(input: CreateProductReviewInput!): CreateProductReviewOutput! -} - -type Query { - productReviewRatingsMetadata(): ProductReviewRatingsMetadata! @doc(description: "Metadata required by clients to render ratings & reviews section.") -} - type StoreConfig { magento_giftregistry_general_enabled : String magento_giftregistry_general_max_registrant : String } + +directive @doc( + description: String +) on FIELD_DEFINITION | ENUM_VALUE | OBJECT | INPUT_OBJECT | INPUT_FIELD_DEFINITION From c064297ef885b2208c39c455cf598e853f569f1d Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 1 Jun 2020 12:04:32 -0500 Subject: [PATCH 053/479] Updated changes related to invoice --- .../graph-ql/coverage/customer-orders.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 722725a34..d15f382ad 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -176,11 +176,12 @@ type Invoice { id: ID! @doc("the ID of the invoice, used for API purposes") number: String! @doc("sequential invoice number") total: InvoiceTotal! @doc("invoice total amount details") - items: [InvoiceItem]! @doc("invoiced product details") + items: [InvoiceItemInterface]! @doc("invoiced product details") + comments: [InvoiceComment] } @doc("Invoice item details") -type InvoiceItem { +type InvoiceItemInterface { id: ID! @doc("invoice item unique identifier") #base64encode(invoiceItemId) order_item_id: String @doc("link to order item") product_name: String @doc("name of the base product") @@ -191,6 +192,18 @@ type InvoiceItem { quantity_invoiced: Float @doc("number of invoiced items") } +type InvoiceItem implements InvoiceItemInterface { +} + +type BundledInvoiceItem implements InvoiceItemInterface { + child_items: [InvoiceItemInterface] +} + +type InvoiceComment { + timestamp: String! + message: String +} + @doc("Invoice total amount details") type InvoiceTotal implements SalesTotalAmountInterface { total_shipping: Money! @doc("order shipping amount") From 3d75f1b4dbd6541095a3c9f8a608dae0bb6d87f2 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 1 Jun 2020 12:43:59 -0500 Subject: [PATCH 054/479] Update customer-orders.md --- .../graph-ql/coverage/customer-orders.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index d15f382ad..66c617f20 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -70,6 +70,7 @@ type CustomerOrder { billing_address: CustomerAddress! @doc("billing address for the order") carrier: String! @doc("shipping carrier for the order delivery") method: String! @doc("shipping method for the order") + comments: [CommentItem]! @doc("comments on the order") } ``` @@ -113,7 +114,6 @@ type OrderItemOption { value: String! @doc("value of the option") } ``` - ### Payment Method Schema To provide more customization for different payment solutions, the payment method will be represented by own type instead of simple string: @@ -177,7 +177,7 @@ type Invoice { number: String! @doc("sequential invoice number") total: InvoiceTotal! @doc("invoice total amount details") items: [InvoiceItemInterface]! @doc("invoiced product details") - comments: [InvoiceComment] + comments: [CommentItem]! @doc("comments on the invoice") } @doc("Invoice item details") @@ -199,11 +199,6 @@ type BundledInvoiceItem implements InvoiceItemInterface { child_items: [InvoiceItemInterface] } -type InvoiceComment { - timestamp: String! - message: String -} - @doc("Invoice total amount details") type InvoiceTotal implements SalesTotalAmountInterface { total_shipping: Money! @doc("order shipping amount") @@ -273,7 +268,13 @@ type ShipmentTracking { number: String @doc("tracking number of the order shipment") } ``` - +### Comments +```graphql +type CommentItem { + timestamp: String! @doc("The timestamp of the comment") + message: String! @doc("the comment message") +} +``` The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. ## Additional Types From 064555f28296cc2ee37b5dd9d1a2cc2164d6e563 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 1 Jun 2020 12:46:30 -0500 Subject: [PATCH 055/479] Update customer-orders.md --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 66c617f20..9952f32cd 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -268,7 +268,7 @@ type ShipmentTracking { number: String @doc("tracking number of the order shipment") } ``` -### Comments +## CommentItem type ```graphql type CommentItem { timestamp: String! @doc("The timestamp of the comment") From 467cdec784d64fe693160754351175e21728b1ec Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 1 Jun 2020 12:51:11 -0500 Subject: [PATCH 056/479] Update customer-orders.md --- design-documents/graph-ql/coverage/customer-orders.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 9952f32cd..f590bcdc8 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -268,6 +268,8 @@ type ShipmentTracking { number: String @doc("tracking number of the order shipment") } ``` +The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. + ## CommentItem type ```graphql type CommentItem { @@ -275,7 +277,6 @@ type CommentItem { message: String! @doc("the comment message") } ``` -The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. ## Additional Types From 158568ce14338c89b05595e07cd79ded2df70547 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 1 Jun 2020 15:49:51 -0500 Subject: [PATCH 057/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 6b26792f0..1026296c5 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -5,6 +5,12 @@ type Customer { type Query { giftRegistryTypes: [GiftRegistryType] @doc(description: "Get a list of available gift registry types") + giftRegistrySearch( + registrant_firstname: String!, + registrant_lastname: String!, + gift_registry_type_id: ID, + searchable_dynamic_attributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") + ): [GiftRegistry] @doc(description: "Gift registry search by registrant name and additional searchable attributes.") } type Mutation { @@ -136,11 +142,12 @@ type GiftRegistry { event_name: String! type: GiftRegistryType message: String! - created_on: String! @doc(description: "Creation date") - privacy_settings: GiftRegistryPrivacySettings! - status: GiftRegistryStatus! + created_on: String! @doc(description: "Creation date") @doc(description: "Accessible to the registry owner only") + privacy_settings: GiftRegistryPrivacySettings! @doc(description: "Accessible to the registry owner only") + status: GiftRegistryStatus! @doc(description: "Accessible to the registry owner only") + owner_name: String! registrants: [GiftRegistryRegistrant] - shipping_address: CustomerAddress + shipping_address: CustomerAddress @doc(description: "Accessible to the registry owner only") dynamic_attributes: [GiftRegistryDynamicAttribute] } @@ -170,7 +177,7 @@ type GiftRegistryRegistrant { id: ID! first_name: String! last_name: String! - email: String! + email: String! @doc(description: "Accessible to the registry owner only") dynamic_attributes: [GiftRegistryRegistrantDynamicAttribute] } From 4710d569f3e8c019f231d0d57c14183ead2b66e2 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 2 Jun 2020 09:35:04 -0500 Subject: [PATCH 058/479] CustomerOrder shipping_address, carrier, and method should be nullable because an order can be virtual and not contain this Add "comments" to CreditMemo and OrderShipment --- design-documents/graph-ql/coverage/customer-orders.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index f590bcdc8..d3c5411e6 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -66,10 +66,10 @@ type CustomerOrder { credit_memos: [CreditMemo]! @doc("credit memo list for the order") shipments: [OrderShipment]! @doc("shipment list for the order") payment_methods: [PaymentMethod]! @doc("payment details for the order") - shipping_address: CustomerAddress! @doc("shipping address for the order") + shipping_address: CustomerAddress @doc("shipping address for the order") billing_address: CustomerAddress! @doc("billing address for the order") - carrier: String! @doc("shipping carrier for the order delivery") - method: String! @doc("shipping method for the order") + carrier: String @doc("shipping carrier for the order delivery") + method: String @doc("shipping method for the order") comments: [CommentItem]! @doc("comments on the order") } ``` @@ -219,6 +219,7 @@ type CreditMemo { number: String! @doc("sequential credit memo number") items: [CreditMemoItem]! @doc("items refunded") total: CredtiMemoTotal! @doc("refund total amount details") + comments: [CommentItem]! @doc("comments on the credit memo") } @doc("Credit memo item details") @@ -249,6 +250,7 @@ type OrderShipment { number: String! @doc("sequential credit shipment number") tracking: [ShipmentTracking] @doc("shipment tracking details") items: [ShipmentItem]! @doc("items included in the shipment") + comments: [CommentItem]! @doc("comments on the shipment") } @doc("Order shipment item details") From f42cea09528a611bd7c9796aacb1a902970a823f Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 2 Jun 2020 10:48:41 -0500 Subject: [PATCH 059/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 63 ++++++++++++++++++- .../graph-ql/coverage/gift-registry.md | 16 +++-- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 1026296c5..3854de764 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -149,6 +149,48 @@ type GiftRegistry { registrants: [GiftRegistryRegistrant] shipping_address: CustomerAddress @doc(description: "Accessible to the registry owner only") dynamic_attributes: [GiftRegistryDynamicAttribute] + items: [GiftRegistryItemInterface] +} + +type GiftRegistryItemInterface { + id: String! + quantity: Float! + quantity_fulfilled: Float! + note: String + added_on: String! + product: ProductInterface +} + +type SimpleGiftRegistryItem implements GiftRegistryItemInterface { + customizable_options: [SelectedCustomizableOption] +} + +type BundleGiftRegistryItem implements GiftRegistryItemInterface { + customizable_options: [SelectedCustomizableOption] + bundle_options: [SelectedBundleOption!] +} + +type ConfigurableGiftRegistryItem implements GiftRegistryItemInterface { + customizable_options: [SelectedCustomizableOption] + configurable_options: [SelectedConfigurableOption!] +} + +type DownloadableGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { + customizable_options: [SelectedCustomizableOption] + links: [DownloadableProductLinks] + samples: [DownloadableProductSamples] +} + +type VirtualGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { + customizable_options: [SelectedCustomizableOption] +} + +type GiftCardGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { + sender_name: String! + recepient_name: String! + amount: Money! + message: String + customizable_options: [SelectedCustomizableOption] } interface GiftRegistryDynamicAttributeInterface { @@ -196,6 +238,21 @@ type StoreConfig { magento_giftregistry_general_max_registrant : String } -directive @doc( - description: String -) on FIELD_DEFINITION | ENUM_VALUE | OBJECT | INPUT_OBJECT | INPUT_FIELD_DEFINITION +# This directicve is declared here to make the schema valid. It is already declared in Magento +directive @doc(description: String="") on QUERY + | MUTATION + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | SCHEMA + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 7a55b7b7c..391257c14 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -16,16 +16,14 @@ ### Gift registry visitor adds items from the gift registry to the cart -### Gift registry visitor searches a gift registry by the recipient name or email +### Storefront application retrieves gift registry search form metadata -### Gift registry visitor opens a gift registry using the link from email +### Gift registry visitor searches a gift registry by the recipient name -### Storefront application retrieves gift registry edit form metadata - -### Storefront application retrieves gift registry search form metadata -Search by: - - Registrant name - - Registrant email - - Gift registry ID +Search by registrant name and dynamic attributes. + +Explicitly excluded scenarios: search by ID and by email. +### Gift registry visitor opens a gift registry using the link from email +### Storefront application retrieves gift registry edit form metadata From 0d4117d0fe5a0c52c8d5cf6ec6c662e377a4351a Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 2 Jun 2020 11:24:12 -0500 Subject: [PATCH 060/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 3854de764..170b5164f 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -14,8 +14,21 @@ type Query { } type Mutation { + # All mutations below should only be accessible to the registry owner createGiftRegistry(gift_registry: CreateGiftRegistryInput): CreateGiftRegistryOutput updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput): UpdateGiftRegistryOutput + addProductsToGiftRegistry(gift_registry_id: ID!, items: [GiftRegistryItemInput!]!): AddProductsToGiftRegistryOutput +} + +input GiftRegistryItemInput { + sku: String! + quantity: Float! + parent_sku: String, + note: String, + # see https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md + selected_options: [String!] + # see https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md + entered_options: [EnteredOptionInput!] } input UpdateGiftRegistryInput { @@ -73,6 +86,10 @@ type UpdateGiftRegistryOutput { gift_registry: GiftRegistry } +type AddProductsToGiftRegistryOutput { + gift_registry: GiftRegistry +} + type GiftRegistryType { id: ID! label: String! @@ -175,17 +192,20 @@ type ConfigurableGiftRegistryItem implements GiftRegistryItemInterface { configurable_options: [SelectedConfigurableOption!] } -type DownloadableGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { +# Not currently supported by Magento core +type DownloadableGiftRegistryItem implements GiftRegistryItemInterface { customizable_options: [SelectedCustomizableOption] links: [DownloadableProductLinks] samples: [DownloadableProductSamples] } -type VirtualGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { +# Not currently supported by Magento core +type VirtualGiftRegistryItem implements GiftRegistryItemInterface { customizable_options: [SelectedCustomizableOption] } -type GiftCardGiftRegistryItem implements GiftRegistryItemInterface @doc(description: "Not currently supported by Magento core") { +# Not currently supported by Magento core +type GiftCardGiftRegistryItem implements GiftRegistryItemInterface { sender_name: String! recepient_name: String! amount: Money! From 0980b1560957fcae3d82ad9e60253e7b29f3b46b Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 2 Jun 2020 12:06:18 -0500 Subject: [PATCH 061/479] Remove non-nullable from output fields with complex type --- .../graph-ql/coverage/customer-orders.md | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index d3c5411e6..80e65838b 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -60,17 +60,17 @@ type CustomerOrder { order_date: String! @doc("date when the order was placed") status: String! @doc("current status of the order") number: String! @doc("sequential order number") - items: [OrderItemInterface]! @doc("collection of all the items purchased") - total: OrderTotal! @doc("total amount details for the order") - invoices: [Invoice]! @doc("invoice list for the order") - credit_memos: [CreditMemo]! @doc("credit memo list for the order") - shipments: [OrderShipment]! @doc("shipment list for the order") - payment_methods: [PaymentMethod]! @doc("payment details for the order") + items: [OrderItemInterface] @doc("collection of all the items purchased") + total: OrderTotal @doc("total amount details for the order") + invoices: [Invoice] @doc("invoice list for the order") + credit_memos: [CreditMemo] @doc("credit memo list for the order") + shipments: [OrderShipment] @doc("shipment list for the order") + payment_methods: [PaymentMethod] @doc("payment details for the order") shipping_address: CustomerAddress @doc("shipping address for the order") - billing_address: CustomerAddress! @doc("billing address for the order") + billing_address: CustomerAddress @doc("billing address for the order") carrier: String @doc("shipping carrier for the order delivery") method: String @doc("shipping method for the order") - comments: [CommentItem]! @doc("comments on the order") + comments: [CommentItem] @doc("comments on the order") } ``` @@ -139,7 +139,7 @@ interface SalesTotalAmountInterface { subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") discounts: [Discount] @doc("applied discounts") total_tax: Money! @doc("total tax amount") - taxes: [TaxItem]! @doc("order taxes details") + taxes: [TaxItem] @doc("order taxes details") grand_total: Money! @doc("final total amount including shipping and taxes") base_grand_total: Money! @doc("final total amount in base currency") } @@ -147,7 +147,7 @@ interface SalesTotalAmountInterface { @doc("Order total amounts details") type OrderTotal implements SalesTotalAmountInterface { total_shipping: Money! @doc("order shipping amount") - shipping_handling: ShippingHandling! @doc("shipping and handling for the order") + shipping_handling: ShippingHandling @doc("shipping and handling for the order") } @doc("Shipping handling details") @@ -155,7 +155,7 @@ type ShippingHandling { total_amount: Money! @doc("shipping total amount") amount_inc_tax: Money @doc("shipping amount including tax") amount_exc_tax: Money @doc("shipping amount excluding tax") - taxes: [TaxItem]! @doc("shipping taxes details") + taxes: [TaxItem] @doc("shipping taxes details") } @doc("Tax item details") @@ -175,9 +175,9 @@ The invoice entity will have the similar to the order schema: type Invoice { id: ID! @doc("the ID of the invoice, used for API purposes") number: String! @doc("sequential invoice number") - total: InvoiceTotal! @doc("invoice total amount details") - items: [InvoiceItemInterface]! @doc("invoiced product details") - comments: [CommentItem]! @doc("comments on the invoice") + total: InvoiceTotal @doc("invoice total amount details") + items: [InvoiceItemInterface] @doc("invoiced product details") + comments: [CommentItem] @doc("comments on the invoice") } @doc("Invoice item details") @@ -195,14 +195,14 @@ type InvoiceItemInterface { type InvoiceItem implements InvoiceItemInterface { } -type BundledInvoiceItem implements InvoiceItemInterface { +type BundleInvoiceItem implements InvoiceItemInterface { child_items: [InvoiceItemInterface] } @doc("Invoice total amount details") type InvoiceTotal implements SalesTotalAmountInterface { total_shipping: Money! @doc("order shipping amount") - shipping_handling: ShippingHandling! @doc("shipping and handling for the order") + shipping_handling: ShippingHandling @doc("shipping and handling for the order") } ``` @@ -217,9 +217,9 @@ The credit memo entity will have the similar to the order and invoice schema: type CreditMemo { id: ID! @doc("the ID of the credit memo, used for API purposes") number: String! @doc("sequential credit memo number") - items: [CreditMemoItem]! @doc("items refunded") - total: CredtiMemoTotal! @doc("refund total amount details") - comments: [CommentItem]! @doc("comments on the credit memo") + items: [CreditMemoItem] @doc("items refunded") + total: CredtiMemoTotal @doc("refund total amount details") + comments: [CommentItem] @doc("comments on the credit memo") } @doc("Credit memo item details") @@ -249,8 +249,8 @@ type OrderShipment { id: ID! @doc("the ID of the shipment, used for API purposes") number: String! @doc("sequential credit shipment number") tracking: [ShipmentTracking] @doc("shipment tracking details") - items: [ShipmentItem]! @doc("items included in the shipment") - comments: [CommentItem]! @doc("comments on the shipment") + items: [ShipmentItem] @doc("items included in the shipment") + comments: [CommentItem] @doc("comments on the shipment") } @doc("Order shipment item details") From cc6b0675a63b3b931047174ef845d41aced16466 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 2 Jun 2020 15:36:07 -0500 Subject: [PATCH 062/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 170b5164f..6514f0e11 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -17,10 +17,12 @@ type Mutation { # All mutations below should only be accessible to the registry owner createGiftRegistry(gift_registry: CreateGiftRegistryInput): CreateGiftRegistryOutput updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput): UpdateGiftRegistryOutput - addProductsToGiftRegistry(gift_registry_id: ID!, items: [GiftRegistryItemInput!]!): AddProductsToGiftRegistryOutput + addGiftRegistryItems(gift_registry_id: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput + removeGiftRegistryItems(gift_registry_id: ID!, item_ids: [ID!]!): RemoveGiftRegistryItemsOutput + updateGiftRegistryItems(gift_registry_id: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput } -input GiftRegistryItemInput { +input AddGiftRegistryItemInput { sku: String! quantity: Float! parent_sku: String, @@ -31,18 +33,26 @@ input GiftRegistryItemInput { entered_options: [EnteredOptionInput!] } +input UpdateGiftRegistryItemInput { + id: ID! + quantity: Float! + note: String +} + input UpdateGiftRegistryInput { event_name: String type_id: String message: String privacy_settings: GiftRegistryPrivacySettings status: GiftRegistryStatus - registrants: [UpdateGiftRegistryRegistrantInput!] @doc(description: "As a result of the update, only the records of provided registrants will be affected. If the registrant is missing in the request, its attributes will not be changed") + # TODO: Consider creating separate mutations for registrants + registrants: [UpdateGiftRegistryRegistrantInput!] @doc(description: "In case the field is not provided in the request, no registrant records will be affected. If the field is provided, the records of existing registrants missing in the request will be removed. To preserve an existing registrant while updating the other one, provide its id only. ") shipping_address: GiftRegistryShippingAddressInput dynamic_attributes: [GiftRegistryDynamicAttributeInput!] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") } input CreateGiftRegistryInput { + id: ID @doc(description: "Optional id, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items.") event_name: String! type_id: String! message: String! @@ -86,7 +96,15 @@ type UpdateGiftRegistryOutput { gift_registry: GiftRegistry } -type AddProductsToGiftRegistryOutput { +type AddGiftRegistryItemsOutput { + gift_registry: GiftRegistry +} + +type RemoveGiftRegistryItemsOutput { + gift_registry: GiftRegistry +} + +type UpdateGiftRegistryItemsOutput { gift_registry: GiftRegistry } From 1b1ce9f72ffb0b0c7baaea205eea26f73f688ee6 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 2 Jun 2020 17:01:10 -0500 Subject: [PATCH 063/479] ECP-711: GraphQL schema for Gift Registry --- design-documents/graph-ql/coverage/gift-registry.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 6514f0e11..ea29dae62 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -1,6 +1,6 @@ type Customer { - gitft_registry_list: [GiftRegistry] - gitft_registry(id: ID!): GiftRegistry + gift_registry_list: [GiftRegistry] + gift_registry(id: ID!): GiftRegistry } type Query { @@ -187,7 +187,7 @@ type GiftRegistry { items: [GiftRegistryItemInterface] } -type GiftRegistryItemInterface { +interface GiftRegistryItemInterface { id: String! quantity: Float! quantity_fulfilled: Float! From 1f82bfc880864fcb9f87252fb5ea3358eb55e881 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 2 Jun 2020 17:29:28 -0500 Subject: [PATCH 064/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index ea29dae62..d84bc76cf 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -14,12 +14,19 @@ type Query { } type Mutation { - # All mutations below should only be accessible to the registry owner - createGiftRegistry(gift_registry: CreateGiftRegistryInput): CreateGiftRegistryOutput - updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput): UpdateGiftRegistryOutput + # All mutations below should only be accessible to the registry owner. Guest users should be getting authorization error + + createGiftRegistry(gift_registry: CreateGiftRegistryInput!): CreateGiftRegistryOutput + updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput + removeGiftRegistry(id: ID!): RemoveGiftRegistryOutput + addGiftRegistryItems(gift_registry_id: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput removeGiftRegistryItems(gift_registry_id: ID!, item_ids: [ID!]!): RemoveGiftRegistryItemsOutput updateGiftRegistryItems(gift_registry_id: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput + + addGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput + updateGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput + removeGiftRegistryRegistrants(gift_registry_id: ID!, registrant_ids: [ID!]!): RemoveGiftRegistryRegistrantsOutput } input AddGiftRegistryItemInput { @@ -45,20 +52,17 @@ input UpdateGiftRegistryInput { message: String privacy_settings: GiftRegistryPrivacySettings status: GiftRegistryStatus - # TODO: Consider creating separate mutations for registrants - registrants: [UpdateGiftRegistryRegistrantInput!] @doc(description: "In case the field is not provided in the request, no registrant records will be affected. If the field is provided, the records of existing registrants missing in the request will be removed. To preserve an existing registrant while updating the other one, provide its id only. ") shipping_address: GiftRegistryShippingAddressInput dynamic_attributes: [GiftRegistryDynamicAttributeInput!] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") } input CreateGiftRegistryInput { - id: ID @doc(description: "Optional id, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items.") + id: ID @doc(description: "Optional id, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") event_name: String! type_id: String! message: String! privacy_settings: GiftRegistryPrivacySettings! status: GiftRegistryStatus! - registrants: [CreateGiftRegistryRegistrantInput!]! shipping_address: GiftRegistryShippingAddressInput dynamic_attributes: [GiftRegistryDynamicAttributeInput] } @@ -76,7 +80,7 @@ type UpdateGiftRegistryRegistrantInput { dynamic_attributes: [GiftRegistryDynamicAttributeInput] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") } -type CreateGiftRegistryRegistrantInput { +type AddGiftRegistryRegistrantInput { first_name: String! last_name: String! email: String! @@ -96,6 +100,10 @@ type UpdateGiftRegistryOutput { gift_registry: GiftRegistry } +type RemoveGiftRegistryOutput { + gift_registry: GiftRegistry +} + type AddGiftRegistryItemsOutput { gift_registry: GiftRegistry } @@ -108,6 +116,18 @@ type UpdateGiftRegistryItemsOutput { gift_registry: GiftRegistry } +type AddGiftRegistryRegistrantsOutput { + gift_registry: GiftRegistry +} + +type UpdateGiftRegistryRegistrantsOutput { + gift_registry: GiftRegistry +} + +type RemoveGiftRegistryRegistrantsOutput { + gift_registry: GiftRegistry +} + type GiftRegistryType { id: ID! label: String! From b3063fffafdc12583ecf186f9e548c208f3ab4c5 Mon Sep 17 00:00:00 2001 From: Arvind Date: Fri, 29 May 2020 17:31:35 +0530 Subject: [PATCH 065/479] fixed review comments --- .../coverage/negotiableQuotes.graphqls | 278 ++++-------------- 1 file changed, 64 insertions(+), 214 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index c62fd7d06..14f6f5983 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,88 +1,61 @@ -type Query { - - getNegotiableQuote( - id: ID! - ): NegotiableQuote @doc(description:"Get negotibale quote") - - getAllNegotiableQuotes: [NegotiableQuote]! @doc(description: "Get all negotiable quotes") -} - type Mutation { + recalculateNegotiableQuotePrices( + quote_ids: [String]! + ): RecalculateNegotiableQuotePricesOutput! @doc(description:" This call refreshes item prices, taxes, discounts, cart rules in the negotiable quote. Quotes that are locked for the seller will not be updated.") - updatedPrices( - updatedQuotes: UpdatedPricesInput! - ): UpdatedPricesOutput! @doc(description:" This call refreshes item prices, taxes, discounts, cart rules in the negotiable quote. Quotes that are locked for the seller will not be updated.") - - requestQuote( + requestNegotiableQuote( negotiableQuote: NegotiableQuoteRequestInput! - ): RequestQuoteOutput! @doc(description:"Generates negotiable quote request") - - assignBillingAddressToNegotiableQuote( - billingAddress: NegotiableQuoteBillingAddressInput! - ): AssignBillingAddressToNegotiableQuoteOutput @doc(description:"Assigns new billing address to negotiable quote") - - estimateShippingCostForNegotiableQuote( - shippingAddress: NegotiableQuoteEstimateShippinAddressInput! - ): EstimateShippingCostForNegotiableQuoteOutput @doc(description:"Estimate shipping costs for a negotiable quote") - - estimateShippingCostByAddressId( - shippingAddressId: NegotiableQuoteEstimateShippinAddressIdInput! - ): EstimateShippingCostByAddressIdOutput @doc(description:"Estimate shipping costs for a negotiable quote by Address ID") + ): RequestNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") - estimateShippingInformationForNegotiableQuote( - addressInformation: NegotiableQuoteShippingInformationInput! - ): EstimateShippingInformationForNegotiableQuoteOutput @doc(description:"Get list of payment options and the order totals by address and shipping info") - - setShippingMethodToNegotiableQuote( + setShippingMethodsOnNegotiableQuote( negotiableQuoteShippingMethod: NegotiableQuoteShippingMethodInput! - ): setShippingMethodToNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") - - setCouponCodeToNegotiableQuote( - negotiableQuoteCouponCode: NegotiableQuoteCouponCodeInput! - ): SetCouponCodeToNegotiableQuoteOutput @doc(description:"Set negotiable quote coupon code") - - removeCouponCodeFromNegotiableQuote( - cartId: String! - ): RemoveCouponCodeFromNegotiableQuoteOutput @doc(description:"Remove negotiable quote coupon code") - - setPaymentInformationForNegotiableQuote( - paymentInformation: NegotiableQuotePaymentInformationInput! - ): SetPaymentInformationForNegotiableQuoteOutput! @doc(description:"Sets payment information and the billing address for the negotiable quote") - - createOrderWithPaymentInfoNegotiableQuote( - paymentInformation: NegotiableQuotePaymentInformationInput! - ): CreateOrderWithPaymentInfoNegotiableQuoteOutput! @doc(description:"Assigns payment information and the billing address for the negotiable quote to create order") - - createOrderWithGiftCardForNegotiableQuote( - giftCardAccountData: NegotiableQuoteGiftCardAccountDataInput! - ): CreateOrderWithGiftCardForNegotiableQuoteOutput! @doc(description:"Assigns GiftCard information and cart ID for the negotiable quote to create order") - - removeGiftCardFromNegotiableQuote( - giftCardsData: NegotiableQuotegiftCardsInput! - ): RemoveGiftCardFromNegotiableQuoteOutput! @doc(description:"Removes a gift card that has been applied to a negotiable quote") + ): setShippingMethodsOnNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") } type Customer { - negotiable_quotes( + negotiable_quote ( + id: ID!): NegotiableQuote @doc(description: "Get negotiable quote of a customer") + + negotiable_quotes( page_size: Int = 20, current_page: Int = 1, filter: NegotiableQuoteFilterInput ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") + + estimateShippingCostForNegotiableQuote( + address: NegotiableQuoteShipAddressInput! + ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote") + + estimateShippingCostByAddressIdForNegotiableQuote( + address_id: String! + ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote by Address ID") + + estimateShippingInformationForNegotiableQuote( + address_info: NegotiableQuoteAddressInformationInput! + ): NegotiableQuotePaymentInfo! @doc(description:"Get payment options and the order totals by address and shipping info") } type NegotiableQuote { - billing_address: BillingAddress! @doc(description: "Billing address of negotiable quote") + items: [NegotiableQuoteItem] @doc(description: "Negotiable quote item") + billing_address: BillingAddress @doc(description: "Billing address of negotiable quote") attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") - totals: TotalInfo @doc(description: "Negotiable quote totals info.") + totals: TotalsOutput! @doc(description: "Negotiable quote totals output") payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") + available_payment_methods: [AvailablePaymentMethod] @doc(description: "Available payment methods") } input NegotiableQuoteFilterInput { - ids: FilterEqualTypeInput @doc(description: "Filter Customer Negotibale quotes with an negotiable quote ID or list of negotiable quote IDs") + ids: FilterEqualTypeInput @doc(description: "Filter Customer Negotiable quotes with an negotiable quote ID or list of negotiable quote IDs") name: FilterMatchTypeInput @doc(description: "Filter by display name of the negotiable quote") } +type NegotiableQuoteItem { + id: String! + quantity: Float! + product: ProductInterface! +} + type BillingAddress { id: ID! @doc(description: "The ID of the billing address") firstname: String @doc(description: "The first name of the person associated with the billing address") @@ -91,20 +64,20 @@ type BillingAddress { email: String @doc(description: "The customer email id") company: String @doc(description: "The customer's company") telephone: String @doc(description: "The telephone number") - street: [String] @doc(description: "An array of strings that define the street number and name") + street: [String] @doc(description: "A list of strings that define the street number and name") city: String @doc(description: "The city or town") region: String @doc(description: "The region name") region_id: String @doc(description: "The region ID") region_code: String @doc(description: "The region code") postcode: String @doc(description: "The customer's ZIP or postal code") - country_id: String @doc(description: "Deprecated: use `country_code` instead.") + country_code: String @doc(description: "The country code") same_as_billing: Int @doc(description: "Billing address confirmation") save_in_address_book: Int @doc(description: "Save billing address in address book") } type AttachmentContent @doc(description: "Negotiable quote attachment file") { - base64_encoded_data: String - type: String + base64_encoded_data: String! + mime_type: String! name: String! } @@ -133,67 +106,33 @@ type NegotiableQuotePaymentInfo { totals: TotalsOutput! @doc(description: "List of totals") } +type AvailablePaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") +} + type PaymentMethodsOutput { code: String @doc(description: "Payment method code") title: String @doc(description: "Payment method title") } type TotalsOutput { - grand_total: Float @doc(description: "Grand total") - base_grand_total: Float @doc(description: "Base grand total") - subtotal: Float @doc(description: "Sub total") - base_subtotal: Float @doc(description: "Base sub total") - discount_amount: Float @doc(description: "Discount amount") - subtotal_with_discount: Float @doc(description: "Subtotal with discount") - shipping_amount: Float @doc(description: "Shipping amount") - base_shipping_amount: Float @doc(description: "Base shipping amount") - shipping_discount_amount: Float @doc(description: "Shipping discount amount") - tax_amount: Float @doc(description: "Tax amount") - base_tax_amount: Float @doc(description: "base_tax_amount") - shipping_tax_amount: Float @doc(description: "shipping_tax_amount") - subtotal_incl_tax: Float @doc(description: "subtotal_incl_tax") - shipping_incl_tax: Float @doc(description: "shipping_incl_tax") - base_shipping_incl_tax: Float @doc(description: "base_shipping_incl_tax") - base_currency_code: String @doc(description: "base_currency_code") - quote_currency_code: String @doc(description: "quote_currency_code") - items_qty: Int @doc(description: "items_qty") - items: [ItemsOutput] @doc(description: "Items details") - total_segments: [TotalSegmentOutput] @doc(description: "Total segments") + grand_total: Money + subtotal_including_tax: Money + subtotal_excluding_tax: Money + discount: CartDiscount + subtotal_with_discount_excluding_tax: Money + applied_taxes: [CartTaxItem] } -type ItemsOutput { - item_id: ID @doc(description: "Item ID") - price: Float @doc(description: "Price") - base_price: Float @doc(description: "Base price") - qty: Int @doc(description: "Quantity") - row_total: Float @doc(description: "Row total") - base_row_total: Float @doc(description: "Base row total") - row_total_with_discount: Float @doc(description: "Row total with discount") - tax_amount: Float @doc(description: "Tax amount") - base_tax_amount: Float @doc(description: "Base tax amount") - tax_percent: Float @doc(description: "Tax percent") - discount_amount: Float @doc(description: "Discount amount") - base_discount_amount: Float @doc(description: "Base discount amount") - discount_percent: Float @doc(description: "Discount percent") - price_incl_tax: Float @doc(description: "Price incl. tax") - base_price_incl_tax: Float @doc(description: "Base price incl. tax") - row_total_incl_tax: Float @doc(description: "Row total incl. tax") - base_row_total_incl_tax: Float @doc(description: "Base row total incl. tax") - name: String @doc(description: "Name") +type CartTaxItem { + amount: Money! + label: String! } -type TotalSegmentOutput { - code: String @doc(description: "Segment code") - title: String @doc(description: "Segment title") - value: Float @doc(description: "Segment value") -} - -type TotalInfo { - totals: TotalsOutput! @doc(description: "List of order otals for negotiable quote") -} - -input UpdatedPricesInput { - quote_ids: [String]! @doc(description: "IDs of the updated quotes") +type CartDiscount { + amount: Money! + label: [String!]! } input NegotiableQuoteRequestInput { @@ -203,60 +142,31 @@ input NegotiableQuoteRequestInput { files: [FileInput] @doc(description: "Attached files") } -type RequestQuoteOutput { +type RequestNegotiableQuoteOutput { negotiable_quote:NegotiableQuote! } input FileInput @doc(description: "The list of file attachment codes") { - base64_encoded_data: String - type: String + base64_encoded_data: String! + mime_type: String! name: String! - extension_attributes: [FileAttributeInput] @doc(description: "File extension attributes") } -input FileAttributeInput { - attribute_code: String! @doc(description: "Attribute code") - value: String! @doc(description: "Attribute value") -} - -type UpdatedPricesOutput{ +type RecalculateNegotiableQuotePricesOutput{ negotiable_quote:NegotiableQuote price_updated: Boolean! } -input NegotiableQuoteBillingAddressInput { - cart_id: String! @doc(description: "Cart hash") - address: BillingAddress @doc(description: "Address assigned to negotiable quote") - use_for_shipping: Boolean @doc(description: "Use for shipping") -} - -type AssignBillingAddressToNegotiableQuoteOutput { - id: ID @doc(description: "Negotiable quote billing address id") -} - -input NegotiableQuoteEstimateShippinAddressInput { - cart_id: String! @doc(description: "Cart hash") - address: NegotiableQuoteShipAddressInput @doc(description: "Shipping address assigned to negotiable quote") -} - input NegotiableQuoteShipAddressInput { firstname: String! @doc(description: "The first name of the person associated with the billing address") lastname: String! @doc(description: "The family name of the person associated with the billing address") telephone: String! @doc(description: "The telephone number") - street: [String]! @doc(description: "An array of strings that define the street number and name") + street: [String]! @doc(description: "A list of strings that define the street number and name") city: String! @doc(description: "The city or town") region: String! @doc(description: "The region name") region_id: Int @doc(description: "The region ID") postcode: String! @doc(description: "The customer's ZIP or postal code") - country_id: String! @doc(description: "Deprecated: use `country_code` instead.") -} - -type EstimateShippingCostForNegotiableQuoteOutput { - shipping_methods: [ShippingMethodsOutput]! @doc(description: "All shipping methods for the shipping address") -} - -type EstimateShippingCostByAddressIdOutput { - shipping_methods: [ShippingMethodsOutput]! @doc(description: "All shipping methods for the shipping address") + country_code: String! @doc(description: "The country code") } type ShippingMethodsOutput { @@ -272,16 +182,6 @@ type ShippingMethodsOutput { price_incl_tax: String @doc(description: "Shipping method price with inclusive tax") } -input NegotiableQuoteEstimateShippinAddressIdInput { - cart_id: String! @doc(description: "Cart hash") - address_id: String! @doc(description: "Masked shipping address ID assigned to negotiable quote") -} - -input NegotiableQuoteShippingInformationInput { - cart_id: String! @doc(description: "Cart hash") - address_info: NegotiableQuoteAddressInformationInput @doc(description: "Input for address and shipping info") -} - input NegotiableQuoteAddressInformationInput { shipping_address: NegotiableQuoteShipAddressInput @doc(description: "Shipping address assigned to negotiable quote") billing_address: NegotiableQuoteBillAddressInput @doc(description: "Billing address assigned to negotiable quote") @@ -293,16 +193,12 @@ input NegotiableQuoteBillAddressInput { firstname: String! @doc(description: "The first name of the person associated with the billing address") lastname: String! @doc(description: "The family name of the person associated with the billing address") telephone: String! @doc(description: "The telephone number") - street: [String]! @doc(description: "An array of strings that define the street number and name") + street: [String]! @doc(description: "A list of strings that define the street number and name") city: String! @doc(description: "The city or town") region: String! @doc(description: "The region name") region_id: Int @doc(description: "The region ID") postcode: String! @doc(description: "The customer's ZIP or postal code") - country_id: String! @doc(description: "Deprecated: use `country_code` instead.") -} - -type EstimateShippingInformationForNegotiableQuoteOutput { - payment_information: NegotiableQuotePaymentInfo! @doc(description: "All list of payment options and totals") + country_code: String! @doc(description: "The country code") } input NegotiableQuoteShippingMethodInput { @@ -310,52 +206,6 @@ input NegotiableQuoteShippingMethodInput { shipping_method: String @doc(description: "Input for shipping method") } -type setShippingMethodToNegotiableQuoteOutput { +type setShippingMethodsOnNegotiableQuoteOutput { shipping_method_set: Boolean! } - -input NegotiableQuoteCouponCodeInput { - cart_id: String! @doc(description: "Cart hash") - coupon_code: String @doc(description: "Input for coupon code") -} - -type SetCouponCodeToNegotiableQuoteOutput { - negotiable_quote: NegotiableQuote - coupon_code_set: Boolean -} - -input NegotiableQuotePaymentInformationInput { - cart_id: String! @doc(description: "Cart hash") - payment_method: NegotiableQuotePaymentMethodInput @doc(description: "Payment method assigned to negotiable quote") - billing_address: NegotiableQuoteBillAddressInput @doc(description: "Billing address assigned to negotiable quote") -} - -type SetPaymentInformationForNegotiableQuoteOutput { - payment_information_set: Boolean! -} - -type CreateOrderWithPaymentInfoNegotiableQuoteOutput { - order_id: Int! @doc(description: "Negotiable quote order ID") -} - -input NegotiableQuoteGiftCardAccountDataInput { - cart_id: String! @doc(description: "Cart hash") - gift_cards: [String]! @doc(description: "Gift cards to be assigned") -} - -type CreateOrderWithGiftCardForNegotiableQuoteOutput { - gift_card_assigned: Boolean! -} - -input NegotiableQuotegiftCardsInput { - cart_id: String! @doc(description: "Cart hash") - gift_card_code: String! @doc(description: "Gift cards code to be assigned") -} - -type RemoveCouponCodeFromNegotiableQuoteOutput { - cart: Cart! @doc(description: "Cart") -} - -type RemoveGiftCardFromNegotiableQuoteOutput { - cart: Cart! @doc(description: "Cart") -} From fda88e96e3d929ca2f0de7ef60222a9648edab87 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 3 Jun 2020 13:32:23 -0500 Subject: [PATCH 066/479] ECP-711: GraphQL schema for Gift Registry --- design-documents/graph-ql/coverage/gift-registry.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index d84bc76cf..ab5428c27 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -67,12 +67,12 @@ input CreateGiftRegistryInput { dynamic_attributes: [GiftRegistryDynamicAttributeInput] } -type GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { +input GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { address_data: CustomerAddressInput address_id: Int } -type UpdateGiftRegistryRegistrantInput { +input UpdateGiftRegistryRegistrantInput { id: ID! first_name: String last_name: String @@ -80,14 +80,14 @@ type UpdateGiftRegistryRegistrantInput { dynamic_attributes: [GiftRegistryDynamicAttributeInput] @doc(description: "As a result of the update, only the values of provided attributes will be affected. If the attribute is missing in the request, its value will not be changed") } -type AddGiftRegistryRegistrantInput { +input AddGiftRegistryRegistrantInput { first_name: String! last_name: String! email: String! dynamic_attributes: [GiftRegistryDynamicAttributeInput] } -type GiftRegistryDynamicAttributeInput { +input GiftRegistryDynamicAttributeInput { code: ID! value: String! } From c758bb652c68a0c6c574f60a014960d78bfe14de Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 3 Jun 2020 15:12:54 -0500 Subject: [PATCH 067/479] Return order_item as complete object instead of id only --- design-documents/graph-ql/coverage/customer-orders.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 80e65838b..6a88923e6 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -183,7 +183,7 @@ type Invoice { @doc("Invoice item details") type InvoiceItemInterface { id: ID! @doc("invoice item unique identifier") #base64encode(invoiceItemId) - order_item_id: String @doc("link to order item") + order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") product_type: String @doc("Type of product (e.g. simple, configurable, bundle)") @@ -225,7 +225,7 @@ type CreditMemo { @doc("Credit memo item details") type CreditMemoItem { id: ID! @doc("Credit memo item unique identifier") #base64encode(creditMemoItemId) - order_item_id: String @doc("link to order item") + order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") product_sale_price: Money! @doc("sale price for the base product including selected options") @@ -234,7 +234,7 @@ type CreditMemoItem { } @doc("Credit memo price details") -type CredtiMemoTotal implements SalesTotalAmountInterface { +type CreditMemoTotal implements SalesTotalAmountInterface { } ``` @@ -256,7 +256,7 @@ type OrderShipment { @doc("Order shipment item details") type ShipmentItem{ id: ID! @doc("Shipment item unique identifier") #base64encode(shipmentItemId) - order_item_id: String @doc("link to order item") + order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") product_sale_price: Money! @doc("sale price for the base product") From 0b9bc2c9f2adefc1bd41d7c195fb9958a03f8605 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 3 Jun 2020 16:04:38 -0500 Subject: [PATCH 068/479] Remove product_type from InvoiceItemInterface because it comes from OrderItem, so it's redundant --- design-documents/graph-ql/coverage/customer-orders.md | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 6a88923e6..b193ac326 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -186,7 +186,6 @@ type InvoiceItemInterface { order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") - product_type: String @doc("Type of product (e.g. simple, configurable, bundle)") product_sale_price: Money! @doc("sale price for the base product including selected options") discounts: [Discount] @doc("final discount information for the base product including discounts on options") quantity_invoiced: Float @doc("number of invoiced items") From 246e6d16718062cc2d398809f4300101bb11bfa3 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Wed, 3 Jun 2020 17:08:34 -0500 Subject: [PATCH 069/479] fix: used type when it should be interface --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index b193ac326..a6976ec76 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -82,7 +82,7 @@ The `id` will be a `base64_encode(increment_id)` which in future can be replaced ```graphql @doc("Order item") -type OrderItemInterface { +interface OrderItemInterface { id: ID! @doc("Order item unique identifier") #base64encode(orderItemId) product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") From a669f72cb61e88d043f9b18fa9b5330ad5761805 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Thu, 4 Jun 2020 11:04:44 +0530 Subject: [PATCH 070/479] refactored doc for types and fields --- .../coverage/requisitionList.graphqls | 253 ++++++------------ 1 file changed, 76 insertions(+), 177 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index a21b4b176..413e9e0e5 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -25,210 +25,117 @@ # type Customer { - "Get Requisition Lists of customer" requisitionLists( pageSize: Int = 20, currentPage: Int = 1, filter: RequisitionListFilterInput - ): RequisitionLists + ): RequisitionLists @doc(description: "Get Requisition Lists of customer") } -""" -Provides Customer's Requisition Lists -""" -type RequisitionLists { - "List of Requisition Lists" - items: [RequisitionList] - "Page Information for pagination" - page_info: SearchResultPageInfo - "Total count of Requisition Lists" - total_count: Int +type RequisitionLists @doc(description: "Provides Customer's Requisition Lists") { + items: [RequisitionList] @doc(description: "List of Requisition Lists") + page_info: SearchResultPageInfo @doc(description: "Page Information for pagination") + total_count: Int @doc(description: "Total count of Requisition Lists") } -""" -Requisition List Type -""" -type RequisitionList { - "Unique Identifier of Requisition List" - id: ID! - "Name of the list" - name: String! - "Description of the list" - description: String - "Items in the list" - items: [RequisitionListItemInterface] - "Number of items in list" - items_count: Int! - "Latest Activity" - updated_at: String +type RequisitionList @doc(description: "Requisition List Type") { + id: ID! @doc(description: "Unique Identifier of Requisition List") + name: String! @doc(description: "Name of the list") + description: String @doc(description: "Description of the list") + items: [RequisitionListItemInterface] @doc(description: "Items in the list") + items_count: Int! @doc(description: "Number of items in list") + updated_at: String @doc(description: "Latest Activity") } -""" -Interface type for Requisition List Item -""" -interface RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! +interface RequisitionListItemInterface @doc(description: "Interface type for Requisition List Item") { + id: ID! @doc(description:"Unique Identifier of Requisition List Item") product: ProductInterface! - "Quantity added" - qty: Float! + qty: Float! @doc(description: "Quantity added") } -""" -Requisition List Item Implementation that for Simple and Virtual Products -""" -type DefaultRequisitionListItem implements RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! +type DefaultRequisitionListItem implements RequisitionListItemInterface +@doc(description: "Requisition List Item Implementation that for Simple and Virtual Products") { + id: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - "Quantity added" - qty: Float! - "custom Option selected" - customizable_options: [SelectedCustomizableOption] + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") } -""" -Requisition List Item Implementation that for Downloadable Products -""" -type DownloadableRequisitionListItem implements RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! +type DownloadableRequisitionListItem implements RequisitionListItemInterface +@doc(description: "Requisition List Item Implementation that for Downloadable Products") { + id: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - "Quantity added" - qty: Float! - "custom Option selected" - customizable_options: [SelectedCustomizableOption] - "DownloadableProductLinks defines characteristics of a downloadable product" - links: [DownloadableProductLinks] - "DownloadableProductSamples defines characteristics of a downloadable product" - samples: [DownloadableProductSamples] + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + links: [DownloadableProductLinks] @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") + samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } -""" -Requisition List Item Implementation that for Bundle Products -""" -type BundleRequisitionListItem implements RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! +type BundleRequisitionListItem implements RequisitionListItemInterface +@doc(description: "Requisition List Item Implementation that for Bundle Products") { + id: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - "Quantity added" - qty: Float! - "custom Option selected" - customizable_options: [SelectedCustomizableOption] - "selected bundle options" - bundle_options: [SelectedBundleOption]! + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + bundle_options: [SelectedBundleOption]! @doc(description: "selected bundle options") } -""" -Requisition List Item Implementation that for Configurable Products -""" -type ConfigurableRequisitionListItem implements RequisitionListItemInterface { - "Unique Identifier of Requisition List Item" - id: ID! +type ConfigurableRequisitionListItem implements RequisitionListItemInterface +@doc(description: "Requisition List Item Implementation that for Configurable Products") { + id: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - "Quantity added" - qty: Float! - "custom Option selected" - customizable_options: [SelectedCustomizableOption] - "Configurable options selected" - configurable_options: [SelectedConfigurableOption] + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + configurable_options: [SelectedConfigurableOption] @doc(description: "Configurable options selected") } type Mutation { - - """ - Create Empty Requisition List - """ createRequisitionList( - "name for the list" - name: String!, - "description For the list" - description: String - ): CreateRequisitionListOutput + name: String!, @doc(description: "name for the list") + description: String @doc(description: "description For the list") + ): CreateRequisitionListOutput @doc(description: "Create Empty Requisition List") - """ - Rename a requisition list and change description" - """ renameRequisitionList( - "unique Id of requisition list" - id: ID!, - "new name for list" - name: String!, - "new description For the List" - description: String - ): RenameRequisitionListOutput + id: ID!, @doc(description: "unique Id of requisition list") + name: String!, @doc(description: "new name for list") + description: String @doc(description: "new description For the List") + ): RenameRequisitionListOutput @doc(description: "Rename a requisition list and change description") - """ - Delete a requisition list with Id - """ deleteRequisitionList( - "unique Id of requisition list" - id: ID! - ): DeleteRequisitionListOutput + id: ID! @doc(description: "unique Id of requisition list") + ): DeleteRequisitionListOutput @doc(description: "Delete a requisition list with Id") - """ - Add items to requisition list - """ addProductsToRequisitionList( - "unique Id of requisition list" - id: ID!, - "Products to be added to requisition list" - items: [RequisitionListItemsInput!]! - ): AddProductsToRequisitionListOutput + id: ID!, @doc(description: "unique Id of requisition list") + items: [RequisitionListItemsInput!]! @doc(description: "Products to be added to requisition list") + ): AddProductsToRequisitionListOutput @doc(description: "Add items to requisition list") - """ - Remove Items in requisition list - """ removeRequisitionListItems( - "unique Id of requisition list" - id: ID!, - "unique Ids of Items to be removed from requisition list" - items: [ID!]! - ): RemoveRequisitionListItemsOutput + id: ID!, @doc(description: "unique Id of requisition list") + items: [ID!]! @doc(description: "unique Ids of Items to be removed from requisition list") + ): RemoveRequisitionListItemsOutput @doc(description: "Remove Items in requisition list") - """ - Update Items in requisition list" - """ updateRequisitionListItems( - "unique Id of requisition list" - id: ID!, - "Items to be updated from requisition list" - items: [UpdateRequisitionListItemsInput!]! - ): UpdateRequisitionListItemsOutput + id: ID!, @doc(description: "unique Id of requisition list") + items: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") + ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") - """ - Add Requisition List Items To Customer Cart" - """ addRequisitionListItemToCart( - "unique Id of requisition list" - requisition_list_id: ID!, - "selected requisition list items that are to be added" - item_ids: [ID!]! - ): AddRequisitionListItemToCartOutput + requisition_list_id: ID!, @doc(description: "unique Id of requisition list") + item_ids: [ID!]! @doc(description: "selected requisition list items that are to be added") + ): AddRequisitionListItemToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") - """ - Copy Items from Requisition List to another requisition list" - """ copyItemsBetweenRequisitionList( - "unique Id of source requisition list" - source_id: ID!, - "unique Id of destination requisition list" - destination_id: ID, # If null new requisition list will be created - "selected requisition list items that are to be copied from source" - item_ids: [ID!]! - ): CopyItemsFromRequisitionListOutput + source_id: ID!, @doc(description: "unique Id of source requisition list") + destination_id: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + item_ids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") + ): CopyItemsFromRequisitionListOutput @doc(description: "Copy Items from Requisition List to another requisition list") - """ - Move Items from Requisition List to another requisition List - """ moveItemsBetweenRequisitionList( - "unique Id of source requisition list" - source_id: ID!, - "unique Id of destination requisition list" - destination_id: ID, # If null new requisition list will be created - "selected requisition list items that are to be moved from source" - item_ids: [ID!]! - ): MoveItemsFromRequisitionListOutput + source_id: ID!, @doc(description: "unique Id of source requisition list") + destination_id: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + item_ids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") + ): MoveItemsFromRequisitionListOutput @doc(description: "Move Items from Requisition List to another requisition List") } type RemoveRequisitionListItemsOutput { @@ -244,25 +151,20 @@ type AddProductsToRequisitionListOutput { } input RequisitionListFilterInput { - "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs" - ids: FilterEqualTypeInput, - "Filter by display name of the Requisition list" - name: FilterMatchTypeInput + ids: FilterEqualTypeInput, @doc(description: "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs") + name: FilterMatchTypeInput @doc(description: "Filter by display name of the Requisition list") } input RequisitionListItemsInput { sku: String! quantity: Float parent_sku: String - "selected option ID" - selected_options: [String!] - "entered Options ID" - entered_options: [EnteredOptionInput!] + selected_options: [String!] @doc(description: "selected option ID") + entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") } input UpdateRequisitionListItemsInput { - "unique ID of Requisition List Item" - item_id: ID! + item_id: ID! @doc(description: "unique ID of Requisition List Item") customizable_options: [CustomizableOptionInput] quantity: Float } @@ -284,13 +186,10 @@ type AddRequisitionListItemToCartOutput { } type CopyItemsFromRequisitionListOutput { - "Destination Requisition List" - list : RequisitionList # since source requisition list is not mutated it is not part of the output + list : RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output } type MoveItemsFromRequisitionListOutput { - "Source Requisition List" - source : RequisitionList - "Destination Requisition List" - destination : RequisitionList + source : RequisitionList @doc(description: "Source Requisition List") + destination : RequisitionList @doc(description: "Destination Requisition List") } From e4bcd5d7439915510c9af37213c88d8a9b08c02c Mon Sep 17 00:00:00 2001 From: Arvind Date: Thu, 4 Jun 2020 18:22:14 +0530 Subject: [PATCH 071/479] review comments fixed --- .../coverage/negotiableQuotes.graphqls | 57 ++----------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 14f6f5983..e73e5e726 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,8 +1,4 @@ type Mutation { - recalculateNegotiableQuotePrices( - quote_ids: [String]! - ): RecalculateNegotiableQuotePricesOutput! @doc(description:" This call refreshes item prices, taxes, discounts, cart rules in the negotiable quote. Quotes that are locked for the seller will not be updated.") - requestNegotiableQuote( negotiableQuote: NegotiableQuoteRequestInput! ): RequestNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") @@ -23,26 +19,21 @@ type Customer { ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") estimateShippingCostForNegotiableQuote( - address: NegotiableQuoteShipAddressInput! + address: NegotiableQuoteShippingAddressInput! ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote") estimateShippingCostByAddressIdForNegotiableQuote( address_id: String! ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote by Address ID") - - estimateShippingInformationForNegotiableQuote( - address_info: NegotiableQuoteAddressInformationInput! - ): NegotiableQuotePaymentInfo! @doc(description:"Get payment options and the order totals by address and shipping info") } type NegotiableQuote { items: [NegotiableQuoteItem] @doc(description: "Negotiable quote item") - billing_address: BillingAddress @doc(description: "Billing address of negotiable quote") attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") totals: TotalsOutput! @doc(description: "Negotiable quote totals output") payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") - available_payment_methods: [AvailablePaymentMethod] @doc(description: "Available payment methods") + available_payment_methods: [PaymentMethodsOutput] @doc(description: "Available payment methods") } input NegotiableQuoteFilterInput { @@ -56,25 +47,6 @@ type NegotiableQuoteItem { product: ProductInterface! } -type BillingAddress { - id: ID! @doc(description: "The ID of the billing address") - firstname: String @doc(description: "The first name of the person associated with the billing address") - lastname: String @doc(description: "The family name of the person associated with the billing address") - customer_id: String @doc(description: "The customer ID") - email: String @doc(description: "The customer email id") - company: String @doc(description: "The customer's company") - telephone: String @doc(description: "The telephone number") - street: [String] @doc(description: "A list of strings that define the street number and name") - city: String @doc(description: "The city or town") - region: String @doc(description: "The region name") - region_id: String @doc(description: "The region ID") - region_code: String @doc(description: "The region code") - postcode: String @doc(description: "The customer's ZIP or postal code") - country_code: String @doc(description: "The country code") - same_as_billing: Int @doc(description: "Billing address confirmation") - save_in_address_book: Int @doc(description: "Save billing address in address book") -} - type AttachmentContent @doc(description: "Negotiable quote attachment file") { base64_encoded_data: String! mime_type: String! @@ -106,11 +78,6 @@ type NegotiableQuotePaymentInfo { totals: TotalsOutput! @doc(description: "List of totals") } -type AvailablePaymentMethod { - code: String! @doc(description: "The payment method code") - title: String! @doc(description: "The payment method title.") -} - type PaymentMethodsOutput { code: String @doc(description: "Payment method code") title: String @doc(description: "Payment method title") @@ -136,7 +103,7 @@ type CartDiscount { } input NegotiableQuoteRequestInput { - quote_id: String! @doc(description: "Quote hash") + quote_id: ID! @doc(description: "Quote hash") quote_name: String @doc(description: "Quote name") comment: String @doc(description: "Comment") files: [FileInput] @doc(description: "Attached files") @@ -152,12 +119,7 @@ input FileInput @doc(description: "The list of file attachment codes") { name: String! } -type RecalculateNegotiableQuotePricesOutput{ - negotiable_quote:NegotiableQuote - price_updated: Boolean! -} - -input NegotiableQuoteShipAddressInput { +input NegotiableQuoteShippingAddressInput { firstname: String! @doc(description: "The first name of the person associated with the billing address") lastname: String! @doc(description: "The family name of the person associated with the billing address") telephone: String! @doc(description: "The telephone number") @@ -182,13 +144,6 @@ type ShippingMethodsOutput { price_incl_tax: String @doc(description: "Shipping method price with inclusive tax") } -input NegotiableQuoteAddressInformationInput { - shipping_address: NegotiableQuoteShipAddressInput @doc(description: "Shipping address assigned to negotiable quote") - billing_address: NegotiableQuoteBillAddressInput @doc(description: "Billing address assigned to negotiable quote") - shipping_carrier_code: String! @doc(description: "Shipping carrier code") - shipping_method_code: String! @doc(description: "Shipping method code") -} - input NegotiableQuoteBillAddressInput { firstname: String! @doc(description: "The first name of the person associated with the billing address") lastname: String! @doc(description: "The family name of the person associated with the billing address") @@ -202,8 +157,8 @@ input NegotiableQuoteBillAddressInput { } input NegotiableQuoteShippingMethodInput { - quote_id: String! @doc(description: "Quote hash") - shipping_method: String @doc(description: "Input for shipping method") + quote_id: ID! @doc(description: "Quote hash") + shipping_method: [String!]! @doc(description: "Input for shipping method") } type setShippingMethodsOnNegotiableQuoteOutput { From a4556fd08000c34794cd724a4121174b6d6aa1d8 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 4 Jun 2020 17:15:41 -0500 Subject: [PATCH 072/479] customer_order: Fixes on shipping_handling schema fields --- design-documents/graph-ql/coverage/customer-orders.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index a6976ec76..c1f667f97 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -153,9 +153,10 @@ type OrderTotal implements SalesTotalAmountInterface { @doc("Shipping handling details") type ShippingHandling { total_amount: Money! @doc("shipping total amount") - amount_inc_tax: Money @doc("shipping amount including tax") - amount_exc_tax: Money @doc("shipping amount excluding tax") + amount_including_tax: Money @doc("shipping amount including tax") + amount_excluding_tax: Money @doc("shipping amount excluding tax") taxes: [TaxItem] @doc("shipping taxes details") + discounts: [Discount] @doc(description: "The applied discounts to the shipping) } @doc("Tax item details") From ac538b71649714614e2ebfc34bfa26c5f1dcf7fb Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 4 Jun 2020 17:24:58 -0500 Subject: [PATCH 073/479] - add FilterStringTypeInput - correct errata --- design-documents/graph-ql/coverage/customer-orders.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index c1f667f97..700be69c9 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -27,7 +27,7 @@ type CustomerOrders { ```graphql @doc("Allows to extend the list of search criteria for customer orders") input CustomerOrdersFilterInput { - number: String @doc("Order number. Allows to filter orders by fully or partial entered number") + number: FilterStringTypeInput @doc("Order number. Allows to filter orders by fully or partial entered number") status: String @("Order status") createdDate: FilterRangeTypeInput total: CustomerOrdersAmountFilterInput @@ -45,6 +45,13 @@ input SalesItemFilterInput { name: String @doc("Order item name. Allows to filter orders by fully or partial entered order item name") sku: String @doc("Order item SKU. Allows to filter orders by fully or partial entered order item SKU") } + +@doc( "Defines a filter for an input string.") +input FilterStringTypeInput { + in: [String] @doc("Filters items that are exactly the same as entries specified in an array of strings.") + eq: String @doc("Filters items that are exactly the same as the specified string.") + match: String @doc("Defines a filter that performs a fuzzy search using the specified string.") +} ``` > Right now, we don't introduce the filter input for such entities like invoice, credit memo, shipment as all these entities are related to the same order and GraphQL resolvers anyway at first resolve the order and after that other child entities. But, in future, such filter input can be introduced as optional argument. @@ -218,7 +225,7 @@ type CreditMemo { id: ID! @doc("the ID of the credit memo, used for API purposes") number: String! @doc("sequential credit memo number") items: [CreditMemoItem] @doc("items refunded") - total: CredtiMemoTotal @doc("refund total amount details") + total: CreditMemoTotal @doc("refund total amount details") comments: [CommentItem] @doc("comments on the credit memo") } From 42497794e1c68b304392c8fce4fd903cf654ed14 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 4 Jun 2020 20:26:40 -0500 Subject: [PATCH 074/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 17 ++++- .../graph-ql/coverage/gift-registry.md | 66 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index ab5428c27..d782e13d0 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -40,6 +40,12 @@ input AddGiftRegistryItemInput { entered_options: [EnteredOptionInput!] } +# Should be defined in scope of https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md +input EnteredOptionInput { + id: String! + value: String! +} + input UpdateGiftRegistryItemInput { id: ID! quantity: Float! @@ -67,7 +73,7 @@ input CreateGiftRegistryInput { dynamic_attributes: [GiftRegistryDynamicAttributeInput] } -input GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided") { +input GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided. In case both are provided, validation will fail") { address_data: CustomerAddressInput address_id: Int } @@ -160,14 +166,19 @@ enum GiftRegistryDateAttributeFormat { FULL } -type GiftRegistryCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface { +type GiftRegistryCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { show_region: Boolean! } +type GiftRegistryRegionAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { +} -type GiftRegistryEventCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface { +type GiftRegistryEventCountryAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { show_region: Boolean! } +type GiftRegistryEventRegionAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface, GiftRegistrySelectAttributeMetadataInterface { +} + type GiftRegistryEventDateAttributeMetadata implements GiftRegistryDynamicAttributeMetadataInterface, GiftRegistrySearcheableAttributeMetadataInterface { format: GiftRegistryDateAttributeFormat! } diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 391257c14..64a37983a 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -2,6 +2,72 @@ ### Registered customer creates a new gift registry +First, get a list of available gift registry types and dynamic attributes metadata to render the gift registry creation form: +```graphql +{ + giftRegistryTypes { + id + label + dynamic_attributes_metadata { + code + label + attribute_group + input_type + is_required + sort_order + ... on GiftRegistryCountryAttributeMetadata { + show_region + } + ... on GiftRegistryEventCountryAttributeMetadata { + show_region + } + ... on GiftRegistrySearcheableAttributeMetadataInterface { + is_searcheable + } + ... on GiftRegistryEventDateAttributeMetadata { + format + } + ... on GiftRegistrySelectAttributeMetadataInterface { + options { + code + is_default + label + } + } + } + } +} +``` + +Then create a new gift registry based on the user input: +```graphql +mutation { + createGiftRegistry( + gift_registry: { + id: "optional client-generated ID" + event_name: "My Birthday" + type_id: "2" + message: "Pleas come to my birthday" + privacy_settings: PUBLIC + status: ACTIVE + shipping_address: { + address_id: 3 + } + dynamic_attributes: [ + { + code: "event_country" + value: "US" + }, + { + code: "event_date" + value: "6/2/20" + } + ] + } + ) +} +``` + ### Gift registry owner modifies an existing gift registry ### Gift registry owner removes items from an existing gift registry From 87c09eea807520d5076787a2b1ddf95335ec637c Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Sun, 7 Jun 2020 17:01:57 -0500 Subject: [PATCH 075/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 20 ++- .../graph-ql/coverage/gift-registry.md | 124 +++++++++++++++--- 2 files changed, 123 insertions(+), 21 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index d782e13d0..c3b54d77d 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -27,6 +27,20 @@ type Mutation { addGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput updateGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput removeGiftRegistryRegistrants(gift_registry_id: ID!, registrant_ids: [ID!]!): RemoveGiftRegistryRegistrantsOutput + + shareGiftRegistry(id: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput +} + +input ShareGiftRegistryInviteeInput +{ + name: String! + email: String! +} + +input ShareGiftRegistrySenderInput +{ + name: String! + message: String! } input AddGiftRegistryItemInput { @@ -107,7 +121,7 @@ type UpdateGiftRegistryOutput { } type RemoveGiftRegistryOutput { - gift_registry: GiftRegistry + is_removed: Boolean! } type AddGiftRegistryItemsOutput { @@ -134,6 +148,10 @@ type RemoveGiftRegistryRegistrantsOutput { gift_registry: GiftRegistry } +type ShareGiftRegistryOutput { + gift_registry: GiftRegistry +} + type GiftRegistryType { id: ID! label: String! diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 64a37983a..f157f7a2b 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -39,57 +39,141 @@ First, get a list of available gift registry types and dynamic attributes metada } ``` -Then create a new gift registry based on the user input: +Then create a new gift registry based on the user input. Registrants are added using a separate mutation, which can be sent in the same request if gift registry ID is client-side generated. ```graphql -mutation { - createGiftRegistry( - gift_registry: { - id: "optional client-generated ID" - event_name: "My Birthday" - type_id: "2" - message: "Pleas come to my birthday" - privacy_settings: PUBLIC - status: ACTIVE - shipping_address: { - address_id: 3 +# In real query only one should be provided: existing address ID OR address data +mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistryInput!, $giftRegistryId: ID!, $registrantsData: [AddGiftRegistryRegistrantInput!]!) { + createGiftRegistry(gift_registry: $giftRegistryData) { + gift_registry { + id + event_name + shipping_address { + id + street + } + } + } + addGiftRegistryRegistrants(gift_registry_id: $giftRegistryId, registrants: $registrantsData) { + gift_registry { + registrants { + id + first_name + last_name + email + dynamic_attributes { + code + value + label + } } - dynamic_attributes: [ + } + } +} +``` +The following JSON represents query variables for the `CreateGiftRegistryWithRegistrants` mutation above. +```json +{ + "giftRegistryId": "optional client-generated ID", + "giftRegistryData": { + "id": "optional client-generated ID", + "event_name": "My Birthday", + "type_id": "2", + "message": "Pleas come to my birthday", + "privacy_settings":"PUBLIC", + "status": "ACTIVE", + "shipping_address": { + "address_id": 3, + "address_data": { + "firstname": "John", + "lastname": "Doe", + "street": ["123 Some Avenue"], + "city": "Austin", + "region": { + "region_code": "TX" + }, + "postcode": "78758", + "company": "Magento", + "country_code": "US" + } + }, + "dynamic_attributes": [ { - code: "event_country" - value: "US" + "code": "event_country", + "value": "US" }, { - code: "event_date" - value: "6/2/20" + "code": "event_date", + "value": "6/2/20" + } + ] + }, + "registrantsData": [ + { + "email": "John@example.com", + "first_name": "John", + "last_name": "Roller", + "dynamic_attributes": [ + { + "code": "diet", + "value": "none" } ] } - ) + ] } ``` +### Gift registry owner views the list of the gift registries created earlier + ++ + +### Gift registry owner views the existing gift registry details + ++ + ### Gift registry owner modifies an existing gift registry ++ + ### Gift registry owner removes items from an existing gift registry ++ + ### Gift registry owner removes an existing gift registry ++ + ### Gift registry owner adds items to the gift registry from cart +? + ### Gift registry owner adds items to the gift registry from wish list -### Gift registry owner shares a gift registry with friends +? ### Gift registry visitor adds items from the gift registry to the cart +? + ### Storefront application retrieves gift registry search form metadata +? + ### Gift registry visitor searches a gift registry by the recipient name ++ Search by registrant name and dynamic attributes. Explicitly excluded scenarios: search by ID and by email. + +### Gift registry owner shares a gift registry with friends + +When gift registry is shared with the invitees, the email they receive will contain a link. +This link will contain gift registry hash as query parameter and should lead to the page processed by the storefront application. +The application should parse the URL, extract gift registry ID hash and query gift registry details by ID. + + ### Gift registry visitor opens a gift registry using the link from email -### Storefront application retrieves gift registry edit form metadata + From 73faa1648bbfc8f9cee9bed55d95cef4bd798f5b Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 8 Jun 2020 10:28:28 -0500 Subject: [PATCH 076/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.md | 190 +++++++++++++++++- 1 file changed, 181 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index f157f7a2b..ae49616d7 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -125,23 +125,192 @@ The following JSON represents query variables for the `CreateGiftRegistryWithReg ### Gift registry owner views the list of the gift registries created earlier -+ - -### Gift registry owner views the existing gift registry details +```graphql +{ + customer { + gift_registry_list { + id + event_name + created_on + message + } + } +} +``` -+ +### Gift registry owner views and modifies an existing gift registry on edit page -### Gift registry owner modifies an existing gift registry +Render the edit gift registry form and populate it with current gift registry properties: -+ +```graphql +{ + customer { + gift_registry(id: "ID obtained from the list query") { + event_name + message + privacy_settings + status + registrants { + id + first_name + last_name + email + dynamic_attributes { + code + label + value + } + } + shipping_address { + id + firstname + lastname + company + street + city + region { + region + region_code + } + postcode + country_code + telephone + fax + } + dynamic_attributes { + code + group + label + value + } + type { + label + dynamic_attributes_metadata { + code + label + attribute_group + input_type + is_required + sort_order + ... on GiftRegistryCountryAttributeMetadata { + show_region + } + ... on GiftRegistryEventCountryAttributeMetadata { + show_region + } + ... on GiftRegistrySearcheableAttributeMetadataInterface { + is_searcheable + } + ... on GiftRegistryEventDateAttributeMetadata { + format + } + ... on GiftRegistrySelectAttributeMetadataInterface { + options { + code + is_default + label + } + } + } + } + } + } +} +``` -### Gift registry owner removes items from an existing gift registry +Modify gift registry data: -+ +```graphql +mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryData: UpdateGiftRegistryInput!, $registrantsData: [UpdateGiftRegistryRegistrantInput!]!) { + updateGiftRegistry(id: $giftRegistryId, gift_registry: $giftRegistryData) { + gift_registry { + id + event_name + shipping_address { + id + street + } + } + } + updateGiftRegistryRegistrants(gift_registry_id: $giftRegistryId, registrants: $registrantsData) { + gift_registry { + registrants { + id + first_name + last_name + email + dynamic_attributes { + code + value + label + } + } + } + } +} +``` +The JSON below should be used as query variable for the gift registry update mutation above: +```json +{ + "giftRegistryId": "existing-gift-registry-ID", + "giftRegistryData": { + "event_name": "My Birthday", + "type_id": "2", + "message": "Pleas come to my birthday", + "privacy_settings":"PUBLIC", + "status": "ACTIVE", + "shipping_address": { + "address_id": 3, + "address_data": { + "firstname": "John", + "lastname": "Doe", + "street": ["123 Some Avenue"], + "city": "Austin", + "region": { + "region_code": "TX" + }, + "postcode": "78758", + "company": "Magento", + "country_code": "US" + } + }, + "dynamic_attributes": [ + { + "code": "event_country", + "value": "US" + }, + { + "code": "event_date", + "value": "6/2/20" + } + ] + }, + "registrantsData": [ + { + "id": "existing-registrant-id", + "email": "John@example.com", + "first_name": "John", + "last_name": "Roller", + "dynamic_attributes": [ + { + "code": "diet", + "value": "none" + } + ] + } + ] +} +``` ### Gift registry owner removes an existing gift registry -+ +```graphql +mutation RemoveGiftRegistry($giftRegistryId: ID!) { + removeGiftRegistry(id: $giftRegistryId) { + is_removed + } +} +``` ### Gift registry owner adds items to the gift registry from cart @@ -155,6 +324,9 @@ The following JSON represents query variables for the `CreateGiftRegistryWithReg ? +### Gift registry owner removes items from an existing gift registry + + ### Storefront application retrieves gift registry search form metadata ? From 486a9b157c0cac22c670003e988052f089a293c6 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 8 Jun 2020 12:11:25 -0500 Subject: [PATCH 077/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 30 +-- .../graph-ql/coverage/gift-registry.md | 182 +++++++++++++++++- 2 files changed, 184 insertions(+), 28 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index c3b54d77d..c5754cc24 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -243,32 +243,28 @@ interface GiftRegistryItemInterface { note: String added_on: String! product: ProductInterface + selected_customizable_options: [SelectedCustomizableOption] } type SimpleGiftRegistryItem implements GiftRegistryItemInterface { - customizable_options: [SelectedCustomizableOption] } type BundleGiftRegistryItem implements GiftRegistryItemInterface { - customizable_options: [SelectedCustomizableOption] - bundle_options: [SelectedBundleOption!] + selected_bundle_options: [SelectedBundleOption!] } type ConfigurableGiftRegistryItem implements GiftRegistryItemInterface { - customizable_options: [SelectedCustomizableOption] - configurable_options: [SelectedConfigurableOption!] + selected_configurable_options: [SelectedConfigurableOption!] } # Not currently supported by Magento core type DownloadableGiftRegistryItem implements GiftRegistryItemInterface { - customizable_options: [SelectedCustomizableOption] links: [DownloadableProductLinks] samples: [DownloadableProductSamples] } # Not currently supported by Magento core type VirtualGiftRegistryItem implements GiftRegistryItemInterface { - customizable_options: [SelectedCustomizableOption] } # Not currently supported by Magento core @@ -277,7 +273,6 @@ type GiftCardGiftRegistryItem implements GiftRegistryItemInterface { recepient_name: String! amount: Money! message: String - customizable_options: [SelectedCustomizableOption] } interface GiftRegistryDynamicAttributeInterface { @@ -324,22 +319,3 @@ type StoreConfig { magento_giftregistry_general_enabled : String magento_giftregistry_general_max_registrant : String } - -# This directicve is declared here to make the schema valid. It is already declared in Magento -directive @doc(description: String="") on QUERY - | MUTATION - | FIELD - | FRAGMENT_DEFINITION - | FRAGMENT_SPREAD - | INLINE_FRAGMENT - | SCHEMA - | SCALAR - | OBJECT - | FIELD_DEFINITION - | ARGUMENT_DEFINITION - | INTERFACE - | UNION - | ENUM - | ENUM_VALUE - | INPUT_OBJECT - | INPUT_FIELD_DEFINITION diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index ae49616d7..722d826c8 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -314,7 +314,187 @@ mutation RemoveGiftRegistry($giftRegistryId: ID!) { ### Gift registry owner adds items to the gift registry from cart -? +First get the cart item data which can be used to add a new item to the gift registry: + +```graphql +{ + customerCart { + items { + id + quantity + product { + sku + } + ... on SimpleCartItem { + customizable_options { + id + values { + id + value + } + } + } + ... on VirtualCartItem { + customizable_options { + id + values { + id + value + } + } + } + ... on DownloadableCartItem { + customizable_options { + id + values { + id + } + } + } + ... on BundleCartItem { + bundle_options { + id + values { + id + quantity + } + } + customizable_options { + id + values { + id + value + } + } + } + ... on ConfigurableCartItem { + configurable_options { + id + value_id + } + customizable_options { + id + values { + id + value + } + } + } + } + } +} + +``` + +Based on that information we can send a mutation to add the selected item to gift registry. + +```graphql +mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGiftRegistryItemInput!]!) { + addGiftRegistryItems(gift_registry_id: $giftRegistryId, items: $giftRegistryItems) { + gift_registry { + event_name + items { + id + product { + name + thumbnail { + url + } + } + selected_customizable_options { + id + is_required + label + sort_order + values { + id + price { + type + units + value + } + value + label + } + } + added_on + note + quantity + quantity_fulfilled + ... on BundleGiftRegistryItem { + selected_bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + ... on ConfigurableGiftRegistryItem { + selected_configurable_options { + id + option_label + value_id + value_label + } + } + ... on DownloadableGiftRegistryItem { + links { + price + sample_url + sort_order + title + } + samples { + sample_url + sort_order + title + } + } + ... on GiftCardGiftRegistryItem { + sender_name + recepient_name + amount { + currency + value + } + message + } + } + } + } +} +``` + +The following JSON should be provided as query variables for the mutation above: + +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "custom-hat-red", + "quantity": 2.0, + "note": "Really like this color", + "parent_sku": "custom-hat", + "selected_options": [ + "hash from the color option ID and its value ID", + "hash from the size option ID and its value ID" + ], + "entered_options": [ + { + "id": "hash from custom phrase option ID", + "value": "Custom Hat" + } + ] + } + ] +} +``` ### Gift registry owner adds items to the gift registry from wish list From 4ce0e9f6813a4babe8bcc611981eebe67b2e2b26 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 8 Jun 2020 13:56:41 -0500 Subject: [PATCH 078/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 722d826c8..23e99f022 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -498,7 +498,22 @@ The following JSON should be provided as query variables for the mutation above: ### Gift registry owner adds items to the gift registry from wish list -? +Wishlist query does not support fetching the information about selected options and need to be extended. After that the same mutation can be used to add items to gift registry as describe above in the use case of adding items to gift registry from cart. + +```graphql +{ + customer { + wishlist { + items { + qty + product { + sku + } + } + } + } +} +``` ### Gift registry visitor adds items from the gift registry to the cart From fbb85dc401b5f5638ccbde7b423b4ccb19d7ec1a Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 8 Jun 2020 16:11:02 -0500 Subject: [PATCH 079/479] ECP-711: GraphQL schema for Gift Registry - Documented current state of cart schema --- .../graph-ql/coverage/Cart.graphqls | 118 ++++++++++++++++-- .../graph-ql/coverage/add-items-to-cart.md | 47 ------- .../AddBundleProductToCart.graphqls | 59 --------- .../AddConfigurableProductToCart.graphqls | 43 ------- .../AddDownloadableProductToCart.graphqls | 45 ------- .../AddGiftCardProductToCart.graphqls | 49 -------- .../AddGroupedProductToCart.graphqls | 27 ---- .../AddSimpleProductToCart.graphqls | 78 ------------ .../AddVirtualProductToCart.graphqls | 34 ----- .../RemoveItemFromCart.graphqls | 12 -- 10 files changed, 108 insertions(+), 404 deletions(-) delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart.md delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddBundleProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddConfigurableProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddDownloadableProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddGiftCardProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddGroupedProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddSimpleProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/AddVirtualProductToCart.graphqls delete mode 100644 design-documents/graph-ql/coverage/add-items-to-cart/RemoveItemFromCart.graphqls diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index 2cf44bac2..520bb0f28 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -11,22 +11,120 @@ type CartQueryOutput { } type Cart { + id: ID! @doc(description: "The ID of the cart.") + items: [CartItemInterface] + applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") + email: String + shipping_addresses: [ShippingCartAddress]! + billing_address: BillingCartAddress + available_payment_methods: [AvailablePaymentMethod] @doc(description: "Available payment methods") + selected_payment_method: SelectedPaymentMethod + prices: CartPrices + total_quantity: Float! + is_virtual: Boolean! +} + +type AvailablePaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") +} + +type SelectedPaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") + purchase_order_number: String @doc(description: "The purchase order number.") +} + +interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { id: String! + quantity: Float! + prices: CartItemPrices + product: ProductInterface! +} + +type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") { + customizable_options: [SelectedCustomizableOption] +} - line_items_count: Int! - items_quantity: Float! +type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") { + customizable_options: [SelectedCustomizableOption] +} - selected_payment_method: CheckoutPaymentMethod - available_payment_methods: [CheckoutPaymentMethod]! +type ConfigurableCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! + configurable_options: [SelectedConfigurableOption!]! +} - customer: CheckoutCustomer - customer_notes: String +type SelectedConfigurableOption { + id: Int! + option_label: String! + value_id: Int! + value_label: String! +} - gift_cards_amount_used: Money - applied_gift_cards: [CartGiftCard] +type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { + customizable_options: [SelectedCustomizableOption] + links: [DownloadableProductLinks] @doc(description: "An array containing information about the links for the added to cart downloadable product") + samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") +} - is_multishipping: Boolean! - is_virtual: Boolean! +type DownloadableProductLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { + title: String @doc(description: "The display name of the link") + sort_order: Int @doc(description: "A number indicating the sort order") + price: Float @doc(description: "The price of the downloadable product") + sample_url: String @doc(description: "URL to the downloadable sample") +} + +type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { + title: String @doc(description: "The display name of the sample") + sort_order: Int @doc(description: "A number indicating the sort order") + sample_url: String @doc(description: "URL to the downloadable sample") +} + +type BundleCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! + bundle_options: [SelectedBundleOption!]! +} + +type SelectedBundleOption { + id: Int! + label: String! + type: String! + values: [SelectedBundleOptionValue!]! +} + +type SelectedBundleOptionValue { + id: Int! + label: String! + quantity: Float! + price: Float! +} + +type SelectedCustomizableOption { + id: Int! + label: String! + is_required: Boolean! + values: [SelectedCustomizableOptionValue!]! + sort_order: Int! +} + +type SelectedCustomizableOptionValue { + id: Int! + label: String! + value: String! + price: CartItemSelectedOptionValuePrice! +} + +type CartItemSelectedOptionValuePrice { + value: Float! + units: String! + type: PriceTypeEnum! +} + +enum PriceTypeEnum @doc(description: "This enumeration the price type.") { + FIXED + PERCENT + DYNAMIC } type CheckoutCustomer { diff --git a/design-documents/graph-ql/coverage/add-items-to-cart.md b/design-documents/graph-ql/coverage/add-items-to-cart.md deleted file mode 100644 index 3f58d4981..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart.md +++ /dev/null @@ -1,47 +0,0 @@ -**Overview** - -:warning: Current proposal is deprecated in favor of [add-items-to-cart-single-mutation.md](add-items-to-cart-single-mutation.md) - -As a Magento developer, I need to manipulate the shopping cart via GraphQL so that I can programmatically create orders on behalf of a shopper. - -GraphQL needs to provide sufficient mutations (ways to create/update/delete data) for a developer to build out the storefront checkout experience for a shopper. - -**Use cases:** -- Both guest and registered shoppers can add new items to cart -- Both guest and registered shoppers can update item qty in cart -- Both guest and registered shoppers can remove items from cart -- Both guest and registered shoppers can update the configuration (for a configurable product) or quantity of a previously added configurable product in cart - - Edit Item link > Product page > Update configuration or qty > Update Cart - -**Main decision points:** - -- Separate mutations for each product type while adding items to cart. Each operation will be supporting bulk use case -- Uniform interface for guest vs customer -- Separate mutations for each checkout step - - Create empty cart - - Add items to cart - - Set shipment method - - Set payment method - - Set addresses - - Same granularity for updates and removals -- Possibility to combine mutations for checkout steps -- Can create "order in one call" mutation in the future if needed -- Hashed IDs for cart items -- Single input object -- Async nature of the flow must be supported on the client side (via AJAX calls) -- Server-side asynchronous mutations can be supported in the future on framework level in a way similar to Async REST. - -**Proposed schema for adding items to cart:** - -- [AddSimpleProductToCart](add-items-to-cart/AddSimpleProductToCart.graphqls) -- [AddBundleProductToCart](add-items-to-cart/AddBundleProductToCart.graphqls) -- [AddConfigurableProductToCart](add-items-to-cart/AddConfigurableProductToCart.graphqls) -- [AddDownloadableProductToCart](add-items-to-cart/AddDownloadableProductToCart.graphqls) -- [AddGiftCardProductToCart](add-items-to-cart/AddGiftCardProductToCart.graphqls) -- [AddGroupedProductToCart](add-items-to-cart/AddGroupedProductToCart.graphqls) -- [AddVirtualProductToCart](add-items-to-cart/AddVirtualProductToCart.graphqls) - - -**My Account area impacted:** -- Cart -- Minicart diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddBundleProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddBundleProductToCart.graphqls deleted file mode 100644 index e92f12223..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddBundleProductToCart.graphqls +++ /dev/null @@ -1,59 +0,0 @@ -type Mutation { - addBundleProductsToCart(input: AddBundleProductsToCartInput): AddBundleProductsToCartOutput - updateBundleProductsInCart(input: UpdateBundleProductsInCartInput): UpdateBundleProductsInCartOutput -} - -input UpdateBundleProductsInCartInput { - cart_id: String! - cartItems: [UpdateBundleProductCartItemInput!]! -} - -input UpdateBundleProductCartItemInput { - details: UpdateCartItemDetailsInput! - bundle_options:[BundleOptionInput!] - customizable_options:[CustomizableOptionInput] -} - -input AddBundleProductsToCartInput { - cart_id: String! - cartItems: [BundleProductCartItemInput!]! -} - -input BundleProductCartItemInput { - details: CartItemDetailsInput! - bundle_options:[BundleOptionInput!]! - customizable_options:[CustomizableOptionInput!] -} - -input BundleOptionInput { - id: Int! - quantity: Float! - value: [String!]! -} - -type AddBundleProductsToCartOutput { - cart: Cart! -} - -type BundleCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption]! - bundle_options: [SelectedBundleOption!]! -} - -type SelectedBundleOption { - id: Int! - label: String! - type: String! - # No quantity here even though it is set on option level in the input - values: [SelectedBundleOptionValue!]! - sort_order: Int! -} - -type SelectedBundleOptionValue { - id: Int! - label: String! - quantity: Float! # Quantity is displayed on option value level, while is set on option level - price: CartItemSelectedOptionValuePrice! - sort_order: Int! -} - diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddConfigurableProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddConfigurableProductToCart.graphqls deleted file mode 100644 index 7dc069af0..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddConfigurableProductToCart.graphqls +++ /dev/null @@ -1,43 +0,0 @@ -type Mutation { - addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput - updateConfigurableProductsInCart(input: UpdateConfigurableProductsInCartInput): UpdateConfigurableProductsInCartOutput -} - -input UpdateConfigurableProductsInCartInput { - cart_id: String! - cartItems: [UpdateConfigurableProductCartItemInput!]! -} - -input UpdateConfigurableProductCartItemInput { - details: UpdateCartItemDetailsInput! - variant_sku: String - customizable_options:[CustomizableOptionInput] -} - -input AddConfigurableProductsToCartInput { - cart_id: String! - cartItems: [ConfigurableProductCartItemInput!]! -} - -input ConfigurableProductCartItemInput { - details: CartItemDetailsInput! - variant_sku: String! - customizable_options:[CustomizableOptionInput!] -} - -type AddConfigurableProductsToCartOutput { - cart: Cart! -} - -type ConfigurableCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption]! - configurable_options: [SelectedConfigurableOption!]! -} - -type SelectedConfigurableOption { - id: Int! - option_label: String! - value_id: Int! - value_label: String! -} - diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddDownloadableProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddDownloadableProductToCart.graphqls deleted file mode 100644 index 5ac44c289..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddDownloadableProductToCart.graphqls +++ /dev/null @@ -1,45 +0,0 @@ -type Mutation { - addDownloadableProductsToCart(input: AddDownloadableProductsToCartInput): AddDownloadableProductsToCartOutput - updateDownloadableProductsInCart(input: UpdateDownloadableProductsInCartInput): UpdateDownloadableProductsInCartOutput -} - -input UpdateDownloadableProductsInCartInput { - cart_id: String! - cartItems: [UpdateDownloadableProductCartItemInput!]! -} - -input UpdateDownloadableProductCartItemInput { - details: UpdateCartItemDetailsInput! - downloadable_links: [DownloadableLinksInput] - customizable_options:[CustomizableOptionInput] -} - -input AddDownloadableProductsToCartInput { - cart_id: String! - cartItems: [DownloadableProductCartItemInput!]! -} - -input DownloadableProductCartItemInput { - details: CartItemDetailsInput! - downloadable_links: [DownloadableLinksInput!] - customizable_options:[CustomizableOptionInput!] -} - -input DownloadableLinksInput { - id: [Int!]! -} - -type AddDownloadableProductsToCartOutput { - cart: Cart! -} - -type DownloadableCartItem implements CartItemInterface { - links_label: String! - links: [DownloadableCartItemLink!]! - configurable_options: [SelectedConfigurableOption!]! -} - -type DownloadableCartItemLink { - id: Int! - label: String! -} diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddGiftCardProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddGiftCardProductToCart.graphqls deleted file mode 100644 index 92ae1ddb6..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddGiftCardProductToCart.graphqls +++ /dev/null @@ -1,49 +0,0 @@ -type Mutation { - addGiftCardProductsToCart(input: AddGiftCardProductsToCartInput): AddGiftCardProductsToCartOutput - updateGiftCardProductsInCart(input: UpdateGiftCardProductsInCartInput): UpdateGiftCardProductsInCartOutput -} - -input UpdateGiftCardProductsInCartInput { - cart_id: String! - cartItems: [UpdateGiftCardProductCartItemInput!]! -} - -input UpdateGiftCardProductCartItemInput { - details: UpdateCartItemDetailsInput! - sender_name: String - recepient_name: String - amount: MoneyInput # Do we need complex type here or just Float? - message: String - customizable_options:[CustomizableOptionInput] -} - -input AddGiftCardProductsToCartInput { - cart_id: String! - cartItems: [GiftCardProductCartItemInput!]! -} - -input GiftCardProductCartItemInput { - details: CartItemDetailsInput! - sender_name: String! - recepient_name: String! - amount: MoneyInput! # Do we need complex type here or just Float? - message: String - customizable_options:[CustomizableOptionInput!] -} - -input MoneyInput { - value: Float @doc(description: "A number expressing a monetary value") - currency: CurrencyEnum @doc(description: "A three-letter currency code, such as USD or EUR") -} - -type AddGiftCardProductsToCartOutput { - cart: Cart! -} - -type GiftCardCartItem implements CartItemInterface { - sender_name: String! - recepient_name: String! - amount: Money! - message: String - customizable_options: [SelectedCustomizableOption]! -} diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddGroupedProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddGroupedProductToCart.graphqls deleted file mode 100644 index 38f97184f..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddGroupedProductToCart.graphqls +++ /dev/null @@ -1,27 +0,0 @@ -type Mutation { - addGroupedProductsToCart(input: AddGroupedProductsToCartInput): AddGroupedProductsToCartOutput - updateGroupedProductsInCart(input: UpdateGroupedProductsInCartInput): UpdateGroupedProductsInCartOutput -} - -input UpdateGroupedProductsInCartInput { - cart_id: String! - cartItems: [UpdateGroupedProductCartItemInput!]! -} - -input UpdateGroupedProductCartItemInput { - details: UpdateCartItemDetailsInput! -} - -input AddGroupedProductsToCartInput { - cart_id: String! - cartItems: [GroupedProductCartItemInput!]! -} - -input GroupedProductCartItemInput { - details: CartItemDetailsInput! - # the difference from simple products is that grouped products do not support customizable options -} - -type AddGroupedProductsToCartOutput { - cart: Cart! -} diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddSimpleProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddSimpleProductToCart.graphqls deleted file mode 100644 index 8a3d6963f..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddSimpleProductToCart.graphqls +++ /dev/null @@ -1,78 +0,0 @@ -type Mutation { - addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput - updateSimpleProductsInCart(input: UpdateSimpleProductsInCartInput): UpdateSimpleProductsInCartOutput -} - -input UpdateSimpleProductsInCartInput { - cart_id: String! - cartItems: [UpdateSimpleProductCartItemInput!]! -} - -input UpdateSimpleProductCartItemInput { - details: UpdateCartItemDetailsInput! - customizable_options:[CustomizableOptionInput] -} - -input AddSimpleProductsToCartInput { - cart_id: String! - cartItems: [SimpleProductCartItemInput!]! -} - -input SimpleProductCartItemInput { - details: CartItemDetailsInput! - customizable_options:[CustomizableOptionInput!] -} - -input CartItemDetailsInput { - sku: String! - quantity: Float! -} - -input UpdateCartItemDetailsInput { - cart_item_id: String! - quantity: Float -} - -input CustomizableOptionInput { - id: Int! - value: String! -} - -type AddSimpleProductsToCartOutput { - cart: Cart! -} - -type Cart { - items: [CartItemInterface] -} - -interface CartItemInterface @typeResolver(class: "Magento\\CatalogCheckoutGraphQl\\Model\\CartItemInterfaceTypeResolverComposite") { - id: String! # Hashed cart item ID - qty: Float! - product: ProductInterface! -} - -type SimpleCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption] -} - -type SelectedCustomizableOption { - id: Int! - label: String! - type: String! - values: [SelectedCustomizableOptionValue!]! - sort_order: Int! -} - -type SelectedCustomizableOptionValue { - id: Int - label: String! - price: CartItemSelectedOptionValuePrice! - sort_order: Int! -} - -type CartItemSelectedOptionValuePrice { - value: Float! - units: String! - type: PriceTypeEnum! -} diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/AddVirtualProductToCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/AddVirtualProductToCart.graphqls deleted file mode 100644 index 76013ada1..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/AddVirtualProductToCart.graphqls +++ /dev/null @@ -1,34 +0,0 @@ -type Mutation { - # for now this mutation is identical to addSimpleProductsToCart and exists as a syntax sugar. Also it allows product type based customizations - addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput - updateVirtualProductsInCart(input: UpdateVirtualProductsInCartInput): UpdateVirtualProductsInCartOutput -} - -input UpdateVirtualProductsInCartInput { - cart_id: String! - cartItems: [UpdateVirtualProductCartItemInput!]! -} - -input UpdateVirtualProductCartItemInput { - details: UpdateCartItemDetailsInput! - customizable_options:[CustomizableOptionInput] -} - -input AddVirtualProductsToCartInput { - cart_id: String! - cartItems: [VirtualProductCartItemInput!]! -} - -input VirtualProductCartItemInput { - details: CartItemDetailsInput! - customizable_options:[CustomizableOptionInput!] -} - -type AddVirtualProductsToCartOutput { - cart: Cart! -} - -# Custom cart item type can be used to customize rendering when there are no physical producs available, e.g. skip shipping -type VirtualCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption] -} diff --git a/design-documents/graph-ql/coverage/add-items-to-cart/RemoveItemFromCart.graphqls b/design-documents/graph-ql/coverage/add-items-to-cart/RemoveItemFromCart.graphqls deleted file mode 100644 index f1039fc86..000000000 --- a/design-documents/graph-ql/coverage/add-items-to-cart/RemoveItemFromCart.graphqls +++ /dev/null @@ -1,12 +0,0 @@ -type Mutation { - removeItemFromCart(input: RemoveItemFromCartInput): RemoveItemFromCartOutput -} - -input RemoveItemFromCartInput { - cart_id: String! - cart_item_id: String! -} - -type RemoveItemFromCartOutput { - cart: Cart! -} From a6404a455b34f8a616a050cfed5cff3586d9a018 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 8 Jun 2020 17:27:10 -0500 Subject: [PATCH 080/479] ECP-711: GraphQL schema for Gift Registry - Added customizable_options to CartItemInterface - Added id_v2: ID to DownloadableProductLinks to identify specific link and be able to add it to gift registry - Added child_sku to ConfigurableCartItem, need to be able to add to gift registry - Making sure client does not have to generate hash of the option. It should be available to move items from cart to registry and wishlist --- design-documents/graph-ql/coverage/Cart.graphqls | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index 520bb0f28..d81040c90 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -40,22 +40,22 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ quantity: Float! prices: CartItemPrices product: ProductInterface! + customizable_options: [SelectedCustomizableOption] } type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") { - customizable_options: [SelectedCustomizableOption] } type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") { - customizable_options: [SelectedCustomizableOption] } type ConfigurableCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption]! + child_sku: String! @doc(description: "SKU of the simple product corresponding to a set of selected configurable options.") configurable_options: [SelectedConfigurableOption!]! } type SelectedConfigurableOption { + value_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry") id: Int! option_label: String! value_id: Int! @@ -63,12 +63,12 @@ type SelectedConfigurableOption { } type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { - customizable_options: [SelectedCustomizableOption] links: [DownloadableProductLinks] @doc(description: "An array containing information about the links for the added to cart downloadable product") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } type DownloadableProductLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { + link_hash: String! @doc(description: "Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry") title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") price: Float @doc(description: "The price of the downloadable product") @@ -82,12 +82,12 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de } type BundleCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption]! bundle_options: [SelectedBundleOption!]! } type SelectedBundleOption { id: Int! + values_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move bundle product to wishlist or gift registry") label: String! type: String! values: [SelectedBundleOptionValue!]! @@ -101,6 +101,7 @@ type SelectedBundleOptionValue { } type SelectedCustomizableOption { + values_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry") id: Int! label: String! is_required: Boolean! From 93eead7fbea6e98491e0534e1f7eff5d692f8ca4 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 9 Jun 2020 11:17:35 -0500 Subject: [PATCH 081/479] ECP-711: GraphQL schema for Gift Registry - Extended Wishlist schema to provide information about the selected options --- .../graph-ql/coverage/Cart.graphqls | 6 ++--- .../graph-ql/coverage/Wishlist.graphqls | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index d81040c90..d24d007cd 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -51,7 +51,7 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car type ConfigurableCartItem implements CartItemInterface { child_sku: String! @doc(description: "SKU of the simple product corresponding to a set of selected configurable options.") - configurable_options: [SelectedConfigurableOption!]! + configurable_options: [SelectedConfigurableOption!] } type SelectedConfigurableOption { @@ -63,7 +63,7 @@ type SelectedConfigurableOption { } type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { - links: [DownloadableProductLinks] @doc(description: "An array containing information about the links for the added to cart downloadable product") + links: [DownloadableProductLinks] @doc(description: "An array containing information about the selected links") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } @@ -82,7 +82,7 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de } type BundleCartItem implements CartItemInterface { - bundle_options: [SelectedBundleOption!]! + bundle_options: [SelectedBundleOption!] } type SelectedBundleOption { diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 513b3db8d..1c678ef3b 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -59,10 +59,32 @@ type WishlistOutput @deprecated(reason: "Deprecated: `Wishlist` type should be u updated_at: String @deprecated } -type WishlistItem { +type WishlistItemInterface { id: ID qty: Float description: String added_at: String product: ProductInterface + customizable_options: [SelectedCustomizableOption] } + +type SimpleWishlistItem implements WishlistItemInterface @doc(description: "Simple Wishlist Item") { +} + +type VirtualWishlistItem implements WishlistItemInterface @doc(description: "Virtual Wishlist Item") { +} + +type ConfigurableWishlistItem implements WishlistItemInterface { + child_sku: String! @doc(description: "SKU of the simple product corresponding to a set of selected configurable options.") + configurable_options: [SelectedConfigurableOption!] +} + +type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "Downloadable Wishlist Item") { + links: [DownloadableProductLinks] @doc(description: "An array containing information about the selected links") + samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") +} + +type BundleWishlistItem implements WishlistItemInterface { + bundle_options: [SelectedBundleOption!] +} + From 17ef7fe6831f92d494ad8a614879b9ce18d2fc05 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 9 Jun 2020 15:14:20 -0500 Subject: [PATCH 082/479] ECP-711: GraphQL schema for Gift Registry - Fixed code style or query/mutation argument names --- .../coverage/AddProductsToCart.graphqls | 2 +- .../graph-ql/coverage/Wishlist.graphqls | 6 ++--- .../graph-ql/coverage/gift-registry.graphqls | 24 +++++++++---------- .../graph-ql/coverage/gift-registry.md | 10 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls index 345fee93b..f3e82899c 100644 --- a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls +++ b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls @@ -1,5 +1,5 @@ type Mutation { - addProductsToCart(cart_id: String!, cart_items: [CartItemInput!]!): AddProductsToCartOutput + addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput } input CartItemInput { diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 1c678ef3b..3fe56ee5e 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -5,9 +5,9 @@ type Query { type Mutation { createWishlist(name: String!): ID # Multiple wishlists Commerce functionality removeWishlist(id: ID!): Boolean # Commerce fucntionality - in Opens Source we assume customer always has one wishlist - addProductsToWishlist(wishlist_id: ID!, wishlist_items: [WishlistItemInput!]!): AddProductsToWishlistOutput - removeProductsFromWishlist(wishlist_id: ID!, wishlist_items_ids: [ID!]!): RemoveProductsFromWishlistOutput - updateProductsInWishlist(wishlist_id: ID!, wishlist_items: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput + removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput + updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput } type Customer { diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index c5754cc24..839fadd44 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -6,27 +6,27 @@ type Customer { type Query { giftRegistryTypes: [GiftRegistryType] @doc(description: "Get a list of available gift registry types") giftRegistrySearch( - registrant_firstname: String!, - registrant_lastname: String!, - gift_registry_type_id: ID, - searchable_dynamic_attributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") + registrantFirstname: String!, + registrantLastname: String!, + giftRegistryTypeId: ID, + searchableDynamicAttributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") ): [GiftRegistry] @doc(description: "Gift registry search by registrant name and additional searchable attributes.") } type Mutation { # All mutations below should only be accessible to the registry owner. Guest users should be getting authorization error - createGiftRegistry(gift_registry: CreateGiftRegistryInput!): CreateGiftRegistryOutput - updateGiftRegistry(id: ID!, gift_registry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput + createGiftRegistry(giftRegistry: CreateGiftRegistryInput!): CreateGiftRegistryOutput + updateGiftRegistry(id: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput removeGiftRegistry(id: ID!): RemoveGiftRegistryOutput - addGiftRegistryItems(gift_registry_id: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput - removeGiftRegistryItems(gift_registry_id: ID!, item_ids: [ID!]!): RemoveGiftRegistryItemsOutput - updateGiftRegistryItems(gift_registry_id: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput + addGiftRegistryItems(giftRegistryId: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput + removeGiftRegistryItems(giftRegistryId: ID!, itemIds: [ID!]!): RemoveGiftRegistryItemsOutput + updateGiftRegistryItems(giftRegistryId: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput - addGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput - updateGiftRegistryRegistrants(gift_registry_id: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput - removeGiftRegistryRegistrants(gift_registry_id: ID!, registrant_ids: [ID!]!): RemoveGiftRegistryRegistrantsOutput + addGiftRegistryRegistrants(giftRegistryId: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput + updateGiftRegistryRegistrants(giftRegistryId: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput + removeGiftRegistryRegistrants(giftRegistryId: ID!, registrantIds: [ID!]!): RemoveGiftRegistryRegistrantsOutput shareGiftRegistry(id: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput } diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 23e99f022..4f237cfcc 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -43,7 +43,7 @@ Then create a new gift registry based on the user input. Registrants are added u ```graphql # In real query only one should be provided: existing address ID OR address data mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistryInput!, $giftRegistryId: ID!, $registrantsData: [AddGiftRegistryRegistrantInput!]!) { - createGiftRegistry(gift_registry: $giftRegistryData) { + createGiftRegistry(giftRegistry: $giftRegistryData) { gift_registry { id event_name @@ -53,7 +53,7 @@ mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistry } } } - addGiftRegistryRegistrants(gift_registry_id: $giftRegistryId, registrants: $registrantsData) { + addGiftRegistryRegistrants(giftRegistryId: $giftRegistryId, registrants: $registrantsData) { gift_registry { registrants { id @@ -222,7 +222,7 @@ Modify gift registry data: ```graphql mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryData: UpdateGiftRegistryInput!, $registrantsData: [UpdateGiftRegistryRegistrantInput!]!) { - updateGiftRegistry(id: $giftRegistryId, gift_registry: $giftRegistryData) { + updateGiftRegistry(id: $giftRegistryId, giftRegistry: $giftRegistryData) { gift_registry { id event_name @@ -232,7 +232,7 @@ mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryDa } } } - updateGiftRegistryRegistrants(gift_registry_id: $giftRegistryId, registrants: $registrantsData) { + updateGiftRegistryRegistrants(giftRegistryId: $giftRegistryId, registrants: $registrantsData) { gift_registry { registrants { id @@ -390,7 +390,7 @@ Based on that information we can send a mutation to add the selected item to gif ```graphql mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGiftRegistryItemInput!]!) { - addGiftRegistryItems(gift_registry_id: $giftRegistryId, items: $giftRegistryItems) { + addGiftRegistryItems(giftRegistryId: $giftRegistryId, items: $giftRegistryItems) { gift_registry { event_name items { From e3f8ea6514825d76c22ddbb0882bc805e1ddb044 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 9 Jun 2020 21:01:13 -0500 Subject: [PATCH 083/479] ECP-711: GraphQL schema for Gift Registry - Fixing cart schema to make it compatible with gift registry --- .../graph-ql/coverage/Cart.graphqls | 26 ++++++++++++++----- .../graph-ql/coverage/Wishlist.graphqls | 20 +++----------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index d24d007cd..89e0c8dfe 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -55,7 +55,7 @@ type ConfigurableCartItem implements CartItemInterface { } type SelectedConfigurableOption { - value_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry") + id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry") id: Int! option_label: String! value_id: Int! @@ -63,12 +63,13 @@ type SelectedConfigurableOption { } type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { - links: [DownloadableProductLinks] @doc(description: "An array containing information about the selected links") + links: [DownloadableProductLinks] @deprecated(description: "Type was renamed from plural to singular, also link now has different ID type") + links_v2: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } -type DownloadableProductLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { - link_hash: String! @doc(description: "Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry") +type DownloadableProductLink @doc(description: "Defines characteristics of a downloadable product") { + id: ID! @doc(description: "Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry") title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") price: Float @doc(description: "The price of the downloadable product") @@ -86,8 +87,9 @@ type BundleCartItem implements CartItemInterface { } type SelectedBundleOption { + id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move bundle product to wishlist or gift registry") id: Int! - values_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move bundle product to wishlist or gift registry") + child_sku: String! label: String! type: String! values: [SelectedBundleOptionValue!]! @@ -101,7 +103,7 @@ type SelectedBundleOptionValue { } type SelectedCustomizableOption { - values_hash: String! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry") + id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry") id: Int! label: String! is_required: Boolean! @@ -122,6 +124,18 @@ type CartItemSelectedOptionValuePrice { type: PriceTypeEnum! } +type GiftCardCartItem implements CartItemInterface { + sender_name: String! + recepient_name: String! + amount: SelectedGiftCardAmount + message: String +} + +type SelectedGiftCardAmount { + id: ID! @doc(description: "Hash from the type of the option and value") + value: Money! +} + enum PriceTypeEnum @doc(description: "This enumeration the price type.") { FIXED PERCENT diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 3fe56ee5e..5107e1b7a 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -1,7 +1,3 @@ -type Query { - wishlist: WishlistOutput @deprecated(reason: "Moved under `Customer` `wishlists`") -} - type Mutation { createWishlist(name: String!): ID # Multiple wishlists Commerce functionality removeWishlist(id: ID!): Boolean # Commerce fucntionality - in Opens Source we assume customer always has one wishlist @@ -17,11 +13,11 @@ type Customer { type Wishlist { id: ID - items: [WishlistItem] + items: [WishlistItemInterface] items_count: Int sharing_code: String updated_at: String - #name: String should be added in Commerce edition + name: String @doc(description: "Avaialble in Commerce edition only") } input WishlistItemUpdateInput { @@ -51,15 +47,7 @@ input WishlistItemInput { entered_options: [EnteredOptionInput!] } -type WishlistOutput @deprecated(reason: "Deprecated: `Wishlist` type should be used instead") { - items: [WishlistItem] @deprecated - items_count: Int @deprecated - name: String @deprecated - sharing_code: String @deprecated - updated_at: String @deprecated -} - -type WishlistItemInterface { +interface WishlistItemInterface { id: ID qty: Float description: String @@ -80,7 +68,7 @@ type ConfigurableWishlistItem implements WishlistItemInterface { } type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "Downloadable Wishlist Item") { - links: [DownloadableProductLinks] @doc(description: "An array containing information about the selected links") + links: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } From bc3d54251d95fe72fea9893f74c8b3520fe690e1 Mon Sep 17 00:00:00 2001 From: Arvind Date: Fri, 5 Jun 2020 10:19:39 +0530 Subject: [PATCH 084/479] fixed review comments --- .../coverage/negotiableQuotes.graphqls | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index e73e5e726..d8fea82ed 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,11 +1,15 @@ type Mutation { - requestNegotiableQuote( - negotiableQuote: NegotiableQuoteRequestInput! + createNegotiableQuote( + cart_id: ID! @doc(description: "Cart ID") + quote_name: String @doc(description: "Quote name") + comment: String @doc(description: "Comment") + files: [FileInput!] @doc(description: "Attached files") ): RequestNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") setShippingMethodsOnNegotiableQuote( - negotiableQuoteShippingMethod: NegotiableQuoteShippingMethodInput! - ): setShippingMethodsOnNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") + cart_id: ID! @doc(description: "Cart ID") + shipping_methods: [String!]! @doc(description: "Input for shipping method") + ): SetShippingMethodsOnNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") } type Customer { @@ -28,12 +32,13 @@ type Customer { } type NegotiableQuote { + quote_id: ID! @doc(description: "Negotiable quote ID") items: [NegotiableQuoteItem] @doc(description: "Negotiable quote item") attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") totals: TotalsOutput! @doc(description: "Negotiable quote totals output") payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") - available_payment_methods: [PaymentMethodsOutput] @doc(description: "Available payment methods") + available_shipping_methods: [ShippingMethodsOutput]! @doc(description: "Available shipping methods") } input NegotiableQuoteFilterInput { @@ -42,7 +47,7 @@ input NegotiableQuoteFilterInput { } type NegotiableQuoteItem { - id: String! + id: ID! quantity: Float! product: ProductInterface! } @@ -102,13 +107,6 @@ type CartDiscount { label: [String!]! } -input NegotiableQuoteRequestInput { - quote_id: ID! @doc(description: "Quote hash") - quote_name: String @doc(description: "Quote name") - comment: String @doc(description: "Comment") - files: [FileInput] @doc(description: "Attached files") -} - type RequestNegotiableQuoteOutput { negotiable_quote:NegotiableQuote! } @@ -144,23 +142,6 @@ type ShippingMethodsOutput { price_incl_tax: String @doc(description: "Shipping method price with inclusive tax") } -input NegotiableQuoteBillAddressInput { - firstname: String! @doc(description: "The first name of the person associated with the billing address") - lastname: String! @doc(description: "The family name of the person associated with the billing address") - telephone: String! @doc(description: "The telephone number") - street: [String]! @doc(description: "A list of strings that define the street number and name") - city: String! @doc(description: "The city or town") - region: String! @doc(description: "The region name") - region_id: Int @doc(description: "The region ID") - postcode: String! @doc(description: "The customer's ZIP or postal code") - country_code: String! @doc(description: "The country code") -} - -input NegotiableQuoteShippingMethodInput { - quote_id: ID! @doc(description: "Quote hash") - shipping_method: [String!]! @doc(description: "Input for shipping method") -} - -type setShippingMethodsOnNegotiableQuoteOutput { +type SetShippingMethodsOnNegotiableQuoteOutput { shipping_method_set: Boolean! } From fde0da37edea8bc79ee451d376ee530c9b2a7933 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 10 Jun 2020 11:17:51 -0500 Subject: [PATCH 085/479] ECP-711: GraphQL schema for Gift Registry - Moved child_sku from bundle option to bundle option value --- design-documents/graph-ql/coverage/Cart.graphqls | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index 89e0c8dfe..bab3e44f8 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -87,9 +87,7 @@ type BundleCartItem implements CartItemInterface { } type SelectedBundleOption { - id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move bundle product to wishlist or gift registry") id: Int! - child_sku: String! label: String! type: String! values: [SelectedBundleOptionValue!]! @@ -98,8 +96,9 @@ type SelectedBundleOption { type SelectedBundleOptionValue { id: Int! label: String! - quantity: Float! price: Float! + quantity: Float! + child_sku: String! } type SelectedCustomizableOption { From 195959fcc24513025948cdfe47fa3101af934201 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 10 Jun 2020 13:14:18 -0500 Subject: [PATCH 086/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 1 + .../graph-ql/coverage/gift-registry.md | 128 +++++++++++------- 2 files changed, 83 insertions(+), 46 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 839fadd44..77d307c8e 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -47,6 +47,7 @@ input AddGiftRegistryItemInput { sku: String! quantity: Float! parent_sku: String, + parent_quantity: Float, note: String, # see https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md selected_options: [String!] diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 4f237cfcc..a6c9f5ddd 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -325,65 +325,34 @@ First get the cart item data which can be used to add a new item to the gift reg product { sku } - ... on SimpleCartItem { - customizable_options { - id - values { - id - value - } - } - } - ... on VirtualCartItem { - customizable_options { - id - values { - id - value - } - } - } - ... on DownloadableCartItem { - customizable_options { - id - values { - id - } - } + customizable_options { + id_v2 } ... on BundleCartItem { bundle_options { - id values { - id + child_sku quantity } } - customizable_options { - id - values { - id - value - } - } } ... on ConfigurableCartItem { + child_sku configurable_options { - id - value_id + id_v2 } - customizable_options { + } + ... on DownloadableCartItem { + links_v2 { id - values { - id - value - } } } + ... on GiftCardCartItem { + id + } } } } - ``` Based on that information we can send a mutation to add the selected item to gift registry. @@ -472,18 +441,43 @@ mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGift The following JSON should be provided as query variables for the mutation above: +Simple product with custom options: + +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "simple-hat", + "quantity": 2.0, + "note": "Really like this color", + "selected_options": [ + "hash based on custom option for the select type goes here" + ], + "entered_options": [ + { + "id": "hash from custom phrase option ID", + "value": "Custom Hat" + } + ] + } + ] +} +``` + +Configurable product with custom options: ```json { "giftRegistryId": "existing-gift-registry-id", "giftRegistryItems": [ { - "sku": "custom-hat-red", + "sku": "custom-hat", "quantity": 2.0, "note": "Really like this color", - "parent_sku": "custom-hat", "selected_options": [ - "hash from the color option ID and its value ID", - "hash from the size option ID and its value ID" + "hash from the color configurable option ID and its value ID", + "hash from the size configurable option ID and its value ID", + "hash based on custom option for the select type goes here" ], "entered_options": [ { @@ -496,6 +490,48 @@ The following JSON should be provided as query variables for the mutation above: } ``` +Bundle product with custom options: + +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "fan-kit-hat", + "parent_sku": "fan-kit", + "quantity": 2.0, + "parent_quantity": 3.0, + "note": "Really like this color. Must be identical for all bundle children.", + "selected_options": [ + "hash based on custom option for the select type goes here. Must be identical for all bundle children." + ], + "entered_options": [ + { + "id": "hash based on custom phrase option goes here. Must be identical for all bundle children.", + "value": "Custom Text" + } + ] + }, + { + "sku": "fan-kit-scarf", + "parent_sku": "fan-kit", + "quantity": 1.0, + "parent_quantity": 3.0, + "note": "Really like this color. Must be identical for all bundle children.", + "selected_options": [ + "hash based on custom option for the select type goes here. Must be identical for all bundle children." + ], + "entered_options": [ + { + "id": "hash based on custom phrase option goes here. Must be identical for all bundle children.", + "value": "Custom Text" + } + ] + } + ] +} +``` + ### Gift registry owner adds items to the gift registry from wish list Wishlist query does not support fetching the information about selected options and need to be extended. After that the same mutation can be used to add items to gift registry as describe above in the use case of adding items to gift registry from cart. From 5e328c0c9b39eb5e17de7e6b1c1cab96271dfba6 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 10 Jun 2020 17:04:40 -0500 Subject: [PATCH 087/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/Cart.graphqls | 34 ++--- .../graph-ql/coverage/Wishlist.graphqls | 12 +- .../graph-ql/coverage/gift-registry.md | 141 +++++++++++++++++- 3 files changed, 161 insertions(+), 26 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index bab3e44f8..ce63f151e 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -54,6 +54,23 @@ type ConfigurableCartItem implements CartItemInterface { configurable_options: [SelectedConfigurableOption!] } +type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { + links: [DownloadableProductLinks] @deprecated(description: "Type was renamed from plural to singular, also link now has different ID type") + links_v2: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") + samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") +} + +type BundleCartItem implements CartItemInterface { + bundle_options: [SelectedBundleOption!] +} + +type GiftCardCartItem implements CartItemInterface { + sender_name: String! + recepient_name: String! + amount: SelectedGiftCardAmount + message: String +} + type SelectedConfigurableOption { id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry") id: Int! @@ -62,12 +79,6 @@ type SelectedConfigurableOption { value_label: String! } -type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { - links: [DownloadableProductLinks] @deprecated(description: "Type was renamed from plural to singular, also link now has different ID type") - links_v2: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") - samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") -} - type DownloadableProductLink @doc(description: "Defines characteristics of a downloadable product") { id: ID! @doc(description: "Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry") title: String @doc(description: "The display name of the link") @@ -82,10 +93,6 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de sample_url: String @doc(description: "URL to the downloadable sample") } -type BundleCartItem implements CartItemInterface { - bundle_options: [SelectedBundleOption!] -} - type SelectedBundleOption { id: Int! label: String! @@ -123,13 +130,6 @@ type CartItemSelectedOptionValuePrice { type: PriceTypeEnum! } -type GiftCardCartItem implements CartItemInterface { - sender_name: String! - recepient_name: String! - amount: SelectedGiftCardAmount - message: String -} - type SelectedGiftCardAmount { id: ID! @doc(description: "Hash from the type of the option and value") value: Money! diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 5107e1b7a..42fd9346a 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -43,13 +43,14 @@ input WishlistItemInput { sku: String quantity: Float parent_sku: String, + parent_quantity: Float, selected_options: [String!] entered_options: [EnteredOptionInput!] } interface WishlistItemInterface { id: ID - qty: Float + quantity: Float description: String added_at: String product: ProductInterface @@ -68,7 +69,7 @@ type ConfigurableWishlistItem implements WishlistItemInterface { } type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "Downloadable Wishlist Item") { - links: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") + links_v2: [DownloadableProductLink] @doc(description: "An array containing information about the selected links") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") } @@ -76,3 +77,10 @@ type BundleWishlistItem implements WishlistItemInterface { bundle_options: [SelectedBundleOption!] } +type GiftCardCartItem implements CartItemInterface { + sender_name: String! + recepient_name: String! + amount: SelectedGiftCardAmount + message: String +} + diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index a6c9f5ddd..7d68b80ec 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -314,7 +314,14 @@ mutation RemoveGiftRegistry($giftRegistryId: ID!) { ### Gift registry owner adds items to the gift registry from cart -First get the cart item data which can be used to add a new item to the gift registry: + +### Adding items to the gift registry + +It should be possible to additems to the gift registry from the product/category page, cart or wishlist. +Selected and entered options are specified in a form of hash, which is based on option type, selected values and potentially quantity. +It is critical to have ability to avoid the hash generation on the client, that is why it must be accessible through products, cart, wishlist and gift registry queries. + +#### Getting details about cart item which needs to be added to gift registry ```graphql { @@ -348,13 +355,64 @@ First get the cart item data which can be used to add a new item to the gift reg } } ... on GiftCardCartItem { + amount { + id + } + } + } + } +} +``` + +#### Getting details wishlist item which needs to be added to gift registry + +Note that if the item is not fully configured, the user must be redirected to the product page to complete selections before the item can be added to the gift registry. + +Query structure is almost identical to the query for getting items + +```graphql +{ + customer { + wishlist { + items { id + quantity + product { + sku + } + customizable_options { + id_v2 + } + ... on BundleWishlistItem { + bundle_options { + values { + child_sku + quantity + } + } + } + ... on ConfigurableWishlistItem { + child_sku + configurable_options { + id_v2 + } + } + ... on DownloadableWishlistItem { + links_v2 { + id + } + } + ... on GiftCardWishlistItem { + id + } } } } } ``` +#### Executing mutation to add items to gift registry + Based on that information we can send a mutation to add the selected item to gift registry. ```graphql @@ -449,7 +507,7 @@ Simple product with custom options: "giftRegistryItems": [ { "sku": "simple-hat", - "quantity": 2.0, + "quantity": 2, "note": "Really like this color", "selected_options": [ "hash based on custom option for the select type goes here" @@ -472,7 +530,7 @@ Configurable product with custom options: "giftRegistryItems": [ { "sku": "custom-hat", - "quantity": 2.0, + "quantity": 2, "note": "Really like this color", "selected_options": [ "hash from the color configurable option ID and its value ID", @@ -499,8 +557,8 @@ Bundle product with custom options: { "sku": "fan-kit-hat", "parent_sku": "fan-kit", - "quantity": 2.0, - "parent_quantity": 3.0, + "quantity": 2, + "parent_quantity": 3, "note": "Really like this color. Must be identical for all bundle children.", "selected_options": [ "hash based on custom option for the select type goes here. Must be identical for all bundle children." @@ -515,8 +573,8 @@ Bundle product with custom options: { "sku": "fan-kit-scarf", "parent_sku": "fan-kit", - "quantity": 1.0, - "parent_quantity": 3.0, + "quantity": 1, + "parent_quantity": 3, "note": "Really like this color. Must be identical for all bundle children.", "selected_options": [ "hash based on custom option for the select type goes here. Must be identical for all bundle children." @@ -532,6 +590,75 @@ Bundle product with custom options: } ``` +Downloadable product with custom options: +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "downloadable-product", + "quantity": 2, + "selected_options": [ + "hash from the selected link A", + "hash from the selected link B", + "hash from the selected custom option" + ], + "entered_options": [ + { + "id": "hash from custom text option ID", + "value": "Custom Entry" + } + ] + } + ] +} +``` + +Gift card product with custom options: +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "giftcard-product", + "quantity": 2, + "selected_options": [ + "hash from the selected gift card amount", + "hash from the selected custom option" + ], + "entered_options": [ + { + "id": "hash from custom text option ID", + "value": "Custom Entry" + } + ] + } + ] +} +``` + +Virtual card product with custom options: +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "giftRegistryItems": [ + { + "sku": "virtual-product", + "quantity": 2, + "selected_options": [ + "hash from the selected custom option" + ], + "entered_options": [ + { + "id": "hash from custom text option ID", + "value": "Custom Entry" + } + ] + } + ] +} +``` + ### Gift registry owner adds items to the gift registry from wish list Wishlist query does not support fetching the information about selected options and need to be extended. After that the same mutation can be used to add items to gift registry as describe above in the use case of adding items to gift registry from cart. From edc4df3ebd8121c51d4a4e4989b3a3b1b2108ff0 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 11 Jun 2020 12:02:45 -0500 Subject: [PATCH 088/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 11 ++--- .../graph-ql/coverage/gift-registry.md | 45 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 77d307c8e..8b2d4f3d1 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -244,23 +244,24 @@ interface GiftRegistryItemInterface { note: String added_on: String! product: ProductInterface - selected_customizable_options: [SelectedCustomizableOption] + customizable_options: [SelectedCustomizableOption] } type SimpleGiftRegistryItem implements GiftRegistryItemInterface { } type BundleGiftRegistryItem implements GiftRegistryItemInterface { - selected_bundle_options: [SelectedBundleOption!] + bundle_options: [SelectedBundleOption!] } type ConfigurableGiftRegistryItem implements GiftRegistryItemInterface { - selected_configurable_options: [SelectedConfigurableOption!] + child_sku: String! @doc(description: "SKU of the simple product corresponding to a set of selected configurable options.") + configurable_options: [SelectedConfigurableOption!] } # Not currently supported by Magento core type DownloadableGiftRegistryItem implements GiftRegistryItemInterface { - links: [DownloadableProductLinks] + links: [DownloadableProductLink] samples: [DownloadableProductSamples] } @@ -272,7 +273,7 @@ type VirtualGiftRegistryItem implements GiftRegistryItemInterface { type GiftCardGiftRegistryItem implements GiftRegistryItemInterface { sender_name: String! recepient_name: String! - amount: Money! + amount: SelectedGiftCardAmount message: String } diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 7d68b80ec..8b566b522 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -659,29 +659,48 @@ Virtual card product with custom options: } ``` -### Gift registry owner adds items to the gift registry from wish list - -Wishlist query does not support fetching the information about selected options and need to be extended. After that the same mutation can be used to add items to gift registry as describe above in the use case of adding items to gift registry from cart. +### Gift registry visitor adds items from the gift registry to the cart +The following query returns enough data to add product to cart or wishlist. ```graphql { - customer { - wishlist { - items { - qty - product { - sku + giftRegistry(id: "existing-gift-registry-id") { + items { + id + quantity + product { + sku + } + customizable_options { + id_v2 + } + ... on BundleGiftRegistryItem { + bundle_options { + values { + child_sku + quantity + } + } + } + ... on ConfigurableGiftRegistryItem { + child_sku + configurable_options { + id_v2 } } + ... on DownloadableGiftRegistryItem { + links { + id + } + } + ... on GiftCardGiftRegistryItem { + id + } } } } ``` -### Gift registry visitor adds items from the gift registry to the cart - -? - ### Gift registry owner removes items from an existing gift registry From 0a1ee7e3a172e87f66df9151fb2946e5530f2810 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 11 Jun 2020 13:30:58 -0500 Subject: [PATCH 089/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 1 + .../graph-ql/coverage/gift-registry.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 8b2d4f3d1..95c3af5ed 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -11,6 +11,7 @@ type Query { giftRegistryTypeId: ID, searchableDynamicAttributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") ): [GiftRegistry] @doc(description: "Gift registry search by registrant name and additional searchable attributes.") + giftRegistry(id: ID!): GiftRegistry @doc(description: "This query is intended for guests and some fields of GiftRegistry will not be availalbe") } type Mutation { diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 8b566b522..5077fcb8d 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -703,6 +703,25 @@ The following query returns enough data to add product to cart or wishlist. ### Gift registry owner removes items from an existing gift registry +```graphql +mutation RemoveGiftRegistryItem($giftRegistryId: ID!, $itemIds: [ID!]!) { + removeGiftRegistryItems(giftRegistryId: $giftRegistryId, itemIds: $itemIds) { + gift_registry { + items { + id + } + } + } +} +``` + +Query variables: +```json +{ + "giftRegistryId": "existing-gift-registry-id", + "itemIds": ["item-one-id", "item-two-id"] +} +``` ### Storefront application retrieves gift registry search form metadata From ef45948a5f7cba70afed2b6dabb50af816f606b6 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 11 Jun 2020 18:04:09 -0500 Subject: [PATCH 090/479] ECP-711: GraphQL schema for Gift Registry --- .../graph-ql/coverage/gift-registry.graphqls | 2 +- .../graph-ql/coverage/gift-registry.md | 135 ++++++++++++++++-- 2 files changed, 124 insertions(+), 13 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index 95c3af5ed..a3c6dc8fb 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -151,7 +151,7 @@ type RemoveGiftRegistryRegistrantsOutput { } type ShareGiftRegistryOutput { - gift_registry: GiftRegistry + is_shared: Boolean! } type GiftRegistryType { diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 5077fcb8d..8f2eaaa70 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -198,9 +198,6 @@ Render the edit gift registry form and populate it with current gift registry pr ... on GiftRegistryEventCountryAttributeMetadata { show_region } - ... on GiftRegistrySearcheableAttributeMetadataInterface { - is_searcheable - } ... on GiftRegistryEventDateAttributeMetadata { format } @@ -312,9 +309,6 @@ mutation RemoveGiftRegistry($giftRegistryId: ID!) { } ``` -### Gift registry owner adds items to the gift registry from cart - - ### Adding items to the gift registry It should be possible to additems to the gift registry from the product/category page, cart or wishlist. @@ -723,17 +717,69 @@ Query variables: } ``` -### Storefront application retrieves gift registry search form metadata +### Gift registry visitor searches a gift registry by the recipient name -? +First, storefront application retrieves gift registry search form metadata: -### Gift registry visitor searches a gift registry by the recipient name +```graphql +{ + giftRegistryTypes { + id + label + dynamic_attributes_metadata { + code + label + attribute_group + input_type + is_required + sort_order + ... on GiftRegistryCountryAttributeMetadata { + show_region + } + ... on GiftRegistryEventCountryAttributeMetadata { + show_region + } + ... on GiftRegistrySearcheableAttributeMetadataInterface { + is_searcheable + } + ... on GiftRegistryEventDateAttributeMetadata { + format + } + ... on GiftRegistrySelectAttributeMetadataInterface { + options { + code + is_default + label + } + } + } + } +} +``` -+ Search by registrant name and dynamic attributes. -Explicitly excluded scenarios: search by ID and by email. +Explicitly excluded scenarios: search by ID and by email. +```graphql +{ + giftRegistrySearch( + registrantFirstname: "John", + registrantLastname: "Roller", + giftRegistryTypeId: "2", + searchableDynamicAttributes: [{code: "event_country", value: "US"}] + ) { + id + event_name + dynamic_attributes { + code + group + label + value + } + } +} +``` ### Gift registry owner shares a gift registry with friends @@ -741,7 +787,72 @@ When gift registry is shared with the invitees, the email they receive will cont This link will contain gift registry hash as query parameter and should lead to the page processed by the storefront application. The application should parse the URL, extract gift registry ID hash and query gift registry details by ID. +```graphql +mutation ShareGiftRegistry($id: ID!, $sender: ShareGiftRegistrySenderInput!, $invitees: [ShareGiftRegistryInviteeInput!]!) { + shareGiftRegistry(id: $id, sender: $sender, invitees: $invitees) { + is_shared + } +} +``` -### Gift registry visitor opens a gift registry using the link from email +The following JSON should be provided as query variables: +```json +{ + "id": "existing-gift-registry-id", + "sender": { + "message": "Hi, please come to my birth day", + "name": "John Roller" + }, + "invitees": [ + { + "email": "invitee@example.com", + "name": "John Doe" + } + ] +} +``` + +### Gift registry visitor opens a gift registry using the link from email +```graphql +{ + giftRegistry(id: "ID obtained from the invitation link in email") { + event_name + message + dynamic_attributes { + code + group + label + value + } + type { + label + dynamic_attributes_metadata { + code + label + attribute_group + input_type + is_required + sort_order + ... on GiftRegistryCountryAttributeMetadata { + show_region + } + ... on GiftRegistryEventCountryAttributeMetadata { + show_region + } + ... on GiftRegistryEventDateAttributeMetadata { + format + } + ... on GiftRegistrySelectAttributeMetadataInterface { + options { + code + is_default + label + } + } + } + } + } +} +``` From cf1a1c25c90eff99e11f45da078bc816d0286a85 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 12 Jun 2020 11:27:52 -0500 Subject: [PATCH 091/479] ECP-711: GraphQL schema for Gift Registry --- design-documents/graph-ql/coverage/gift-registry.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 8f2eaaa70..2f4ae8b51 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -1,3 +1,9 @@ +## Configuration + +The following configurations must be exposed via existing `storeConfig` query: +- Enable gift registry +- Maximum registrants + ## Use Cases ### Registered customer creates a new gift registry From f93b9b5e49923d0e8eb9bdad34268b29da4c292e Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 12 Jun 2020 13:51:22 -0500 Subject: [PATCH 092/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 design-documents/graph-ql/coverage/reward-points.md diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md new file mode 100644 index 000000000..cd458974e --- /dev/null +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -0,0 +1,20 @@ +## Configuration + +See https://docs.magento.com/user-guide/configuration/customers/reward-points.html + +The following settings should be accessible via `storeConfig` query: +- Reward points functionality status on the storefront: enabled/disabled +- Enable reward points history for the customer +- Reward points redemption minimum threshold +- Whether to show a message in shopping cart about the rewards points earned for the purchase, as well as the customer’s current reward point balance +- Number of points customer gets for registration +- Number of points for newsletter subscription +- Number of points for referral, when invitee registers on the site +- Number of points for referral, when invitee places an initial order on the site +- Number of points for writing a review +- Reward points default subscription status + +Scenarios which may need these settings include: +- Reward program promotions and details +- Customer registration +- Rendering of the reward points section in the customer account From 7940d9daaf3213ad1013b95d385c63c565ad58de Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 12 Jun 2020 15:29:54 -0500 Subject: [PATCH 093/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md index cd458974e..63e87c2df 100644 --- a/design-documents/graph-ql/coverage/reward-points.md +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -18,3 +18,22 @@ Scenarios which may need these settings include: - Reward program promotions and details - Customer registration - Rendering of the reward points section in the customer account + +## Use cases + +### View reward points information in customer account + +The following information should be available to customer in his account when reward points functionality is enabled on the site: + - Balance in points and currency + - Exchange rate from points to currency (redemption rate) + - Exchange rate from currency to points (earning rate) + - Balance history, should include the following fields: + - Balance in points + - Amount in currency + - Balance change in points + - Reason for balance change + - Date + - "Balance Updates" email subscription status + - "Points Expiration Notification" email subscription status + + From 2c92249da4c27f6af9d2f51ea89d03f0a191424f Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Fri, 12 Jun 2020 16:55:36 -0500 Subject: [PATCH 094/479] Customer orders schema - create interfaces for each item type (OrderItemInterface, etc.) - define item types for products with special fields (e.g. BundleOrderItem) - remove SalesTotalAmountInterface because it is not needed --- .../graph-ql/coverage/customer-orders.md | 102 +++++++++++++----- 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 700be69c9..b66bba0c3 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -76,7 +76,7 @@ type CustomerOrder { shipping_address: CustomerAddress @doc("shipping address for the order") billing_address: CustomerAddress @doc("billing address for the order") carrier: String @doc("shipping carrier for the order delivery") - method: String @doc("shipping method for the order") + shipping_method: String @doc("shipping method for the order") comments: [CommentItem] @doc("comments on the order") } ``` @@ -88,8 +88,7 @@ The `id` will be a `base64_encode(increment_id)` which in future can be replaced ### Order Item ```graphql -@doc("Order item") -interface OrderItemInterface { +interface OrderItemInterface @doc("Order item details") { id: ID! @doc("Order item unique identifier") #base64encode(orderItemId) product_name: String @doc("name of the base product") product_sku: String! @doc("SKU of the base product") @@ -112,7 +111,20 @@ type OrderItem implements OrderItemInterface { } type BundleOrderItem implements OrderItemInterface { - child_items: [OrderItemInterface] + bundle_options: [SelectedBundleOptionItems] @doc("A list of bundle options that are assigned to the bundle product") +} + +type GiftCardOrderItem implements OrderItemInterface { + gift_card_amount: Money! @doc("Amount of value on gift card") + gift_card_sender: String @doc("Name of gift card sender") + gift_card_recipient: String @doc("Name of gift card recipient") + gift_card_message: String @doc("Message accompanying gift card") +} + +type SelectedBundleOptionItems { + id: ID! @doc("The unique identifier of the option") + label: String! @doc("The label of the option") + items: [OrderItemInterface] @doc("A list of products that represent the values of the parent option") } @doc("Represents order item options like selected or entered") @@ -141,20 +153,16 @@ type PaymentMethod { As entities like order, invoice, credit memo might have complex amounts type: ```graphql -@doc("Interface to provide sales amounts") -interface SalesTotalAmountInterface { +@doc("Order total amounts details") +type OrderTotal { subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") discounts: [Discount] @doc("applied discounts") total_tax: Money! @doc("total tax amount") taxes: [TaxItem] @doc("order taxes details") grand_total: Money! @doc("final total amount including shipping and taxes") base_grand_total: Money! @doc("final total amount in base currency") -} -​ -@doc("Order total amounts details") -type OrderTotal implements SalesTotalAmountInterface { - total_shipping: Money! @doc("order shipping amount") - shipping_handling: ShippingHandling @doc("shipping and handling for the order") + total_shipping: Money! @doc("order shipping amount") + shipping_handling: ShippingHandling @doc("shipping and handling for the order") } @doc("Shipping handling details") @@ -163,7 +171,7 @@ type ShippingHandling { amount_including_tax: Money @doc("shipping amount including tax") amount_excluding_tax: Money @doc("shipping amount excluding tax") taxes: [TaxItem] @doc("shipping taxes details") - discounts: [Discount] @doc(description: "The applied discounts to the shipping) + discounts: [Discount] @doc("The applied discounts to the shipping) } @doc("Tax item details") @@ -177,7 +185,7 @@ type TaxItem { ## Invoice Type Schema The invoice entity will have the similar to the order schema: - +The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. ```graphql @doc("Invoice details") type Invoice { @@ -189,7 +197,7 @@ type Invoice { } @doc("Invoice item details") -type InvoiceItemInterface { +interface InvoiceItemInterface { id: ID! @doc("invoice item unique identifier") #base64encode(invoiceItemId) order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") @@ -203,21 +211,32 @@ type InvoiceItem implements InvoiceItemInterface { } type BundleInvoiceItem implements InvoiceItemInterface { - child_items: [InvoiceItemInterface] + bundle_options: [SelectedBundleInvoiceOptionItems] @doc("A list of bundle options that are assigned to the bundle product") +} + +type SelectedBundleInvoiceOptionItems { + id: ID! @doc("The unique identifier of the option") + label: String! @doc("The label of the option") + items: [InvoiceItemInterface] @doc("A list of products that represent the values of the parent option") } @doc("Invoice total amount details") -type InvoiceTotal implements SalesTotalAmountInterface { +type InvoiceTotal { + subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") + discounts: [Discount] @doc("applied discounts") + total_tax: Money! @doc("total tax amount") + taxes: [TaxItem] @doc("order taxes details") + grand_total: Money! @doc("final total amount including shipping and taxes") + base_grand_total: Money! @doc("final total amount in base currency") total_shipping: Money! @doc("order shipping amount") shipping_handling: ShippingHandling @doc("shipping and handling for the order") } ``` -The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. - ## Refund Type Schema The credit memo entity will have the similar to the order and invoice schema: +The `id` will be a `base64_encode_encode(increment_id)` which in future can be replaced by UUID. ```graphql @doc("Credit memo details") @@ -230,7 +249,7 @@ type CreditMemo { } @doc("Credit memo item details") -type CreditMemoItem { +interface CreditMemoItemInterface { id: ID! @doc("Credit memo item unique identifier") #base64encode(creditMemoItemId) order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") @@ -240,16 +259,32 @@ type CreditMemoItem { quantity_invoiced: Float @doc("number of invoiced items") } -@doc("Credit memo price details") -type CreditMemoTotal implements SalesTotalAmountInterface { +type CreditMemoItem implements CreditMemoItemInterface { +} +type BundleCreditMemoItem implements CreditMemoIntemInterface { + bundle_options: [CreditMemoItemSelectedBundleOptions] } -``` -The `id` will be a `base64_encode_encode(increment_id)` which in future can be replaced by UUID. +type CreditMemoItemSelectedBundleOptions { + id: ID! @doc("The unique identifier of the option") + label: String! @doc("The label of the option") + items: [CreditMemoItemInterface] @doc("A list of products that represent the values of the parent option") +} -## Shipment Type Schema +@doc("Credit memo price details") +type CreditMemoTotal { + subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") + discounts: [Discount] @doc("applied discounts") + total_tax: Money! @doc("total tax amount") + taxes: [TaxItem] @doc("order taxes details") + grand_total: Money! @doc("final total amount including shipping and taxes") + base_grand_total: Money! @doc("final total amount in base currency") +} +``` +## Shipment Type Schema +The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. ```graphql @doc("Order shipment details") type OrderShipment { @@ -261,7 +296,7 @@ type OrderShipment { } @doc("Order shipment item details") -type ShipmentItem{ +interface ShipmentItemInterface { id: ID! @doc("Shipment item unique identifier") #base64encode(shipmentItemId) order_item: OrderItemInterface @doc("associated order item") product_name: String @doc("name of the base product") @@ -270,6 +305,19 @@ type ShipmentItem{ quantity_shipped: Float! @doc("number of shipped items") } +type ShipmentItem implements ShipmentItemInterface { +} + +type BundleShipmentItem implements ShipmentItemInterface { + bundle_options: [ShipmentItemSelectedBundleOptions] +} + +type ShipmentItemSelectedBundleOptions { + id: ID! @doc("The unique identifier of the option") + label: String! @doc("The label of the option") + items: [ShipmentItemInterface] @doc("A list of products that represent the values of the parent option") +} + @doc("Order shipment tracking details") type ShipmentTracking { title: String! @doc("shipment tracking title") @@ -277,7 +325,7 @@ type ShipmentTracking { number: String @doc("tracking number of the order shipment") } ``` -The `id` will be a `base64_encode(increment_id)` which in future can be replaced by UUID. + ## CommentItem type ```graphql From 974d6418a977dea3e124ad44f5219509e941ff65 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 12 Jun 2020 17:19:23 -0500 Subject: [PATCH 095/479] ECP-756: GraphQL Schema for Rewards --- design-documents/graph-ql/coverage/reward-points.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md index 63e87c2df..a4fdd4109 100644 --- a/design-documents/graph-ql/coverage/reward-points.md +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -35,5 +35,7 @@ The following information should be available to customer in his account when re - Date - "Balance Updates" email subscription status - "Points Expiration Notification" email subscription status - +### Use reward points on checkout + +### View reward points applied on checkout From f2b32c9fb0cc1c25f6c2fba6ee7f557f4b32bd9a Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 15 Jun 2020 11:36:22 -0500 Subject: [PATCH 096/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.graphqls | 38 +++++++++++++++++++ .../graph-ql/coverage/reward-points.md | 36 ++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 design-documents/graph-ql/coverage/reward-points.graphqls diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls new file mode 100644 index 000000000..ca52acf48 --- /dev/null +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -0,0 +1,38 @@ +type Customer { + reward_points: RewardPoints @doc(description: "Customer reward points") +} + +type RewardPoints { + balance: RewardPointsBalance @doc(description: "Current reward points balance") + exchange_rate: RewardPointsExchangeRate @doc(description: "Current reward points exchange rate") + subscription_status: RewardPointsSubscriptionStatus @doc(description: "Reward points related email subscription status") + balance_history: [RewardPointsBalanceHistoryItem] @doc(description: "Reward points balance history. Visibility of this section depends on the configuration") +} + +type RewardPointsBalance { + points: Float! @doc(description: "Reward points balance in points") + amount: Money! @doc(description: "Reward points balance in store currency") +} + +type RewardPointsExchangeRates @doc (description: "Exchange rates depend on the customer group"){ + earning: Float! @doc(description: "How many points are earned per 1 currency spent") + redemption: Float! @doc(description: "How many points need to be redeemed to get 1 currency discount at the checkout") +} + +type RewardPointsSubscriptionStatus { + balance_updates: SubscriptionStatus! @doc(description: "Customer subscription status to 'Reward points balance updates' emails") + points_expiration_notifications: SubscriptionStatus! @doc(description: "Customer subscription status to 'Reward points expiration notifications' emails") +} + +enum SubscriptionStatus { + SUBSCRIBED + NOT_SUBSCRIBED +} + +type RewardPointsBalanceHistoryItem { + balance: RewardPointsBalance @doc(description: "Reward points balance after the completion of the transaction") + points_change: Float! @doc(description: "Number of points added or deducted in the transaction") + change_reason: String! @doc(description: "Reason for balance change") + date: String! @doc(description: "Transaction date") +} + diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md index a4fdd4109..cb6e7c942 100644 --- a/design-documents/graph-ql/coverage/reward-points.md +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -36,6 +36,42 @@ The following information should be available to customer in his account when re - "Balance Updates" email subscription status - "Points Expiration Notification" email subscription status + ```graphql +{ + customer { + reward_points { + balance { + points + amount { + value + currency + } + } + exchange_rates { + earning + redemption + } + subscription_status { + balance_updates + points_expiration_notifications + } + balance_history { + balance { + points + amount { + value + currency + } + } + points_change + change_reason + date + } + } + } +} +``` + ### Use reward points on checkout ### View reward points applied on checkout From 3047ed68af8ca5d469ed0b735f8dca8d75322f10 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 15 Jun 2020 16:12:02 -0500 Subject: [PATCH 097/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.graphqls | 29 +++++++--- .../graph-ql/coverage/reward-points.md | 54 +++++++++++++++---- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index ca52acf48..72fc1b78b 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -2,16 +2,33 @@ type Customer { reward_points: RewardPoints @doc(description: "Customer reward points") } +type Cart { + applied_reward_points: RewardPointsAmount +} + +type Mutation { + applyRewardPointsToCart(cartId: ID!): ApplyRewardPointsToCartOutput + removeRewardPointsFromCart(cartId: ID!): RemoveRewardPointsFromCartOutput +} + +type ApplyRewardPointsToCartOutput { + cart: Cart +} + +type RemoveRewardPointsFromCartOutput { + cart: Cart +} + type RewardPoints { - balance: RewardPointsBalance @doc(description: "Current reward points balance") - exchange_rate: RewardPointsExchangeRate @doc(description: "Current reward points exchange rate") + balance: RewardPointsAmount @doc(description: "Current reward points balance") + exchange_rates: RewardPointsExchangeRates @doc(description: "Current reward points exchange rates") subscription_status: RewardPointsSubscriptionStatus @doc(description: "Reward points related email subscription status") balance_history: [RewardPointsBalanceHistoryItem] @doc(description: "Reward points balance history. Visibility of this section depends on the configuration") } -type RewardPointsBalance { - points: Float! @doc(description: "Reward points balance in points") - amount: Money! @doc(description: "Reward points balance in store currency") +type RewardPointsAmount { + points: Float! @doc(description: "Reward points amount in points") + money: Money! @doc(description: "Reward points amount in store currency") } type RewardPointsExchangeRates @doc (description: "Exchange rates depend on the customer group"){ @@ -30,7 +47,7 @@ enum SubscriptionStatus { } type RewardPointsBalanceHistoryItem { - balance: RewardPointsBalance @doc(description: "Reward points balance after the completion of the transaction") + balance: RewardPointsAmount @doc(description: "Reward points balance after the completion of the transaction") points_change: Float! @doc(description: "Number of points added or deducted in the transaction") change_reason: String! @doc(description: "Reason for balance change") date: String! @doc(description: "Transaction date") diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md index cb6e7c942..b5ada5271 100644 --- a/design-documents/graph-ql/coverage/reward-points.md +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -42,7 +42,7 @@ The following information should be available to customer in his account when re reward_points { balance { points - amount { + money { value currency } @@ -57,12 +57,12 @@ The following information should be available to customer in his account when re } balance_history { balance { - points - amount { - value - currency - } - } + points + money { + value + currency + } + } points_change change_reason date @@ -72,6 +72,40 @@ The following information should be available to customer in his account when re } ``` -### Use reward points on checkout - -### View reward points applied on checkout +### Apply/remove reward points to/from cart and view applied reward points summary + +Apply reward points to the cart and view applied reward points balance: + +```graphql +mutation { + applyRewardPointsToCart(cartId: "existing-cart-id") { + cart { + applied_reward_points { + money { + currency + value + } + points + } + } + } +} +``` + +Remove applied reward points from the cart and view applied balance: + +```graphql +mutation { + removeRewardPointsFromCart(cartId: "existing-cart-id") { + cart { + applied_reward_points { + money { + currency + value + } + points + } + } + } +} +``` From 208835c2bdcb8342c33a299b7dbe8717ccdc5f7c Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 15 Jun 2020 17:44:23 -0500 Subject: [PATCH 098/479] Change type for bundle_options fields --- .../graph-ql/coverage/customer-orders.md | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index b66bba0c3..f8fdc0983 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -111,7 +111,7 @@ type OrderItem implements OrderItemInterface { } type BundleOrderItem implements OrderItemInterface { - bundle_options: [SelectedBundleOptionItems] @doc("A list of bundle options that are assigned to the bundle product") + bundle_options: [ItemSelectedBundleOption] @doc("A list of bundle options that are assigned to the bundle product") } type GiftCardOrderItem implements OrderItemInterface { @@ -121,10 +121,18 @@ type GiftCardOrderItem implements OrderItemInterface { gift_card_message: String @doc("Message accompanying gift card") } -type SelectedBundleOptionItems { - id: ID! @doc("The unique identifier of the option") - label: String! @doc("The label of the option") - items: [OrderItemInterface] @doc("A list of products that represent the values of the parent option") +type ItemSelectedBundleOption { + id: ID! @doc(description: "The unique identifier of the option") + label: String! @doc(description: "The label of the option") + values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") +} + +type ItemSelectedBundleOptionValue { + id: ID! @doc("unique identifier of option value") + product_name: String! @doc("product name for option value") + product_sku: String! @doc("product sku for option value") + quantity: Float! @doc("quantitity of value selected") + price: Money! @doc("Option value price. price for single quantity") } @doc("Represents order item options like selected or entered") @@ -211,13 +219,7 @@ type InvoiceItem implements InvoiceItemInterface { } type BundleInvoiceItem implements InvoiceItemInterface { - bundle_options: [SelectedBundleInvoiceOptionItems] @doc("A list of bundle options that are assigned to the bundle product") -} - -type SelectedBundleInvoiceOptionItems { - id: ID! @doc("The unique identifier of the option") - label: String! @doc("The label of the option") - items: [InvoiceItemInterface] @doc("A list of products that represent the values of the parent option") + bundle_options: [ItemSelectedBundleOption] @doc("A list of bundle options that are assigned to the bundle product") } @doc("Invoice total amount details") @@ -263,13 +265,7 @@ type CreditMemoItem implements CreditMemoItemInterface { } type BundleCreditMemoItem implements CreditMemoIntemInterface { - bundle_options: [CreditMemoItemSelectedBundleOptions] -} - -type CreditMemoItemSelectedBundleOptions { - id: ID! @doc("The unique identifier of the option") - label: String! @doc("The label of the option") - items: [CreditMemoItemInterface] @doc("A list of products that represent the values of the parent option") + bundle_options: [ItemSelectedBundleOption] } @doc("Credit memo price details") @@ -309,13 +305,7 @@ type ShipmentItem implements ShipmentItemInterface { } type BundleShipmentItem implements ShipmentItemInterface { - bundle_options: [ShipmentItemSelectedBundleOptions] -} - -type ShipmentItemSelectedBundleOptions { - id: ID! @doc("The unique identifier of the option") - label: String! @doc("The label of the option") - items: [ShipmentItemInterface] @doc("A list of products that represent the values of the parent option") + bundle_options: [ItemSelectedBundleOption] } @doc("Order shipment tracking details") From ae14f373375790594a3599d5d5a526db74588ade Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 15 Jun 2020 18:01:32 -0500 Subject: [PATCH 099/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.graphqls | 15 +++++++++++ .../graph-ql/coverage/reward-points.md | 27 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index 72fc1b78b..5b3887469 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -53,3 +53,18 @@ type RewardPointsBalanceHistoryItem { date: String! @doc(description: "Transaction date") } +type StoreConfig { + magento_reward_general_is_enabled: String @doc(description: "Reward points functionality status: enabled/disabled") + magento_reward_general_is_enabled_on_front: String @doc(description: "Reward points functionality status on the storefront: enabled/disabled") + magento_reward_general_publish_history: String @doc(description: "Enable reward points history for the customer") + magento_reward_general_min_points_balance: String @doc(description: "Reward points redemption minimum threshold") + magento_reward_points_order: String @doc(description: "Whether customer earns points for shopping according to the reward point exchange rate. In Luma this also controls whether to show a message in shopping cart about the rewards points earned for the purchase, as well as the customer’s current reward point balance") + magento_reward_points_register: String @doc(description: "Number of points customer gets for registration") + magento_reward_points_newsletter: String @doc(description: "Number of points for newsletter subscription") + magento_reward_points_invitation_customer: String @doc(description: "Number of points for referral, when invitee registers on the site") + magento_reward_points_invitation_customer_limit: String @doc(description: "Maximum number of registration referrals that will qualify for rewards") + magento_reward_points_invitation_order: String @doc(description: "Number of points for referral, when invitee places an initial order on the site") + magento_reward_points_invitation_order_limit: String @doc(description: "Maximum number of order placements by invitees that will qualify for rewards") + magento_reward_points_review: String @doc(description: "Number of points for writing a review") + magento_reward_points_review_limit: String @doc(description: "Maximum number of reviews that will qualify for the rewards") +} diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/reward-points.md index b5ada5271..1aeb86b3e 100644 --- a/design-documents/graph-ql/coverage/reward-points.md +++ b/design-documents/graph-ql/coverage/reward-points.md @@ -3,22 +3,45 @@ See https://docs.magento.com/user-guide/configuration/customers/reward-points.html The following settings should be accessible via `storeConfig` query: +- Reward points functionality status: enabled/disabled - Reward points functionality status on the storefront: enabled/disabled - Enable reward points history for the customer - Reward points redemption minimum threshold -- Whether to show a message in shopping cart about the rewards points earned for the purchase, as well as the customer’s current reward point balance +- Whether customer earns points for shopping according to the reward point exchange rate. In Luma this also controls whether to show a message in shopping cart about the rewards points earned for the purchase, as well as the customer’s current reward point balance - Number of points customer gets for registration - Number of points for newsletter subscription - Number of points for referral, when invitee registers on the site +- Maximum number of registration referrals that will qualify for rewards - Number of points for referral, when invitee places an initial order on the site +- Maximum number of order placements by invitees that will qualify for rewards - Number of points for writing a review -- Reward points default subscription status +- Maximum number of reviews that will qualify for the rewards Scenarios which may need these settings include: - Reward program promotions and details - Customer registration - Rendering of the reward points section in the customer account +```graphql +{ + storeConfig { + magento_reward_general_is_enabled + magento_reward_general_is_enabled_on_front + magento_reward_general_publish_history + magento_reward_general_min_points_balance + magento_reward_points_order + magento_reward_points_register + magento_reward_points_newsletter + magento_reward_points_invitation_customer + magento_reward_points_invitation_customer_limit + magento_reward_points_invitation_order + magento_reward_points_invitation_order_limit + magento_reward_points_review + magento_reward_points_review_limit + } +} +``` + ## Use cases ### View reward points information in customer account From aabc62c771acac1e0f11d5d28b813f34d0cd469f Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 15 Jun 2020 18:04:10 -0500 Subject: [PATCH 100/479] ECP-756: GraphQL Schema for Rewards --- .../graph-ql/coverage/reward-points.graphqls | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index 5b3887469..e3c1e390f 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -1,14 +1,14 @@ type Customer { - reward_points: RewardPoints @doc(description: "Customer reward points") + reward_points: RewardPoints @doc(description: "Customer reward points details") } type Cart { - applied_reward_points: RewardPointsAmount + applied_reward_points: RewardPointsAmount @doc(description: "Reward points ammount applied to the cart") } type Mutation { - applyRewardPointsToCart(cartId: ID!): ApplyRewardPointsToCartOutput - removeRewardPointsFromCart(cartId: ID!): RemoveRewardPointsFromCartOutput + applyRewardPointsToCart(cartId: ID!): ApplyRewardPointsToCartOutput @doc(description: "Apply all available points up to the cart total. Partial redemption is not available") + removeRewardPointsFromCart(cartId: ID!): RemoveRewardPointsFromCartOutput @doc(description: "Cancel usage of the previously applied reward points to cart") } type ApplyRewardPointsToCartOutput { @@ -23,7 +23,7 @@ type RewardPoints { balance: RewardPointsAmount @doc(description: "Current reward points balance") exchange_rates: RewardPointsExchangeRates @doc(description: "Current reward points exchange rates") subscription_status: RewardPointsSubscriptionStatus @doc(description: "Reward points related email subscription status") - balance_history: [RewardPointsBalanceHistoryItem] @doc(description: "Reward points balance history. Visibility of this section depends on the configuration") + balance_history: [RewardPointsBalanceHistoryItem] @doc(description: "Reward points balance history. This field will be set to null if disabled in the admin") } type RewardPointsAmount { From 7d6e681b88d5605dd540907e620dff31a7d59d94 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Tue, 16 Jun 2020 14:32:20 -0500 Subject: [PATCH 101/479] Proposal: Deprecate Mutation.createEmptyCart, add Mutation.createGuestCart --- ...ate-createEmptyCart-add-createGuestCart.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md diff --git a/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md b/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md new file mode 100644 index 000000000..41cdd2cb1 --- /dev/null +++ b/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md @@ -0,0 +1,49 @@ +# Deprecate and replace `Mutation.createEmptyCart` + +## What + +- Mark `Mutation.createEmptyCart` as deprecated +- Add `Mutation.createGuestCart` as a replacement that includes `Cart` in its return type + +## Why + +This PR started from a Slack discussion about mutations and client-side caching. At one point [@sirugh](https://github.com/sirugh) mentioned: + +> This makes me wonder if the createCart mutation should return a cart type instead of just the id + +There are 2 problems that I'm aware of with the current schema returning just an `ID` instead of the `Cart` type: + +1. Requires manual cache handling with libraries like Apollo, to link the `ID` back to a `Cart` object +2. Requires 2 round trips for the client to get cart data for a guest. Even though the cart is empty, it still has defaults the pwa-studio components will use to render an empty cart + +During discussions, it was also noted that `createEmptyCart` has some behavior that's not clearly described by our schema. That is, If a customer is logged-in and has items in the cart, `createEmptyCart` returns an ID referencing a cart that is _not_ empty, which is extremely unintuitive. It was mentioned that the `pwa-studio` code [aliases this query](https://github.com/magento/pwa-studio/blob/38d652a4fbc797a4ac8ac0c3efa611003152c090/packages/venia-ui/lib/queries/createCart.graphql#L4) because of that. + +## Intended Usage +Because the UI knows if it has a token for a customer, `Query.cart` should be used by the UI to get the cart for a logged-in user. If there is _not_ a customer token present, the UI should use `createGuestCart`, which will return a `Cart` object, the same return type as `Query.cart`. + + +## Proposed Changes + +```diff +type Mutation { ++ createGuestCart( ++ input: CreateGuestCartInput ++ ): CreateCartOutput @doc(description: "Create a new shopping cart") + createEmptyCart( + input: createEmptyCartInput ++ ): String @deprecated(reason: "Use `Mutation.createGuestCart`, or `Query.cart` for logged-in shoppers") +- ): String @doc(description:"Creates an empty shopping cart for a guest or logged in user") +} + ++input CreateGuestCartInput { ++ cart_id: String @doc(description: "Optional client-generated ID") ++} ++ ++type CreateGuestCartOutput { ++ cart: Cart ++} + +input createEmptyCartInput { + cart_id: String +} +``` From d7d5b5a1a20c35e9ca656649f0db271de9318044 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 17 Jun 2020 09:47:43 -0500 Subject: [PATCH 102/479] Catalog images tech vision --- design-documents/media/catalog-images.md | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 design-documents/media/catalog-images.md diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md new file mode 100644 index 000000000..77c8ad7ac --- /dev/null +++ b/design-documents/media/catalog-images.md @@ -0,0 +1,76 @@ +# Catalog Images + +## Asset Management - High-Level Vision + +### Terminology + +* **Asset** - anything that exists in a binary format and comes with the right to use. + * **Image**, **video** are types of assets +* [DAM (Digital Asset Management)](https://en.wikipedia.org/wiki/Digital_asset_management) - a system responsible for asset management (store, create, update, delete, organize) +* [CDN (Content delivery network)](https://en.wikipedia.org/wiki/Content_delivery_network) - a system responsible for content delivery. In scope of this document, for delivery of images and video. + * CDN may be part of DAM (if DAM provides public URLs for assets), or DAM can be integrated with CDN +* **Image transformation** - resizing, rotation, watermarking and other automated transformations on an original image. + * Image transformation is responsibility of either DAM or CDN. This includes resizing, rotation, watermarking and so on. + * Client should be able to fetch transformed images by its original URL with additional parameters + * Transformation parameters may differ based on the CDN/DAM providing the transformation service +* **Magento back office (Magento Admin)** - in scope of this document, a system for products and categories management. + * Links assets to products and categories + * Provides basic functionality for images and video transformation. Long-term, should be offloaded to specialized systems (DAM, CDN) +* **Catalog Store-Front Application** - application providing product and category information suitable for store-front client scenarios. + * Serves URLs of original assets. + * Should not be aware of asset management functionality (no knowledge about underlying integration with external DAM systems). + * Might have Base CDN URL. This is similar to current Base Media URL and serves the same purpose, but might exist in case both sources of assets should be supported (Magento and CDN). + +## Scenarios + +### Backoffice Scenarios + +#### Assign an image to a product (using DAM integration) + +This is a future desired scenario, provided here for better understanding of the future picture. + +1. Admin opens product edit page +2. Admin uses asset picker UI (provided as part of DAM integration) to select necessary image +3. Admin clicks "Save" + * Image is linked to the product as provided by DAM + * Image path relative to DAM base URL is stored as image path + * Asset is assigned to the product in DAM + +#### Assign an image to a category + +Similar to the product edit scenario + +### Store-Front Scenarios + +#### Display an image on product details or products list page + +1. Client (GraphQL server) requests product details (including images) from the SF application. +2. SF application returns full image URL of the original image + +## Synchronization from Backoffice to Store-Front + +### Concepts: + +1. Store-Fronts stores only original image URLs +2. Image URLs support image transformation by parameters (e.g., `https://some.domain.com/media/catalog/product/1/2/3.jpg?w=100&h=100`) + 1. Image transformation is performed by CDN. The URLs passed from the Backoffice to Store-Front are CDN-based. + 2. Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. Benefits of this approach are not clear. Such scenario is assumed useful for development scenarios only. +3. Image types (`small`, `thumbnail`, etc) are included in the information stored on Store-Front side. + + +### Questions: + +1. Do we sync full image URL to SF or provide Base CDN URL as configuration for the store? + 1. Full URLs as part of product data: full reindex will be required in case CDN URL changes. + 2. Base CDN URL as a store configuration + relative asset path as part of product data: added complexity of the SF App due to additional knowledge about CDN/Media Base URLs (especially in case multiple should be supported). + +## Dependencies + +1. To allow only original image URLs stored at the Store-Front side, new format of transformed images should be supported with transformation parameters are passed as parameters to the original URL. Current transformed URL: `https://magento.store.com/media/catalog/product/cache/1/2/3/.jpg` (where `` is generated based on transformation parameters and those become invisible). Desired transformed URL: `https://store.cdn.com/media/catalog/product/1/2/3/product-image.jpg?w=100&h=100` (transformation parameters are clearly visible). + 1. On first iteration we can just serve original image URLs by Store-Front. This would fully cover use cases where Base Media URL is a URL of CDN that supports image transformation, and client is responsible for transformed image URLs generation. + +## Breaking Changes + +1. Current GraphQL returns transformed images instead of originals (❗ validate this. Might be just broken URL, as it's not clear which exact transformation GraphQL provides from the entire list). Problems with this approach: + 1. Transformation depends on Magento theme, which is beyond GraphQL scope (GraphQL knows nothing about old Magento themes, like Luma). + 2. GraphQL clients can't perform or request necessary transformation, only predefined (by irrelevant Magento themes) transformations are provided. From 1a231e52b1403033ac9a2b243b0a4e5bcd806ca7 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 17 Jun 2020 10:15:03 -0500 Subject: [PATCH 103/479] Make destination param nullable in mergeCarts --- .../mergeCarts-optional-destination.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 design-documents/graph-ql/coverage/mergeCarts-optional-destination.md diff --git a/design-documents/graph-ql/coverage/mergeCarts-optional-destination.md b/design-documents/graph-ql/coverage/mergeCarts-optional-destination.md new file mode 100644 index 000000000..1107ceede --- /dev/null +++ b/design-documents/graph-ql/coverage/mergeCarts-optional-destination.md @@ -0,0 +1,40 @@ +# `mergeCarts` - Make `destination_cart_id` optional + +## What + +- Make the `destination_cart_id` argument optional in the `mergeCarts` mutation + +## Why + +This came up in a discussion with [@sirugh](https://github.com/sirugh) from the [`pwa-studio`](https://github.com/magento/pwa-studio) team. + +When a user logs in (creates a new token), one of the first things the UI needs to do is merge the current guest cart (if items are present) into the customer account's cart. + +If `destination_cart_id` is required, this requires 3 round trips: + +1. Call for `Mutation.generateCustomerToken` +2. Call for `Query.cart` to get customer cart ID +3. Call for `Mutation.mergeCarts` to merge guest cart ID into customer cart + +Because a customer can only have a single cart, and this API only works for authenticated users, `destination_cart_id` is an unnecessary requirement here. If we make it optional, the login + cart upgrade for UI can happen in a single request: + +```graphql +# Mutations run serially, in-order. So `mergeCarts` will only execute +# if `generateCustomerToken` succeeds +mutation LoginAndMergeCarts($email: String!, $password: String!, $guestCartID: String!) { + generateCustomerToken(email: $email, password: $password) { + token + } + mergeCarts(source_cart_id: $guestCartID) { + ID + } +} +``` + +## Proposed Change +```diff +type Mutation { +- mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! ++ mergeCarts(source_cart_id: String!, destination_cart_id: String): Cart! +} +``` \ No newline at end of file From e1e39e869595aeed0f7decedd72602d0c148f6be Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 17 Jun 2020 13:37:33 -0500 Subject: [PATCH 104/479] ECP-756: GraphQL Schema for Rewards --- design-documents/graph-ql/coverage/reward-points.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index e3c1e390f..00a4a153f 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -12,11 +12,11 @@ type Mutation { } type ApplyRewardPointsToCartOutput { - cart: Cart + cart: Cart! } type RemoveRewardPointsFromCartOutput { - cart: Cart + cart: Cart! } type RewardPoints { From 8e9236610b5c9708dfc30a9411acb4426c8774b5 Mon Sep 17 00:00:00 2001 From: Saravanan Date: Thu, 18 Jun 2020 16:46:44 +0530 Subject: [PATCH 105/479] Added replace or merge option for addRequisitionListItemToCart mutation --- design-documents/graph-ql/coverage/requisitionList.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 413e9e0e5..180bfeca3 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -122,7 +122,8 @@ type Mutation { addRequisitionListItemToCart( requisition_list_id: ID!, @doc(description: "unique Id of requisition list") - item_ids: [ID!]! @doc(description: "selected requisition list items that are to be added") + item_ids: [ID!]!, @doc(description: "selected requisition list items that are to be added") + is_replace: Boolean @doc(description: "replaces or merges the existing cart items with requisition list items") ): AddRequisitionListItemToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") copyItemsBetweenRequisitionList( From 579fb3a60c00dea4f6f88ea90e93f9371c7dd557 Mon Sep 17 00:00:00 2001 From: Arvind Date: Thu, 18 Jun 2020 18:57:37 +0530 Subject: [PATCH 106/479] fixed review comments --- .../coverage/negotiableQuotes.graphqls | 165 ++++++++++++------ 1 file changed, 115 insertions(+), 50 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index d8fea82ed..e02bf6d13 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,14 +1,36 @@ type Mutation { createNegotiableQuote( - cart_id: ID! @doc(description: "Cart ID") - quote_name: String @doc(description: "Quote name") + cartId: ID! @doc(description: "Cart ID") + quoteName: String @doc(description: "Quote name") comment: String @doc(description: "Comment") files: [FileInput!] @doc(description: "Attached files") - ): RequestNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") + ): CreateNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") + + addNegotiableQuoteComments( + cartId: ID! @doc(description: "Cart ID") + comment: String @doc(description: "Comment") + files: [FileInput!] @doc(description: "Attached files") + ): AddNegotiableQuoteCommentsOutput! @doc(description:"Generates negotiable quote request") + + updateNegotiableQuoteItemsQuantity( + cart_id: ID!, @doc(description: "unique Id of negotiable quote") + items: [ID!]! @doc(description: "unique Ids of Items to be update in negotiable quote") + quantity: Float, @doc(description: "quantity of the item to be updated") + ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Remove Items in negotiable quote") + + removeNegotiableQuoteItems( + cart_id: ID!, @doc(description: "unique Id of negotiable quote") + items: [ID!]! @doc(description: "unique Ids of Items to be removed from negotiable quote") + ): RemoveNegotiableQuoteItemsOutput @doc(description: "Remove Items in negotiable quote") + + setShippingAddressesOnNegotiableQuote( + cart_id: ID! + shipping_addresses: [NegotiableQuoteShippingAddressInput!]! + ): SetShippingAddressesOnNegotiableQuoteOutput @doc(description: "Assign shipping address to Negotiable quote") setShippingMethodsOnNegotiableQuote( - cart_id: ID! @doc(description: "Cart ID") - shipping_methods: [String!]! @doc(description: "Input for shipping method") + cartId: ID! @doc(description: "Cart ID") + shippingMethods: [ShippingMethodInput]! @doc(description: "Input for shipping method") ): SetShippingMethodsOnNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") } @@ -17,9 +39,9 @@ type Customer { id: ID!): NegotiableQuote @doc(description: "Get negotiable quote of a customer") negotiable_quotes( - page_size: Int = 20, - current_page: Int = 1, - filter: NegotiableQuoteFilterInput + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") estimateShippingCostForNegotiableQuote( @@ -33,11 +55,16 @@ type Customer { type NegotiableQuote { quote_id: ID! @doc(description: "Negotiable quote ID") - items: [NegotiableQuoteItem] @doc(description: "Negotiable quote item") + items: [NegotiableQuoteItemInterface] @doc(description: "Negotiable quote item") attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") - totals: TotalsOutput! @doc(description: "Negotiable quote totals output") + historyLog: [NegotiableQuoteHistoryLog] @doc(description: "Negotiable history log") + prices: CartPrices! @doc(description: "Negotiable quote totals output") + created_by: String @doc(description: "Negotiable quote creator") + created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") + updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") + status: String @doc(description: "Negotiable quote status") available_shipping_methods: [ShippingMethodsOutput]! @doc(description: "Available shipping methods") } @@ -46,10 +73,61 @@ input NegotiableQuoteFilterInput { name: FilterMatchTypeInput @doc(description: "Filter by display name of the negotiable quote") } -type NegotiableQuoteItem { +type NegotiableQuoteItemInterface { id: ID! quantity: Float! + stock: Float! + prices: NegotiableQuoteItemPrices + product: ProductInterface! +} + +type NegotiableQuoteItemPrices { + price: Money! + row_total: Money! + row_total_including_tax: Money! + discounts: [Discount] @doc(description:"An array of discounts to be applied to the quote item") + total_item_discount: Money @doc(description:"The total of all discounts applied to the item") +} + +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the quote as a whole or to an item.") { + amount: Money! @doc(description:"The amount of the discount") + label: String! @doc(description:"A description of the discount") +} + +type DefaultNegotiableQuoteItem implements NegotiableQuoteItemInterface +@doc(description: "Negotiable Quote Item Implementation for Simple and Virtual Products") { + id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") + product: ProductInterface! + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") +} + +type DownloadableRequisitionListItem implements NegotiableQuoteItemInterface +@doc(description: "Negotiable Quote Item Implementation that for Downloadable Products") { + id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") product: ProductInterface! + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + links: [DownloadableProductLinks] @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") + samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") +} + +type BundleRequisitionListItem implements NegotiableQuoteItemInterface +@doc(description: "Negotiable Quote Item Implementation that for Bundle Products") { + id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") + product: ProductInterface! + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + bundle_options: [SelectedBundleOption]! @doc(description: "selected bundle options") +} + +type ConfigurableRequisitionListItem implements NegotiableQuoteItemInterface +@doc(description: "Negotiable Quote Item Implementation that for Configurable Products") { + id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") + product: ProductInterface! + qty: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") + configurable_options: [SelectedConfigurableOption] @doc(description: "Configurable options selected") } type AttachmentContent @doc(description: "Negotiable quote attachment file") { @@ -59,28 +137,26 @@ type AttachmentContent @doc(description: "Negotiable quote attachment file") { } type NegotiableQuoteComment { - entity_id: ID! - parent_id: ID! - creator_type: Int - is_decline: Int - is_draft: Int - creator_id: ID! - comment: String + id: ID! + parent_id: String! + author: String! + text: String created_at: String - attachments: [NegotiableQuoteCommentAttachments] + attachments: [String] } -type NegotiableQuoteCommentAttachments { - attachment_id: ID! - comment_id: ID - file_name: String - file_path: String - file_type: String +type NegotiableQuoteHistoryLog { + id: ID! + author: String! + action: String! + comment: String + status: String + created_at: String } type NegotiableQuotePaymentInfo { payment_methods: [PaymentMethodsOutput]! @doc(description: "All list of payment options and totals") - totals: TotalsOutput! @doc(description: "List of totals") + prices: CartPrices! @doc(description: "List of totals") } type PaymentMethodsOutput { @@ -88,26 +164,19 @@ type PaymentMethodsOutput { title: String @doc(description: "Payment method title") } -type TotalsOutput { - grand_total: Money - subtotal_including_tax: Money - subtotal_excluding_tax: Money - discount: CartDiscount - subtotal_with_discount_excluding_tax: Money - applied_taxes: [CartTaxItem] +type CreateNegotiableQuoteOutput { + negotiable_quote:NegotiableQuote! } -type CartTaxItem { - amount: Money! - label: String! +type AddNegotiableQuoteCommentsOutput { + negotiable_quote:NegotiableQuote! } -type CartDiscount { - amount: Money! - label: [String!]! +type UpdateNegotiableQuoteItemsQuantityOutput { + negotiable_quote:NegotiableQuote! } -type RequestNegotiableQuoteOutput { +type RemoveNegotiableQuoteItemsOutput{ negotiable_quote:NegotiableQuote! } @@ -118,15 +187,7 @@ input FileInput @doc(description: "The list of file attachment codes") { } input NegotiableQuoteShippingAddressInput { - firstname: String! @doc(description: "The first name of the person associated with the billing address") - lastname: String! @doc(description: "The family name of the person associated with the billing address") - telephone: String! @doc(description: "The telephone number") - street: [String]! @doc(description: "A list of strings that define the street number and name") - city: String! @doc(description: "The city or town") - region: String! @doc(description: "The region name") - region_id: Int @doc(description: "The region ID") - postcode: String! @doc(description: "The customer's ZIP or postal code") - country_code: String! @doc(description: "The country code") + address: CartAddressInput } type ShippingMethodsOutput { @@ -143,5 +204,9 @@ type ShippingMethodsOutput { } type SetShippingMethodsOnNegotiableQuoteOutput { - shipping_method_set: Boolean! + negotiable_quote: NegotiableQuote } + + type SetShippingAddressesOnNegotiableQuoteOutput{ + negotiable_quote: NegotiableQuote + } \ No newline at end of file From 5dad73f00fba37c92fd4c45fa5465f6dffd122f0 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 18 Jun 2020 11:41:46 -0500 Subject: [PATCH 107/479] ECP-711: GraphQL schema for Gift Registry --- design-documents/graph-ql/coverage/Cart.graphqls | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index ce63f151e..f77f74cdd 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -13,7 +13,7 @@ type CartQueryOutput { type Cart { id: ID! @doc(description: "The ID of the cart.") items: [CartItemInterface] - applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") + applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code. By default Magento supports only one coupon.") email: String shipping_addresses: [ShippingCartAddress]! billing_address: BillingCartAddress @@ -72,7 +72,8 @@ type GiftCardCartItem implements CartItemInterface { } type SelectedConfigurableOption { - id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry") + # Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry + id_v2: ID! id: Int! option_label: String! value_id: Int! @@ -80,7 +81,8 @@ type SelectedConfigurableOption { } type DownloadableProductLink @doc(description: "Defines characteristics of a downloadable product") { - id: ID! @doc(description: "Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry") + # Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry + id: ID! title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") price: Float @doc(description: "The price of the downloadable product") @@ -109,7 +111,8 @@ type SelectedBundleOptionValue { } type SelectedCustomizableOption { - id_v2: ID! @doc(description: "Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry") + # Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry + id_v2: ID! id: Int! label: String! is_required: Boolean! @@ -131,7 +134,8 @@ type CartItemSelectedOptionValuePrice { } type SelectedGiftCardAmount { - id: ID! @doc(description: "Hash from the type of the option and value") + # Hash from the type of the option and value + id: ID! value: Money! } From bb67393fb1f161dc1fb2afa4c49ed0e2a8b85545 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 18 Jun 2020 13:33:52 -0500 Subject: [PATCH 108/479] ECP-711: GraphQL schema for Gift Registry --- design-documents/graph-ql/coverage/Cart.graphqls | 4 ++-- design-documents/graph-ql/coverage/Wishlist.graphqls | 2 +- design-documents/graph-ql/coverage/gift-registry.graphqls | 2 +- design-documents/graph-ql/coverage/gift-registry.md | 2 +- design-documents/graph-ql/coverage/gift-wrapping.md | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index f77f74cdd..355c52e37 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -66,7 +66,7 @@ type BundleCartItem implements CartItemInterface { type GiftCardCartItem implements CartItemInterface { sender_name: String! - recepient_name: String! + recipient_name: String! amount: SelectedGiftCardAmount message: String } @@ -139,7 +139,7 @@ type SelectedGiftCardAmount { value: Money! } -enum PriceTypeEnum @doc(description: "This enumeration the price type.") { +enum PriceTypeEnum { FIXED PERCENT DYNAMIC diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 42fd9346a..613445e9a 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -79,7 +79,7 @@ type BundleWishlistItem implements WishlistItemInterface { type GiftCardCartItem implements CartItemInterface { sender_name: String! - recepient_name: String! + recipient_name: String! amount: SelectedGiftCardAmount message: String } diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/gift-registry.graphqls index a3c6dc8fb..9efa70f2d 100644 --- a/design-documents/graph-ql/coverage/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/gift-registry.graphqls @@ -273,7 +273,7 @@ type VirtualGiftRegistryItem implements GiftRegistryItemInterface { # Not currently supported by Magento core type GiftCardGiftRegistryItem implements GiftRegistryItemInterface { sender_name: String! - recepient_name: String! + recipient_name: String! amount: SelectedGiftCardAmount message: String } diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/gift-registry.md index 2f4ae8b51..1de1e857c 100644 --- a/design-documents/graph-ql/coverage/gift-registry.md +++ b/design-documents/graph-ql/coverage/gift-registry.md @@ -484,7 +484,7 @@ mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGift } ... on GiftCardGiftRegistryItem { sender_name - recepient_name + recipient_name amount { currency value diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index 897ac4098..a6090b989 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -82,7 +82,7 @@ type GiftOptionsPrices { } type GiftMessage { - to: String! @doc(description: "Recepient name") + to: String! @doc(description: "Recipient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") } @@ -189,7 +189,7 @@ type SetGiftOptionsOnCartOutput { } type GiftMessageInput { - to: String! @doc(description: "Recepient name") + to: String! @doc(description: "Recipient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") } From 5f6d59d768ad2b3d65971ce77d5d381d770e3baf Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 18 Jun 2020 16:32:37 -0500 Subject: [PATCH 109/479] Make `RewardPoints.balance_history` a list of non-nullables --- design-documents/graph-ql/coverage/reward-points.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index 00a4a153f..cbb0035e0 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -23,7 +23,7 @@ type RewardPoints { balance: RewardPointsAmount @doc(description: "Current reward points balance") exchange_rates: RewardPointsExchangeRates @doc(description: "Current reward points exchange rates") subscription_status: RewardPointsSubscriptionStatus @doc(description: "Reward points related email subscription status") - balance_history: [RewardPointsBalanceHistoryItem] @doc(description: "Reward points balance history. This field will be set to null if disabled in the admin") + balance_history: [RewardPointsBalanceHistoryItem!] @doc(description: "Reward points balance history. This field will be set to null if disabled in the admin") } type RewardPointsAmount { From 474257e9b86ca17d101f3ddbea2f7ac800d22327 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 22 Jun 2020 17:31:20 -0500 Subject: [PATCH 110/479] Update catalog-images.md --- design-documents/media/catalog-images.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 77c8ad7ac..e10de09e4 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -51,7 +51,8 @@ Similar to the product edit scenario ### Concepts: -1. Store-Fronts stores only original image URLs +1. Store-Front stores only original image URLs +2. Store-Front is not responsible for physical images. This is responsibility of DAM (which can be a specialized DAM or Magento Back office). 2. Image URLs support image transformation by parameters (e.g., `https://some.domain.com/media/catalog/product/1/2/3.jpg?w=100&h=100`) 1. Image transformation is performed by CDN. The URLs passed from the Backoffice to Store-Front are CDN-based. 2. Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. Benefits of this approach are not clear. Such scenario is assumed useful for development scenarios only. From 5e375fef9208114732d40cbf1aa79fb42293cc00 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 22 Jun 2020 17:59:44 -0500 Subject: [PATCH 111/479] Add proposed schema for fetching available stores --- .../graph-ql/coverage/available-stores.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 design-documents/graph-ql/coverage/available-stores.md diff --git a/design-documents/graph-ql/coverage/available-stores.md b/design-documents/graph-ql/coverage/available-stores.md new file mode 100644 index 000000000..2c7c95aa3 --- /dev/null +++ b/design-documents/graph-ql/coverage/available-stores.md @@ -0,0 +1,73 @@ +# Get available stores for website + +### Use case: +Implementing a store switcher; it is necessary to know which stores are available and some basic info about them (i.e. store code) + +### Implementation detail: +Based on the store code passed via header (or default), returns the storeConfig for all stores available under the same website. + +### Proposed schema +```graphql +type Query { + availableStores: [StoreConfig] @doc(description: "Get a list of available stores") +} + +# Existing schema +type StoreConfig @doc(description: "The type contains information about a store config") { + id : Int @doc(description: "The ID number assigned to the store") + code : String @doc(description: "A code assigned to the store to identify it") + website_id : Int @doc(description: "The ID number assigned to the website store belongs") + locale : String @doc(description: "Store locale") + base_currency_code : String @doc(description: "Base currency code") + default_display_currency_code : String @doc(description: "Default display currency code") + timezone : String @doc(description: "Timezone of the store") + weight_unit : String @doc(description: "The unit of weight") + base_url : String @doc(description: "Base URL for the store") + base_link_url : String @doc(description: "Base link URL for the store") + base_static_url : String @doc(description: "Base static URL for the store") + base_media_url : String @doc(description: "Base media URL for the store") + secure_base_url : String @doc(description: "Secure base URL for the store") + secure_base_link_url : String @doc(description: "Secure base link URL for the store") + secure_base_static_url : String @doc(description: "Secure base static URL for the store") + secure_base_media_url : String @doc(description: "Secure base media URL for the store") + store_name : String @doc(description: "Name of the store") + # ... more fields added from other modules and 3rd parties +} +``` + +Sample query: +```graphql +query { + availableStores { + id + code + locale + timezone + base_url + } +} +``` + +Sample response: +```json +{ + "data": { + "availableStores": [ + { + "id": 1, + "code": "default", + "locale": "en_US", + "timezone": "America/Chicago", + "base_url": "http://magento.test/" + }, + { + "id": 2, + "code": "German", + "locale": "de_DE", + "timezone": "Europe/Berlin", + "base_url": "http://magento.test/" + } + ] + } +} +``` From ffb509b047a58448e8866f9a28c9b3948799592c Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 23 Jun 2020 10:09:54 -0500 Subject: [PATCH 112/479] Return full simple product for configurable cart item instead of just sku --- design-documents/graph-ql/coverage/Cart.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index 355c52e37..f46cc6c48 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -50,7 +50,7 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car } type ConfigurableCartItem implements CartItemInterface { - child_sku: String! @doc(description: "SKU of the simple product corresponding to a set of selected configurable options.") + configured_variant: SimpleProduct! @doc(description: "Simple product corresponding to configured product.") configurable_options: [SelectedConfigurableOption!] } From fb3132e74a7627838c73f0a56e7d4f02a8046726 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 23 Jun 2020 10:37:21 -0500 Subject: [PATCH 113/479] Change configurable_options prefix to match user state --- design-documents/graph-ql/coverage/Cart.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/Cart.graphqls index f46cc6c48..28f54eff8 100644 --- a/design-documents/graph-ql/coverage/Cart.graphqls +++ b/design-documents/graph-ql/coverage/Cart.graphqls @@ -51,7 +51,8 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car type ConfigurableCartItem implements CartItemInterface { configured_variant: SimpleProduct! @doc(description: "Simple product corresponding to configured product.") - configurable_options: [SelectedConfigurableOption!] + configurable_options: [SelectedConfigurableOption!] @deprecated(reason: "use configured_options") + configured_options: [SelectedConfigurableOption!] @doc(description: "Configured options for product") } type DownloadableCartItem implements CartItemInterface @doc(description: "Downloadable Cart Item") { From 84b1b3a381be4adaf903213639f02bd49c5a4ef0 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 23 Jun 2020 10:48:28 -0500 Subject: [PATCH 114/479] Update description --- design-documents/graph-ql/coverage/available-stores.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/available-stores.md b/design-documents/graph-ql/coverage/available-stores.md index 2c7c95aa3..c67ade318 100644 --- a/design-documents/graph-ql/coverage/available-stores.md +++ b/design-documents/graph-ql/coverage/available-stores.md @@ -9,7 +9,7 @@ Based on the store code passed via header (or default), returns the storeConfig ### Proposed schema ```graphql type Query { - availableStores: [StoreConfig] @doc(description: "Get a list of available stores") + availableStores: [StoreConfig] @doc(description: "Get a list of available store views and their config information.") } # Existing schema From ffa9f2b916409fd1f94588cde10e2bfb10aeb456 Mon Sep 17 00:00:00 2001 From: Arvind Date: Wed, 24 Jun 2020 12:36:19 +0530 Subject: [PATCH 115/479] review comments fixed --- .../coverage/negotiableQuotes.graphqls | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index e02bf6d13..792bf949b 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,7 +1,7 @@ type Mutation { createNegotiableQuote( cartId: ID! @doc(description: "Cart ID") - quoteName: String @doc(description: "Quote name") + quoteName: String! @doc(description: "Quote name") comment: String @doc(description: "Comment") files: [FileInput!] @doc(description: "Attached files") ): CreateNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") @@ -10,13 +10,13 @@ type Mutation { cartId: ID! @doc(description: "Cart ID") comment: String @doc(description: "Comment") files: [FileInput!] @doc(description: "Attached files") - ): AddNegotiableQuoteCommentsOutput! @doc(description:"Generates negotiable quote request") + ): AddNegotiableQuoteCommentsOutput! @doc(description:"Add comments to negotiable quote") - updateNegotiableQuoteItemsQuantity( + updateNegotiableQuoteItemQuantity( cart_id: ID!, @doc(description: "unique Id of negotiable quote") - items: [ID!]! @doc(description: "unique Ids of Items to be update in negotiable quote") - quantity: Float, @doc(description: "quantity of the item to be updated") - ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Remove Items in negotiable quote") + item: ID!, @doc(description: "unique Id of Item to be update in negotiable quote") + quantity: Float @doc(description: "quantity of the item to be updated") + ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Update items quantity in negotiable quote") removeNegotiableQuoteItems( cart_id: ID!, @doc(description: "unique Id of negotiable quote") @@ -44,13 +44,6 @@ type Customer { currentPage: Int = 1 ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") - estimateShippingCostForNegotiableQuote( - address: NegotiableQuoteShippingAddressInput! - ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote") - - estimateShippingCostByAddressIdForNegotiableQuote( - address_id: String! - ): [ShippingMethodsOutput]! @doc(description:"Estimate shipping costs for a negotiable quote by Address ID") } type NegotiableQuote { @@ -195,12 +188,12 @@ type ShippingMethodsOutput { method_code: String @doc(description: "Shipping method code") carrier_title: String @doc(description: "Shiping carrier title") method_title: String @doc(description: "Shipping method title") - amount: String @doc(description: "Shipping amount") - base_amount: String @doc(description: "Shipping base amount") + amount: Money @doc(description: "Shipping amount") + base_amount: Money @doc(description: "Shipping base amount") available: String @doc(description: "Shipping method availble") error_message: String @doc(description: "Shipping method error message") - price_excl_tax: String @doc(description: "Shipping method price with exclusive tax") - price_incl_tax: String @doc(description: "Shipping method price with inclusive tax") + price_excl_tax: Money @doc(description: "Shipping method price with exclusive tax") + price_incl_tax: Money @doc(description: "Shipping method price with inclusive tax") } type SetShippingMethodsOnNegotiableQuoteOutput { @@ -209,4 +202,4 @@ type SetShippingMethodsOnNegotiableQuoteOutput { type SetShippingAddressesOnNegotiableQuoteOutput{ negotiable_quote: NegotiableQuote - } \ No newline at end of file +} \ No newline at end of file From a013a016399b3ea0fde91ec8ae22037fcb3f8036 Mon Sep 17 00:00:00 2001 From: Saravanan Date: Thu, 25 Jun 2020 11:47:53 +0530 Subject: [PATCH 116/479] added new mutation to clear cart Items --- .../graph-ql/coverage/requisitionList.graphqls | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 180bfeca3..b285c997f 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -122,8 +122,7 @@ type Mutation { addRequisitionListItemToCart( requisition_list_id: ID!, @doc(description: "unique Id of requisition list") - item_ids: [ID!]!, @doc(description: "selected requisition list items that are to be added") - is_replace: Boolean @doc(description: "replaces or merges the existing cart items with requisition list items") + item_ids: [ID!]! @doc(description: "selected requisition list items that are to be added") ): AddRequisitionListItemToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") copyItemsBetweenRequisitionList( @@ -137,6 +136,10 @@ type Mutation { destination_id: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created item_ids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") ): MoveItemsFromRequisitionListOutput @doc(description: "Move Items from Requisition List to another requisition List") + + clearCustomerCart( + cart_id: String! @doc(description: "masked Cart Id") + ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } type RemoveRequisitionListItemsOutput { @@ -194,3 +197,7 @@ type MoveItemsFromRequisitionListOutput { source : RequisitionList @doc(description: "Source Requisition List") destination : RequisitionList @doc(description: "Destination Requisition List") } + +type ClearCustomerCartOutput { + cart: Cart +} From 4ccb1fed207f508b4ad5706bdc395dcd58e3e950 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Thu, 25 Jun 2020 19:56:35 +0530 Subject: [PATCH 117/479] req list items pagiantion --- .../graph-ql/coverage/requisitionList.graphqls | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 413e9e0e5..d1476899f 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -42,11 +42,20 @@ type RequisitionList @doc(description: "Requisition List Type") { id: ID! @doc(description: "Unique Identifier of Requisition List") name: String! @doc(description: "Name of the list") description: String @doc(description: "Description of the list") - items: [RequisitionListItemInterface] @doc(description: "Items in the list") + items( + currentPage: Int = 1, + pageSize: Int = 20 + ): RequistionListItems items_count: Int! @doc(description: "Number of items in list") updated_at: String @doc(description: "Latest Activity") } +type RequistionListItems { + items: [RequisitionListItemInterface] @doc(description: "Requisition List items list") + page_info: SearchResultPageInfo + total_pages: Int @doc(description: "total count of req list items") +} + interface RequisitionListItemInterface @doc(description: "Interface type for Requisition List Item") { id: ID! @doc(description:"Unique Identifier of Requisition List Item") product: ProductInterface! From 15f6f1f4c43614c96dcf08684949b640fbd5d23e Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 25 Jun 2020 11:11:14 -0500 Subject: [PATCH 118/479] Added design document for how to handle nullability in GraphQL --- design-documents/graph-ql/nullability.md | 159 +++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 design-documents/graph-ql/nullability.md diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md new file mode 100644 index 000000000..eb966ffdd --- /dev/null +++ b/design-documents/graph-ql/nullability.md @@ -0,0 +1,159 @@ +# Nullability in GraphQL + +By default, all field values in GraphQL are _nullable_ + +```graphql +type Query { + product(id: ID): Product # Server can also return "null" +} +``` + +GraphQL provides a wrapper/modifier that can be used to disallow null values. It's opt-in on a per-field basis + +```graphql +type Query { + product(id: ID): Product! # Server cannot return "null" +} +``` + +## Why non-nullable? + +Non-nullable fields help form a contract with clients that makes it safe for them to work with a field's value without having to inspect its shape for possible `null` values. + +For clients that are written in a language with a static type system, the compiler will force them to wrap their code in conditionals anytime they're accessing a possibly null field. These checks can become tedious if every single field is nullable. + +For clients that are written in more dynamic languages (JavaScript), developers need to be very aware of nullable fields in the schema to prevent trying to accessing data on a `null` value. This leads to lots of defensive coding (or bugs). + +Making fields non-nullable alleviates some of these issues for clients. + +## Why nullable? + +When a field's resolver has an error, GraphQL requires that the value of the field be set to `null`. But, if a field is non-nullable, a `null` value would break that contract. + +Instead, an error in a resolver for a non-nullable field propagates up to the nearest nullable ancestor field. This can end up causing a client to lose more data farther up the response tree, even though that data may have been resolved without errors. + +### Example: Bad use of non-nullable field + +In this example, a client wants to query for data to render a product details page. But, because the `related_products` field is non-nullable, the client will lose access to data like `name` and `price` if `related_products` throws any errors. + +Ideally, a UI would get the critical product data, render the page, and just not render the "Related Products" display. + +**Schema**: +```graphql +type Query { + product(id: ID): Product +} + +type Product { + id: ID! + name: String! + price: Money! + description: String! + url: String! + related_products: [RelatedProduct!]! +} +``` +**Query**: +```graphql +query ProductDetailsPage($id: ID) { + product(id: $id) { + id + name + price + description + # related_products can't be null. When the backing service is + # down/unreachable at the time this query runs, we can't fulfill + # the contract + related_products { + id + name + price + url + } + } +} +``` + +**Response** + +Note that no data was obtained by the client for `product` +```js +{ + "data": { + "product": null + }, + "errors": [ + // details about failure in related_products field populated here + ] +} +``` + +## Backwards Compatibility + +- It is a safe, non-breaking change to move a field from nullable to non-nullable +- It is an unsafe, breaking change to move a field from non-nullable to nullable + +With this in mind, when in doubt, the safer route is to make a field nullable, with the agreement that we'll iterate and improve schemas as we get feedback on usage from clients. + +## Recommendations + +### Scalars + +If the field's value is obtained from a backing data source, and that value is required in the backing data source, then the field in the GraphQL schema should be non-nullable + +#### Example +```graphql +type Product { + id: ID! # Products service would not allow a product without an ID, so id is non-nullable + color: String # Backing products DB does not require color, so it's nullable +} +``` + +### Object Types + +Object type fields are frequently used to create joins between types. Object type fields should be nullable when the backing data is likely to live in a different service from the parent object type. This rule will require more thought since it depends where we think domain boundaries will be drawn as we decompose to services. + +#### Example +```graphql +type Product { + price: Price! # non-nullable, because price is highly likely to be part of a products service + recommended_products: ProductRecommendations # nullable, because product recommendations would likely be a function that lives outside of the products service +} +``` + +### List Types + +List fields have [2 distinct forms of nullability](http://spec.graphql.org/draft/#sec-Combining-List-and-Non-Null): + +1. Field nullability (whether field can return `null` instead of a list) +2. List nullability (whether a list can have `null` items inside it) + +These forms can be composed together in various ways: +```graphql +type Example { + foo1: [Foo] # foo1 can be null or a list. If list, can have nulls in it + foo2: [Foo!]! # foo2 must be a list, and every entry must be a 'Foo' + foo3: [Foo!] # foo3 can be a list or null. If list, every item must be a 'Foo' + foo4: [Foo]! # foo4 must be a list, and the list can have null values +} +``` + +There are 2 rules of thumb to follow. Note that these can be combined together depending on the use-case + +#### Non-Nullable List _Field_ + +A list _field_ should be non-nullable when its absence would make the parent type fairly useless. + +An example is a `Product` type with an `options` field. When rendering a product detail page and asking for `Product.options`, you typically wouldn't want to keep rendering the page if `options` fails, because a shopper can't do much with a configurable product without the options. + +If a product has no options at all, an empty list can still be returned (`null` would only be used to represent an error state). + +The syntax for a non-nullable list _field_ is `fieldname: [TypeName]!` + +#### Non-Nullable List + +A _list_ should be non-nullable when the absence of at least 1 item in the list would make the parent type useless. + +Using the `Product.options` example again, it's extremely rare that you would want to render a product page with only _some_ of the configured options - what if you're missing a required one in the list? + +The syntax for a non-nullable _list_ is `[TypeName!]` From 9a0541e6626f37f9a9f94b84d001b4c22d1d7739 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 25 Jun 2020 12:37:00 -0500 Subject: [PATCH 119/479] Refactor based on feedback --- design-documents/graph-ql/nullability.md | 100 ++++++++++++++++------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index eb966ffdd..4c6490f80 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -4,7 +4,7 @@ By default, all field values in GraphQL are _nullable_ ```graphql type Query { - product(id: ID): Product # Server can also return "null" + product(id: ID): Product # Server can also return "null" } ``` @@ -12,7 +12,7 @@ GraphQL provides a wrapper/modifier that can be used to disallow null values. It ```graphql type Query { - product(id: ID): Product! # Server cannot return "null" + product(id: ID): Product! # Server cannot return "null" } ``` @@ -50,7 +50,7 @@ type Product { price: Money! description: String! url: String! - related_products: [RelatedProduct!]! + related_products: RelatedProducts! } ``` **Query**: @@ -65,10 +65,12 @@ query ProductDetailsPage($id: ID) { # down/unreachable at the time this query runs, we can't fulfill # the contract related_products { - id - name - price - url + items { + id + name + price + url + } } } } @@ -97,27 +99,55 @@ With this in mind, when in doubt, the safer route is to make a field nullable, w ## Recommendations -### Scalars +### Top-level Query fields should always be nullable -If the field's value is obtained from a backing data source, and that value is required in the backing data source, then the field in the GraphQL schema should be non-nullable +Because of error propagation, if an error occurs in a non-null field on the `Query` root, the client will lose data from all other top-level queries. + +```graphql +type Query { + # If a client queries for both fields, and one of them fails, + # the client will receive no data + product(id: ID): Product! + category(id: ID): Category! +} +``` + +### Primary keys (id field) should always be non-nullable + +It very rarely makes sense to have a resource that _can_ have an ID but might not. + +IDs are extremely important for caching in most GraphQL clients, so it's worthwhile to be safe here. -#### Example ```graphql type Product { - id: ID! # Products service would not allow a product without an ID, so id is non-nullable - color: String # Backing products DB does not require color, so it's nullable + id: ID! # Rarely makes sense for this to be nullable } ``` -### Object Types +### Consider the Parent Type + +If you're not dealing with an `id` field or a top-level `Query` field, the most important question to ask is: + +**Is the parent object still usable if this field has an error?** + +#### Example: Parent still usable with field error + +```graphql +type Product { + # Recommended products are not critical data on a product page, and a UI can represent + # a product safely without related products, so we keep the field nullable + recommended_products: ProductRecommendations +} +``` -Object type fields are frequently used to create joins between types. Object type fields should be nullable when the backing data is likely to live in a different service from the parent object type. This rule will require more thought since it depends where we think domain boundaries will be drawn as we decompose to services. +#### Example: Parent not usable with field error -#### Example ```graphql type Product { - price: Price! # non-nullable, because price is highly likely to be part of a products service - recommended_products: ProductRecommendations # nullable, because product recommendations would likely be a function that lives outside of the products service + # A user would not be able to add a product to the cart from the Product + # details page if this field fails, because it may have required options. + # We make the field's type non-nullable + options: ProductOptions! } ``` @@ -125,7 +155,7 @@ type Product { List fields have [2 distinct forms of nullability](http://spec.graphql.org/draft/#sec-Combining-List-and-Non-Null): -1. Field nullability (whether field can return `null` instead of a list) +1. Field nullability (same as all other fields) 2. List nullability (whether a list can have `null` items inside it) These forms can be composed together in various ways: @@ -138,22 +168,30 @@ type Example { } ``` -There are 2 rules of thumb to follow. Note that these can be combined together depending on the use-case - -#### Non-Nullable List _Field_ +To decide whether the _field_ should be nullable, use the other recommendations provided in this document. -A list _field_ should be non-nullable when its absence would make the parent type fairly useless. +When deciding whether a _List_ should be nullable, the most important question to ask is: -An example is a `Product` type with an `options` field. When rendering a product detail page and asking for `Product.options`, you typically wouldn't want to keep rendering the page if `options` fails, because a shopper can't do much with a configurable product without the options. +**"Is the parent object still usable if at least one item in this list has an error?"** -If a product has no options at all, an empty list can still be returned (`null` would only be used to represent an error state). +#### Example: Parent not usable if an item in List has an error -The syntax for a non-nullable list _field_ is `fieldname: [TypeName]!` - -#### Non-Nullable List - -A _list_ should be non-nullable when the absence of at least 1 item in the list would make the parent type useless. +```graphql +type Product { + # Note: The "!" inside of the List ([]) means the List is non-nullable + # Making the list non-nullable guarantees to the client that, if the receive + # a list of product options, it will be complete/without errors + options: [ProductOption!]! +} +``` -Using the `Product.options` example again, it's extremely rare that you would want to render a product page with only _some_ of the configured options - what if you're missing a required one in the list? +#### Example: Parent still usable if an item in List has an error -The syntax for a non-nullable _list_ is `[TypeName!]` +```graphql +type Product { + # The absence of a "!" inside the list means that we could fail + # to fetch a nested field in a related product, and it won't + # impact our ability to render the rest of the product page + related_products: [RelatedProduct] +} +``` \ No newline at end of file From f0f22b11eef2d7f15ed52729ffd98283c975d1f4 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 25 Jun 2020 12:54:13 -0500 Subject: [PATCH 120/479] Add some references for back-compat --- design-documents/graph-ql/nullability.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index 4c6490f80..272847296 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -97,6 +97,8 @@ Note that no data was obtained by the client for `product` With this in mind, when in doubt, the safer route is to make a field nullable, with the agreement that we'll iterate and improve schemas as we get feedback on usage from clients. +**Note**: The [GraphQL reference implementation](https://github.com/graphql/graphql-js) has a `findBreakingChanges` feature that can be inspected to see a [list of all known schema changes that cause breaks for clients](https://github.com/graphql/graphql-js/blob/2232ebdef828566f3add3ed2a31709d3c1710c0e/src/utilities/findBreakingChanges.js#L41-L67). It [explicitly notes](https://github.com/graphql/graphql-js/blob/2232ebdef828566f3add3ed2a31709d3c1710c0e/src/utilities/findBreakingChanges.js#L461) that moving from a nullable value to a non-nullable value of the same type is safe. + ## Recommendations ### Top-level Query fields should always be nullable From 78491accaef98cb6cb1e7daf154b9398ee3cdf17 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 25 Jun 2020 12:57:51 -0500 Subject: [PATCH 121/479] Clarify language around parent vs nearest nullable ancestor --- design-documents/graph-ql/nullability.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index 272847296..5cc622205 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -30,7 +30,14 @@ Making fields non-nullable alleviates some of these issues for clients. When a field's resolver has an error, GraphQL requires that the value of the field be set to `null`. But, if a field is non-nullable, a `null` value would break that contract. -Instead, an error in a resolver for a non-nullable field propagates up to the nearest nullable ancestor field. This can end up causing a client to lose more data farther up the response tree, even though that data may have been resolved without errors. +Instead, an error in a resolver for a non-nullable field propagates up to the nearest parent field. It that field is also non-nullable, the error propagates up to the next parent field. This propagation continues until either: + +- A nullable parent field is found +- We reach the root `Query` object + + A non-nullable type in the wrong place can end up causing a client to lose more data farther up the response tree, even though that data may have been resolved without errors. + +_[Further reading on the relationship between non-null types and errors](http://spec.graphql.org/draft/#sec-Errors-and-Non-Nullability)_ ### Example: Bad use of non-nullable field From 75e8980aad537b4558c2757fe5ad0fdf2be55af5 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 26 Jun 2020 17:43:21 -0500 Subject: [PATCH 122/479] Add document with suggested guidelines around mutation error design --- .../graph-ql/mutation-error-design.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 design-documents/graph-ql/mutation-error-design.md diff --git a/design-documents/graph-ql/mutation-error-design.md b/design-documents/graph-ql/mutation-error-design.md new file mode 100644 index 000000000..f903bf1ec --- /dev/null +++ b/design-documents/graph-ql/mutation-error-design.md @@ -0,0 +1,153 @@ +# Mutation Error Design + +Mutations are _the_ single way to trigger side-effects in a GraphQL API. Side-effects are where the large majority of our errors are going to happen, as a result of changing some underlying data. + +Today, the Magento 2 GraphQL schema does not do a great job of describing what could go wrong as a result of running a mutation. We should fix that! + +## Example of the problem + +Take this simplified schema to add an item to a shopper's cart: + +```graphql +type Mutation { + addItemToCart(sku: String!, quantity: Float): Cart +} +``` + +This schema only describes one side-effect that can happen as a result of adding an item to the cart: it succeeds! But some things can go wrong when adding an item to the cart, and we know what many of those things are: + +- Item went out of stock since the product page was loaded +- Merchant disabled the product since the product page was loaded +- Cart hit the max quantity allowed of item per customer. Full qty selected was not added, but some were +- Extensions can add additional rules that would limit adding an item to the cart + +Now put yourself in the shoes of a UI developer: how do you know all the error states your UI needs to cover? For most of the Magento 2 mutations today, you would need run the mutation itself (or dig through PHP) to see what will be returned in the `errors` key of the GraphQL response: + +```js +{ + "data": { + "cart": { + // cart data here + } + }, + "errors": [{ + "message": "Item 'cool-hat' not added to cart (Out of Stock)", + "path": ["addItemToCart"] + }] +} +``` + +This has some problems: + +1. It's unreasonable to expect a UI developer to exercise every possible input to a resolver to find the error states manually +2. These errors are not enforced by the schema, so they're not versioned with the schema. +3. If the UI wants to customize the message for a specific error state, they'd have to match on the exact response string, because there's no concept of error codes + +Until recently, we didn't need to think quite as carefully about modeling errors and versioning them, because we owned the single reference theme for the application (Luma). In the world of headless UIs, it's critical that our errors are versioned and discoverable. + +## Solution: Design Mutation errors into the schema + +If we move our known error states for various mutations into the schema design itself, we'll get a few benefits: + +1. Versioning of errors +2. Discoverability of error states for UI devs + +### Example: Fixing our `addItemToCart` mutation + +In the previous example of `addItemToCart`, we had the following schema: + +```graphql +type Mutation { + addItemToCart(sku: String!, quantity: Float): Cart +} +``` + +Let's design some known error states directly into the API + +```graphql +type Mutation { + addItemToCart(sku: String!, quantity: Float): AddItemToCartOutput +} + +type AddItemToCartOutput { + # can still get the entire state of the cart post-mutation + cart: Cart + # can directly access errors that a shopper should be notified about + addItemUserErrors: [AddItemUserError!]! +} + +type AddItemUserError { + # if a UI has their own text for message, they can just + # not use this field, but serves as a descriptive default + message: string! + type: AddItemUserErrorType! +} + +# This enum is just an example. Non-exhaustive, and of course +# not all errors we'd want a specific type for +enum AddItemUserErrorType { + OUT_OF_STOCK + MAX_QTY_FOR_USER + NOT_AVAILABLE +} +``` + +With this new schema, we get the following query and response: + +```graphql +query { + addItemToCart(sku: "abc", quantity: 1) { + cart { + items { + # select item fields + } + } + + addItemUserErrors { + message + type + } + } +} +``` + +```js +{ + "data": { + "cart": { + // cart data here + }, + "addItemUserErrors": [{ + "message": "Item 'cool-hat' not added to cart (Out of Stock)", + "type": "OUT_OF_STOCK" + }] + } +} +``` +## Should we still use the `errors` key at all? + +Yes! _But_, we should consider the `errors` key to be of similar use to the `catch` clause when using `try/catch` in a programming language. + +The `errors` key should be for _exceptional_ circumstances, not for describing well-known states in an ecommerce app. It's unlikely you'd write this application code on the backend: + +```javascript +try { + var product = addItemToCart('sku'); + return product; +} catch (error) { + if (error.code === 'OUT_OF_STOCK') { + // + } else if (error.code === 'MAX_QTY_FOR_USER') { + // + } else { + // something else failed, maybe the DB connection? + } +} +``` + +Instead, you would likely have your `addItemToCart` function return something that describes the various possible results of the operation. + +## Open Questions + +1. Should we be consistent with the field name that represents these first-class errors? `userErrors` vs something like `addToCartUserErrors` +2. Should there be a basic interface that mutations should implement to enforce the pattern of returning a list of user errors? \ No newline at end of file From 719887debb33dd534fd6cb32d4daca6880f20fa8 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Sat, 27 Jun 2020 00:18:05 -0500 Subject: [PATCH 123/479] Update mutation-error-design.md --- design-documents/graph-ql/mutation-error-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/mutation-error-design.md b/design-documents/graph-ql/mutation-error-design.md index f903bf1ec..002acb92a 100644 --- a/design-documents/graph-ql/mutation-error-design.md +++ b/design-documents/graph-ql/mutation-error-design.md @@ -95,7 +95,7 @@ enum AddItemUserErrorType { With this new schema, we get the following query and response: ```graphql -query { +mutation { addItemToCart(sku: "abc", quantity: 1) { cart { items { @@ -150,4 +150,4 @@ Instead, you would likely have your `addItemToCart` function return something th ## Open Questions 1. Should we be consistent with the field name that represents these first-class errors? `userErrors` vs something like `addToCartUserErrors` -2. Should there be a basic interface that mutations should implement to enforce the pattern of returning a list of user errors? \ No newline at end of file +2. Should there be a basic interface that mutations should implement to enforce the pattern of returning a list of user errors? From fd30e921b839ca833e2330a52dd3855754bc68a0 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 6 Jul 2020 10:01:39 -0500 Subject: [PATCH 124/479] Clarify security requirement to not expose other websites --- design-documents/graph-ql/coverage/available-stores.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/available-stores.md b/design-documents/graph-ql/coverage/available-stores.md index c67ade318..caebce390 100644 --- a/design-documents/graph-ql/coverage/available-stores.md +++ b/design-documents/graph-ql/coverage/available-stores.md @@ -3,8 +3,10 @@ ### Use case: Implementing a store switcher; it is necessary to know which stores are available and some basic info about them (i.e. store code) -### Implementation detail: -Based on the store code passed via header (or default), returns the storeConfig for all stores available under the same website. +### Security Requirement: +Based on the store code passed via header (or default), returns the storeConfig for other stores available under the same website. + +This query MUST NOT expose other websites or stores available under websites other than the current website. ### Proposed schema ```graphql From 133730d9a548aed7486378e028cb16f92ae8816e Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 6 Jul 2020 17:27:18 -0500 Subject: [PATCH 125/479] Added section to nullability doc about scalars --- design-documents/graph-ql/nullability.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index 5cc622205..3d6d20e0c 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -133,6 +133,16 @@ type Product { } ``` +### Scalars should be non-nullable when required in backing data source + +Scalars form the leaves of a response in GraphQL, and are the bits of data the client really cares about. Because of this, we should strive to make scalars non-nullable when possible. + +The exceptions to this rule are: + +- A scalar field _should_ be nullable if it can be empty in the application/backing data source +- A scalar field _should_ be nullable if it's likely to come from a different service than its parent object (fairly rare) + + ### Consider the Parent Type If you're not dealing with an `id` field or a top-level `Query` field, the most important question to ask is: From 3fcbe433dd0fef4487297e07b363267477acfa27 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 6 Jul 2020 17:30:01 -0500 Subject: [PATCH 126/479] Fix md formatting in nullability doc --- design-documents/graph-ql/nullability.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index 3d6d20e0c..c7219774e 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -52,8 +52,8 @@ type Query { } type Product { - id: ID! - name: String! + id: ID! + name: String! price: Money! description: String! url: String! @@ -63,7 +63,7 @@ type Product { **Query**: ```graphql query ProductDetailsPage($id: ID) { - product(id: $id) { + product(id: $id) { id name price @@ -93,7 +93,7 @@ Note that no data was obtained by the client for `product` }, "errors": [ // details about failure in related_products field populated here - ] + ] } ``` @@ -155,7 +155,7 @@ If you're not dealing with an `id` field or a top-level `Query` field, the most type Product { # Recommended products are not critical data on a product page, and a UI can represent # a product safely without related products, so we keep the field nullable - recommended_products: ProductRecommendations + recommended_products: ProductRecommendations } ``` From 24606abbb79801a08eb6c1a2ae5f0d05b5202e6b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 6 Jul 2020 17:32:17 -0500 Subject: [PATCH 127/479] List nullability >> List item nullability --- design-documents/graph-ql/nullability.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/nullability.md index c7219774e..9624db4a2 100644 --- a/design-documents/graph-ql/nullability.md +++ b/design-documents/graph-ql/nullability.md @@ -175,7 +175,7 @@ type Product { List fields have [2 distinct forms of nullability](http://spec.graphql.org/draft/#sec-Combining-List-and-Non-Null): 1. Field nullability (same as all other fields) -2. List nullability (whether a list can have `null` items inside it) +2. List item nullability (whether a list can have `null` items inside it) These forms can be composed together in various ways: ```graphql @@ -189,7 +189,7 @@ type Example { To decide whether the _field_ should be nullable, use the other recommendations provided in this document. -When deciding whether a _List_ should be nullable, the most important question to ask is: +When deciding whether _List items_ should be nullable, the most important question to ask is: **"Is the parent object still usable if at least one item in this list has an error?"** @@ -197,8 +197,8 @@ When deciding whether a _List_ should be nullable, the most important question t ```graphql type Product { - # Note: The "!" inside of the List ([]) means the List is non-nullable - # Making the list non-nullable guarantees to the client that, if the receive + # Note: The "!" inside of the List ([]) means the list items are non-nullable + # Making the list items non-nullable guarantees to the client that, if they receive # a list of product options, it will be complete/without errors options: [ProductOption!]! } From 64dd1636979ae1e75e75ea2aa41f404ca156463f Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 8 Jul 2020 18:49:49 +0530 Subject: [PATCH 128/479] requisition list update item mutation with support for adding config options --- design-documents/graph-ql/coverage/requisitionList.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index d1476899f..1728b4429 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -174,7 +174,8 @@ input RequisitionListItemsInput { input UpdateRequisitionListItemsInput { item_id: ID! @doc(description: "unique ID of Requisition List Item") - customizable_options: [CustomizableOptionInput] + selected_options: [String!] @doc(description: "selected option ID") + entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") quantity: Float } From 935451d96722f7c4bf78d649c469950f4c97e8a7 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 8 Jul 2020 09:36:45 -0500 Subject: [PATCH 129/479] Storefront pricing --- design-documents/storefront/pricing.md | 92 ++++++++++++++++++ .../storefront/pricing/customer-tags.png | Bin 0 -> 44282 bytes .../storefront/pricing/pricebooks.png | Bin 0 -> 55965 bytes 3 files changed, 92 insertions(+) create mode 100644 design-documents/storefront/pricing.md create mode 100644 design-documents/storefront/pricing/customer-tags.png create mode 100644 design-documents/storefront/pricing/pricebooks.png diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md new file mode 100644 index 000000000..19ca332f0 --- /dev/null +++ b/design-documents/storefront/pricing.md @@ -0,0 +1,92 @@ +## Problem statement + +The final price of the product in Magento monolith depends on multiple variables such as current customer group, current website, qty of items in the shopping cart and current date/time. Magento monolith calculates all possible permutations of prices in advance and store them in `price index`. These calculations are expensive and may not be done in reasonable time for large catalogs. + +Few problematic use cases from real merchants: + +- Each customer in the system has unique prices for very few products. Per-customer prices are result of a physical contract with pricing included. +The system has 20,000 customer groups, 10,000 products, promotions are excluded from the system. Magento will generate 200,000,000xCOUNT_OF_WEBSITES records to handle all possible combinations for this merchant. For one website it will consume 17,8 GB of space for data and 22.3 GB for index(40 GB total). Due to external promotion system, prices are synchronized periodically. The synchronization process consumes a lot of resources and time and eventually space. + +- Customer groups are used for many things, but they are also a multiplier for prices +The system has 56 stores, 58 customer groups, 29,000 products, most customer groups are not global and used on one website only. Existing `price index` contains 25,000,000 records. The reindex process takes more than 7 hours. Customer groups are also used for promotions, cms content, product availability, B2B company, tax status and of course pricing. The real count of the prices is 26 times smaller. Potentially, reindex process may take 16 minutes. + +The impact form cases describe above will be doubled(or even tripled) if we introduce a new storefront service with existing index structure inside. Thus, we need some other way to work with prices in storefront. + +## Glossary +- Customer group - allocate each customer to a group and control store behaviour(catalog restrictions, pricing, discounts, B2B, CMS, payments, shipping, etc) according to which group a customer belongs to +- Website, Store View - abstract Magento scopes - https://docs.magento.com/user-guide/configuration/scope.html . Magento modules and third-party extensions may utilize this scope as they want. +- Base price - the initial product price. Different values could be set per website in Magento monolith. +- Tier price - have two meanings `customer-specific price` and `volume based price`. +- Special price - time based price for the product. Original price is strikedout in the UI. (e.g. was ~~$100.00~~, now $99.00) +- Catalog Rule - Magento functionality that allows to apply the same discount to multiple products + + +## Goals + +- Support up to 5,000,000 products and 15,000 customer groups +- Establish efficient sync of prices between Magento monolith and Magento storefront +- Reduce the size of pricing index +- Provide reliable support for personalized prices + +## Solution + +All price dimensions aren't used exclusively for pricing: +multiple `websites` may have the same price, but represent different web domains; `customer group` could represent the company in b2b scenarios or class of customer service; `product` may have different pricing on one website only. +The solution is to reuse data where possible and make separation of dimensions, so `websites` or `customer groups` which are not a part of pricing will not trigger `price index` explosion. + +### Price books + +The `price book`(price list) is a new entity that holds a list of product's prices for a sub-set of catalog. +The `price book` is a non-scoped entity, but it may hold information about linked customers, websites, etc. +Instead of direct lookup in price index by customer_group, website and product, we will detect the customer's price book first, then we will extract two price books from the index: default and current `price book`. +The resulting product price will be the value from current price book if it's exists, otherwise the product price will be extracted from default price book: + +![Price books diagram](pricing/pricebooks.png) + +### Default priceb ook + +A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. + + +### Customer tags instead of customer groups + +Magento monolith uses `customer groups` for customer segmentation globally. There is one-to-one relation between customer and customer group, +so customer could be a member of excatly one group only. However, in modern world, each customer could be a +member of different groups based on current behavior. For example, pricing system works with wholesale and regular buyers, +but recommendation system works with different groups of customers which are based on gender, age, ML-generated groups, etc. + +In order to provide more flexibility in customer segmentation, we may introduce many-to-many. Also, having `customer groups` +which are not bound to pricing functionality make them looks like a regular tags. Thus, we may also rename them to tags: + +![Price books diagram](pricing/customer-tags.png) + +## Scenarios + +#### Simple + +- Admin sets a `base price` for the `simple` product +- `product listing`, `PDP` and `checkout` scenarios contain the `base price` +- Customer or guest are able to buy the product for the `base price` + +#### Customer specific price + +- Admin sets a `base price` for the `simple` product +- Admin sets a `customer-specific price` for the same product +- `product listing`, `PDP` and `checkout` scenarios contain `base price` for guests and `customer-specific price` for selected customer +- Customer is able to buy the product for the `customer-specific price` + +#### Complex product pricing + +- Admin creates a `configurable` product and assign different `base prices` to variations +- `product listing` scenario contains the minimum price of the variations +- `PDP` scenario contains the minimum price of the variation and the price of currently selected variation +- `product listing` scenario contains the price of currently selected variation +- Customer or guest are able to buy the product variation for the price of selected variation + +#### Special prices + +- Admin sets a `base price` for the `simple` product +- Admin sets a `special price` for the same product for the current date +- `product listing`, `PDP` and `checkout` scenarios contain `special price` +- Customers and guests are able to buy the product for the `special price` + diff --git a/design-documents/storefront/pricing/customer-tags.png b/design-documents/storefront/pricing/customer-tags.png new file mode 100644 index 0000000000000000000000000000000000000000..12eed5480a1bc912ec2dda805b851a13720c7651 GIT binary patch literal 44282 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

UnT$zhT;>@&i8DS&9c3R}uVmjNnHSk-^yp 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 Date: Wed, 8 Jul 2020 12:09:10 -0500 Subject: [PATCH 130/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../{ => catalog}/CategoryFiltering.graphqls | 0 .../coverage/{ => catalog}/ProductPrices.graphqls | 0 .../configurable-options-selection.graphqls | 14 ++++++++++++++ .../catalog/configurable-options-selection.md | 14 ++++++++++++++ .../{ => catalog}/product-reviews.graphqls | 0 5 files changed, 28 insertions(+) rename design-documents/graph-ql/coverage/{ => catalog}/CategoryFiltering.graphqls (100%) rename design-documents/graph-ql/coverage/{ => catalog}/ProductPrices.graphqls (100%) create mode 100644 design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls create mode 100644 design-documents/graph-ql/coverage/catalog/configurable-options-selection.md rename design-documents/graph-ql/coverage/{ => catalog}/product-reviews.graphqls (100%) diff --git a/design-documents/graph-ql/coverage/CategoryFiltering.graphqls b/design-documents/graph-ql/coverage/catalog/CategoryFiltering.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/CategoryFiltering.graphqls rename to design-documents/graph-ql/coverage/catalog/CategoryFiltering.graphqls diff --git a/design-documents/graph-ql/coverage/ProductPrices.graphqls b/design-documents/graph-ql/coverage/catalog/ProductPrices.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/ProductPrices.graphqls rename to design-documents/graph-ql/coverage/catalog/ProductPrices.graphqls diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls new file mode 100644 index 000000000..a4318f7ec --- /dev/null +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -0,0 +1,14 @@ +type Query { + configurableOptionsSelectionMetadata(configurableProductSku: ID!, selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Get metadata for the specified configurable options selection") +} + +type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") +{ + configurable_options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") + media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable attributes selection.") +} + +type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { + available_value_ids: [ID!]! @doc(description: "Configurable option values available for further selection.") + attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") +} diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md new file mode 100644 index 000000000..203e16c41 --- /dev/null +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -0,0 +1,14 @@ +## Use cases + +### Render configurable option values available for selection on the product page + +User navigates to the configurable product page. Option values available for selection are rendered on the page. +The user makes a selection for the first option and the list of option values available for selection is updated for the remaining options. The image relevant for the selection is also updated. + +### Add to cart + +After the user makes final selection, the corresponding simple product data becomes available and the product can now be added to cart. + +### Render configurable option values available for selection on the category page + +Category and search result pages do not require the list of configurable option values available for selection. A list of all available options is enough diff --git a/design-documents/graph-ql/coverage/product-reviews.graphqls b/design-documents/graph-ql/coverage/catalog/product-reviews.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/product-reviews.graphqls rename to design-documents/graph-ql/coverage/catalog/product-reviews.graphqls From e5783d8bfe650a3c68797993c4cfe1df99d740a3 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 8 Jul 2020 16:59:27 -0500 Subject: [PATCH 131/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../configurable-options-selection.graphqls | 8 +- .../catalog/configurable-options-selection.md | 137 +++++++++++++++++- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index a4318f7ec..f35b4fc81 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -5,10 +5,16 @@ type Query { type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { configurable_options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") - media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable attributes selection.") + media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") + variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { available_value_ids: [ID!]! @doc(description: "Configurable option values available for further selection.") attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") } + +# Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart +type ConfigurableProductOptionsValues { + id: ID +} diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 203e16c41..9dfcfa18e 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -3,12 +3,145 @@ ### Render configurable option values available for selection on the product page User navigates to the configurable product page. Option values available for selection are rendered on the page. -The user makes a selection for the first option and the list of option values available for selection is updated for the remaining options. The image relevant for the selection is also updated. + +```graphql +{ + configurableOptionsSelectionMetadata(configurableProductSku: "configurable-sku") { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + } +} +``` + +The user makes a selection for the first option and the list of option values available for selection is updated for the remaining options. +The images and videos relevant for the selection are also updated. + +```graphql +{ + configurableOptionsSelectionMetadata( + configurableProductSku: "configurable-sku", + selectedConfigurableOptionValues: ["hash from selected option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + } +} +``` + +### User opens URL leading to configurable product page and configurable option selections are specified in the URL + +In this case URL will have to be resolved first: + +```graphql +{ + urlResolver(url: "http://magento.instance/configurable_product.html?configurable_options[0]=first-selection-hash&configurable_options[1]=second-selection-hash") { + id + type + } +} +``` + +Then the product data along with available selections can be requested in a single query: + +```graphql +{ + products(filter: {sku: {eq: "resolved-sku"}}) { + items { + description { + html + } + name + ... on ConfigurableProduct { + configurable_options { + attribute_code + label + values { + id + value_index + label + swatch_data { + value + } + use_default_value + } + } + } + } + } + configurableOptionsSelectionMetadata( + configurableProductSku: "configurable-sku", + selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + variant { + sku + } + } +} +``` ### Add to cart After the user makes final selection, the corresponding simple product data becomes available and the product can now be added to cart. +```graphql +{ + configurableOptionsSelectionMetadata( + configurableProductSku: "configurable-sku", + selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + variant { + sku + } + } +} +``` + +Information about variant is taken from previous query result and used to add configurable product to cart. + ### Render configurable option values available for selection on the category page -Category and search result pages do not require the list of configurable option values available for selection. A list of all available options is enough +Category and search result pages do not require the list of configurable option values available for selection. A list of all available options is enough. + +It is still possible to make selection on the category page directly after it was renedered for the first time. The `configurableOptionsSelectionMetadata` query can be used in the same way as on product page. + +### Extension points + +`ConfigurableOptionsSelectionMetadata` type can be extended to support additional use cases, which are not currently supported by Magento like: + - Price range for the variants based on configurable options selection + - Low stock notification based on configurable options selection From b09002b42a8de1746cc75fc5dbbeaa66fa2876a0 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 8 Jul 2020 13:08:33 -0500 Subject: [PATCH 132/479] Add document with suggestions for ID fields --- .../graph-ql/id-fields-object-types.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 design-documents/graph-ql/id-fields-object-types.md diff --git a/design-documents/graph-ql/id-fields-object-types.md b/design-documents/graph-ql/id-fields-object-types.md new file mode 100644 index 000000000..8c30cf3e8 --- /dev/null +++ b/design-documents/graph-ql/id-fields-object-types.md @@ -0,0 +1,24 @@ +# ID Fields in Object Types + +When a GraphQL Object Type is used to represent an entity that can be referenced by ID, it _should_ follow these best-practices. + +## Best Practices + +### Do + +- Use the `ID` scalar type +- Use the field name `id` or `_id` + - Most GraphQL client libs automatically use this field for caching. Any other field name will require manual caching logic on the client + - https://www.apollographql.com/docs/react/caching/cache-configuration/#assigning-unique-identifiers + - https://formidable.com/open-source/urql/docs/graphcache/normalized-caching/#key-generation +- Include an ID field anytime an Object Type _could_ have an ID field + +### Do Not +- Include info in the client-facing description describing how the field is encoded/decoded (should be opaque) + - Example: The client shouldn't know if an `ID` is a base64-encoded integer +- Use `String` or `Int` type +- Use duplicate IDs across a concrete type. In other words, if the client wants to produce a cache key, the concetenation of a `__typename` + `id` field should _always_ be unique + +## Examples where an ID is not helpful + +- Wrapper types, like `SearchResultPageInfo` \ No newline at end of file From f409ae1fa3b458eb1fbef2cb86004860c226c559 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 9 Jul 2020 11:39:46 -0500 Subject: [PATCH 133/479] New pricing --- design-documents/storefront/pricing.md | 29 ++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index 19ca332f0..305ffc696 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -43,14 +43,14 @@ The resulting product price will be the value from current price book if it's ex ![Price books diagram](pricing/pricebooks.png) -### Default priceb ook +### Default price book A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. ### Customer tags instead of customer groups -Magento monolith uses `customer groups` for customer segmentation globally. There is one-to-one relation between customer and customer group, +Magento monolith uses `customer groups` for customer segmentation globally. There is one-to-many relation between customer groups and customers, so customer could be a member of excatly one group only. However, in modern world, each customer could be a member of different groups based on current behavior. For example, pricing system works with wholesale and regular buyers, but recommendation system works with different groups of customers which are based on gender, age, ML-generated groups, etc. @@ -60,6 +60,31 @@ which are not bound to pricing functionality make them looks like a regular tags ![Price books diagram](pricing/customer-tags.png) +### Complex products support + +The `minimum prices` of complex products calculated based on variation's prices, variation's availability and variation's stock. +Having variations as a separate products makes `minimum price` and `maximum price` dependent on products which may not +be visible for the current group or in a current catalog. Example: configurable product contains variation #1 - price $10, + 2 - price $9 and 3 - price $12. Let's imagine that variation #2 is visible for people with "VIP access" only, +then `minimum price` of configurable product for basic access will be $10, for "VIP access" - $9. + +This happens because parent product and variation are separate products which could be assigned to different access lists +and price books. In order to mitigate this issue products should be isolated, so product options fully define complex products. + +The case from example above could be handled by two independent configurable products with different set of variations. + +Details will be provided in the separate proposal. + +### Synchronization with monolith + +One of the goals of `price books` is to speedup reindex process. Existing reindex process lives in the monolith and +prepares the prices for luma storefront exclusively. The data produced by this indexer is useless for the new storefront, +so old indexer should be disabled for the installation which uses the new storefront exclusively. + + + + + ## Scenarios #### Simple From 9ecb46b57d5d1c45bb903adeebb371917749defb Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 9 Jul 2020 16:19:49 -0500 Subject: [PATCH 134/479] Added note about marking ID field as non-nullable --- design-documents/graph-ql/id-fields-object-types.md | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/id-fields-object-types.md b/design-documents/graph-ql/id-fields-object-types.md index 8c30cf3e8..76250da04 100644 --- a/design-documents/graph-ql/id-fields-object-types.md +++ b/design-documents/graph-ql/id-fields-object-types.md @@ -7,6 +7,7 @@ When a GraphQL Object Type is used to represent an entity that can be referenced ### Do - Use the `ID` scalar type +- Make the `ID` field non-nullable (syntax: `ID!`) - Use the field name `id` or `_id` - Most GraphQL client libs automatically use this field for caching. Any other field name will require manual caching logic on the client - https://www.apollographql.com/docs/react/caching/cache-configuration/#assigning-unique-identifiers From 48cfec5d10d05a665a4301ba5f07afe75f7d6ab4 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 9 Jul 2020 16:21:21 -0500 Subject: [PATCH 135/479] always with the typos --- design-documents/graph-ql/id-fields-object-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/id-fields-object-types.md b/design-documents/graph-ql/id-fields-object-types.md index 76250da04..82f8b6927 100644 --- a/design-documents/graph-ql/id-fields-object-types.md +++ b/design-documents/graph-ql/id-fields-object-types.md @@ -18,7 +18,7 @@ When a GraphQL Object Type is used to represent an entity that can be referenced - Include info in the client-facing description describing how the field is encoded/decoded (should be opaque) - Example: The client shouldn't know if an `ID` is a base64-encoded integer - Use `String` or `Int` type -- Use duplicate IDs across a concrete type. In other words, if the client wants to produce a cache key, the concetenation of a `__typename` + `id` field should _always_ be unique +- Use duplicate IDs across a concrete type. In other words, if the client wants to produce a cache key, the concatenation of a `__typename` + `id` field should _always_ be unique ## Examples where an ID is not helpful From ee4e5a00a81b6801fff6d838e3466eceb8fdf739 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 9 Jul 2020 17:20:47 -0500 Subject: [PATCH 136/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../configurable-options-selection.graphqls | 4 +- .../catalog/configurable-options-selection.md | 180 ++++++++++++------ 2 files changed, 121 insertions(+), 63 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index f35b4fc81..aec5a5804 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -1,5 +1,5 @@ -type Query { - configurableOptionsSelectionMetadata(configurableProductSku: ID!, selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Get metadata for the specified configurable options selection") +type ConfigurableProduct { + configurable_options_selection_metadata(selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") } type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 9dfcfa18e..dd838f812 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -6,19 +6,43 @@ User navigates to the configurable product page. Option values available for sel ```graphql { - configurableOptionsSelectionMetadata(configurableProductSku: "configurable-sku") { - configurable_options_available_for_selection { - attribute_code - available_value_ids - } - media_gallery { - url - label - position - disabled + products(filter: {sku: {eq: "configurable-sku"}}) { + items { + description { + html + } + name + ... on ConfigurableProduct { + configurable_options { + attribute_code + label + values { + id + value_index + label + swatch_data { + value + } + use_default_value + } + } + configurable_options_selection_metadata { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + } + } } } } + ``` The user makes a selection for the first option and the list of option values available for selection is updated for the remaining options. @@ -26,19 +50,24 @@ The images and videos relevant for the selection are also updated. ```graphql { - configurableOptionsSelectionMetadata( - configurableProductSku: "configurable-sku", - selectedConfigurableOptionValues: ["hash from selected option value"] - ) { - configurable_options_available_for_selection { - attribute_code - available_value_ids - } - media_gallery { - url - label - position - disabled + products(filter: {sku: {eq: "configurable-sku"}}) { + items { + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["hash from selected option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + } + } } } } @@ -63,13 +92,13 @@ Then the product data along with available selections can be requested in a sing { products(filter: {sku: {eq: "resolved-sku"}}) { items { - description { + description { html } name ... on ConfigurableProduct { configurable_options { - attribute_code + attribute_code label values { id @@ -81,27 +110,26 @@ Then the product data along with available selections can be requested in a sing use_default_value } } + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + variant { + sku + } + } } } } - configurableOptionsSelectionMetadata( - configurableProductSku: "configurable-sku", - selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] - ) { - configurable_options_available_for_selection { - attribute_code - available_value_ids - } - media_gallery { - url - label - position - disabled - } - variant { - sku - } - } } ``` @@ -111,22 +139,27 @@ After the user makes final selection, the corresponding simple product data beco ```graphql { - configurableOptionsSelectionMetadata( - configurableProductSku: "configurable-sku", - selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] - ) { - configurable_options_available_for_selection { - attribute_code - available_value_ids - } - media_gallery { - url - label - position - disabled - } - variant { - sku + products(filter: {sku: {eq: "configurable-sku"}}) { + items { + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + variant { + sku + } + } + } } } } @@ -136,9 +169,34 @@ Information about variant is taken from previous query result and used to add co ### Render configurable option values available for selection on the category page -Category and search result pages do not require the list of configurable option values available for selection. A list of all available options is enough. +In case when the facet filter was used on the category page, for example to search "Red" shorts, it would be a good idea to display available sizes in "Red" for each product on the page. This can be achieved with the following query: -It is still possible to make selection on the category page directly after it was renedered for the first time. The `configurableOptionsSelectionMetadata` query can be used in the same way as on product page. +```graphql +{ + products(filter: {category_id: {eq: "shorts category ID"}}) { + items { + name + sku + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["hash from selected red color option"] + ) { + configurable_options_available_for_selection { + attribute_code + available_value_ids + } + media_gallery { + url + label + position + disabled + } + } + } + } + } +} +``` ### Extension points From 54174a65bfe63f22a55e7fdf25efd082064a9969 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 9 Jul 2020 17:05:59 -0500 Subject: [PATCH 137/479] Propose renaming id_v2 to something more permanent, and change type --- .../add-items-to-cart-single-mutation.md | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md b/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md index 16822c1d9..10dbb99d3 100644 --- a/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md +++ b/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md @@ -31,12 +31,12 @@ Each product may have options. Option can be of 2 types (see example below): ] ``` -We can consider "Selected Option" and "ID for Entered Option" as UUID. They meet the criteria: +We can consider "Selected Option" and "ID for Entered Option" as a globally unique identifier. They meet the criteria: - "Selected Option" represents option value, while "ID for Entered Option" represents option - Must be unique across different options - Returned from server -- Used by client as is +- Used by client as is (opaque) Selected options can be used for: - Customizable options such as dropdwon, radiobutton, checkbox, etc @@ -51,14 +51,22 @@ Entered options: #### Option implementation -Product schema should be extended in order to provide option identifier (aka first iteration of "UUID"). -Until introducing UUID lets name this identifier as *"id_v2" +Product schema should be extended in order to provide option identifier (aka first iteration of "UUID"). This field will be named `uid` (unique identifier, or universal ID). -Option *id_v2* is `base64` encoded string, that encodes details for each option and in most cases can be presented as +--- +**Note on Field Name** +`uid` was chosen for several reasons: +- `id` is already reserved as an `Int` field in most types, and we want to avoid breaking changes +- `uuid` implies a specific encoding algorithm, and the client shouldn't know or care +- `id_v2` has a temporary-sounding name (`id_v2` makes me think `id_v3` will be coming). However, there is no need to change field names when changing the format of a field that uses the `ID` type, because clients are not meant to be parsing/formatting/inspecting `ID` values. The `uid` field can hold an integer or a base64-encoded value today, and a real UUID in the future, and it will _not_ be a breaking change. `ID` values are always serialized to a string +--- + + +For product options, for now, *uid* is a `base64` encoded string, wrapper in an `ID` type, that encodes details for each option and in most cases can be presented as `base64("//")` -For example, for customizable drop-down option "Color(id = 1), with values Red(id = 1), Green(id = 2)" id_v2 for Color:Red will looks like `"Y3VzdG9tLW9wdGlvbi8xLzE=" => base64("custom-option/1/1")` +For example, for customizable drop-down option "Color(id = 1), with values Red(id = 1), Green(id = 2)" `uid` for Color:Red will looks like `"Y3VzdG9tLW9wdGlvbi8xLzE=" => base64("custom-option/1/1")` -Here is a GQL query that shows how to add a new field "id_v2: String!" to cover existing cases: +Here is a GQL query that shows how to add a new field "uid: ID!" to cover existing cases: ``` graphql @@ -73,7 +81,7 @@ query { ... on CustomizableRadioOption { title value { - id_v2 # introduce new id_v2 field in CustomizableRadioValue + uid # introduce new uid field in CustomizableRadioValue option_type_id title } @@ -81,7 +89,7 @@ query { ... on CustomizableDropDownOption { title value { - id_v2 # introduce new id_v2 field in CustomizableDropDownValue + uid # introduce new uid field in CustomizableDropDownValue # see \Magento\QuoteGraphQl\Model\Cart\BuyRequest\CustomizableOptionsDataProvider option_type_id title @@ -92,7 +100,7 @@ query { } ... on ConfigurableProduct { variants { attributes { - id_v2 # introduce new id_v2 field in ConfigurableAttributeOption (format: configurable//) + uid # introduce new uid field in ConfigurableAttributeOption (format: configurable//) # see \Magento\ConfigurableProductGraphQl\Model\Cart\BuyRequest\SuperAttributeDataProvider code value_index @@ -100,7 +108,7 @@ query { } } ... on DownloadableProduct { downloadable_product_links { - id_v2 # introduce new id_v2 field in DownloadableProductLinks (format: downloadable/link/) + uid # introduce new uid field in DownloadableProductLinks (format: downloadable/link/) # see \Magento\DownloadableGraphQl\Model\Cart\BuyRequest\DownloadableLinksDataProvider title } @@ -109,7 +117,7 @@ query { sku title options { - id_v2 # introduce new id_v2 field in BundleItemOption (format: bundle///) + uid # introduce new uid field in BundleItemOption (format: bundle///) # see \Magento\BundleGraphQl\Model\Cart\BuyRequest\BundleDataProvider id label @@ -117,7 +125,7 @@ query { } } ... on GiftCardProduct { giftcard_amounts { - id_v2 # introduce new id_v2 field in GiftCardAmounts (format: giftcard/...TBD) + uid # introduce new uid field in GiftCardAmounts (format: giftcard/...TBD) # see \Magento\GiftCard\Model\Quote\Item\CartItemProcessor::convertToBuyRequest value_id website_id @@ -154,7 +162,7 @@ query { In this example we want to add _personalized blue cup to cart_ to cart. - - `selected_options` - predefined and selected by customer options. `base64` encoding will help to use UUID in future. + - `selected_options` - predefined and selected by customer options. `base64` encoding is temporary until Magento supports UUIDs, but this is unknown to the client and purely a server implementation detail. :warning: The encoded value will be returned from server and should be used by client as is. In this example values will be following: From 2c16ef60a0b187b1b9588d4740987712ae31d0ad Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 9 Jul 2020 17:48:40 -0500 Subject: [PATCH 138/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/rma.md | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 design-documents/graph-ql/coverage/rma.md diff --git a/design-documents/graph-ql/coverage/rma.md b/design-documents/graph-ql/coverage/rma.md new file mode 100644 index 000000000..36043716f --- /dev/null +++ b/design-documents/graph-ql/coverage/rma.md @@ -0,0 +1,34 @@ +## Configuration + +The following settings should be accessible via `storeConfig` query: +- Returns functionality status on the storefront: enabled/disabled +- Enable returns on product level + +Scenarios which may need these settings include: +- Return initiation for the specific product from the order +- Rendering of the returns section in the customer account + +```graphql +{ + storeConfig { + sales_magento_rma_enabled + sales_magento_rma_enabled_on_product + } +} +``` + +## Use cases + +### View return list with pagination + +### View return details + +### Create a return + +#### Render return form with dynamic RMA attributes + +#### Add more items to the return + +#### Submit return + +### Leave a return comment From ffbc25c906530310fda974fba1f0c41dc47d543a Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 10 Jul 2020 13:07:27 -0500 Subject: [PATCH 139/479] ECP-765: GraphQL Schema for Configurable Options Selection --- design-documents/graph-ql/coverage/rma.graphqls | 10 ++++++++++ design-documents/graph-ql/coverage/rma.md | 15 +++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 design-documents/graph-ql/coverage/rma.graphqls diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls new file mode 100644 index 000000000..8a0a6d119 --- /dev/null +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -0,0 +1,10 @@ +type Customer { + returns( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + ): CustomerReturns @doc(description: "Information about the company returns.") +} + +type StoreConfig { + sales_magento_rma_enabled @doc(description: "Returns functionality status on the storefront: enabled/disabled.") +} diff --git a/design-documents/graph-ql/coverage/rma.md b/design-documents/graph-ql/coverage/rma.md index 36043716f..ce7ad9870 100644 --- a/design-documents/graph-ql/coverage/rma.md +++ b/design-documents/graph-ql/coverage/rma.md @@ -2,33 +2,40 @@ The following settings should be accessible via `storeConfig` query: - Returns functionality status on the storefront: enabled/disabled -- Enable returns on product level Scenarios which may need these settings include: -- Return initiation for the specific product from the order - Rendering of the returns section in the customer account ```graphql { storeConfig { sales_magento_rma_enabled - sales_magento_rma_enabled_on_product } } ``` ## Use cases -### View return list with pagination +### View return list with pagination in customer account + +### View return list with pagination in order details ### View return details ### Create a return +#### Determine whether any order items are eligible for return + +First, we need to know if there are any order items eligible for return. In the Luma example this will dictate whether "Return" link should be displayed on the order details page. + #### Render return form with dynamic RMA attributes +#### Determine which order items are eligible for return + #### Add more items to the return #### Submit return ### Leave a return comment + +### Create a return for guest order From 120153a0da5d1cc0f872d0a2416cdd871d2f2f9f Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Fri, 10 Jul 2020 16:26:14 -0500 Subject: [PATCH 140/479] Add RewardPointsRate type for exhange rates --- .../graph-ql/coverage/reward-points.graphqls | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/reward-points.graphqls index cbb0035e0..540f74077 100644 --- a/design-documents/graph-ql/coverage/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/reward-points.graphqls @@ -32,8 +32,13 @@ type RewardPointsAmount { } type RewardPointsExchangeRates @doc (description: "Exchange rates depend on the customer group"){ - earning: Float! @doc(description: "How many points are earned per 1 currency spent") - redemption: Float! @doc(description: "How many points need to be redeemed to get 1 currency discount at the checkout") + earning: RewardPointsRate @doc(description: "How many points are earned for a given amount spent") + redemption: RewardPointsRate @doc(description: "How many points must be redeemed to get a given amount of currency discount at the checkout") +} + +type RewardPointsRate { + points: Float! @doc(description: "The number of points for exchange rate. For earnings this this the number of points earned. For redemption this is the number of points needed for redemption.") + currency_amount: Float! @doc(description: "The money value for exchange rate. For earnings this is amount spent to earn the specified points. For redemption this is the amount of money the number of points represents.") } type RewardPointsSubscriptionStatus { From c84dfbac5cecd55dad484214f8bcb8b8a9467881 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 10 Jul 2020 23:02:16 -0500 Subject: [PATCH 141/479] Iterating on Negotiable Quote GraphQL schema design --- .../coverage/negotiableQuotes.graphqls | 370 ++++++++++++------ 1 file changed, 254 insertions(+), 116 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 792bf949b..e7ea2f84a 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -1,90 +1,196 @@ +type Query { + # Implementation Note: Negotiable Quotes belong to the Query type, + # rather than the Customer type, because managers + # in a company can see quotes belonging to their + # employees. If `Customer.negotiable_quotes` is desirable for buyer's + # that aren't managers, we can always add that on + negotiable_quote(id: ID!): NegotiableQuote + negotiable_quotes( + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 + ): [NegotiableQuote] @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") +} + +# Coverage missing: +# - Negotiable Quote Checkout Process (once seller has approved the quote) +# - File Upload (depends on reaching consensus on file upload design - design is in internal wiki) type Mutation { - createNegotiableQuote( - cartId: ID! @doc(description: "Cart ID") - quoteName: String! @doc(description: "Quote name") - comment: String @doc(description: "Comment") - files: [FileInput!] @doc(description: "Attached files") - ): CreateNegotiableQuoteOutput! @doc(description:"Generates negotiable quote request") - - addNegotiableQuoteComments( - cartId: ID! @doc(description: "Cart ID") - comment: String @doc(description: "Comment") - files: [FileInput!] @doc(description: "Attached files") - ): AddNegotiableQuoteCommentsOutput! @doc(description:"Add comments to negotiable quote") - - updateNegotiableQuoteItemQuantity( - cart_id: ID!, @doc(description: "unique Id of negotiable quote") - item: ID!, @doc(description: "unique Id of Item to be update in negotiable quote") - quantity: Float @doc(description: "quantity of the item to be updated") - ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Update items quantity in negotiable quote") + # https://docs.magento.com/user-guide/sales/quote-request.html + requestNegotiableQuote( + input: RequestNegotiableQuoteInput! + ): RequestNegotiableQuoteOutput + + # Covers "Add your Comment" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information + addNegotiableQuoteComment( + input: AddNegotiableQuoteCommentInput! + ): AddNegotiableQuoteCommentOutput + # https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#change-the-quantity + updateNegotiableQuoteQuantities( + input: UpdateNegotiableQuoteQuantitiesInput! + ): UpdateNegotiableQuoteItemsQuantityOutput + + # Covers "Delete Line Item" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#tabbed-sections removeNegotiableQuoteItems( - cart_id: ID!, @doc(description: "unique Id of negotiable quote") - items: [ID!]! @doc(description: "unique Ids of Items to be removed from negotiable quote") - ): RemoveNegotiableQuoteItemsOutput @doc(description: "Remove Items in negotiable quote") + input: RemoveNegotiableQuoteItemsInput! + ): RemoveNegotiableQuoteItemsOutput + + # Covers "buyer can add, update, or delete items" of https://devdocs.magento.com/guides/v2.4/b2b/negotiable-update.html#add-a-new-quote-item-to-the-negotiable-quote + addNegotiableQuoteItems( + input: AddNegotiableQuoteItemsInput! + ): AddNegotiableQuoteItemsOutput + + # Covers first half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request + closeNegotiableQuotes( + input: CloseNegotiableQuotesInput! + ): CloseNegotiableQuotesOutput + + # Covers second half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request + deleteNegotiableQuotes( + input: DeleteNegotiableQuotesInput! + ): DeleteNegotiableQuotesOutput + + # Covers "Select Existing Address" in https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information + # "New Address" flow is covered by Mutation.createCustomerAddress + setNegotiableQuoteShippingAddress( + input: SetNegotiableQuoteShippingAddressInput! + ): SetNegotiableQuoteShippingAddressOutput + + # Pending decision on design of file upload (design doc still pending decisions) + # addNegotiableQuoteFiles( + # input: AddNegotiableQuoteFilesInput! + # ): AddNegotiableQuoteFilesInput +} + +input SetNegotiableQuoteShippingAddressInput { + quote_id: ID! @doc(description: "ID obtained from NegotiableQuote type") + customer_address_id: ID! @doc( + description: "ID obtained from the CustomerAddress type. A new address can be added using Mutation.createCustomerAddress" + ) +} + +type SetNegotiableQuoteShippingAddressOutput { + quote: NegotiableQuote +} - setShippingAddressesOnNegotiableQuote( - cart_id: ID! - shipping_addresses: [NegotiableQuoteShippingAddressInput!]! - ): SetShippingAddressesOnNegotiableQuoteOutput @doc(description: "Assign shipping address to Negotiable quote") +input AddNegotiableQuoteItemsInput { + # TODO: Should be modeled similar to the new, single add to cart mutation +} - setShippingMethodsOnNegotiableQuote( - cartId: ID! @doc(description: "Cart ID") - shippingMethods: [ShippingMethodInput]! @doc(description: "Input for shipping method") - ): SetShippingMethodsOnNegotiableQuoteOutput @doc(description:"Set negotiable quote shipping method") +input DeleteNegotiableQuotesInput { + quote_ids: [ID!]! @doc(description: "A List of IDs obtained from NegotiableQuote types") } -type Customer { - negotiable_quote ( - id: ID!): NegotiableQuote @doc(description: "Get negotiable quote of a customer") +type DeleteNegotiableQuotesOutput { + # Implementation Note: We don't make the deleted quotes accessible + # because "deleted" means they're hidden from the storefront UI. They will + # still be visible (and labeled as deleted) for a Seller in the admin + negotiable_quotes( + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 + ): [NegotiableQuote]! @doc(description: "List of negotiable quotes available to customer") +} + +input CloseNegotiableQuotesInput { + quote_ids: [ID!]! @doc(description: "A List of IDs obtained from NegotiableQuote types") +} +type CloseNegotiableQuotesOutput { + closed_quotes: [NegotiableQuote]! @doc(description: "Quotes that were just closed") negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): [NegotiableQuote] @doc(description: "Get negotiable quotes of a customer") + ): [NegotiableQuote] @doc(description: A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") +} + +input RemoveNegotiableQuoteItemsInput { + quote_ID: ID! + quote_item_ids: [ID!]! +} + +input UpdateNegotiableQuoteQuantitiesInput { + quote_id: ID! + items: [NegotiableQuoteItemQuantityInput!]! +} + +input NegotiableQuoteItemQuantityInput { + quote_item_id: ID! + quantity: Float! +} + +input RequestNegotiableQuoteInput { + cart_id: ID! + quote_name: String! + comment: NegotiableQuoteCommentInput! + # files (attachments) to be added at a later date when file upload design has been finalized +} +input NegotiableQuoteCommentInput { + comment: String! + # files (attachments) to be added at a later date when file upload design has been finalized +} + +input AddNegotiableQuoteCommentInput { + quote_id: ID! + comment: NegotiableQuoteCommentInput! +} + +type NegotiableQuoteComment { + id: ID! + created_at: String! + author: NegotiableQuoteUser! + creator_type: NegotiableQuoteCommentCreatorType! + value: String! @doc(description: "A simple (non-html) comment submitted by a seller or buyer") +} + +enum NegotiableQuoteCommentCreatorType { + BUYER + SELLER } type NegotiableQuote { - quote_id: ID! @doc(description: "Negotiable quote ID") - items: [NegotiableQuoteItemInterface] @doc(description: "Negotiable quote item") - attachements: [AttachmentContent] @doc(description: "Negotiable quote Attachements") - comments: [NegotiableQuoteComment] @doc(description: "Negotiable quote comments") - historyLog: [NegotiableQuoteHistoryLog] @doc(description: "Negotiable history log") - prices: CartPrices! @doc(description: "Negotiable quote totals output") - created_by: String @doc(description: "Negotiable quote creator") + id: ID! + name: String! + items: [NegotiableQuoteItemInterface!] + # Attachment Support is dependent on headless File Upload design + # attachments: [AttachmentContent] + comments: [NegotiableQuoteComment!] + history: [NegotiableQuoteHistoryEntry!] + prices: CartPrices + buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") - payment_info: NegotiableQuotePaymentInfo! @doc(description: "Negotiable quote payment info.") - status: String @doc(description: "Negotiable quote status") - available_shipping_methods: [ShippingMethodsOutput]! @doc(description: "Available shipping methods") + status: NegotiableQuoteStatus! +} + +# Implementation Note: Using the values from the "Buyer Status" column of the state mapping table in devdocs. +# These states are identical between storefront/admin, but we name them differently for buyers. +# https://devdocs.magento.com/guides/v2.4/b2b/negotiable-quote.html#quote-statuses +enum NegotiableQuoteStatus { + SUBMITTED + PENDING + UPDATED + OPEN + ORDERED + CLOSED + DECLINED + EXPIRED } input NegotiableQuoteFilterInput { - ids: FilterEqualTypeInput @doc(description: "Filter Customer Negotiable quotes with an negotiable quote ID or list of negotiable quote IDs") - name: FilterMatchTypeInput @doc(description: "Filter by display name of the negotiable quote") + ids: FilterEqualTypeInput @doc(description: "Filter by Negotiable Quote ID(s)") + name: FilterMatchTypeInput @doc(description: "Filter by Negotiable Quote name") } -type NegotiableQuoteItemInterface { +interface NegotiableQuoteItemInterface { id: ID! quantity: Float! - stock: Float! - prices: NegotiableQuoteItemPrices - product: ProductInterface! -} - -type NegotiableQuoteItemPrices { - price: Money! - row_total: Money! - row_total_including_tax: Money! - discounts: [Discount] @doc(description:"An array of discounts to be applied to the quote item") - total_item_discount: Money @doc(description:"The total of all discounts applied to the item") -} - -type Discount @doc(description:"Defines an individual discount. A discount can be applied to the quote as a whole or to an item.") { - amount: Money! @doc(description:"The amount of the discount") - label: String! @doc(description:"A description of the discount") + stock: Float + price: CartItemPrices! + product: ProductInterface } type DefaultNegotiableQuoteItem implements NegotiableQuoteItemInterface @@ -123,83 +229,115 @@ type ConfigurableRequisitionListItem implements NegotiableQuoteItemInterface configurable_options: [SelectedConfigurableOption] @doc(description: "Configurable options selected") } -type AttachmentContent @doc(description: "Negotiable quote attachment file") { - base64_encoded_data: String! - mime_type: String! - name: String! -} - -type NegotiableQuoteComment { +type NegotiableQuoteHistoryEntry { id: ID! - parent_id: String! - author: String! - text: String + author: NegotiableQuoteUser! + change_type: NegotiableQuoteHistoryEntryChangeType! created_at: String - attachments: [String] + changes: NegotiableQuoteHistoryChanges } -type NegotiableQuoteHistoryLog { - id: ID! - author: String! - action: String! - comment: String - status: String - created_at: String +# Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API +# support for the Negotiable Quote history log feature. If we find these types aren't ideal during implementation, +# let's iterate rather than stay glued to them. +# +# Resources +# Diffing class: https://github.com/magento/magento2b2b/blob/5547298c3dde48234ce74ed8ae9000fce3fe01b1/app/code/Magento/NegotiableQuote/Model/History/DiffProcessor.php +# Block for UI: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/Block/Quote/History.php +# Usage in UI: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml +# +# There is currently no support for the `Custom Log` feature supported by the Luma UI, but we can add it on +# https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L316-L363 +# +# Implementation Note: Fields in NegotiableQuoteHistoryChanges should be null +# when no change is present +type NegotiableQuoteHistoryChanges { + status: NegotiableQuoteHistoryStatusChange + comment: NegotiableQuoteHistoryCommentChange + total: NegotiableQuoteHistoryTotalChange + expiration: NegotiableQuoteHistoryExpirationChange + quantity: NegotiableQuoteHistoryQuantityChange + products_removed: NegotiableQuoteHistoryProductsRemovedChange + products_added: NegotiableQuoteHistoryProductsAddedChange } -type NegotiableQuotePaymentInfo { - payment_methods: [PaymentMethodsOutput]! @doc(description: "All list of payment options and totals") - prices: CartPrices! @doc(description: "List of totals") +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L40-L73 +type NegotiableQuoteHistoryStatusChange { + old_status: NegotiableQuoteStatus @doc(description: "Will be null for the first history entry on a Negotiable Quote") + new_status: NegotiableQuoteStatus! } -type PaymentMethodsOutput { - code: String @doc(description: "Payment method code") - title: String @doc(description: "Payment method title") +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L239-L258 +type NegotiableQuoteHistoryCommentChange { + comment: String! @doc(description: "A simple (non-html) comment submitted by a seller or buyer") } -type CreateNegotiableQuoteOutput { - negotiable_quote:NegotiableQuote! +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L259-L294 +type NegotiableQuoteHistoryTotalChange { + old_price: Money! + new_price: Money! } -type AddNegotiableQuoteCommentsOutput { - negotiable_quote:NegotiableQuote! +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L74-L103 +type NegotiableQuoteHistoryExpirationChange { + old_expiration: String @doc(description: "Old quote expiration. Will be 'null' if not previously set") + new_expiration: String! } -type UpdateNegotiableQuoteItemsQuantityOutput { - negotiable_quote:NegotiableQuote! +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L104-L127 +type NegotiableQuoteHistoryQuantityChange { + # think it's just quantities? } -type RemoveNegotiableQuoteItemsOutput{ - negotiable_quote:NegotiableQuote! +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L132-L146 +type NegotiableQuoteHistoryProductsRemovedChange { + # Need to cover removal from catalog and removal from order } -input FileInput @doc(description: "The list of file attachment codes") { - base64_encoded_data: String! - mime_type: String! - name: String! +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 +type NegotiableQuoteHistoryProductsAddedChange { + +} + +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L172-L197 +type NegotiableQuoteHistoryAddressChange { + +} +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L316-L363 +type NegotiableQuoteCustomLogChange { + title: String! + old_value: String + new_value: String! +} + +enum NegotiableQuoteHistoryEntryChangeType { + CREATED + UPDATED + CLOSED + UPDATED_BY_SYSTEM } -input NegotiableQuoteShippingAddressInput { - address: CartAddressInput +type RequestNegotiableQuoteOutput { + quote: NegotiableQuote } -type ShippingMethodsOutput { - carrier_code: String @doc(description: "Shipping carrier code") - method_code: String @doc(description: "Shipping method code") - carrier_title: String @doc(description: "Shiping carrier title") - method_title: String @doc(description: "Shipping method title") - amount: Money @doc(description: "Shipping amount") - base_amount: Money @doc(description: "Shipping base amount") - available: String @doc(description: "Shipping method availble") - error_message: String @doc(description: "Shipping method error message") - price_excl_tax: Money @doc(description: "Shipping method price with exclusive tax") - price_incl_tax: Money @doc(description: "Shipping method price with inclusive tax") +type RemoveNegotiableQuoteItemsOutput { + quote: NegotiableQuote } -type SetShippingMethodsOnNegotiableQuoteOutput { - negotiable_quote: NegotiableQuote +type AddNegotiableQuoteCommentOutput { + comment: NegotiableQuoteComment @doc(description: "The newly added comment") + quote: NegotiableQuote } - type SetShippingAddressesOnNegotiableQuoteOutput{ - negotiable_quote: NegotiableQuote -} \ No newline at end of file +type UpdateNegotiableQuoteItemsQuantityOutput { + quote: NegotiableQuote +} + +# Implementation Note: We don't want to expose the `Customer` object of the Seller to the client, and we don't +# have much of a permissions model in the storefront to limit access by field. We're using a limited view +# instead, excluding the ID because a Seller's ID shouldn't be exposed to the client. +type NegotiableQuoteUser @doc(description: "A limited view of a Buyer or Seller in the Negotiable Quote Process") { + firstname: String! + lastname: String! +} From 594da206df28b2fb7e96e4880bdeaede179d790b Mon Sep 17 00:00:00 2001 From: Saravanan Date: Mon, 13 Jul 2020 08:25:46 +0530 Subject: [PATCH 142/479] changed the arguments to camel case --- .../coverage/requisitionList.graphqls | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index b285c997f..6b5bf8dbf 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -121,29 +121,29 @@ type Mutation { ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") addRequisitionListItemToCart( - requisition_list_id: ID!, @doc(description: "unique Id of requisition list") - item_ids: [ID!]! @doc(description: "selected requisition list items that are to be added") + requisitionListId: ID!, @doc(description: "unique Id of requisition list") + itemIds: [ID!]! @doc(description: "selected requisition list items that are to be added") ): AddRequisitionListItemToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") copyItemsBetweenRequisitionList( - source_id: ID!, @doc(description: "unique Id of source requisition list") - destination_id: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - item_ids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") + sourceId: ID!, @doc(description: "unique Id of source requisition list") + destinationId: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + itemIds: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") ): CopyItemsFromRequisitionListOutput @doc(description: "Copy Items from Requisition List to another requisition list") moveItemsBetweenRequisitionList( - source_id: ID!, @doc(description: "unique Id of source requisition list") - destination_id: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - item_ids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") + sourceId: ID!, @doc(description: "unique Id of source requisition list") + destinationId: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + itemIds: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") ): MoveItemsFromRequisitionListOutput @doc(description: "Move Items from Requisition List to another requisition List") clearCustomerCart( - cart_id: String! @doc(description: "masked Cart Id") + cartId: String! @doc(description: "masked Cart Id") ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } type RemoveRequisitionListItemsOutput { - requisition_list : RequisitionList + requisitionList : RequisitionList } type UpdateRequisitionListItemsOutput { From 4f5b0ad800b8596a628e7145943bf704a89b87c4 Mon Sep 17 00:00:00 2001 From: Saravanan Date: Mon, 13 Jul 2020 20:01:38 +0530 Subject: [PATCH 143/479] changed the field to underscore --- design-documents/graph-ql/coverage/requisitionList.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/requisitionList.graphqls index 6b5bf8dbf..2cefbf080 100644 --- a/design-documents/graph-ql/coverage/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/requisitionList.graphqls @@ -143,7 +143,7 @@ type Mutation { } type RemoveRequisitionListItemsOutput { - requisitionList : RequisitionList + requisition_list : RequisitionList } type UpdateRequisitionListItemsOutput { From 88c77bdebeff1becf73bc722c639fa81891b7725 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 13 Jul 2020 17:48:13 -0500 Subject: [PATCH 144/479] Add input type and output type for Mutation.addNegotiableQuoteitems --- .../graph-ql/coverage/negotiableQuotes.graphqls | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index e7ea2f84a..dadd4e945 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -63,6 +63,13 @@ type Mutation { # ): AddNegotiableQuoteFilesInput } +type AddNegotiableQuoteItemsOutput { + quote: NegotiableQuote + # TODO: We'll probably want to add the same + # errors that can happen when adding an item + # to a normal cart object +} + input SetNegotiableQuoteShippingAddressInput { quote_id: ID! @doc(description: "ID obtained from NegotiableQuote type") customer_address_id: ID! @doc( @@ -75,7 +82,8 @@ type SetNegotiableQuoteShippingAddressOutput { } input AddNegotiableQuoteItemsInput { - # TODO: Should be modeled similar to the new, single add to cart mutation + quote_id: ID! @doc(description: "ID from a NegotiableQuote object") + cart_items: [CartItemInput!]! } input DeleteNegotiableQuotesInput { From 63b7c0c1a3c5cc0b40b81088f823bd2c612f1775 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 13 Jul 2020 17:53:37 -0500 Subject: [PATCH 145/479] More field descriptions --- .../coverage/negotiableQuotes.graphqls | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index dadd4e945..c3af22a00 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -4,7 +4,7 @@ type Query { # in a company can see quotes belonging to their # employees. If `Customer.negotiable_quotes` is desirable for buyer's # that aren't managers, we can always add that on - negotiable_quote(id: ID!): NegotiableQuote + negotiable_quote(id: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, @@ -19,43 +19,43 @@ type Mutation { # https://docs.magento.com/user-guide/sales/quote-request.html requestNegotiableQuote( input: RequestNegotiableQuoteInput! - ): RequestNegotiableQuoteOutput + ): RequestNegotiableQuoteOutput @doc(description: "Request a new Negotiable Quote for a buyer") # Covers "Add your Comment" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information addNegotiableQuoteComment( input: AddNegotiableQuoteCommentInput! - ): AddNegotiableQuoteCommentOutput + ): AddNegotiableQuoteCommentOutput @doc(description: "Append a new comment from the buyer to a Negotiable Quote") # https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#change-the-quantity updateNegotiableQuoteQuantities( input: UpdateNegotiableQuoteQuantitiesInput! - ): UpdateNegotiableQuoteItemsQuantityOutput + ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Change the quantity of 1 or more items already in a NegotiableQuote") # Covers "Delete Line Item" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#tabbed-sections removeNegotiableQuoteItems( input: RemoveNegotiableQuoteItemsInput! - ): RemoveNegotiableQuoteItemsOutput + ): RemoveNegotiableQuoteItemsOutput @doc(description: "Remove 1 or more products from a Negotiable Quote") - # Covers "buyer can add, update, or delete items" of https://devdocs.magento.com/guides/v2.4/b2b/negotiable-update.html#add-a-new-quote-item-to-the-negotiable-quote + # Covers "buyer can add items" of https://devdocs.magento.com/guides/v2.4/b2b/negotiable-update.html#add-a-new-quote-item-to-the-negotiable-quote addNegotiableQuoteItems( input: AddNegotiableQuoteItemsInput! - ): AddNegotiableQuoteItemsOutput + ): AddNegotiableQuoteItemsOutput @doc(description: "Add 1 or more products to a Negotiable Quote") # Covers first half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request closeNegotiableQuotes( input: CloseNegotiableQuotesInput! - ): CloseNegotiableQuotesOutput + ): CloseNegotiableQuotesOutput @doc(description: "Mark a Negotiable Quote as closed, leaving it visible in the storefront") # Covers second half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request deleteNegotiableQuotes( input: DeleteNegotiableQuotesInput! - ): DeleteNegotiableQuotesOutput + ): DeleteNegotiableQuotesOutput @doc(description: "Delete a Negotiable Quote, removing it from the display in the storefront") # Covers "Select Existing Address" in https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information # "New Address" flow is covered by Mutation.createCustomerAddress setNegotiableQuoteShippingAddress( input: SetNegotiableQuoteShippingAddressInput! - ): SetNegotiableQuoteShippingAddressOutput + ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a Negotiable Quote") # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( From 773ac5af0e97a8ee664d436e91cf70ae6c8b39c9 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 13 Jul 2020 18:08:20 -0500 Subject: [PATCH 146/479] Add design for NegotiableQuoteHistoryProductsRemovedChange --- .../graph-ql/coverage/negotiableQuotes.graphqls | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index c3af22a00..9f35a90d7 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -102,7 +102,7 @@ type DeleteNegotiableQuotesOutput { } input CloseNegotiableQuotesInput { - quote_ids: [ID!]! @doc(description: "A List of IDs obtained from NegotiableQuote types") + quote_ids: [ID!]! @doc(description: "A List of IDs from NegotiableQuote objects") } type CloseNegotiableQuotesOutput { @@ -264,9 +264,10 @@ type NegotiableQuoteHistoryChanges { comment: NegotiableQuoteHistoryCommentChange total: NegotiableQuoteHistoryTotalChange expiration: NegotiableQuoteHistoryExpirationChange - quantity: NegotiableQuoteHistoryQuantityChange + products_changed: NegotiableQuoteHistoryProductsChange products_removed: NegotiableQuoteHistoryProductsRemovedChange products_added: NegotiableQuoteHistoryProductsAddedChange + custom_changes: NegotiableQuoteCustomLogChange } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L40-L73 @@ -293,13 +294,15 @@ type NegotiableQuoteHistoryExpirationChange { } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L104-L127 -type NegotiableQuoteHistoryQuantityChange { - # think it's just quantities? +type NegotiableQuoteHistoryProductsChange { + # TODO: needs to model qty_changed and options_changed } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L132-L146 type NegotiableQuoteHistoryProductsRemovedChange { - # Need to cover removal from catalog and removal from order + # Open Question: Should `removed_from_catalog` ID type represent the SKU or the product id? + removed_from_catalog: [ID!] @doc(description: "List of product skus removed from Seller's catalog") + removed_from_quote: [ProductInterface!] @doc(description: "List of products removed by a Buyer or Seller") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 From 351305dcbcf5f2848bb72565015897e7d796ce5d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 13 Jul 2020 21:34:25 -0500 Subject: [PATCH 147/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/rma.graphqls | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls index 8a0a6d119..de1666af3 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -2,7 +2,36 @@ type Customer { returns( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - ): CustomerReturns @doc(description: "Information about the company returns.") + ): Returns @doc(description: "Information about the company returns.") +} + +type Returns { + items: [Return] @doc(description: "List of returns") + page_info: SearchResultPageInfo @doc(description: "Pagination metadata") + total_count: Int @doc(description: "Total count of customer returns") +} + +type Return @doc(description: "Customer return") { + number: ID! @doc(description: "Customer return number") + date: String! @doc(description: "Customer return date") + # TODO: Why is this required, maybe for guests, need to verify + ship_from: String @doc(description: "The name of the person returning the item") + return_status: ReturnStatus +} + +enum ReturnStatus { + PENDING + AUTHORIZED + PARTIALLY_AUTHORIZED + RECEIVED + RECEIVED_ON_ITEM + APPROVED + APPROVED_ON_ITEM + REJECTED + REJECTED_ON_ITEM + DENIED + CLOSED + PROCESSED_CLOSED } type StoreConfig { From 355296afa7f18f2a5ffc3e1e6a931c9c2af548e9 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 14 Jul 2020 10:31:08 -0500 Subject: [PATCH 148/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../catalog/configurable-options-selection.graphqls | 5 +++-- .../catalog/configurable-options-selection.md | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index aec5a5804..2131630b7 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -4,7 +4,7 @@ type ConfigurableProduct { type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { - configurable_options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") + options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } @@ -16,5 +16,6 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt # Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart type ConfigurableProductOptionsValues { - id: ID + id: ID! + is_available_for_selection: Boolean! } diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index dd838f812..5f713a9d3 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -18,6 +18,7 @@ User navigates to the configurable product page. Option values available for sel label values { id + is_available_for_selection value_index label swatch_data { @@ -27,7 +28,7 @@ User navigates to the configurable product page. Option values available for sel } } configurable_options_selection_metadata { - configurable_options_available_for_selection { + options_available_for_selection { attribute_code available_value_ids } @@ -56,7 +57,7 @@ The images and videos relevant for the selection are also updated. configurable_options_selection_metadata( selectedConfigurableOptionValues: ["hash from selected option value"] ) { - configurable_options_available_for_selection { + options_available_for_selection { attribute_code available_value_ids } @@ -102,6 +103,7 @@ Then the product data along with available selections can be requested in a sing label values { id + is_available_for_selection value_index label swatch_data { @@ -113,7 +115,7 @@ Then the product data along with available selections can be requested in a sing configurable_options_selection_metadata( selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] ) { - configurable_options_available_for_selection { + options_available_for_selection { attribute_code available_value_ids } @@ -145,7 +147,7 @@ After the user makes final selection, the corresponding simple product data beco configurable_options_selection_metadata( selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] ) { - configurable_options_available_for_selection { + options_available_for_selection { attribute_code available_value_ids } @@ -181,7 +183,7 @@ In case when the facet filter was used on the category page, for example to sear configurable_options_selection_metadata( selectedConfigurableOptionValues: ["hash from selected red color option"] ) { - configurable_options_available_for_selection { + options_available_for_selection { attribute_code available_value_ids } From b1f343e990ed9d1a713656ac2ec8e6f50f06e53e Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 14 Jul 2020 16:38:05 -0500 Subject: [PATCH 149/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/rma.graphqls | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls index de1666af3..5824d0d0d 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -12,11 +12,37 @@ type Returns { } type Return @doc(description: "Customer return") { - number: ID! @doc(description: "Customer return number") - date: String! @doc(description: "Customer return date") - # TODO: Why is this required, maybe for guests, need to verify - ship_from: String @doc(description: "The name of the person returning the item") + id: ID! @doc(description: "Customer return number") + order_id: String! + creation_date: String! @doc(description: "Customer return request date") + customer_email: String! + customer_name: String @doc(description: "The name of the person returning the item") return_status: ReturnStatus + shipping: ReturnShipping +} + +type ReturnShipping { + address: ReturnShippingAddress + # Tracking is not currently supported by Magento + tracking: ReturnShippingTracking +} + +type ReturnShippingTracking { + id: ID! + shipping_method: String + carier: String + tracking_number: String + status: String +} + +type ReturnShippingAddress { + contact_name: String + street: [String]! + city: String! + region: Region! + postcode: String! + country: Country! + telephone: String } enum ReturnStatus { From 0777771f389ba696b8357ece82ffbb200f215e6c Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 15 Jul 2020 12:02:29 -0500 Subject: [PATCH 150/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/rma.graphqls | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls index 5824d0d0d..85ea6643d 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -3,6 +3,7 @@ type Customer { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): Returns @doc(description: "Information about the company returns.") + return(id: ID!): Return @doc(description: "Get customer return details by its ID.") } type Returns { @@ -12,13 +13,39 @@ type Returns { } type Return @doc(description: "Customer return") { - id: ID! @doc(description: "Customer return number") - order_id: String! - creation_date: String! @doc(description: "Customer return request date") - customer_email: String! + id: ID! @doc(description: "Return number") + order_id: String! @doc(description: "ID of the order based on which the return was created.") + creation_date: String! @doc(description: "The date when the return was requested.") + customer_email: String! @doc(description: "Email of the customer who created the return.") customer_name: String @doc(description: "The name of the person returning the item") - return_status: ReturnStatus - shipping: ReturnShipping + status: ReturnStatus @doc(description: "Return status.") + shipping: ReturnShipping @doc(description: "Shipping information for the return.") + comments: [ReturnComment] @doc(description: "A list of comments posted for the return.") + items: [ReturnItem] @doc(description: "A list of items being returned.") +} + +type ReturnItem { + id: ID! + product: ProductInterface! + # Do we need reference to order ID? How about entered and selected options + dynamic_attributes: [ReturnDynamicAttribute] + request_quantity: Float! + quantity: Float! + # TODO: Should enums be separated for Return and Return item? + status: ReturnStatus! +} + +type ReturnDynamicAttribute { + code: ID! + label: String! + value: String! +} + +type ReturnComment { + id: ID! @doc(description: "Comment ID.") + created_by: String! @doc(description: "The name of the user who created posted the comment.") + created_at: String! @doc(description: "The date and time when the comment was posted.") + text: String! @doc(description: "The comment text.") } type ReturnShipping { From c91e25ef826c8287a34b07607601fc25fc9b0e39 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 15 Jul 2020 12:45:03 -0500 Subject: [PATCH 151/479] Update negotiable quote history log to support multiple status changes per history entry --- .../coverage/negotiableQuotes.graphqls | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 9f35a90d7..7e757bc0b 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -67,7 +67,9 @@ type AddNegotiableQuoteItemsOutput { quote: NegotiableQuote # TODO: We'll probably want to add the same # errors that can happen when adding an item - # to a normal cart object + # to a normal cart object, but will also have + # some additional errors (i.e can't add an item + # when a negotiable quote is in some statuses) } input SetNegotiableQuoteShippingAddressInput { @@ -257,11 +259,15 @@ type NegotiableQuoteHistoryEntry { # There is currently no support for the `Custom Log` feature supported by the Luma UI, but we can add it on # https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L316-L363 # -# Implementation Note: Fields in NegotiableQuoteHistoryChanges should be null -# when no change is present +# Implementation Notes: +# - Fields in NegotiableQuoteHistoryChanges should be null when no change is present +# - An Object Type is used, rather than a list, because the UI likely wants +# to render some of these differently from others in the UI, and a List would require us to guess +# at the desired ordering for the client. This is also more extensible with less risk of breaking +# changes (compared to extending a union or shared interface) type NegotiableQuoteHistoryChanges { - status: NegotiableQuoteHistoryStatusChange - comment: NegotiableQuoteHistoryCommentChange + statuses: NegotiableQuoteHistoryStatusesChange + comment_added: NegotiableQuoteHistoryCommentChange total: NegotiableQuoteHistoryTotalChange expiration: NegotiableQuoteHistoryExpirationChange products_changed: NegotiableQuoteHistoryProductsChange @@ -276,9 +282,17 @@ type NegotiableQuoteHistoryStatusChange { new_status: NegotiableQuoteStatus! } +# Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L48 +type NegotiableQuoteHistoryStatusesChange { + changes: [NegotiableQuoteHistoryStatusChange!]! +} + # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L239-L258 type NegotiableQuoteHistoryCommentChange { + # Implementation Note: Comments are append-only and don't support editing, which explains + # the absence of `old_*` and `new_*` fields comment: String! @doc(description: "A simple (non-html) comment submitted by a seller or buyer") + # Need to add attachment support when file upload has been designed/implemented } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L259-L294 @@ -295,6 +309,7 @@ type NegotiableQuoteHistoryExpirationChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L104-L127 type NegotiableQuoteHistoryProductsChange { + # TODO: List of changes for both product quantities and selected options # TODO: needs to model qty_changed and options_changed } @@ -307,15 +322,15 @@ type NegotiableQuoteHistoryProductsRemovedChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 type NegotiableQuoteHistoryProductsAddedChange { - + # TODO: List of products added and their respective options. } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L172-L197 type NegotiableQuoteHistoryAddressChange { - + # TODO } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L316-L363 -type NegotiableQuoteCustomLogChange { +type NegotiableQuoteCustomLogChange @doc(description: "Custom log entries added by 3rd party extensions") { title: String! old_value: String new_value: String! From 5138fe589abdaf9550abdecb4b2fa57908ba426a Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 15 Jul 2020 13:00:15 -0500 Subject: [PATCH 152/479] New pricing --- design-documents/storefront/pricing.md | 149 +++++++++++++++++- .../storefront/pricing/pricing-fallback.png | Bin 0 -> 88228 bytes 2 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 design-documents/storefront/pricing/pricing-fallback.png diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index 305ffc696..a6c2f8cf9 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -1,6 +1,9 @@ ## Problem statement -The final price of the product in Magento monolith depends on multiple variables such as current customer group, current website, qty of items in the shopping cart and current date/time. Magento monolith calculates all possible permutations of prices in advance and store them in `price index`. These calculations are expensive and may not be done in reasonable time for large catalogs. +The final price of the product in Magento monolith depends on multiple variables such as current customer group, current +website, qty of items in the shopping cart and current date/time. +Magento monolith calculates all possible permutations of prices in advance and store them in `price index`. These +calculations are expensive and may not be done in reasonable time for large catalogs. Few problematic use cases from real merchants: @@ -43,10 +46,124 @@ The resulting product price will be the value from current price book if it's ex ![Price books diagram](pricing/pricebooks.png) -### Default price book +Guidelines: +* There is no need to resolve price book on each HTTP call. Resolved price book could be stored in JWT during "login" step +and reused for consequent requests. +* In order to minimize "query-time" work, customer should have exactly one resolved price book. -A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. +### Price book types + +#### Default price book + +A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. +Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. + +Other words, there is always some price for sku in `default price book`. + +#### Fixed price book + +This price book type overrides the `default price book` prices and accepts a fixed price for a set of products. (e.g. $10) + +#### Time-based price books + +`Special price` functionality could be represented as time based `price books` which in addition to standard price book fields +contain `start` and `end` dates. Pricing service will resolve `time-based price books` only in specified range of dates. + +#### Volume price books + +Volume price books provide discounts based on the number of ordered items. Prices provided by `volume price books` do not + participate in minimum price calculations (excluded from price on product listing page). + +### Price book API + +```proto +syntax = "proto3"; + +package magento.pricing.api; + +// Creates a new price book +// All fields are required. +// Throws invalid argument error if some argument is missing +message PriceBookInput { + // Client side generated price book ID + string id = 1; + + // Price book name (e.g. "10% off on selected products") + string name = 2; + + // Customer groups associated with price book + // A combination of customer group and website must be unique. Error will be returned in case when combination is + // already occupied by another price book. + repeated string customer_groups = 3; + + // Website ids associated with price book + // A combination of customer group and website must be unique. Error will be returned in case when combination is + // already occupied by another price book. + repeated string website_ids = 4; +} + +message PriceBookDeleteInput { + string id = 1; +} + +message AssignProductsInput { + message ProductPriceInput { + string product_id = 1; + float price = 2; + float regular_price = 3; + } + repeated ProductPriceInput prices = 1; +} + +message UnassignProducts { + repeated string product_ids = 1; +} + +message PriceBookCreateResult { + int32 status = 1; +} + +message PriceBookDeleteResult { + int32 status = 1; +} + +message PriceBookAssignProductsResult { + int32 status = 1; +} + +message PriceBookUnassignProductsResult { + int32 status = 1; +} + + +message GetPricesInput { + string price_book_id = 1; + repeated string product_ids = 2; +} + +message GetPricesOutput { + message ProductPrice { + string product_id = 2; + // Price without applied discounts + float regular_price = 3; + // Price with applied discounts + float price = 4; + float minimum_price = 5; + float maximum_price = 6; + } + + repeated ProductPrice prices = 1; +} + + +service PriceBook { + rpc create(PriceBookInput) returns (PriceBookCreateResult); + rpc delete(PriceBookDeleteInput) returns (PriceBookDeleteResult); + rpc assignProducts(PriceBookInput) returns (PriceBookAssignProductsResult); + rpc unassignProducts(UnassignProducts) returns (PriceBookUnassignProductsResult); +} +``` ### Customer tags instead of customer groups @@ -66,15 +183,37 @@ The `minimum prices` of complex products calculated based on variation's prices, Having variations as a separate products makes `minimum price` and `maximum price` dependent on products which may not be visible for the current group or in a current catalog. Example: configurable product contains variation #1 - price $10, 2 - price $9 and 3 - price $12. Let's imagine that variation #2 is visible for people with "VIP access" only, -then `minimum price` of configurable product for basic access will be $10, for "VIP access" - $9. +then the desired `minimum price` of configurable product for basic access will be $10, for "VIP access" - $9. However, there is +only one price book in the system, so we can hold only one value. This happens because parent product and variation are separate products which could be assigned to different access lists and price books. In order to mitigate this issue products should be isolated, so product options fully define complex products. -The case from example above could be handled by two independent configurable products with different set of variations. +The case from example above could be handled by two independent configurable products with different sets of variations +for different access lists. Details will be provided in the separate proposal. +### Prices fallback + +A most efficient way to work with prices on catalog scenarios is to create a prices' projection in `catalog` service. This +way we can retrieve prices in one query together with other product's information. + +This approach will work fine till some limits. There are always some limits on data we can handle in any service. The +`catalog` service is not an exception. It's designed to handle a large amount of products, but product itself is not infinite. +The target number of EAV attributes in the product is `300`, the limit of underlying storage is `10,000` attributes per product. +If we move closer to the limits, system may slow down and eventually fail. + +These limits mean that we can't implement `personalized pricing` feature by storing all prices in product documents. +Even a large list of price books will be challenging for such approach. For such extreme use cases we may introduce a +`prices fallback` on a service which is designed to work with the large amount of prices. + +![Price fallback](pricing/pricing-fallback.png) + +Consequences: +- Aggregation functions like facets in search service will not work properly for products with large amount of prices. Only default or limited number of prices will be available for faceting. +- Second request obviously affects performance. A good news, performance will be affected only in queries which fetch products with large amount of prices. Other queries or slices with small products will not be affected. + ### Synchronization with monolith One of the goals of `price books` is to speedup reindex process. Existing reindex process lives in the monolith and diff --git a/design-documents/storefront/pricing/pricing-fallback.png b/design-documents/storefront/pricing/pricing-fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..22a6381aba55a40a675a37474954f14afdfcf38d GIT binary patch literal 88228 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νWfLecLP*N=8lPq0t?sS4Gce-EZ?2&;)vAq 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 T&2Pe>v7b14`bfcH=l}i>{Pq0N literal 0 HcmV?d00001 From b9e69853f3f4dd91cca6d6e602dbaf6934f7cf79 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Wed, 15 Jul 2020 15:24:05 -0500 Subject: [PATCH 153/479] Order Address type is added for shipping and billing address on orders schema --- .../graph-ql/coverage/customer-orders.md | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index f8fdc0983..d4cffa62e 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -73,8 +73,8 @@ type CustomerOrder { credit_memos: [CreditMemo] @doc("credit memo list for the order") shipments: [OrderShipment] @doc("shipment list for the order") payment_methods: [PaymentMethod] @doc("payment details for the order") - shipping_address: CustomerAddress @doc("shipping address for the order") - billing_address: CustomerAddress @doc("billing address for the order") + shipping_address: OrderAddress @doc("shipping address for the order") + billing_address: OrderAddress @doc("billing address for the order") carrier: String @doc("shipping carrier for the order delivery") shipping_method: String @doc("shipping method for the order") comments: [CommentItem] @doc("comments on the order") @@ -325,6 +325,30 @@ type CommentItem { } ``` +## OrderAddress type +```graphql + +type OrderAddress @doc(description: "OrderAddress contains detailed information about the order billing and shipping addresses"){ + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "The unique ID for a pre-defined region") + country_code: CountryCodeEnum @doc(description: "The customer's order country") + street: [String] @doc(description: "An array of strings that define the street number and name") + company: String @doc(description: "The customer's order company") + telephone: String @doc(description: "The telephone number") + fax: String @doc(description: "The fax number") + postcode: String @doc(description: "The customer's order ZIP or postal code") + city: String @doc(description: "The city or town") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") +} + + +``` + ## Additional Types The `KeyValue` type will provide a possibility to use key-value pairs: From 2d532aefc8df6bb6333d7af51c3ca1ecef3ce196 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 15 Jul 2020 17:31:56 -0500 Subject: [PATCH 154/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/rma.graphqls | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls index 85ea6643d..8db609882 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -1,3 +1,6 @@ + +# TODO: Custom attribute metadata can be retrieved using customAttributeMetadata query. A new query should be added to fetch all attributes for entities that do not support attribute sets, like returns. + type Customer { returns( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), @@ -28,17 +31,18 @@ type ReturnItem { id: ID! product: ProductInterface! # Do we need reference to order ID? How about entered and selected options - dynamic_attributes: [ReturnDynamicAttribute] + custom_attributes: [CustomAttribute] request_quantity: Float! quantity: Float! # TODO: Should enums be separated for Return and Return item? status: ReturnStatus! } -type ReturnDynamicAttribute { - code: ID! +# See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/custom-attributes-container.md +type CustomAttribute { + id: ID! label: String! - value: String! + value: String! @doc(description: "JSON encoded value of the attribute.") } type ReturnComment { From 121a89293b6ac5192b67e10c2f9d7b66c22d6165 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 16 Jul 2020 08:12:44 -0500 Subject: [PATCH 155/479] Order Address type added non-nullable fields --- .../graph-ql/coverage/customer-orders.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index d4cffa62e..75a7b7cb2 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -329,18 +329,18 @@ type CommentItem { ```graphql type OrderAddress @doc(description: "OrderAddress contains detailed information about the order billing and shipping addresses"){ - firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") - lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + firstname: String! @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String! @doc(description: "The family name of the person associated with the shipping/billing address") middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "The unique ID for a pre-defined region") + region_id: ID @doc(description: "The unique ID for a pre-defined region") country_code: CountryCodeEnum @doc(description: "The customer's order country") - street: [String] @doc(description: "An array of strings that define the street number and name") + street: [String]! @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's order company") - telephone: String @doc(description: "The telephone number") + telephone: String! @doc(description: "The telephone number") fax: String @doc(description: "The fax number") postcode: String @doc(description: "The customer's order ZIP or postal code") - city: String @doc(description: "The city or town") + city: String! @doc(description: "The city or town") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III") vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") From 71fc80ab5ccb7d972935d25aa18f47fb90a0d50d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 16 Jul 2020 10:08:46 -0500 Subject: [PATCH 156/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/rma.graphqls | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/rma.graphqls index 8db609882..f6033053a 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/rma.graphqls @@ -1,5 +1,17 @@ +type Query{ + # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/framework/attributes-metadata.md + pageSpecificCustomAttributes( + page_type: CustomAttributesPageEnum! + ): CustomAttributeMetadata +} -# TODO: Custom attribute metadata can be retrieved using customAttributeMetadata query. A new query should be added to fetch all attributes for entities that do not support attribute sets, like returns. +enum CustomAttributesPageEnum { + RETURN_EDIT_FORM + # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/framework/attributes-metadata.md + # PRODUCTS_COMPARE + # PRODUCTS_LISTING + # ADVANCED_CATALOG_SEARCH +} type Customer { returns( From 973afab2798754c5167a1ec5cd69d57c6555e617 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 16 Jul 2020 10:55:50 -0500 Subject: [PATCH 157/479] Admin Panel Configuration propagation to Storefront --- design-documents/storefront/Readme.md | 15 +++++ .../storefront/configuraiton-propagation.md | 65 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 design-documents/storefront/Readme.md create mode 100644 design-documents/storefront/configuraiton-propagation.md diff --git a/design-documents/storefront/Readme.md b/design-documents/storefront/Readme.md new file mode 100644 index 000000000..25bf15c97 --- /dev/null +++ b/design-documents/storefront/Readme.md @@ -0,0 +1,15 @@ +# Storefront + +This section of documents contains architectural and design decisions related to separation of Magento Storefront as a separate application. + +## High-Level Vision of Application Separation + +![monolith to services separation](https://app.lucidchart.com/publicSegments/view/24d251f7-ba0e-4c90-9190-3f397ad650b2/image.png) + +Where **Domain** is application domain, such as Catalog, Customer, etc. +See [Magento Service Isolation Vision](../service-isolation.md) for more details on domains and services. + +## Resources + +1. [Magento Service Isolation Vision](../service-isolation.md) +2. [Catalog Storefront project overview](https://github.com/magento/catalog-storefront/wiki/Catalog-Storefront-Service) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md new file mode 100644 index 000000000..dea6de637 --- /dev/null +++ b/design-documents/storefront/configuraiton-propagation.md @@ -0,0 +1,65 @@ +# Configuration Propagation from Admin Panel to Storefront + +## Problem Statement + +Existing Magento monolith is responsible for the whole cycle of data and user workflow. +Magento Admin Panel contains configuration settings for all areas of the application, including Admin Panel behavior, storefront UI, etc. + +With Storefront being a separate service (or set of services), should configuration settings in Magento Admin Panel be propagated to storefront service? +If yes, in what way? + +## Configuration Propagation Guidelines + +This document covers general agreements for decisions about configuration propagation from Admin Panel to storefront. +A separate design decision should be made and documented for specific use cases. + +There are three use cases for the configuration, based on its impact. + +### 1. Configuration that impacts Admin Panel behavior + +Examples: admin ACLs that allow or deny parts of functionality for the admin user, reindex on update or by schedule. + +This type of configuration has nothing to do with Storefront and should not be propagated to storefront, as well as should not be exposed via export API. + +### 2. Configuration that impacts data provided by Storefront + +Example: Base Media URL is used for calculation of media image URLs returned by Storefront API. + +This type of configuration should be propagated to Storefront in one of the following ways: + +1. Pre-calculate final data on back office side and provide final result to the Storefront during synchronization. + 1. Pros: simpler implementation of Storefront due to eliminated necessity for Storefront to keep knowledge about additional configuration, including data calculation algorithms. + 2. Cons: massive (up to full) reindexation necessary in case the configuration is changed. + +To choose the right approach in a specific case, consider the following: + +1. How frequently the configuration is expected to change? + 1. Frequent configuration changes leading every time to massive reindexation may be unacceptable. + 2. Configuration changes expected a few times in the store lifespan may not be worth additional complexity on the Storefront side and full reindexation may be better in this case. +2. Is it acceptable to have significant delay in data propagation after the configuration change? + 1. Changing Base URL may be not a big issue, especially if a redirect can be setup. So it may be acceptable to have URLs to be fully updated in a few hours. + 2. Changes in prices, on the other side, may not stand long delays. + +### 3. Configuration that impacts UI representation of Storefront data + +Example: include or exclude taxes in displayed prices. + +This kind of configuration has nothing to do with Storefront data itself, and so it is suggested to have a separate service that provides this configuration. +Client application may call the specialized configuration service to get configuration it needs. +Some clients may not need all or part of UI configuration. +GraphQL serves as a single entry point for both data and UI requests to simplify client implementation and have more control of which APIs are publicly exposed. + +![UI Configuration Propagation](https://app.lucidchart.com/publicSegments/view/b7ec5763-eb23-48ac-9092-6b92821040fb/image.png) + +Pros and cons of a separate service compared to configuration being part of the domain service are described bellow. + +Pros: + +1. More control of the deployment, scalability, technologies for the domain and config services. +2. Ability to exclude config service from deployment in case it becomes unnecessary for the specific use case. + 1. In case of multi-tenant deployment, of course it can't be done based on specific customer needs, but still may allow resources optimization in case specific merchant does not need UI configuration propagated from Magento. + 1. Even in case of multi-tenant deployment, it is easier to eliminate configuration feature (and free resources needed for it) if it's represented by a separate service. + +Cons: + +1. Additional request to configuration service instead of in-process request to the storage. From b1807e7d26b0f6d544942c903edd995b1b8133ba Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 16 Jul 2020 11:53:07 -0500 Subject: [PATCH 158/479] ECP-766: Returns Schema Design --- .../coverage/{rma.graphqls => returns.graphqls} | 12 ++++++++++-- .../graph-ql/coverage/{rma.md => returns.md} | 0 2 files changed, 10 insertions(+), 2 deletions(-) rename design-documents/graph-ql/coverage/{rma.graphqls => returns.graphqls} (88%) rename design-documents/graph-ql/coverage/{rma.md => returns.md} (100%) diff --git a/design-documents/graph-ql/coverage/rma.graphqls b/design-documents/graph-ql/coverage/returns.graphqls similarity index 88% rename from design-documents/graph-ql/coverage/rma.graphqls rename to design-documents/graph-ql/coverage/returns.graphqls index f6033053a..5749eceeb 100644 --- a/design-documents/graph-ql/coverage/rma.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -6,7 +6,8 @@ type Query{ } enum CustomAttributesPageEnum { - RETURN_EDIT_FORM + RETURN_ITEM_EDIT_FORM + RETURN_ITEMS_LISTING # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/framework/attributes-metadata.md # PRODUCTS_COMPARE # PRODUCTS_LISTING @@ -17,10 +18,17 @@ type Customer { returns( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - ): Returns @doc(description: "Information about the company returns.") + ): Returns @doc(description: "Information about the customer returns.") return(id: ID!): Return @doc(description: "Get customer return details by its ID.") } +type CustomerOrder { + returns( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + ): Returns @doc(description: "Information about the order returns.") +} + type Returns { items: [Return] @doc(description: "List of returns") page_info: SearchResultPageInfo @doc(description: "Pagination metadata") diff --git a/design-documents/graph-ql/coverage/rma.md b/design-documents/graph-ql/coverage/returns.md similarity index 100% rename from design-documents/graph-ql/coverage/rma.md rename to design-documents/graph-ql/coverage/returns.md From 0e996b8557840c6d21a00f5fa02a3d5b8daa0818 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 16 Jul 2020 13:40:00 -0500 Subject: [PATCH 159/479] On Order Address type added street non-nullable fields --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 75a7b7cb2..b748aa2d9 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -335,7 +335,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information region: String @doc(description: "The state or province name") region_id: ID @doc(description: "The unique ID for a pre-defined region") country_code: CountryCodeEnum @doc(description: "The customer's order country") - street: [String]! @doc(description: "An array of strings that define the street number and name") + street: [String!] @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's order company") telephone: String! @doc(description: "The telephone number") fax: String @doc(description: "The fax number") From 6e385e3c71b5997b3ee8fbe502f1a11b939e53ef Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 16 Jul 2020 14:13:41 -0500 Subject: [PATCH 160/479] New pricing --- design-documents/storefront/pricing.md | 48 +++++++++--------- .../storefront/pricing/pricing-structure.png | Bin 0 -> 60174 bytes 2 files changed, 24 insertions(+), 24 deletions(-) create mode 100644 design-documents/storefront/pricing/pricing-structure.png diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index a6c2f8cf9..98f72b328 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -52,29 +52,38 @@ and reused for consequent requests. * In order to minimize "query-time" work, customer should have exactly one resolved price book. -### Price book types - -#### Default price book +### Default price book A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. Other words, there is always some price for sku in `default price book`. -#### Fixed price book - -This price book type overrides the `default price book` prices and accepts a fixed price for a set of products. (e.g. $10) - -#### Time-based price books - -`Special price` functionality could be represented as time based `price books` which in addition to standard price book fields -contain `start` and `end` dates. Pricing service will resolve `time-based price books` only in specified range of dates. - -#### Volume price books +### Synchronization with monolith -Volume price books provide discounts based on the number of ordered items. Prices provided by `volume price books` do not - participate in minimum price calculations (excluded from price on product listing page). +One of the goals of `price books` is to speedup reindex process. Existing reindex process lives in the monolith and +prepares the prices for luma storefront exclusively. The data produced by this indexer is useless for the new storefront, +so old indexer should be disabled for the installation which uses the new storefront exclusively. +The following diagram shows the pricing structure for a given product, website and customer group. It also shows respective +`price books` on the storefront. + +![Pricing structure](pricing/pricing-structure.png) + +- t1 - New product was created. Every product should have some base price, so new product also introduce one price in the system. + - Storefront receives new base price and put it in the default price book +- t2 - New price for customer group was introduced on the monolith side + - Message broker checks if price book for specified customer group exists on storefront and create new price book if needed + - Message broker assigns product to the new price book and set appropriate price +- t3 - New catalog price rule was created on the monolith side. Catalog price rule has start and end dates. + - Message broker checks if price book for specified customer group exists on storefront and create new price book if needed + - Message broker detects products matched by the rule + - Message broker calculate prices apply discounts for selected products and write prices to price book +- Alternative t3 + - Monolith detects matched products and fire `price_changed` events for those products. Event includes information about affected customer groups. Product matches are stored in cache for the later use. + - Message broker call monolith for prices of affected products + - Monolith calculates prices based on the cache (product matches) + - Message broker store prices in price book ### Price book API ```proto @@ -214,15 +223,6 @@ Consequences: - Aggregation functions like facets in search service will not work properly for products with large amount of prices. Only default or limited number of prices will be available for faceting. - Second request obviously affects performance. A good news, performance will be affected only in queries which fetch products with large amount of prices. Other queries or slices with small products will not be affected. -### Synchronization with monolith - -One of the goals of `price books` is to speedup reindex process. Existing reindex process lives in the monolith and -prepares the prices for luma storefront exclusively. The data produced by this indexer is useless for the new storefront, -so old indexer should be disabled for the installation which uses the new storefront exclusively. - - - - ## Scenarios diff --git a/design-documents/storefront/pricing/pricing-structure.png b/design-documents/storefront/pricing/pricing-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..0c5d8693d302d6d5ab10c0aa1af9df0b2d484f9c GIT binary patch literal 60174 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 GgeCw{9Mebu literal 0 HcmV?d00001 From e0bea763d5e0470246ec1ad2174e96f22a0bf4a9 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 16 Jul 2020 15:53:07 -0500 Subject: [PATCH 161/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 29 +++++++++++++++++++ design-documents/graph-ql/coverage/returns.md | 2 ++ 2 files changed, 31 insertions(+) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 5749eceeb..ccf9d6936 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -5,6 +5,35 @@ type Query{ ): CustomAttributeMetadata } +type Mutation { + createReturn(input: CreateReturnInput!): CreateReturnOutput @doc(description: "Create a new return.") +} + +input CreateReturnInput { + items: [CreateReturnItemInput]! + comment_text: String + # Do we need contact_email? It is present in the form when requesting return as a guest +} + +input CreateReturnItemInput { + orderItemId: ID! + quantity_to_return: Float! + selected_custom_attributes: [ID!] + entered_custom_attributes: [EnteredCustomAttributeInput!] +} + +input EnteredCustomAttributeInput { + id: ID! + value: String! +} + +type CreateReturnOutput { + returns( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + ): Returns +} + enum CustomAttributesPageEnum { RETURN_ITEM_EDIT_FORM RETURN_ITEMS_LISTING diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index ce7ad9870..9b3d7431d 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -39,3 +39,5 @@ First, we need to know if there are any order items eligible for return. In the ### Leave a return comment ### Create a return for guest order + +Guest orders are not accessible via GraphQL yet, but the schema of returns will be identical to the one for customer orders. From 627ee5f805fd451a8962dcbb055bf18ee00c3e35 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 16 Jul 2020 16:37:14 -0500 Subject: [PATCH 162/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../coverage/catalog/configurable-options-selection.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 5f713a9d3..74b1c5880 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -205,3 +205,7 @@ In case when the facet filter was used on the category page, for example to sear `ConfigurableOptionsSelectionMetadata` type can be extended to support additional use cases, which are not currently supported by Magento like: - Price range for the variants based on configurable options selection - Low stock notification based on configurable options selection + +### Long term vision + +In the future all option types will be unified to support additional use cases like conflicting custom options, or price range based on custom + configurable options selection. The new query will be introduced on the top level, and current solution being specific to configurable options only will be deprecated. From b669bab6930756f4485f2e775a46a1435c4d40d2 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 16 Jul 2020 17:20:28 -0500 Subject: [PATCH 163/479] Switch to CartItemInterface to represent items on a NegotiableQuote --- .../coverage/negotiableQuotes.graphqls | 51 +++---------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 7e757bc0b..34fbeb864 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -164,7 +164,7 @@ enum NegotiableQuoteCommentCreatorType { type NegotiableQuote { id: ID! name: String! - items: [NegotiableQuoteItemInterface!] + items: [NegotiableQuoteItem!] # Attachment Support is dependent on headless File Upload design # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] @@ -190,55 +190,16 @@ enum NegotiableQuoteStatus { EXPIRED } +type NegotiableQuoteItem @doc(description: "A line item on a Negotiable Quote") { + id: ID! @doc(description: "ID of the line item (not product) in a Negotiable Quote") + item: CartItemInterface @doc(description: "Product assigned to line item, with selected configurations") +} + input NegotiableQuoteFilterInput { ids: FilterEqualTypeInput @doc(description: "Filter by Negotiable Quote ID(s)") name: FilterMatchTypeInput @doc(description: "Filter by Negotiable Quote name") } -interface NegotiableQuoteItemInterface { - id: ID! - quantity: Float! - stock: Float - price: CartItemPrices! - product: ProductInterface -} - -type DefaultNegotiableQuoteItem implements NegotiableQuoteItemInterface -@doc(description: "Negotiable Quote Item Implementation for Simple and Virtual Products") { - id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") - product: ProductInterface! - qty: Float! @doc(description: "Quantity added") - customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") -} - -type DownloadableRequisitionListItem implements NegotiableQuoteItemInterface -@doc(description: "Negotiable Quote Item Implementation that for Downloadable Products") { - id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") - product: ProductInterface! - qty: Float! @doc(description: "Quantity added") - customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") - links: [DownloadableProductLinks] @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") - samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") -} - -type BundleRequisitionListItem implements NegotiableQuoteItemInterface -@doc(description: "Negotiable Quote Item Implementation that for Bundle Products") { - id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") - product: ProductInterface! - qty: Float! @doc(description: "Quantity added") - customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") - bundle_options: [SelectedBundleOption]! @doc(description: "selected bundle options") -} - -type ConfigurableRequisitionListItem implements NegotiableQuoteItemInterface -@doc(description: "Negotiable Quote Item Implementation that for Configurable Products") { - id: ID! @doc(description: "Unique Identifier of Negotiable Quote Item") - product: ProductInterface! - qty: Float! @doc(description: "Quantity added") - customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") - configurable_options: [SelectedConfigurableOption] @doc(description: "Configurable options selected") -} - type NegotiableQuoteHistoryEntry { id: ID! author: NegotiableQuoteUser! From 82511a28ddf3b4ef3a727b2d96c16cb4a44f153e Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 16 Jul 2020 17:28:55 -0500 Subject: [PATCH 164/479] Add note about single add to cart mutation --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 34fbeb864..11d5c4b8a 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -85,6 +85,8 @@ type SetNegotiableQuoteShippingAddressOutput { input AddNegotiableQuoteItemsInput { quote_id: ID! @doc(description: "ID from a NegotiableQuote object") + # Implementation Note: This *should* be compatible with the new, single + # add to cart mutation. https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/AddProductsToCart.graphqls cart_items: [CartItemInput!]! } From 09d65144ae8ecb9543e132b8653504790f738c00 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 16 Jul 2020 17:44:12 -0500 Subject: [PATCH 165/479] Wishlist updates proposed in Magento 2 Commerce PR (internal) --- design-documents/graph-ql/coverage/Wishlist.graphqls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 613445e9a..9996db790 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -7,8 +7,9 @@ type Mutation { } type Customer { - wishlist: Wishlist! @doc(description: "Customer wishlist") # Commerce will extend filed with required `id` argument `wishlists(ids: ID!)` - wishlists: [Wishlist!]! @doc(description: "Customer multiple wishlists") # Multiple wishlists Commerce functionality + wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlistByID") + wishlistByID(id: ID!): Wishlist + wishlists: [Wishlist!]! @doc(description: "Customer wishlists are limited to a max of 1 wishlist in Magento Open Source") } type Wishlist { From b34f946ba02028929daf9a5cca67dcb39e3fdf0c Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 17 Jul 2020 10:14:28 -0500 Subject: [PATCH 166/479] ECP-765: GraphQL Schema for Configurable Options Selection --- .../graph-ql/coverage/returns.graphqls | 18 +++++++++++++----- design-documents/graph-ql/coverage/returns.md | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index ccf9d6936..76ccba30b 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -7,6 +7,7 @@ type Query{ type Mutation { createReturn(input: CreateReturnInput!): CreateReturnOutput @doc(description: "Create a new return.") + addReturnComment(input: AddReturnCommentInput!): AddReturnCommentOutput @doc(description: "Add a comment to an existing return.") } input CreateReturnInput { @@ -16,7 +17,7 @@ input CreateReturnInput { } input CreateReturnItemInput { - orderItemId: ID! + order_item_id: ID! quantity_to_return: Float! selected_custom_attributes: [ID!] entered_custom_attributes: [EnteredCustomAttributeInput!] @@ -28,10 +29,16 @@ input EnteredCustomAttributeInput { } type CreateReturnOutput { - returns( - pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), - currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - ): Returns + return: Return +} + +input AddReturnCommentInput { + return_id: ID! + comment_text: String! +} + +type AddReturnCommentOutput { + return: Return } enum CustomAttributesPageEnum { @@ -56,6 +63,7 @@ type CustomerOrder { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): Returns @doc(description: "Information about the order returns.") + items_eligible_for_return: [OrderItemInterface] @doc(description: "A list of order items eligible for return.") } type Returns { diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 9b3d7431d..2c444d0b4 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -26,7 +26,7 @@ Scenarios which may need these settings include: #### Determine whether any order items are eligible for return -First, we need to know if there are any order items eligible for return. In the Luma example this will dictate whether "Return" link should be displayed on the order details page. +First, we need to know if there are any order items eligible for return. In the Luma example this is dictating whether "Return" link will be displayed on the order details page. #### Render return form with dynamic RMA attributes From b85401422f65f3086899f347961234499e1d0c8d Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Fri, 17 Jul 2020 16:07:54 -0500 Subject: [PATCH 167/479] Admin Panel Configuration propagation to Storefront --- design-documents/storefront/configuraiton-propagation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index dea6de637..cfc0283bb 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -30,7 +30,9 @@ This type of configuration should be propagated to Storefront in one of the foll 1. Pre-calculate final data on back office side and provide final result to the Storefront during synchronization. 1. Pros: simpler implementation of Storefront due to eliminated necessity for Storefront to keep knowledge about additional configuration, including data calculation algorithms. 2. Cons: massive (up to full) reindexation necessary in case the configuration is changed. - +2. Move/duplicate calculation logic in the Storefront service based on original data is synced from the back office. + 1. Pros and cons are opposite to option 1. So this option is valuable in case of expected frequent change of configuration that impacts a lot of data. + To choose the right approach in a specific case, consider the following: 1. How frequently the configuration is expected to change? From a4eb269d8cc84efbfaf9c0c917eb94e108d9b34d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 20 Jul 2020 10:21:26 -0500 Subject: [PATCH 168/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 22 +++++++++++++------ design-documents/graph-ql/coverage/returns.md | 4 ++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 76ccba30b..e5bb4ac04 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -91,8 +91,7 @@ type ReturnItem { custom_attributes: [CustomAttribute] request_quantity: Float! quantity: Float! - # TODO: Should enums be separated for Return and Return item? - status: ReturnStatus! + status: ReturnItemStatus! } # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/custom-attributes-container.md @@ -138,16 +137,25 @@ enum ReturnStatus { AUTHORIZED PARTIALLY_AUTHORIZED RECEIVED - RECEIVED_ON_ITEM + PARTIALLY_RECEIVED APPROVED - APPROVED_ON_ITEM + PARTIALLY_APPROVED REJECTED - REJECTED_ON_ITEM + PARTIALLY_REJECTED DENIED + PROCESSED_AND_CLOSED CLOSED - PROCESSED_CLOSED +} + +enum ReturnItemStatus { + PENDING + AUTHORIZED + RECEIVED + APPROVED + REJECTED + DENIED } type StoreConfig { - sales_magento_rma_enabled @doc(description: "Returns functionality status on the storefront: enabled/disabled.") + sales_magento_rma_enabled: String! @doc(description: "Returns functionality status on the storefront: enabled/disabled.") } diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 2c444d0b4..2ca274043 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -41,3 +41,7 @@ First, we need to know if there are any order items eligible for return. In the ### Create a return for guest order Guest orders are not accessible via GraphQL yet, but the schema of returns will be identical to the one for customer orders. + +### Specify shipping and tracking + +When return is authorized, the customer should specify shipping and tracking information From 08490e5a657dc7087242da3d78973dcc01c34347 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 20 Jul 2020 11:28:06 -0500 Subject: [PATCH 169/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/returns.md | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 2ca274043..05050ad69 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -18,10 +18,114 @@ Scenarios which may need these settings include: ### View return list with pagination in customer account +```graphql +{ + customer { + returns(pageSize: 10, currentPage: 2) { + items { + id + creation_date + customer_name + status + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } +} +``` + ### View return list with pagination in order details +```graphql +{ + customer { + orders(filter: {number: {eq: "00000008"}}) { + items { + returns(pageSize: 10, currentPage: 1) { + items { + id + creation_date + customer_name + status + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } + } + } +} +``` + ### View return details +```graphql +{ + customer { + return(id: "0000003") { + id + order_id + creation_date + customer_email + customer_name + status + shipping { + tracking { + id + carier + shipping_method + tracking_number + status + } + address { + contact_name + street + city + region { + name + } + postcode + country { + full_name_locale + } + telephone + } + } + comments { + id + text + created_at + created_by + } + items { + id + product { + sku + name + } + custom_attributes { + id + label + value + } + request_quantity + quantity + status + } + } + } +} +``` + ### Create a return #### Determine whether any order items are eligible for return From b2aeca6a36936862774b16cdd2dce995bb135b13 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Mon, 20 Jul 2020 13:59:23 -0500 Subject: [PATCH 170/479] New pricing --- design-documents/storefront/pricing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index 98f72b328..eda469116 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -84,6 +84,7 @@ The following diagram shows the pricing structure for a given product, website a - Message broker call monolith for prices of affected products - Monolith calculates prices based on the cache (product matches) - Message broker store prices in price book + ### Price book API ```proto @@ -109,7 +110,7 @@ message PriceBookInput { // Website ids associated with price book // A combination of customer group and website must be unique. Error will be returned in case when combination is // already occupied by another price book. - repeated string website_ids = 4; + repeated string 1 = 4; } message PriceBookDeleteInput { From c72485f4f21efa398c7c467b77b5cbbffec4e003 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Mon, 20 Jul 2020 14:09:49 -0500 Subject: [PATCH 171/479] New pricing --- design-documents/storefront/pricing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index eda469116..fc04a219d 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -103,14 +103,14 @@ message PriceBookInput { string name = 2; // Customer groups associated with price book - // A combination of customer group and website must be unique. Error will be returned in case when combination is + // A combination of customer group and currency must be unique. Error will be returned in case when combination is // already occupied by another price book. repeated string customer_groups = 3; - // Website ids associated with price book - // A combination of customer group and website must be unique. Error will be returned in case when combination is + // Currency of price book + // A combination of customer group and currency must be unique. Error will be returned in case when combination is // already occupied by another price book. - repeated string 1 = 4; + string currency = 4; } message PriceBookDeleteInput { From 7747047c08d853b13bec192699888ecffca6e8ba Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 20 Jul 2020 16:05:17 -0500 Subject: [PATCH 172/479] Admin Panel Configuration propagation to Storefront - added all options for UI configuration --- .../storefront/configuraiton-propagation.md | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index cfc0283bb..c05b3a8cc 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -46,22 +46,79 @@ To choose the right approach in a specific case, consider the following: Example: include or exclude taxes in displayed prices. -This kind of configuration has nothing to do with Storefront data itself, and so it is suggested to have a separate service that provides this configuration. +This kind of configuration has nothing to do with Storefront data itself, but is still necessary for the client to know how to display the data. + +This section describes possible implementation options. + +#### 3.1. Configuration as Part of Domain Service + +UI Configuration is synced to the Storefront domain service, similarly to how the data is synced. +This may be done by means of a separate data flow as configuration usually doesn't change together with the data. + +Is UI config included in the data (on the level of entities)? + +![UI Configuration Propagation as Part of Domain Service](https://app.lucidchart.com/publicSegments/view/4805a8df-abe8-4605-96e2-20266b2f2876/image.png) + +Pros: + +1. Simplicity in case of Magento as back office. Same/similar data sync can be implemented for configuration +2. In-process call to storage for UI configuration + +Cons: + +1. Potential difficulties with UI configuration exclusion from deployment in case it is not needed, as it is part of the service + +#### 3.2. Domain-Specific Configuration Service + +Each domain service that requires UI configuration has a companion UI Configuration service. Client application may call the specialized configuration service to get configuration it needs. Some clients may not need all or part of UI configuration. -GraphQL serves as a single entry point for both data and UI requests to simplify client implementation and have more control of which APIs are publicly exposed. -![UI Configuration Propagation](https://app.lucidchart.com/publicSegments/view/b7ec5763-eb23-48ac-9092-6b92821040fb/image.png) +GraphQL serves as a single entry point for both data and UI requests to simplify client implementation and have more control of which APIs are publicly exposed. -Pros and cons of a separate service compared to configuration being part of the domain service are described bellow. +![UI Configuration Propagation as Domain-Specific Configuration Service](https://app.lucidchart.com/publicSegments/view/c3c9f0a7-1780-416f-ab3f-caeeebb15680/image.png) Pros: 1. More control of the deployment, scalability, technologies for the domain and config services. -2. Ability to exclude config service from deployment in case it becomes unnecessary for the specific use case. - 1. In case of multi-tenant deployment, of course it can't be done based on specific customer needs, but still may allow resources optimization in case specific merchant does not need UI configuration propagated from Magento. - 1. Even in case of multi-tenant deployment, it is easier to eliminate configuration feature (and free resources needed for it) if it's represented by a separate service. +2. More flexibility in the future + 1. Easier path for UI configuration disablement in case it becomes unnecessary. For example, if it turnes out that client apps want to have full ownership of UI configuration Cons: 1. Additional request to configuration service instead of in-process request to the storage. + +#### 3.3. Single UI Configuration Service for All Domain Services + +UI configuration settings are fed into a single UI Configuration service and read from it when necessary. + +As, most likelly, all configuration options will look like key-value pairs, it might be reasonable to unify all of them under umpbrella of a single serfvice and so provide more uniformity of working with such configuration service. +This is a variation of option 3.2. + +![UI Configuration Propagation as Single UI Configuration Service](https://app.lucidchart.com/publicSegments/view/cc77dc21-77ac-4eb4-b766-0f9afc8c11d5/image.png) + +Pros: + +1. More control of the deployment, scalability, technologies for the domain and config services. Assuming, all config services has the same technical requirements. + +Cons: + +1. Additional request to configuration service instead of in-process request to the storage. + +#### Integration with 3rd-party Data-Provider System + +Data-prover system is a system that provides domain-specific data. For example, [PIM (product information management)](https://en.wikipedia.org/wiki/Product_information_management) system. + +In case of integration with 3rd-party data-provider systems, a separate data flow process is setup to sync configuration when it's changed. +If PIM doesn't provide all necessary configuration settings, a different management system may be used as the source. + +Additional configuration may be provided by a separate client application dedicated to the configuration management. +Also, the additional configuration may be, in reality, the only source of configuration, in case PIM system doesn't provide such configuration. + +In case of UI Configuration provided as part of the domain service: + +![3rd-party PIM integration - UI Configuration as part of domain service](https://app.lucidchart.com/publicSegments/view/0e81c178-7480-41ca-8e7d-a37e6bdb6b6d/image.png) + +In case of UI Configuration provided by a separate service: + +![3rd-party PIM integration - UI Configuration as separate service](https://app.lucidchart.com/publicSegments/view/6051ef66-4b8f-44aa-9211-c6e7399bc9e2/image.png) From aa7fe225006d40ff5be8ab92e35736c5e7a01db8 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 20 Jul 2020 16:18:08 -0500 Subject: [PATCH 173/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 8 ++ design-documents/graph-ql/coverage/returns.md | 115 +++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index e5bb4ac04..22c1b8c71 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -159,3 +159,11 @@ enum ReturnItemStatus { type StoreConfig { sales_magento_rma_enabled: String! @doc(description: "Returns functionality status on the storefront: enabled/disabled.") } + +type Attribute { + id: ID! +} + +type AttributeOption { + id: ID! +} diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 05050ad69..71d387408 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -130,18 +130,127 @@ Scenarios which may need these settings include: #### Determine whether any order items are eligible for return -First, we need to know if there are any order items eligible for return. In the Luma example this is dictating whether "Return" link will be displayed on the order details page. +There is a need to know if any order items are eligible for return. In the Luma example this is dictating whether "Return" link will be displayed on the order details page. + +```graphql +{ + customer { + orders(filter: {number: {eq: "00000008"}}) { + items { + items_eligible_for_return { + id + product_name + } + } + } + } +} +``` #### Render return form with dynamic RMA attributes +```graphql +{ + pageSpecificCustomAttributes(page_type: RETURN_ITEM_EDIT_FORM) { + items { + id + attribute_code + attribute_type + input_type + attribute_options { + id + label + value + } + } + } +} +``` + +Existing schema of `Attribute` and `AttributeOption` must be extended to provide `ID` field, which will be used in mutations to specify custom attribute values for returns. + #### Determine which order items are eligible for return -#### Add more items to the return +```graphql +{ + customer { + orders(filter: {number: {eq: "00000008"}}) { + items { + items_eligible_for_return { + id + product_name + } + } + } + } +} +``` -#### Submit return +#### Submit a return with multiple items and comments + +```graphql +mutation { + createReturn( + input: { + items: [ + { + order_item_id: "0000004", + quantity_to_return: 1, + selected_custom_attributes: ["encoded-custom-select-attribute-value-id"], + entered_custom_attributes: [{id: "encoded-custom-text-attribute-id", value: "Custom attribute value"}] + } + ], + comment_text: "Return comment" + } + ) { + return { + id + items { + id + quantity + request_quantity + product { + sku + name + } + custom_attributes { + id + label + value + } + } + comments { + created_at + created_by + text + } + } + } +} +``` ### Leave a return comment +```graphql +mutation { + addReturnComment( + input: { + return_id: "000000001", + comment_text: "Another return comment" + } + ) { + return { + id + comments { + created_at + created_by + text + } + } + } +} +``` + ### Create a return for guest order Guest orders are not accessible via GraphQL yet, but the schema of returns will be identical to the one for customer orders. From 2e06dd447c03c0978492f06c95f7eb84036a23f7 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 21 Jul 2020 11:19:30 -0500 Subject: [PATCH 174/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 46 ++++-- design-documents/graph-ql/coverage/returns.md | 134 +++++++++++++++--- 2 files changed, 148 insertions(+), 32 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 22c1b8c71..3e6d107f7 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -8,12 +8,13 @@ type Query{ type Mutation { createReturn(input: CreateReturnInput!): CreateReturnOutput @doc(description: "Create a new return.") addReturnComment(input: AddReturnCommentInput!): AddReturnCommentOutput @doc(description: "Add a comment to an existing return.") + addReturnTracking(input: AddReturnTrackingInput!): AddReturnTrackingOutput @doc(description: "Add tracking information to the return.") + removeReturnTracking(input: RemoveReturnTrackingInput!): RemoveReturnTrackingOutput @doc(description: "Remove tracking information from the return.") } input CreateReturnInput { items: [CreateReturnItemInput]! comment_text: String - # Do we need contact_email? It is present in the form when requesting return as a guest } input CreateReturnItemInput { @@ -41,6 +42,24 @@ type AddReturnCommentOutput { return: Return } +input AddReturnTrackingInput { + return_id: ID! + carrier_id: ID! + tracking_number: String! +} + +type AddReturnTrackingOutput { + return: Return +} + +input RemoveReturnTrackingInput { + return_tracking_id: ID! +} + +type RemoveReturnTrackingOutput { + return: Return +} + enum CustomAttributesPageEnum { RETURN_ITEM_EDIT_FORM RETURN_ITEMS_LISTING @@ -82,12 +101,12 @@ type Return @doc(description: "Customer return") { shipping: ReturnShipping @doc(description: "Shipping information for the return.") comments: [ReturnComment] @doc(description: "A list of comments posted for the return.") items: [ReturnItem] @doc(description: "A list of items being returned.") + available_shipping_carriers: [ReturnShippingCarrier] @doc(description: "A list of shipping carriers available for returns.") } type ReturnItem { id: ID! product: ProductInterface! - # Do we need reference to order ID? How about entered and selected options custom_attributes: [CustomAttribute] request_quantity: Float! quantity: Float! @@ -109,17 +128,26 @@ type ReturnComment { } type ReturnShipping { - address: ReturnShippingAddress - # Tracking is not currently supported by Magento - tracking: ReturnShippingTracking + address: ReturnShippingAddress @doc(description: "Return shipping address, which is specified by the admin.") + tracking(id: ID): [ReturnShippingTracking] @doc(description: "Tracking information for all or a single tracking record when ID is provided.") +} + +type ReturnShippingCarrier { + id: ID! + label: String! } type ReturnShippingTracking { id: ID! - shipping_method: String - carier: String - tracking_number: String - status: String + carier: ReturnShippingCarrier! + tracking_number: String! + status_type: ReturnShippingTrackingStatusType! + status: String! +} + +enum ReturnShippingTrackingStatusType { + INFORMATION + ERROR } type ReturnShippingAddress { diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 71d387408..392830360 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -78,28 +78,6 @@ Scenarios which may need these settings include: customer_email customer_name status - shipping { - tracking { - id - carier - shipping_method - tracking_number - status - } - address { - contact_name - street - city - region { - name - } - postcode - country { - full_name_locale - } - telephone - } - } comments { id text @@ -121,6 +99,28 @@ Scenarios which may need these settings include: quantity status } + shipping { + tracking { + id + carier { + label + } + tracking_number + } + address { + contact_name + street + city + region { + code + } + country { + full_name_english + } + postcode + telephone + } + } } } } @@ -257,4 +257,92 @@ Guest orders are not accessible via GraphQL yet, but the schema of returns will ### Specify shipping and tracking -When return is authorized, the customer should specify shipping and tracking information +When return is authorized by the admin user, the customer can specify shipping and tracking information. + +First, the client needs to get shipping cariers that can be used for returns: + +```graphql +{ + customer { + return(id: "0000003") { + available_shipping_carriers { + id + label + } + } + } +} +``` + +Then tracking information can be submitted: + +```graphql +mutation { + addReturnTracking( + input: { + return_id: "000005", + carrier_id: "carrier-id", + tracking_number: "4234213" + } + ) { + return { + shipping { + tracking { + id + carier { + label + } + tracking_number + } + } + } + } +} +``` + +If the user decides to view the status of the return, it can be retrieved using the following query: + +```graphql +{ + customer { + return(id: "0000003") { + shipping { + tracking(id: "return-tracking-id") { + id + carier { + label + } + tracking_number + status + status_type + } + } + } + } +} +``` + +In case the return shipping needs to be removed, the following mutation can be used: + +```graphql +mutation { + removeReturnTracking( + input: { + return_tracking_id: "return-tracking-id" + } + ) { + return { + shipping { + tracking { + id + carier { + label + } + tracking_number + } + } + } + } +} + +``` From 9b9e63dc11a2fac8b4f2137f60a22996c2cdd04e Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Tue, 21 Jul 2020 11:56:02 -0500 Subject: [PATCH 175/479] wording --- design-documents/storefront/pricing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index fc04a219d..ad0a885ca 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -54,7 +54,7 @@ and reused for consequent requests. ### Default price book -A `default price book` is a predefined system price book that contains `base prices` for __all__ products in the system. User-defined `price books` may contain only sub-set of products. +A `default price book` is a predefined system price book that contains `base prices` for __ALL__ products in the system. User-defined `price books` may contain only sub-set of products. Default `price book` should be used as fallback storage if the price for a specific product doesn't exist in other resolved pricebook. Other words, there is always some price for sku in `default price book`. @@ -185,7 +185,7 @@ but recommendation system works with different groups of customers which are bas In order to provide more flexibility in customer segmentation, we may introduce many-to-many. Also, having `customer groups` which are not bound to pricing functionality make them looks like a regular tags. Thus, we may also rename them to tags: -![Price books diagram](pricing/customer-tags.png) +![Customer tags](pricing/customer-tags.png) ### Complex products support From 4ec8b44091a8c6865b46eea2238ea38c2b1b7593 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 22 Jul 2020 10:20:38 -0500 Subject: [PATCH 176/479] PR feedback update --- design-documents/graph-ql/coverage/Wishlist.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index 9996db790..fe32f7043 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -7,8 +7,8 @@ type Mutation { } type Customer { - wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlistByID") - wishlistByID(id: ID!): Wishlist + wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlist_v2") + wishlist_v2(id: ID!): Wishlist wishlists: [Wishlist!]! @doc(description: "Customer wishlists are limited to a max of 1 wishlist in Magento Open Source") } From ad76bb7ac80c06a2d9d3761339b0eacae8e8904b Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 22 Jul 2020 11:58:42 -0500 Subject: [PATCH 177/479] wording --- design-documents/storefront/pricing.md | 74 ++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index ad0a885ca..d9d0f3c01 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -75,12 +75,8 @@ The following diagram shows the pricing structure for a given product, website a - t2 - New price for customer group was introduced on the monolith side - Message broker checks if price book for specified customer group exists on storefront and create new price book if needed - Message broker assigns product to the new price book and set appropriate price -- t3 - New catalog price rule was created on the monolith side. Catalog price rule has start and end dates. - - Message broker checks if price book for specified customer group exists on storefront and create new price book if needed - - Message broker detects products matched by the rule - - Message broker calculate prices apply discounts for selected products and write prices to price book -- Alternative t3 - - Monolith detects matched products and fire `price_changed` events for those products. Event includes information about affected customer groups. Product matches are stored in cache for the later use. +- t3-t7 + - Monolith detects products matched by the rule and fire `price_changed` events for those products. Event includes information about affected customer groups and websites. "Product by rule" matches are stored in cache for the later use. - Message broker call monolith for prices of affected products - Monolith calculates prices based on the cache (product matches) - Message broker store prices in price book @@ -103,18 +99,18 @@ message PriceBookInput { string name = 2; // Customer groups associated with price book - // A combination of customer group and currency must be unique. Error will be returned in case when combination is + // A combination of customer group and website must be unique. Error will be returned in case when combination is // already occupied by another price book. repeated string customer_groups = 3; - // Currency of price book - // A combination of customer group and currency must be unique. Error will be returned in case when combination is + // Websites associated with price book + // A combination of customer group and website must be unique. Error will be returned in case when combination is // already occupied by another price book. - string currency = 4; + repeated string websites = 4; } message PriceBookDeleteInput { - string id = 1; + string id = 1; } message AssignProductsInput { @@ -255,3 +251,59 @@ Consequences: - `product listing`, `PDP` and `checkout` scenarios contain `special price` - Customers and guests are able to buy the product for the `special price` +## Appendix + +### Price calculation algorithm for single website and customer group +```php +getSpecialPriceFrom >= $time && $product->getSpecialPriceTo <= $time) + ? $product->getSpecialPrice + : 0; + + $fixedPrice = min($product->getBasePrice(), $specialPrice); + + $groupPrice = $product->getGroupPrice($customerGroup); + if ($groupPrice) { + $fixedPrice = ($groupPrice->getType() == 'percentage') + ? $product->getBasePrice() - $product->getBasePrice() / 100 * $groupPrice->getValue() + : $groupPrice->getValue; + + } + return $fixedPrice; +}; + + +$fixedPrices = array_map($fixedCalculator, $product->getChildren()); +$fixedPrices[] = $fixedCalculator($product); + +$minFixedPrice = min($fixedPrices); + +// DISCOUNT CALCULATIONS. WE ASSUME THAT ALL RULES ALREADY RESOLVED AND WE HAVE A LIST OF RULES PER PRODUCT +$discountCalculator = function ($product) use ($customerGroup, $time) { + $discountedPrice = $product->getBasePrice(); + foreach ($product->getRules()->sortByPriority() as $rule) { + if ($rule->getDateFrom >= $time && $rule->getDateTo <= $time) { + $discountedPrice = $rule->getDiscount()->getType() == 'percentage' + ? $discountedPrice - $discountedPrice / 100 * $rule->getDiscount()->getValue() + : $discountedPrice - $rule->getDiscount()->getValue(); + + if ($rule->hasStopConsequentRules) { + break; + } + } + } + return $discountedPrice; +}; + +$discountedPrices = array_map($discountCalculator, $product->getChildren()); +$discountedPrices[] = $discountCalculator($product); +$minDiscountedPrice = min($discountedPrices); + +$finalPrice = min($minFixedPrice, $minDiscountedPrice); +``` + From 3951e011c501418ed6d5d2a3e693c6ca5c3d046e Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 22 Jul 2020 12:05:35 -0500 Subject: [PATCH 178/479] wording --- design-documents/storefront/pricing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index d9d0f3c01..b7fb48a6e 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -71,8 +71,10 @@ The following diagram shows the pricing structure for a given product, website a ![Pricing structure](pricing/pricing-structure.png) - t1 - New product was created. Every product should have some base price, so new product also introduce one price in the system. + - Monolith fires `price_changed` event for new product. Event specifies product_id and default website scope. - Storefront receives new base price and put it in the default price book - t2 - New price for customer group was introduced on the monolith side + - Monolith detects change in product price and fires `price_changed` event. Event specifies product_id, specified websites and customer group scope. - Message broker checks if price book for specified customer group exists on storefront and create new price book if needed - Message broker assigns product to the new price book and set appropriate price - t3-t7 From 9daa31bbb4eb2c5d31f5108d6a1aa33c71a30233 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Wed, 22 Jul 2020 12:14:18 -0500 Subject: [PATCH 179/479] removed search --- design-documents/graph-ql/coverage/search.md | 82 -------------------- 1 file changed, 82 deletions(-) delete mode 100644 design-documents/graph-ql/coverage/search.md diff --git a/design-documents/graph-ql/coverage/search.md b/design-documents/graph-ql/coverage/search.md deleted file mode 100644 index 8b0e6c325..000000000 --- a/design-documents/graph-ql/coverage/search.md +++ /dev/null @@ -1,82 +0,0 @@ -# Queries - -```graphql -type Query { - #Filter supports multiple clauses which will be wrapped in logical AND operator - productSearch( - phrase: String!, - "Desired size of the search result page" - pageSize: Int = 20, - currentPage: Int = 1, - filter: [SearchClauseInput], - sort: [ProductSearchSortInput] - ): ProductSearchResponse! -} - -input ProductSearchSortInput -{ - attribute: String! - direction: SortEnum! -} - -# If from or to fields are omitted, $gte or $lte filter will be applied -input SearchRangeInput { - from: Float - to: Float -} - -input SearchClauseInput { - attribute_code: String! - in: [String] - eq: String - range: SearchRangeInput -} - -type ProductSearchResponse { - items: [ProductSearchItem] - facets: [Aggregation] - facets_values: [Aggregation] - suggestions: [String] - related_terms: [String] - page_info: SearchResultPageInfo - total_count: Int -} - -type ProductSearchItem { - product: ProductInterface! - highlights: [Highlight] -} - -type Highlight { - attribute: String! - value: String! - matched_words: [String]! -} - -interface Bucket { - #Human readable bucket title - title: String! -} - -type StatsBucket implements Bucket { - min: Float! - max: Float! -} - -type ScalarBucket implements Bucket { - #Could be used for filtering and may contain non-human readable data - id: ID! - count: Int! -} - -type RangeBucket implements Bucket { - from: Float! - to: Float! - count: Int! -} - -interface Aggregation { - attribute_code: String! - buckets: [Bucket]! -} -``` \ No newline at end of file From 018a0079ca87debdfcc72b3c8560550c0cc8d685 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 22 Jul 2020 12:51:01 -0500 Subject: [PATCH 180/479] PR feedback --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 11d5c4b8a..fce78e19e 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -119,7 +119,7 @@ type CloseNegotiableQuotesOutput { } input RemoveNegotiableQuoteItemsInput { - quote_ID: ID! + quote_id: ID! quote_item_ids: [ID!]! } From 96d19dccbaa7695ed32f0f2e5b6edfd3ef730ce3 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 22 Jul 2020 13:01:05 -0500 Subject: [PATCH 181/479] pr feedback --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index fce78e19e..c29ad41ba 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -4,8 +4,8 @@ type Query { # in a company can see quotes belonging to their # employees. If `Customer.negotiable_quotes` is desirable for buyer's # that aren't managers, we can always add that on - negotiable_quote(id: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") - negotiable_quotes( + negotiableQuote(id: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") + negotiableQuotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 From f9bc01cedc185d733b6ca36673654ba38a531f50 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 22 Jul 2020 14:27:10 -0500 Subject: [PATCH 182/479] Admin Panel Configuration propagation to Storefront --- .../storefront/configuraiton-propagation.md | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index c05b3a8cc..ad873c0d7 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -50,25 +50,35 @@ This kind of configuration has nothing to do with Storefront data itself, but is This section describes possible implementation options. -#### 3.1. Configuration as Part of Domain Service +#### 3.1. Configuration is Responsibility of the Client -UI Configuration is synced to the Storefront domain service, similarly to how the data is synced. -This may be done by means of a separate data flow as configuration usually doesn't change together with the data. +Client application (such as PWA) is responsible for the UI configuration, either hard-coded or by means of a service. +UI configuration is not synced from Magento Admin Panel to Storefront. -Is UI config included in the data (on the level of entities)? +#### 3.2. Configuration is Provided by Magento Back Office API -![UI Configuration Propagation as Part of Domain Service](https://app.lucidchart.com/publicSegments/view/4805a8df-abe8-4605-96e2-20266b2f2876/image.png) +Rely on current GraphQL API for providing UI Configuration. +No additional Store Front service is created to serve such configuration. +GraphQL entry point can proxy to the Magento Back Office GraphQL for simplicity in API usage. + +#### 3.3. Single UI Configuration Service for All Domain Services + +UI configuration settings are fed into a single UI Configuration service and read from it when necessary. + +As, most likelly, all configuration options will look like key-value pairs, it might be reasonable to unify all of them under umpbrella of a single serfvice and so provide more uniformity of working with such configuration service. +This is a variation of option 3.2. + +![UI Configuration Propagation as Single UI Configuration Service](https://app.lucidchart.com/publicSegments/view/cc77dc21-77ac-4eb4-b766-0f9afc8c11d5/image.png) Pros: -1. Simplicity in case of Magento as back office. Same/similar data sync can be implemented for configuration -2. In-process call to storage for UI configuration +1. More control of the deployment, scalability, technologies for the domain and config services. Assuming, all config services has the same technical requirements. -Cons: +Cons: -1. Potential difficulties with UI configuration exclusion from deployment in case it is not needed, as it is part of the service +1. Additional request to configuration service instead of in-process request to the storage. -#### 3.2. Domain-Specific Configuration Service +#### 3.4. Domain-Specific Configuration Service Each domain service that requires UI configuration has a companion UI Configuration service. Client application may call the specialized configuration service to get configuration it needs. @@ -88,25 +98,28 @@ Cons: 1. Additional request to configuration service instead of in-process request to the storage. -#### 3.3. Single UI Configuration Service for All Domain Services +#### 3.5. Configuration as Part of Domain Service -UI configuration settings are fed into a single UI Configuration service and read from it when necessary. +UI Configuration is synced to the Storefront domain service, similarly to how the data is synced. +This may be done by means of a separate data flow as configuration usually doesn't change together with the data. -As, most likelly, all configuration options will look like key-value pairs, it might be reasonable to unify all of them under umpbrella of a single serfvice and so provide more uniformity of working with such configuration service. -This is a variation of option 3.2. +Is UI config included in the data (on the level of entities)? -![UI Configuration Propagation as Single UI Configuration Service](https://app.lucidchart.com/publicSegments/view/cc77dc21-77ac-4eb4-b766-0f9afc8c11d5/image.png) +![UI Configuration Propagation as Part of Domain Service](https://app.lucidchart.com/publicSegments/view/4805a8df-abe8-4605-96e2-20266b2f2876/image.png) Pros: -1. More control of the deployment, scalability, technologies for the domain and config services. Assuming, all config services has the same technical requirements. +1. Simplicity in case of Magento as back office. Same/similar data sync can be implemented for configuration +2. In-process call to storage for UI configuration -Cons: +Cons: -1. Additional request to configuration service instead of in-process request to the storage. +1. Potential difficulties with UI configuration exclusion from deployment in case it is not needed, as it is part of the service #### Integration with 3rd-party Data-Provider System +This section is relevant only in case Storefront services provide UI Configuration (options 3-6). + Data-prover system is a system that provides domain-specific data. For example, [PIM (product information management)](https://en.wikipedia.org/wiki/Product_information_management) system. In case of integration with 3rd-party data-provider systems, a separate data flow process is setup to sync configuration when it's changed. From 5fe221feb1d628d65b039d1303da87c8d7dd534b Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 22 Jul 2020 14:41:41 -0500 Subject: [PATCH 183/479] Admin Panel Configuration propagation to Storefront --- .../storefront/configuraiton-propagation.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index ad873c0d7..d35d597da 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -27,7 +27,7 @@ Example: Base Media URL is used for calculation of media image URLs returned by This type of configuration should be propagated to Storefront in one of the following ways: -1. Pre-calculate final data on back office side and provide final result to the Storefront during synchronization. +1. Pre-calculate final data on back office side and provide final result to the Storefront during synchronization. Minimize synchronization by updating only entities that have been really affected. 1. Pros: simpler implementation of Storefront due to eliminated necessity for Storefront to keep knowledge about additional configuration, including data calculation algorithms. 2. Cons: massive (up to full) reindexation necessary in case the configuration is changed. 2. Move/duplicate calculation logic in the Storefront service based on original data is synced from the back office. @@ -41,6 +41,12 @@ To choose the right approach in a specific case, consider the following: 2. Is it acceptable to have significant delay in data propagation after the configuration change? 1. Changing Base URL may be not a big issue, especially if a redirect can be setup. So it may be acceptable to have URLs to be fully updated in a few hours. 2. Changes in prices, on the other side, may not stand long delays. +3. Do 3rd-party systems provide similar configuration? + 1. If 3rd-party systems don't have equivalent configuration, how will it be populated in the Storefront service? It might be better to avoid Magento-specific concepts to simplify integrations, and instead provide indexed data to the Storefront service. + 1. If a configuration option is pretty common among 3rd-party systems, it may make sense to reflect it in the Storefront service. + +Expected consequences are described in the decision document for each case. +For example, time for configuration propagation in case reindexation is chosen, logic duplication/complexity in case configuration is propagated to Storefront application, performance impact for Storefront read API or for synchronization. ### 3. Configuration that impacts UI representation of Storefront data @@ -52,7 +58,13 @@ This section describes possible implementation options. #### 3.1. Configuration is Responsibility of the Client +:white_check_mark: Accepted option. + Client application (such as PWA) is responsible for the UI configuration, either hard-coded or by means of a service. + +Justification: Storefront service is responsible for providing data, client applications may vary significantly and may just want to hard-code many of the options or take settings from different sources. +Until it is confirmed by the client developers (PWA, AEM, other teams) that UI Configuration API backed by Magento system configuration is necessary, Storefront efforts should not focus on supporting such API. + UI configuration is not synced from Magento Admin Panel to Storefront. #### 3.2. Configuration is Provided by Magento Back Office API From 80496cbfa812b05f4b21906cc3c223d6f99aa406 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 22 Jul 2020 14:48:01 -0500 Subject: [PATCH 184/479] Create gift-card.md --- .../graph-ql/coverage/gift-card.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 design-documents/graph-ql/coverage/gift-card.md diff --git a/design-documents/graph-ql/coverage/gift-card.md b/design-documents/graph-ql/coverage/gift-card.md new file mode 100644 index 000000000..480815b6f --- /dev/null +++ b/design-documents/graph-ql/coverage/gift-card.md @@ -0,0 +1,82 @@ +# Problem + +1. For a gift card, 6 entered options are possible. Sender name/email, Recipient name/email, Message, Custom gift card amount. + Not all fields are needed/required for the clients to fill in. This options metadata should be made available to the clients when adding a giftcard product to cart. +2. When clients send this data they need a uid to distinguish each option and uid is not supposed to be generated on the client side. So when we pass in the options metadata a server generated uid should be made available to the clients. + +## Expecations + +This is in reference to this PR [Solution Architecture](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md) +on single mutation for adding items to cart. So any changes proposed should respect the input interfaces described in this PR. + +## Proposal + +Add a new field gift_card_options under the GiftCardProduct type. The values can leverage the exisiting CustomizableOptionInterface. + +``` +type GiftCardProduct { + gift_card_options: [CustomizableOptionInterface]! +} +``` + +On querying the gift_card_options the sample response should look like this. + +``` +{ + gift_card_options: [ + { + title: "Sender Name", + required: true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_SENDER_NAME) + } + }, + { + title: "Sender Email", + required: true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_SENDER_EMAIL) + } + }, + # Recipient name and email should not be returned if the product type is Physical. + { + title: "Recipient Name", + required: true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_RECIPIENT_NAME) + } + }, + { + title: "Recipient Email", + required: true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_RECIPIENT_EMAIL) + } + }, + # Message can be optional/required + { + title: "Message", + required: false/true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_MESSAGE) + } + }, + # Custom giftcard amount can be optional/required + { + title: "Custom Giftcard Amount", + required: false/true + "__typename": "CustomizableFieldOption" + value: { + uid: "Y3VzdG9tLW9wdGlvbi8xNzE" #base64_encode(giftcard/Magento\GiftCard\Model\Giftcard\Option::KEY_CUSTOM_GIFTCARD_AMOUNT) + } + }, + ] +} +``` +This enables the clients to know what options to fill in along with the uids. +When the gift card is added to the cart the entered options can be passed in the same way as [Solution Architecture](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md). From 71c7ef42bc6b82a37e15d119f9e3a4b1eaa2e124 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 22 Jul 2020 14:56:04 -0500 Subject: [PATCH 185/479] Fixed the return type. --- design-documents/graph-ql/coverage/gift-card.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/gift-card.md b/design-documents/graph-ql/coverage/gift-card.md index 480815b6f..8b80d0b4c 100644 --- a/design-documents/graph-ql/coverage/gift-card.md +++ b/design-documents/graph-ql/coverage/gift-card.md @@ -15,7 +15,7 @@ Add a new field gift_card_options under the GiftCardProduct type. The values can ``` type GiftCardProduct { - gift_card_options: [CustomizableOptionInterface]! + gift_card_options: [CustomizableOptionInterface!]! } ``` From e33222e2ab777d94017218bf04c7fd0816c038f7 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 22 Jul 2020 15:10:46 -0500 Subject: [PATCH 186/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/returns.graphqls | 12 ++++++++---- design-documents/graph-ql/coverage/returns.md | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 3e6d107f7..572a4e32b 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -13,7 +13,7 @@ type Mutation { } input CreateReturnInput { - items: [CreateReturnItemInput]! + items: [CreateReturnItemInput!]! comment_text: String } @@ -31,6 +31,10 @@ input EnteredCustomAttributeInput { type CreateReturnOutput { return: Return + returns( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + ): Returns @doc(description: "Information about the customer returns.") } input AddReturnCommentInput { @@ -53,7 +57,7 @@ type AddReturnTrackingOutput { } input RemoveReturnTrackingInput { - return_tracking_id: ID! + return_shipping_tracking_id: ID! } type RemoveReturnTrackingOutput { @@ -81,7 +85,7 @@ type CustomerOrder { returns( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), - ): Returns @doc(description: "Information about the order returns.") + ): Returns @doc(description: "Returns associated with this order.") items_eligible_for_return: [OrderItemInterface] @doc(description: "A list of order items eligible for return.") } @@ -93,7 +97,7 @@ type Returns { type Return @doc(description: "Customer return") { id: ID! @doc(description: "Return number") - order_id: String! @doc(description: "ID of the order based on which the return was created.") + order_id: ID! @doc(description: "ID of the order based on which the return was created.") creation_date: String! @doc(description: "The date when the return was requested.") customer_email: String! @doc(description: "Email of the customer who created the return.") customer_name: String @doc(description: "The name of the person returning the item") diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 392830360..cd6a35a00 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -229,6 +229,8 @@ mutation { } ``` +Alternatively, the collection of returns associated with the order can be requested. + ### Leave a return comment ```graphql @@ -328,7 +330,7 @@ In case the return shipping needs to be removed, the following mutation can be u mutation { removeReturnTracking( input: { - return_tracking_id: "return-tracking-id" + return_shipping_tracking_id: "return-tracking-id" } ) { return { From 4db849dda116a2de02add217c5af36c4c0df2b59 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 22 Jul 2020 17:37:06 -0500 Subject: [PATCH 187/479] fix paging output for negotiable quotes --- .../graph-ql/coverage/negotiableQuotes.graphqls | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index c29ad41ba..de48495f6 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -9,7 +9,13 @@ type Query { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): [NegotiableQuote] @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") + ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") +} + +type NegotiableQuotesOutput { + items: [NegotiableQuote]! + page_info: SearchResultPageInfo! + total_count: Int! } # Coverage missing: @@ -102,7 +108,7 @@ type DeleteNegotiableQuotesOutput { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): [NegotiableQuote]! @doc(description: "List of negotiable quotes available to customer") + ): NegotiableQuotesOutput @doc(description: "List of negotiable quotes available to customer") } input CloseNegotiableQuotesInput { @@ -115,7 +121,7 @@ type CloseNegotiableQuotesOutput { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): [NegotiableQuote] @doc(description: A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") + ): NegotiableQuotesOutput @doc(description: A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") } input RemoveNegotiableQuoteItemsInput { From aaf229f8bdaa03509085f98783abf28bf85195a0 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 22 Jul 2020 17:39:22 -0500 Subject: [PATCH 188/479] value >> text --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index de48495f6..0670837ee 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -161,7 +161,7 @@ type NegotiableQuoteComment { created_at: String! author: NegotiableQuoteUser! creator_type: NegotiableQuoteCommentCreatorType! - value: String! @doc(description: "A simple (non-html) comment submitted by a seller or buyer") + text: String! @doc(description: "A simple (non-html) comment submitted by a seller or buyer") } enum NegotiableQuoteCommentCreatorType { From e95077d9e45f6466ce3f1a34bc8cdfff9fd926e0 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 23 Jul 2020 14:03:55 -0500 Subject: [PATCH 189/479] Admin Panel Configuration propagation to Storefront --- design-documents/storefront/configuraiton-propagation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index d35d597da..c7224082a 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -50,7 +50,7 @@ For example, time for configuration propagation in case reindexation is chosen, ### 3. Configuration that impacts UI representation of Storefront data -Example: include or exclude taxes in displayed prices. +Example: number of products on products listing page. This kind of configuration has nothing to do with Storefront data itself, but is still necessary for the client to know how to display the data. From 48f2db6cc9f18a50b181b6a6b76cb0dbc81722cb Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 23 Jul 2020 14:50:03 -0500 Subject: [PATCH 190/479] Admin Panel Configuration propagation to Storefront --- .../storefront/configuraiton-propagation.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index c7224082a..9b8651a9c 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -73,6 +73,17 @@ Rely on current GraphQL API for providing UI Configuration. No additional Store Front service is created to serve such configuration. GraphQL entry point can proxy to the Magento Back Office GraphQL for simplicity in API usage. +Two options are possible, second expands on top of the first one. + +Client holds knowledge about two sources (one for data and one for config) and handles requests. +This can be the first step. + +![Service Configuration - Direct Back Office API](https://app.lucidchart.com/publicSegments/view/aeae7ddc-7a1f-4c94-88aa-2309dca63c05/image.png) + +GraphQL handles requests routing to either Storefront domain service (for data) or to Magento Back Office GraphQL (for UI Config). + +![Service Configuration - GraphQL Proxy](https://app.lucidchart.com/publicSegments/view/775a580e-fdb0-4bda-a532-eef04767396a/image.png) + #### 3.3. Single UI Configuration Service for All Domain Services UI configuration settings are fed into a single UI Configuration service and read from it when necessary. From b6d61a6234069309ee158f054a153fa0986005fa Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Thu, 23 Jul 2020 16:22:21 -0500 Subject: [PATCH 191/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 47 +++++++++++-------- design-documents/graph-ql/coverage/returns.md | 41 +++++++++++----- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 572a4e32b..949c81ab4 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -6,22 +6,22 @@ type Query{ } type Mutation { - createReturn(input: CreateReturnInput!): CreateReturnOutput @doc(description: "Create a new return.") + requestReturn(input: RequestReturnInput!): RequestReturnOutput @doc(description: "Create a new return.") addReturnComment(input: AddReturnCommentInput!): AddReturnCommentOutput @doc(description: "Add a comment to an existing return.") addReturnTracking(input: AddReturnTrackingInput!): AddReturnTrackingOutput @doc(description: "Add tracking information to the return.") - removeReturnTracking(input: RemoveReturnTrackingInput!): RemoveReturnTrackingOutput @doc(description: "Remove tracking information from the return.") + removeReturnTracking(input: RemoveReturnTrackingInput!): RemoveReturnTrackingOutput @doc(description: "Remove a single tracked shipment from the return.") } -input CreateReturnInput { - items: [CreateReturnItemInput!]! +input RequestReturnInput { + items: [RequestReturnItemInput!]! comment_text: String } -input CreateReturnItemInput { - order_item_id: ID! - quantity_to_return: Float! - selected_custom_attributes: [ID!] - entered_custom_attributes: [EnteredCustomAttributeInput!] +input RequestReturnItemInput { + order_item_id: ID! @doc(description: "ID of the order associated with the return.") + quantity_to_return: Float! @doc(description: "The quantity of the item requested to be returned.") + selected_custom_attributes: [ID!] @doc(description: "Values of return item attributes defined by the merchant, e.g. select attributes.") + entered_custom_attributes: [EnteredCustomAttributeInput!] @doc(description: "Values of return item attributes defined by the merchant, e.g. file or text attributes.") } input EnteredCustomAttributeInput { @@ -29,7 +29,7 @@ input EnteredCustomAttributeInput { value: String! } -type CreateReturnOutput { +type RequestReturnOutput { return: Return returns( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), @@ -89,6 +89,10 @@ type CustomerOrder { items_eligible_for_return: [OrderItemInterface] @doc(description: "A list of order items eligible for return.") } +type OrderItemInterface { + eligible_for_return: Boolean @doc(description: "Indicates whether the order item is eligible for return.") +} + type Returns { items: [Return] @doc(description: "List of returns") page_info: SearchResultPageInfo @doc(description: "Pagination metadata") @@ -110,11 +114,11 @@ type Return @doc(description: "Customer return") { type ReturnItem { id: ID! - product: ProductInterface! - custom_attributes: [CustomAttribute] - request_quantity: Float! - quantity: Float! - status: ReturnItemStatus! + product: ProductInterface! @doc(description: "The product being returned.") + custom_attributes: [CustomAttribute] @doc(description: "Return item custom attributes, which are marked by the admin to be visible on the storefront.") + request_quantity: Float! @doc(description: "The quantity of the item requested to be returned.") + quantity: Float! @doc(description: "The quantity of the items authorized by the merchant to be returned .") + status: ReturnItemStatus! @doc(description: "The return status of the item being returned.") } # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/custom-attributes-container.md @@ -126,7 +130,8 @@ type CustomAttribute { type ReturnComment { id: ID! @doc(description: "Comment ID.") - created_by: String! @doc(description: "The name of the user who created posted the comment.") + author_firstname: String! @doc(description: "First name of the user who posted the comment.") + author_lastname: String! @doc(description: "Last name of the user who posted the comment.") created_at: String! @doc(description: "The date and time when the comment was posted.") text: String! @doc(description: "The comment text.") } @@ -143,10 +148,14 @@ type ReturnShippingCarrier { type ReturnShippingTracking { id: ID! - carier: ReturnShippingCarrier! + carrier: ReturnShippingCarrier! tracking_number: String! - status_type: ReturnShippingTrackingStatusType! - status: String! + status: ReturnShippingTrackingStatus +} + +type ReturnShippingTrackingStatus { + text: String! + type: ReturnShippingTrackingStatusType! } enum ReturnShippingTrackingStatusType { diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index cd6a35a00..49e83ec4a 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -102,7 +102,7 @@ Scenarios which may need these settings include: shipping { tracking { id - carier { + carrier { label } tracking_number @@ -126,7 +126,7 @@ Scenarios which may need these settings include: } ``` -### Create a return +### Create a return request #### Determine whether any order items are eligible for return @@ -147,6 +147,23 @@ There is a need to know if any order items are eligible for return. In the Luma } ``` +Alternative approach is to use a flag added to order items: + +```graphql +{ + customer { + orders(filter: {number: {eq: "00000008"}}) { + items { + items { + id + eligible_for_return + } + } + } + } +} +``` + #### Render return form with dynamic RMA attributes ```graphql @@ -186,11 +203,11 @@ Existing schema of `Attribute` and `AttributeOption` must be extended to provide } ``` -#### Submit a return with multiple items and comments +#### Submit a return request with multiple items and comments ```graphql mutation { - createReturn( + requestReturn( input: { items: [ { @@ -261,7 +278,7 @@ Guest orders are not accessible via GraphQL yet, but the schema of returns will When return is authorized by the admin user, the customer can specify shipping and tracking information. -First, the client needs to get shipping cariers that can be used for returns: +First, the client needs to get shipping carriers that can be used for returns: ```graphql { @@ -291,7 +308,7 @@ mutation { shipping { tracking { id - carier { + carrier { label } tracking_number @@ -302,7 +319,7 @@ mutation { } ``` -If the user decides to view the status of the return, it can be retrieved using the following query: +If the user decides to view the status of the specific tracking item, it can be retrieved using the following query: ```graphql { @@ -311,12 +328,14 @@ If the user decides to view the status of the return, it can be retrieved using shipping { tracking(id: "return-tracking-id") { id - carier { + carrier { label } tracking_number - status - status_type + status { + text + type + } } } } @@ -337,7 +356,7 @@ mutation { shipping { tracking { id - carier { + carrier { label } tracking_number From 9ee3bc3fcdee07576d1c9b20d11ea18e68228a58 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 24 Jul 2020 11:29:41 -0500 Subject: [PATCH 192/479] ECP-766: Returns Schema Design --- .../graph-ql/coverage/returns.graphqls | 10 ++++--- design-documents/graph-ql/coverage/returns.md | 30 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 949c81ab4..425ce0693 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -18,7 +18,7 @@ input RequestReturnInput { } input RequestReturnItemInput { - order_item_id: ID! @doc(description: "ID of the order associated with the return.") + order_item_id: ID! @doc(description: "ID of the order item associated with the return.") quantity_to_return: Float! @doc(description: "The quantity of the item requested to be returned.") selected_custom_attributes: [ID!] @doc(description: "Values of return item attributes defined by the merchant, e.g. select attributes.") entered_custom_attributes: [EnteredCustomAttributeInput!] @doc(description: "Values of return item attributes defined by the merchant, e.g. file or text attributes.") @@ -54,6 +54,7 @@ input AddReturnTrackingInput { type AddReturnTrackingOutput { return: Return + return_shipping_tracking: ReturnShippingTracking } input RemoveReturnTrackingInput { @@ -100,8 +101,9 @@ type Returns { } type Return @doc(description: "Customer return") { - id: ID! @doc(description: "Return number") - order_id: ID! @doc(description: "ID of the order based on which the return was created.") + id: ID! + number: String! @doc(description: "Human-readable return number") + order: CustomerOrder @doc(description: "The order associated with the return.") creation_date: String! @doc(description: "The date when the return was requested.") customer_email: String! @doc(description: "Email of the customer who created the return.") customer_name: String @doc(description: "The name of the person returning the item") @@ -114,7 +116,7 @@ type Return @doc(description: "Customer return") { type ReturnItem { id: ID! - product: ProductInterface! @doc(description: "The product being returned.") + order_item: OrderItemInterface! @doc(description: "Order item provides access to the product being returned, including selected/entered options information.") custom_attributes: [CustomAttribute] @doc(description: "Return item custom attributes, which are marked by the admin to be visible on the storefront.") request_quantity: Float! @doc(description: "The quantity of the item requested to be returned.") quantity: Float! @doc(description: "The quantity of the items authorized by the merchant to be returned .") diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 49e83ec4a..98e2a0c45 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -71,9 +71,11 @@ Scenarios which may need these settings include: ```graphql { customer { - return(id: "0000003") { - id - order_id + return(id: "23as452gsa") { + number + order { + number + } creation_date customer_email customer_name @@ -86,9 +88,9 @@ Scenarios which may need these settings include: } items { id - product { - sku - name + order_item { + product_sku + product_name } custom_attributes { id @@ -211,7 +213,7 @@ mutation { input: { items: [ { - order_item_id: "0000004", + order_item_id: "absdfj2l3415", quantity_to_return: 1, selected_custom_attributes: ["encoded-custom-select-attribute-value-id"], entered_custom_attributes: [{id: "encoded-custom-text-attribute-id", value: "Custom attribute value"}] @@ -226,9 +228,9 @@ mutation { id quantity request_quantity - product { - sku - name + order_item { + product_sku + product_name } custom_attributes { id @@ -254,7 +256,7 @@ Alternatively, the collection of returns associated with the order can be reques mutation { addReturnComment( input: { - return_id: "000000001", + return_id: "23as452gsa", comment_text: "Another return comment" } ) { @@ -283,7 +285,7 @@ First, the client needs to get shipping carriers that can be used for returns: ```graphql { customer { - return(id: "0000003") { + return(id: "asdgah2341") { available_shipping_carriers { id label @@ -299,7 +301,7 @@ Then tracking information can be submitted: mutation { addReturnTracking( input: { - return_id: "000005", + return_id: "23as452gsa", carrier_id: "carrier-id", tracking_number: "4234213" } @@ -324,7 +326,7 @@ If the user decides to view the status of the specific tracking item, it can be ```graphql { customer { - return(id: "0000003") { + return(id: "23as452gsa") { shipping { tracking(id: "return-tracking-id") { id From 009bcdd3acd746361fb3967ed7ff1d08c7c9b80f Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 24 Jul 2020 15:44:00 -0500 Subject: [PATCH 193/479] Remove global uniqueness requirement from graphql single add to cart mutation doc --- .../graph-ql/coverage/add-items-to-cart-single-mutation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md b/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md index 10dbb99d3..dc8f7915a 100644 --- a/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md +++ b/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md @@ -31,7 +31,7 @@ Each product may have options. Option can be of 2 types (see example below): ] ``` -We can consider "Selected Option" and "ID for Entered Option" as a globally unique identifier. They meet the criteria: +We can consider "Selected Option" and "ID for Entered Option" as a unique identifier. They meet the criteria: - "Selected Option" represents option value, while "ID for Entered Option" represents option - Must be unique across different options From eebbeca8ef458ca6c6ea2b6e37fefecf3552be5a Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Fri, 24 Jul 2020 17:47:24 -0500 Subject: [PATCH 194/479] more about monolith price api --- design-documents/storefront/pricing.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index b7fb48a6e..26206cb93 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -83,6 +83,13 @@ The following diagram shows the pricing structure for a given product, website a - Monolith calculates prices based on the cache (product matches) - Message broker store prices in price book + +Price calculations should be done on the monolith side (see appendix for calculation details). These calculations should +be made in runtime based on raw data from the database. +The critical part is to have very granular price detection mechanism which should be firing `price_changed` events for +specific websites, customer groups, products and sub-products. Example: if product price was changed for single customer +group, only this customer group should be present in event and price calculations must be done also for single customer group only. + ### Price book API ```proto From df5a46ab0d5dfc006b37f2ca0f4de0853f04571d Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 27 Jul 2020 17:25:51 -0500 Subject: [PATCH 195/479] Proposed changes for Wishlist schema modifications --- .../graph-ql/coverage/Wishlist.graphqls | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/Wishlist.graphqls index fe32f7043..ccd81d0d2 100644 --- a/design-documents/graph-ql/coverage/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/Wishlist.graphqls @@ -8,13 +8,14 @@ type Mutation { type Customer { wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlist_v2") - wishlist_v2(id: ID!): Wishlist - wishlists: [Wishlist!]! @doc(description: "Customer wishlists are limited to a max of 1 wishlist in Magento Open Source") + wishlist_v2(id: ID!): Wishlist # This query will be added in the ce + wishlists: [Wishlist!]! @doc(description: "Customer wishlists are limited to a max of 1 wishlist in Magento Open Source") # This query will be added in the ce } type Wishlist { id: ID - items: [WishlistItemInterface] + items: [WishlistItem] @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") + items_v2: [WishlistItemInterface] @doc(description: "An array of items in the customer's wishlist") items_count: Int sharing_code: String updated_at: String @@ -24,7 +25,7 @@ type Wishlist { input WishlistItemUpdateInput { wishlist_item_id: ID quantity: Float - selected_options: [String!] + selected_options: [ID!] entered_options: [EnteredOptionInput!] } @@ -45,7 +46,7 @@ input WishlistItemInput { quantity: Float parent_sku: String, parent_quantity: Float, - selected_options: [String!] + selected_options: [ID!] entered_options: [EnteredOptionInput!] } @@ -78,9 +79,11 @@ type BundleWishlistItem implements WishlistItemInterface { bundle_options: [SelectedBundleOption!] } -type GiftCardCartItem implements CartItemInterface { +type GiftCardWishlistItem implements WishlistItemInterface { sender_name: String! - recipient_name: String! + sender_email: String! + recipient_name: String + recipient_email: String amount: SelectedGiftCardAmount message: String } From 7fc33a3efed443847051ca272cc2d1047f355a14 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 28 Jul 2020 11:16:47 -0500 Subject: [PATCH 196/479] Schema changes to describe behavior and fix some return types --- design-documents/graph-ql/coverage/company.md | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index e08e26a9b..79c08497d 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -2,6 +2,12 @@ `Sales representative` is an admin user(merchant side) who is assigned to work with the company. Thus we can't use exsting `Customer` type for sales representative +# Behavioral Notes +When a feature is disabled (e.g. Company) queries/mutations related to that feature should return a null response, along with an error indicating that the feature is not available. + +Update/Create mutations should return the entity they acted upon. +Delete mutations return a field indicating success or failure. In the case of failure, an error should be returned indicating the error that occurred (suitable for storefront). + # Queries ```graphql @@ -164,7 +170,7 @@ type UpdateCompanyTeamOutput @doc(description: "Update company team output data } type DeleteCompanyTeamOutput @doc(description: "Delete company team output data schema.") { - status: Boolean! @doc(description: "Status of delete operation: true - success; false - fail.") + success: Boolean! @doc(description: "Indicates whether or not the delete operation succeeded.") } type CreateCompanyOutput @doc(description: "Create company output data schema.") { @@ -184,23 +190,23 @@ type UpdateCompanyUserOutput @doc(description: "Update company user output data } type DeleteCompanyUserOutput @doc(description: "Delete company user output data schema.") { - status: Boolean! @doc(description: "Status of delete operation: true - success; false - fail.") + success: Boolean! @doc(description: "Indicates whether or not the delete operation succeeded.") } type CreateCompanyRoleOutput @doc(description: "Create company role output data schema.") { - user: CompanyRole! @doc(description: "New company role instance.") + role: CompanyRole! @doc(description: "New company role instance.") } type UpdateCompanyRoleOutput @doc(description: "Update company role output data schema.") { - user: CompanyRole! @doc(description: "Updated company role instance.") + role: CompanyRole! @doc(description: "Updated company role instance.") } type DeleteCompanyRoleOutput @doc(description: "Delete company role output data schema.") { - status: Boolean! @doc(description: "Status of delete operation: true - success; false - fail.") + success: Boolean! @doc(description: "Indicates whether or not the delete operation succeeded.") } type UpdateCompanyHierarchyOutput @doc(description: "Update company hierarchy output data schema.") { - status: Boolean! @doc(description: "Status of update operation: true - success; false - fail.") + company: Company! @doc(description: "Updated company instance.") } @@ -310,4 +316,4 @@ type Customer { telephone: String @doc(description: "Company User phone number.") status: CompanyUserStatusEnum @doc(description: "Company User status.") } -``` \ No newline at end of file +``` From bd1c1508a59c216698b1fdf0210de9dc83882f56 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Tue, 28 Jul 2020 11:52:02 -0500 Subject: [PATCH 197/479] Consistent use of Structure instead of Hierarchy --- design-documents/graph-ql/coverage/company.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/company.md index e1bd2e1b1..64d7287ac 100644 --- a/design-documents/graph-ql/coverage/company.md +++ b/design-documents/graph-ql/coverage/company.md @@ -43,7 +43,7 @@ type Company @doc(description: "Company entity output data schema.") { role(id: ID!): CompanyRole @doc(description: "Returns company role by id.") acl_resources: [CompanyAclResource] @doc(description: "Returns the list of all permission resources.") structure( - rootID: ID = 0 @doc(description: "Tree depth to begin query") + root_id: ID = 0 @doc(description: "Tree depth to begin query") depth: Int = 10 @doc(description: "Specifies how deeply results are fetched") ): CompanyStructure @doc(description: "Company structure of teams and customers in depth-first order") team(id: ID!): CompanyTeam @doc(description: "Returns company team data by id.") @@ -88,7 +88,7 @@ type CompanyRoles @doc(description: "Output data schema for an object returned b type CompanyRole @doc(description: "Company role output data schema returned in response to a query by Role id.") { id: ID! @doc(description: "Role id.") name: String @doc(description: "Role name.") - users_count: Int @doc(description: "Total number of Users with such Role within Company Hierarchy.") + users_count: Int @doc(description: "Total number of Users with such Role within Company Structure.") permissions: [CompanyAclResource] @doc(description: "A list of permission resources defined for a Role.") } @@ -119,12 +119,12 @@ union CompanyStructureEntity = CompanyTeam | Customer type CompanyStructureItem @doc(description: "Company Team and Customer structure") { id: ID! @doc(description: "ID of the item in the hierarchy") - parentID: ID @doc(description: "ID of the parent item in the hierarchy") + parent_id: ID @doc(description: "ID of the parent item in the hierarchy") entity: CompanyStructureEntity } type CompanyStructure { - items: CompanyStructureItem[] + items: [CompanyStructureItem] } type CompanyTeam @doc(description: "Company Team entity output data schema.") { @@ -155,7 +155,7 @@ type Mutation { createCompanyRole(input: CompanyRoleCreateInput!): CreateCompanyRoleOutput @doc(description:"Create new Company role.") updateCompanyRole(input: CompanyRoleUpdateInput!): UpdateCompanyRoleOutput @doc(description:"Update Company role data.") deleteCompanyRole(id: ID!): DeleteCompanyRoleOutput @doc(description:"Delete Company Role by ID.") - updateCompanyHierarchy(input: CompanyHierarchyUpdateInput!): UpdateCompanyHierarchyOutput @doc(description:"Update Company Hierarchy element's parent node assignment.") + updateCompanyStructure(input: CompanyStructureUpdateInput!): UpdateCompanyStructureOutput @doc(description:"Update Company Structure element's parent node assignment.") createCompanyTeam(input: CompanyTeamCreateInput!): CreateCompanyTeamOutput @doc(description:"Create Company Team.") updateCompanyTeam(input: CompanyTeamUpdateInput!): UpdateCompanyTeamOutput @doc(description:"Update Company Team data.") deleteCompanyTeam(id: ID!): DeleteCompanyTeamOutput @doc(description:"Delete Company Team entity by ID.") @@ -205,7 +205,7 @@ type DeleteCompanyRoleOutput @doc(description: "Delete company role output data success: Boolean! @doc(description: "Indicates whether or not the delete operation succeeded.") } -type UpdateCompanyHierarchyOutput @doc(description: "Update company hierarchy output data schema.") { +type UpdateCompanyStructureOutput @doc(description: "Update company structure output data schema.") { company: Company! @doc(description: "Updated company instance.") } @@ -263,7 +263,7 @@ input CompanyUserCreateInput @doc(description: "Defines the input data schema fo email: String! @doc(description: "Company user's email address. Required.") telephone: String! @doc(description: "Company user's phone number. Required.") status: Int! @doc(description: "Company user's status ID. Required.") - target_id: ID @doc(description: "A target structure element ID within a Company's Hierarchy for a user to be assigned to.") + target_id: ID @doc(description: "A target structure element ID within a Company's Structure for a user to be assigned to.") } input CompanyUserUpdateInput @doc(description: "Defines the input data schema for updating an existing Customer - Company user.") { @@ -288,15 +288,15 @@ input CompanyRoleUpdateInput @doc(description: "Defines the input data schema fo permissions: [String!] @doc(description: "A list of Role permission resources. Array value for a field, if provided, should consist only of string values.") } -input CompanyHierarchyUpdateInput @doc(description: "Defines the input data schema for updating the Company Hierarchy.") { - tree_id: ID! @doc(description: "Company Hierarchy element's hierarchical ID that is being moved to another parent. Required.") - parent_tree_id: ID! @doc(description: "A target parent element tree ID within a Company's Hierarchy. Required.") +input CompanyStructureUpdateInput @doc(description: "Defines the input data schema for updating the Company Structure.") { + tree_id: ID! @doc(description: "Company Structure element's hierarchical ID that is being moved to another parent. Required.") + parent_tree_id: ID! @doc(description: "A target parent element tree ID within a Company's Structure. Required.") } input CompanyTeamCreateInput @doc(description: "Defines the input data schema for creating a new Company team.") { name: String! @doc(description: "Team name. Required.") description: String @doc(description: "Team description.") - target_id: ID @doc(description: "A target structure element ID within a Company's Hierarchy for a team to be assigned to.") + target_id: ID @doc(description: "A target structure element ID within a Company's Structure for a team to be assigned to.") } input CompanyTeamUpdateInput @doc(description: "Defines the input data schema for updating an existing Company team.") { From cbd28c077cbc6b5c50dc5bc412974a428c0b29ae Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Tue, 28 Jul 2020 16:06:12 -0500 Subject: [PATCH 198/479] Admin Panel Configuration propagation to Storefront - named configuration categories - removed rejected options --- .../storefront/configuraiton-propagation.md | 86 ++----------------- 1 file changed, 8 insertions(+), 78 deletions(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index 9b8651a9c..caf99394e 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -15,6 +15,12 @@ A separate design decision should be made and documented for specific use cases. There are three use cases for the configuration, based on its impact. +Magento configuration is divided into three categories, based on impact on Storefront services: + +1. Admin configuration - impacts Admin Panel behavior +2. Data configuration - impacts data provided by Storefront +3. UI Configuration - impacts UI representation of Storefront data + ### 1. Configuration that impacts Admin Panel behavior Examples: admin ACLs that allow or deny parts of functionality for the admin user, reindex on update or by schedule. @@ -54,12 +60,11 @@ Example: number of products on products listing page. This kind of configuration has nothing to do with Storefront data itself, but is still necessary for the client to know how to display the data. -This section describes possible implementation options. +This section describes possible implementation options, depending on the client use case. +For other considered options see [older revision](https://github.com/magento/architecture/blob/48f2db6cc9f18a50b181b6a6b76cb0dbc81722cb/design-documents/storefront/configuraiton-propagation.md) of the document. #### 3.1. Configuration is Responsibility of the Client -:white_check_mark: Accepted option. - Client application (such as PWA) is responsible for the UI configuration, either hard-coded or by means of a service. Justification: Storefront service is responsible for providing data, client applications may vary significantly and may just want to hard-code many of the options or take settings from different sources. @@ -83,78 +88,3 @@ This can be the first step. GraphQL handles requests routing to either Storefront domain service (for data) or to Magento Back Office GraphQL (for UI Config). ![Service Configuration - GraphQL Proxy](https://app.lucidchart.com/publicSegments/view/775a580e-fdb0-4bda-a532-eef04767396a/image.png) - -#### 3.3. Single UI Configuration Service for All Domain Services - -UI configuration settings are fed into a single UI Configuration service and read from it when necessary. - -As, most likelly, all configuration options will look like key-value pairs, it might be reasonable to unify all of them under umpbrella of a single serfvice and so provide more uniformity of working with such configuration service. -This is a variation of option 3.2. - -![UI Configuration Propagation as Single UI Configuration Service](https://app.lucidchart.com/publicSegments/view/cc77dc21-77ac-4eb4-b766-0f9afc8c11d5/image.png) - -Pros: - -1. More control of the deployment, scalability, technologies for the domain and config services. Assuming, all config services has the same technical requirements. - -Cons: - -1. Additional request to configuration service instead of in-process request to the storage. - -#### 3.4. Domain-Specific Configuration Service - -Each domain service that requires UI configuration has a companion UI Configuration service. -Client application may call the specialized configuration service to get configuration it needs. -Some clients may not need all or part of UI configuration. - -GraphQL serves as a single entry point for both data and UI requests to simplify client implementation and have more control of which APIs are publicly exposed. - -![UI Configuration Propagation as Domain-Specific Configuration Service](https://app.lucidchart.com/publicSegments/view/c3c9f0a7-1780-416f-ab3f-caeeebb15680/image.png) - -Pros: - -1. More control of the deployment, scalability, technologies for the domain and config services. -2. More flexibility in the future - 1. Easier path for UI configuration disablement in case it becomes unnecessary. For example, if it turnes out that client apps want to have full ownership of UI configuration - -Cons: - -1. Additional request to configuration service instead of in-process request to the storage. - -#### 3.5. Configuration as Part of Domain Service - -UI Configuration is synced to the Storefront domain service, similarly to how the data is synced. -This may be done by means of a separate data flow as configuration usually doesn't change together with the data. - -Is UI config included in the data (on the level of entities)? - -![UI Configuration Propagation as Part of Domain Service](https://app.lucidchart.com/publicSegments/view/4805a8df-abe8-4605-96e2-20266b2f2876/image.png) - -Pros: - -1. Simplicity in case of Magento as back office. Same/similar data sync can be implemented for configuration -2. In-process call to storage for UI configuration - -Cons: - -1. Potential difficulties with UI configuration exclusion from deployment in case it is not needed, as it is part of the service - -#### Integration with 3rd-party Data-Provider System - -This section is relevant only in case Storefront services provide UI Configuration (options 3-6). - -Data-prover system is a system that provides domain-specific data. For example, [PIM (product information management)](https://en.wikipedia.org/wiki/Product_information_management) system. - -In case of integration with 3rd-party data-provider systems, a separate data flow process is setup to sync configuration when it's changed. -If PIM doesn't provide all necessary configuration settings, a different management system may be used as the source. - -Additional configuration may be provided by a separate client application dedicated to the configuration management. -Also, the additional configuration may be, in reality, the only source of configuration, in case PIM system doesn't provide such configuration. - -In case of UI Configuration provided as part of the domain service: - -![3rd-party PIM integration - UI Configuration as part of domain service](https://app.lucidchart.com/publicSegments/view/0e81c178-7480-41ca-8e7d-a37e6bdb6b6d/image.png) - -In case of UI Configuration provided by a separate service: - -![3rd-party PIM integration - UI Configuration as separate service](https://app.lucidchart.com/publicSegments/view/6051ef66-4b8f-44aa-9211-c6e7399bc9e2/image.png) From 08440336db8238e2e1014c72f0d4387c09006ce2 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 28 Jul 2020 16:58:06 -0500 Subject: [PATCH 199/479] ECP-766: Returns Schema Design --- design-documents/graph-ql/coverage/returns.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 425ce0693..5e5f1d690 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -200,7 +200,8 @@ enum ReturnItemStatus { } type StoreConfig { - sales_magento_rma_enabled: String! @doc(description: "Returns functionality status on the storefront: enabled/disabled.") + # sales_magento/rma/enabled + returns_enabled: String! @doc(description: "Returns functionality status on the storefront: enabled/disabled.") } type Attribute { From 7c6b4e0f41c4d6a2a19030c463addc06b5233ea2 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 29 Jul 2020 09:42:07 -0500 Subject: [PATCH 200/479] Catalog images tech vision --- design-documents/media/catalog-images.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index e10de09e4..a4f80df7d 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -1,6 +1,6 @@ # Catalog Images -## Asset Management - High-Level Vision +## Asset Delivery - High-Level Vision ### Terminology @@ -9,6 +9,7 @@ * [DAM (Digital Asset Management)](https://en.wikipedia.org/wiki/Digital_asset_management) - a system responsible for asset management (store, create, update, delete, organize) * [CDN (Content delivery network)](https://en.wikipedia.org/wiki/Content_delivery_network) - a system responsible for content delivery. In scope of this document, for delivery of images and video. * CDN may be part of DAM (if DAM provides public URLs for assets), or DAM can be integrated with CDN +* **Asset delivery** - providing publicly available URL for the asset. Usually involves CDN. This is different from asset management, which focuses on the admin side of interactions with the assets, while delivery is the cliend side of it. * **Image transformation** - resizing, rotation, watermarking and other automated transformations on an original image. * Image transformation is responsibility of either DAM or CDN. This includes resizing, rotation, watermarking and so on. * Client should be able to fetch transformed images by its original URL with additional parameters From 7a3c481b5d3573666d65a4769be9c3a08cba2fab Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 29 Jul 2020 09:44:24 -0500 Subject: [PATCH 201/479] Admin Panel Configuration propagation to Storefront --- design-documents/storefront/configuraiton-propagation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/storefront/configuraiton-propagation.md b/design-documents/storefront/configuraiton-propagation.md index caf99394e..2532fa6a8 100644 --- a/design-documents/storefront/configuraiton-propagation.md +++ b/design-documents/storefront/configuraiton-propagation.md @@ -60,7 +60,8 @@ Example: number of products on products listing page. This kind of configuration has nothing to do with Storefront data itself, but is still necessary for the client to know how to display the data. -This section describes possible implementation options, depending on the client use case. +This section describes possible implementation options. +Both options are possible and acceptable. The correct option should be selected based on the specific use case and client requirements. For other considered options see [older revision](https://github.com/magento/architecture/blob/48f2db6cc9f18a50b181b6a6b76cb0dbc81722cb/design-documents/storefront/configuraiton-propagation.md) of the document. #### 3.1. Configuration is Responsibility of the Client From 72f69008bdd9763e9ca9ea84cb23963e88d08fce Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 29 Jul 2020 14:16:12 -0500 Subject: [PATCH 202/479] Admin Panel Configuration propagation to Storefront --- design-documents/media/catalog-images.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index a4f80df7d..f72ebdff0 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -24,6 +24,8 @@ ## Scenarios +https://app.lucidchart.com/documents/view/b270d1f2-5d2a-4c36-bde5-e84e1b48919b + ### Backoffice Scenarios #### Assign an image to a product (using DAM integration) From 406ed1dc902d4aa10df4014b198eab2b1d85663a Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 29 Jul 2020 14:48:20 -0500 Subject: [PATCH 203/479] Admin Panel Configuration propagation to Storefront --- design-documents/media/catalog-images.md | 40 ++++++++++++------ .../media/img/images-upload-config.png | Bin 0 -> 142595 bytes 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 design-documents/media/img/images-upload-config.png diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index f72ebdff0..d0feb771b 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -24,13 +24,14 @@ ## Scenarios -https://app.lucidchart.com/documents/view/b270d1f2-5d2a-4c36-bde5-e84e1b48919b +![Asset flow](https://app.lucidchart.com/publicSegments/view/21c14319-73c6-4bb4-9d5f-e71a11a58321/image.png) -### Backoffice Scenarios +#### Asset Management -#### Assign an image to a product (using DAM integration) +1. Asset is uploaded to DAM +2. DAM may perform transformations -This is a future desired scenario, provided here for better understanding of the future picture. +#### Assign an image to a product 1. Admin opens product edit page 2. Admin uses asset picker UI (provided as part of DAM integration) to select necessary image @@ -38,17 +39,28 @@ This is a future desired scenario, provided here for better understanding of the * Image is linked to the product as provided by DAM * Image path relative to DAM base URL is stored as image path * Asset is assigned to the product in DAM - -#### Assign an image to a category - -Similar to the product edit scenario - -### Store-Front Scenarios +4. Asset relation is synced to Storefront service #### Display an image on product details or products list page -1. Client (GraphQL server) requests product details (including images) from the SF application. -2. SF application returns full image URL of the original image +1. User opens PDP (product details page) +2. PWA application loads and requests product details from GraphQL application +3. GraphQL application requests product details (including asset URLs) from the SF service. +4. SF service returns full image URL of the original image +5. PWA application fetches asset from the CDN by the provided URL + * PWA may include transformation parameters +6. CDN returns the asset + * The asset is requested from origin if necessary. Origin may perform necessary transformations + * CDN may perform necessary transformations + +Asset transformation is responsibility of either DAM or CDN, depending on the system setup. +Both services may provide some level of transformations. +CDN usually provides more basic transformations (resize, rotation, crop, etc), while DAM may provide more smart transformations (e.g., smart crop). +Client application (PWA) does not care which part is performing transformations, it must only follow supported URL format when including transformation parameters. + +In first stage (and until further necessary) it is assumed that client application (PWA) is responsible for knowing format of transformed image URL, and so client developer should have knowledge of which DAM/CDN it works with. + +As a more smart step, backend application can provide information necessary for URL formatting. ## Synchronization from Backoffice to Store-Front @@ -58,15 +70,15 @@ Similar to the product edit scenario 2. Store-Front is not responsible for physical images. This is responsibility of DAM (which can be a specialized DAM or Magento Back office). 2. Image URLs support image transformation by parameters (e.g., `https://some.domain.com/media/catalog/product/1/2/3.jpg?w=100&h=100`) 1. Image transformation is performed by CDN. The URLs passed from the Backoffice to Store-Front are CDN-based. - 2. Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. Benefits of this approach are not clear. Such scenario is assumed useful for development scenarios only. + 2. Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. Benefits of this approach are not clear. Such scenario is assumed useful for development scenarios. [Images Upload Configuration](./img/images-upload-config.png) allows to cap image size, which makes the issue less critical less critical. 3. Image types (`small`, `thumbnail`, etc) are included in the information stored on Store-Front side. - ### Questions: 1. Do we sync full image URL to SF or provide Base CDN URL as configuration for the store? 1. Full URLs as part of product data: full reindex will be required in case CDN URL changes. 2. Base CDN URL as a store configuration + relative asset path as part of product data: added complexity of the SF App due to additional knowledge about CDN/Media Base URLs (especially in case multiple should be supported). + 3. What do wee do with secure/unsecure URLs in case of full URL? ## Dependencies diff --git a/design-documents/media/img/images-upload-config.png b/design-documents/media/img/images-upload-config.png new file mode 100644 index 0000000000000000000000000000000000000000..977805b3747b006fc627296409fcd56350d9965e GIT binary patch literal 142595 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

+_Hh4eXvHV`b8)O`l8=n+tc#5fv(2;UxzCzgh2 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 qKVA6WDe&)3yl>l(x7^3=9cS^o4x!73_*^r@ literal 0 HcmV?d00001 From 8a372afd3322bc42e55519806de15e5a835e345f Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Wed, 29 Jul 2020 16:56:01 -0500 Subject: [PATCH 204/479] Catalog images tech vision --- design-documents/media/catalog-images.md | 105 +++++++++++++++++------ 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index d0feb771b..6c3004ad8 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -8,7 +8,6 @@ * **Image**, **video** are types of assets * [DAM (Digital Asset Management)](https://en.wikipedia.org/wiki/Digital_asset_management) - a system responsible for asset management (store, create, update, delete, organize) * [CDN (Content delivery network)](https://en.wikipedia.org/wiki/Content_delivery_network) - a system responsible for content delivery. In scope of this document, for delivery of images and video. - * CDN may be part of DAM (if DAM provides public URLs for assets), or DAM can be integrated with CDN * **Asset delivery** - providing publicly available URL for the asset. Usually involves CDN. This is different from asset management, which focuses on the admin side of interactions with the assets, while delivery is the cliend side of it. * **Image transformation** - resizing, rotation, watermarking and other automated transformations on an original image. * Image transformation is responsibility of either DAM or CDN. This includes resizing, rotation, watermarking and so on. @@ -26,64 +25,116 @@ ![Asset flow](https://app.lucidchart.com/publicSegments/view/21c14319-73c6-4bb4-9d5f-e71a11a58321/image.png) -#### Asset Management +### Asset Management 1. Asset is uploaded to DAM 2. DAM may perform transformations -#### Assign an image to a product +### Assign an image to a product 1. Admin opens product edit page 2. Admin uses asset picker UI (provided as part of DAM integration) to select necessary image 3. Admin clicks "Save" - * Image is linked to the product as provided by DAM - * Image path relative to DAM base URL is stored as image path - * Asset is assigned to the product in DAM -4. Asset relation is synced to Storefront service + * Image is linked to the product as provided by DAM + * Image path relative to DAM base URL is stored as image path + * Asset is assigned to the product in DAM +4. Asset relation is synced to Storefront service in a form of full URL to the asset, as part of product data + * URL to the **original** image is synced + * Each asset may also include specialized type: `thumbnail`, `small`, etc. The type is not related to the image size or quality and only helps the client understand where the image is supposed to be displayed + * The asset itself is not synced to the Storefront and is stored in its original location (in the system responsible for its management: either external DAM or Magento Back Admin) -#### Display an image on product details or products list page +### Display an image on product details or products list page 1. User opens PDP (product details page) 2. PWA application loads and requests product details from GraphQL application 3. GraphQL application requests product details (including asset URLs) from the SF service. -4. SF service returns full image URL of the original image +4. SF service returns full image URL of the **original** image 5. PWA application fetches asset from the CDN by the provided URL * PWA may include transformation parameters 6. CDN returns the asset * The asset is requested from origin if necessary. Origin may perform necessary transformations * CDN may perform necessary transformations +#### Asset Transformations + Asset transformation is responsibility of either DAM or CDN, depending on the system setup. Both services may provide some level of transformations. CDN usually provides more basic transformations (resize, rotation, crop, etc), while DAM may provide more smart transformations (e.g., smart crop). Client application (PWA) does not care which part is performing transformations, it must only follow supported URL format when including transformation parameters. -In first stage (and until further necessary) it is assumed that client application (PWA) is responsible for knowing format of transformed image URL, and so client developer should have knowledge of which DAM/CDN it works with. +In the first phase (and until further necessary) it is assumed that client application (PWA) is responsible for knowing format of transformed image URL, and so client developer should have knowledge of which DAM/CDN it works with. +As a more smart step, backend application (via GraphQL) can provide information necessary for URL formatting. -As a more smart step, backend application can provide information necessary for URL formatting. +Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. +In the first phase, this is not going to be supported. +This may cause performance issues on pages with many assets loaded (such as product listing), but it is assumed that production systems should use CDN with image transformation support. +Scenario with no CDN is assumed to be a development workflow and loading unresized images is considered less critical in this situation, especially assuming [Images Upload Configuration](./img/images-upload-config.png) allows to cap image size. -## Synchronization from Backoffice to Store-Front +## Risks -### Concepts: +This section summarizes potential issues with certain scenarios. -1. Store-Front stores only original image URLs -2. Store-Front is not responsible for physical images. This is responsibility of DAM (which can be a specialized DAM or Magento Back office). -2. Image URLs support image transformation by parameters (e.g., `https://some.domain.com/media/catalog/product/1/2/3.jpg?w=100&h=100`) - 1. Image transformation is performed by CDN. The URLs passed from the Backoffice to Store-Front are CDN-based. - 2. Image transformation can be done by web server on the Magento side (e.g., Store-Front), but this looks less efficient than using a CDN. Benefits of this approach are not clear. Such scenario is assumed useful for development scenarios. [Images Upload Configuration](./img/images-upload-config.png) allows to cap image size, which makes the issue less critical less critical. -3. Image types (`small`, `thumbnail`, etc) are included in the information stored on Store-Front side. +### Sync full image URL from Magento Admin to Store Front -### Questions: +**Risk**: Changing Base Media URL will require full resync of Catalog to update image URLs. +**Mitigation**: Follow this procedure for changing Base Media URL: -1. Do we sync full image URL to SF or provide Base CDN URL as configuration for the store? - 1. Full URLs as part of product data: full reindex will be required in case CDN URL changes. - 2. Base CDN URL as a store configuration + relative asset path as part of product data: added complexity of the SF App due to additional knowledge about CDN/Media Base URLs (especially in case multiple should be supported). - 3. What do wee do with secure/unsecure URLs in case of full URL? +1. Setup infrastructure so that new URL redirects to the old URL if asset doesn't exist. +2. Wait for the sync to finish. +3. Disable/drop old URL. + +**Alternative**: Sync path to the assets and Base Media URL, add logic of composing full URL to the Storefront application. + +The decision of selecting full sync approach is based on: + +1. It is expected that Base Media URL is a rare event. +2. Mitigation steps are straightforward and may be necessary anyways. +3. Additional logic adds complexity. In this case, potential complexity is in: + 1. Fallback for websites -> stores -> store views + 2. In the future, Magento needs to be integrated with DAM. In case of integration in mixed mode (where both Magento-managed assets and DAM-managed assets are supported), the logic of composing the URL becomes even more complex as it requires Storefront to have knowledge of assets relation to products. + +Base on the above, it looks more reasonable to have simple logic on Storefront side than trying to avoid full resync. + +### Full offload of image transformations to DAM/CDN + +**Risks**: Systems with no CDN/DAM will get performance hit on pages with many assets (such as product listing). +**Mitigation**: Possible options: + +1. Upload pre-resized thumbnail images. +2. Utilize "Images Upload Configuration" to ensure huge images are not uploaded. + +In case the feature is highly requested, it can be implemented on the level of web server. +There was PoC made for resizing imaged by Nginx. -## Dependencies +### Storefront application provides only original URL -1. To allow only original image URLs stored at the Store-Front side, new format of transformed images should be supported with transformation parameters are passed as parameters to the original URL. Current transformed URL: `https://magento.store.com/media/catalog/product/cache/1/2/3/.jpg` (where `` is generated based on transformation parameters and those become invisible). Desired transformed URL: `https://store.cdn.com/media/catalog/product/1/2/3/product-image.jpg?w=100&h=100` (transformation parameters are clearly visible). - 1. On first iteration we can just serve original image URLs by Store-Front. This would fully cover use cases where Base Media URL is a URL of CDN that supports image transformation, and client is responsible for transformed image URLs generation. +**Risks**: + +1. Client application may use incorrect transformed URL. +2. Clients may need to be rewritten when switching to a different CDN/DAM. +3. Clients may be broken when switching to a different CDN/DAM. + +**Mitigation**: Align client development with infrastructure of the back office. + +**Alternative**: Provide information necessary for forming correct transformation URL by Storefront API. + +Then client can rely on this information to build correct transformed URL dynamically and doesn't need to know about CDN/DAM used behind. +Example: +``` +transformationParams: + - width: w + - height: h + - quality: q + +url = origUrl + '?' + transformationParams[width] + '=100&' + transformationParams[height] + '=100&' + transformationParams[quality] + '=80' + +> https://my.dam.com/catalog/product/my/product.jpg?w=100&h=100&q=80 +``` + +## Questions: + +1. Do we sync full image URL to SF or provide Base CDN URL as configuration for the store? + 1. What do wee do with secure/unsecure URLs in case of full URL? ## Breaking Changes From 18519880bea184911178b45895e22f092f842355 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Thu, 30 Jul 2020 09:44:03 -0500 Subject: [PATCH 205/479] initial commit --- .../catalog/product-options-and-variants.md | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 design-documents/storefront/catalog/product-options-and-variants.md diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md new file mode 100644 index 000000000..ca60a589a --- /dev/null +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -0,0 +1,320 @@ + +## Domain Overview + +Magento allows customizing a product before adding the product to the cart. +Customization happens through the customizable options defined by the merchant. +Options can represent customization, other products, digital goods so on. + +All Magento product types represented through the options. + + +### Definitions + +* **ProductOption** - represents a product characteristic which allows editing before adding to cart. +ProductOption has to have a label, information on how option values should be displayed, +and is this option mandatory. +* **ProductOptionValue**s - belong to a particular option as to a group and represent selections which allowed by the option. +Option value could by display label, also one or may values could be pre-selected. +* **ProductVariant** - represent the final selection of one or multiple option values that will characterize the customized product in a shopping cart. +Depends on the business scenario, a particular product variant could be linked with an existing product, +with a price, or an inventory record, or no be linked to any. +Even with no entities associated with the variant, it still has great value for a catalog because the presence of the variant says that such composition of options and their values described by the variant makes sense so that it can be purchased. + +### Actual usages + +The application distinguishes two approaches to manage options: + +**Approach 1, "One to many"**: Multiple options selections lead a shopper to a selection of the single variation. + +Example: configurable products +![](https://app.lucidchart.com/publicSegments/view/26cd0b67-13c1-44e4-8b61-cada36c67010/image.png) + +**Approach 2, "One to one"**: The selection of multiple options leads to multiple product variants. + +Examples: bundle product, customizable product, downloadable products. +![](https://app.lucidchart.com/publicSegments/view/fea3f950-6f2f-4e46-90e4-f8ee4b6877f7/image.png) +Both of approaches could be used together. + +Example: configurable product with customizable option. + + +## Competitors analysis +During my research, I analyzed documentation on how options are covered by the competitors. +commercetools and Shopify attack this domain with product variants that represent all the possible options. +Despite the domain simplification by reducing the number of entities both systems constrained with the size of a product which makes it impossible to compose complex business cases that we frequently may face in the Magento ecosystem. + +[Commercetools: Modeling Products](https://docs.commercetools.com/product-modeling-products) + +[Shopify: Variants](https://help.shopify.com/en/manual/products/variants) + +Bigcommerce has a more complex option representation then Shopify or Commercetools. +in some way, I would compare their options features with Magento 2 x functionalities. +Bigcommerce segregates options from variants and allows to associate intersection of options with a new product, and as a result, I believe, struggles from the same disease as Magento does - redundancy in the variant data. + +[Bigcommerce: Product Options](https://support.bigcommerce.com/s/article/Options-SKUs-Rules) + +## Goals + +* Eliminate hard dependency on prices from options to make possible use promotions and B2B prices for customizable options, bundle, and downloadable products. +* Eliminate variants redundancy + * Catalog should not have a variant per each option values combination, only if necessary. + * Catalog should not create a product per variant if this product never used. +* Hide the variant matrix from storefront client, use it only in the context of operations. +* Align a similar option with a single data structure to manage them together. + + +## Modeling Options and Variants Data Objects + +The great advantage of Magento 2 - modularity was not a real thing for the product options during Magento the lifetime. +Since times of Magento 1.x option prices coupled into options, inventory records ignored even options with assigned SKUs. +The only thing that had to play it together "Bundle" product was designed too complex to treat it as universal solution it had to become. + +That's why, with the storefront project, we have an opportunity to resolve it. + +Taking into account the Magento experience (good, bad, ugly) and solutions proposed on the market. +I could define the set of requirements that I would want to see in a new options implementation: + +* Options are a predefined list of possible product customizations. This list belongs to the product exclusively; it is measurable, and limited only by the business requirements. So it always easy to return even a complete set of options for rendering PDP initially. + +* Variants are a unique intersection of one or may option values. The variant matrix does not belong to a product as property. The variant matrix is stored managed separately from products. The primary role of variants is to distinguish the possible intersections of options from impossible. +The variants matrix is never meant to be returned to the storefront as is. The variants matrix will be used for filtering options allowed at the storefront after one or many options selected. + +### Variants, Prices & Inventory +Variants could link the options intersection with a product, a price, or an inventory record but any of these relations is not mandatory. +So, a variant can request data from the different domains if such data assigned. +Such a decision brings us unseen before the level of the flexibility, since there is no difference between product price and variant price, and as the consequence variant price, could be included in B2B pricing. + +We do not have such behaviors either Magento 1 or 2 and as a result, the whole layer of product types such as bundle and downloadable do not support B2B prices or special prices for options. +Great gap especially taking into account that [Shopify supports advanced price management for variants](https://help.shopify.com/en/manual/sell-online/wholesale/channel/price-lists-customers#choose-a-price-list-type). + +The link on a product that exists in the same domain could be done through the shared ID, which as for me makes sense since I can see a possible scenario when variation that initially had only a price will be "promoted" to a product. For example due to integration reasons, to pass additional attributes data along with variant to Google Merchant Center. + +### Enhanced option values + +To reach a better user experience, +the option could be extended with an image that represents it +or info URL that provides additional information about option value. + +[Magento: Swatches](https://docs.magento.com/user-guide/catalog/swatches.html) + +[Magento: Downloadable products, options with samples](https://docs.magento.com/user-guide/catalog/product-create-downloadable.html) + +Both types of resources could be specified as attributes of `ProductOptionValue`. + +```proto +syntax = "proto3"; + +message Product { + repeated ProductOption options = 100; +} + +message ProductOptionValue { + string id = 1; + string label = 2; + string sortOrder = 3; + string isDefault = 4; + string imageUrl = 5; + string infoUrl = 6; +} + +message ProductOption { + string id = 1; + string label = 2; + string sortOrder = 3; + string isRequired = 4; + string renderType = 6; + repeated ProductOptionValue values = 5; +} + +message ProductVariant { + repeated string optionValueId = 1; + string id = 2; + string productIdentifierInPricing = 500; #* + string productIdentifierInInventory = 600; #* +} +#* to avoid unnecessary network calls the variant has to know does it have a link on another domain, type, and proper name of a field representing this link TBD. +``` + +![](https://app.lucidchart.com/publicSegments/view/eed59f85-7d04-46ac-9d7e-eaa77073017e/image.png) + +### Explaining data and operations through the pseudo SQL + +Lets review pseudo SQL schema, which implements the picture described above. +This is not the instruction for implementation, tables intentionally do not have all the fields of data objects. +The example proposed to show the relations and operations that we have in the domain. + +```sql +create table products ( + object_id char(36) not null, + name varchar(128) not null, + primary key (object_id) +); +``` +Table `products` stores registry of products. +```sql + +create table product_options ( + option_id char(36) not null, + object_id char(36) not null, + label varchar(64), + primary key (option_id) +); + +alter table product_options + add foreign key fk_product_options_object_id (object_id) references products(object_id); +``` +Table `product_options` - represents registry of characteristics that allows customization, + for instance, for apparel items, it could be "Color" and "Size". +```sql +create table product_option_values ( + value_id char(36) not null, + option_id char(36) not null, + label varchar(64) not null, + primary key (value_id) +); + +alter table product_option_values + add foreign key fk_product_options_product_id (option_id) references product_options(option_id); +``` +Table `product_option_values` - actual values that could be used to customize products, categorized by options. + + +```sql +create table product_variant_matrix ( + value_id char(36) not null, + object_id char(36) not null, + weight tinyint not null, + primary key (value_id, object_id) +); + +alter table product_variant_matrix + add foreign key fk_product_variant_matrix_value_id (value_id) references product_option_values(value_id); +``` + +`product_variant_matrix` - Stores the correlation between option values and variants, by using this correlation, we can say which of option combination is real. +Field `product_variant_matrix.weight` - says how many options should match to match the whole variant. + +The following script models data from the picture above. +Starting here I will use fancy values for primary keys instead of UUID to make further scripts more readable. +I assume that the human eye cannot efficiently analyze tens UUID signatures. +```sql +insert into products (object_id, name) +values ('t-shirt', 'T-Shirt'); +insert into product_options (option_id, object_id, label) +values + ('t-shirt/color', 't-shirt', 'Color'), + ('t-shirt/size', 't-shirt', 'Size') +; +insert into product_option_values (value_id, option_id, label) +values + ('t-shirt/color/red', 't-shirt/color', 'Red'), + ('t-shirt/color/green', 't-shirt/color', 'Green'), + ('t-shirt/size/l', 't-shirt/size', 'L'), + ('t-shirt/size/m', 't-shirt/size', 'M') +; +insert into product_variant_matrix (value_id, object_id, weight) +values + ('t-shirt/size/l', 'l-red', 2), ('t-shirt/color/red', 'l-red', 2), + ('t-shirt/size/m', 'm-red', 2), ('t-shirt/color/red', 'm-red', 2), + ('t-shirt/size/m', 'm-green', 2), ('t-shirt/color/green', 'm-green', 2); +``` + +So far, all looks pretty nice with such an approach product may return information for all available options with the single request. +```sql +mysql> select p.name, po.label as option_label, pov.label as option_value_label + -> from products p + -> inner join product_options po on p.object_id = po.object_id + -> inner join product_option_values pov on po.option_id = pov.option_id + -> where p.object_id = 't-shirt'; ++---------+--------------+--------------------+ +| name | option_label | option_value_label | ++---------+--------------+--------------------+ +| T-Shirt | Color | Green | +| T-Shirt | Size | L | +| T-Shirt | Size | M | +| T-Shirt | Color | Red | ++---------+--------------+--------------------+ +4 rows in set (0.01 sec) +``` + +Let's assume that we have chosen one option value from the list. +Starting this point we can look into variants to analyze remaining options. +From the proposed example, we have chosen "Size": "M". + +```sql +mysql> select object_id, value_id, weight + -> from product_variant_matrix + -> where value_id in ('t-shirt/size/m'); ++-----------+----------+--------+ +| object_id | value_id | weight | ++-----------+----------+--------+ +| m-green | m | 2 | +| m-red | m | 2 | ++-----------+----------+--------+ +2 rows in set (0.01 sec) +``` + +As you may see, our selection has matched two variants. +Both variants have weight two, +which means that we have to match at least two values to match the whole variant, +but we used only one, +which means that we can request the remaining options, +that correspond to our current selection. + +```sql +select distinct value_id +from product_variant_matrix pvm +where pvm.object_id in ('m-green', 'm-red') and value_id not in ('t-shirt/size/m'); +``` + +The remaining option values could be found in values assigned to the matched variants minus values that we selected at the previous step. + +```sql +mysql> select p.name, po.label as option_label, pov.label as option_value_label + -> from products p + -> inner join product_options po on p.object_id = po.object_id + -> inner join product_option_values pov on po.option_id = pov.option_id + -> where p.object_id = 't-shirt' and pov.value_id in ('t-shirt/color/red', 't-shirt/color/green'); ++---------+--------------+--------------------+ +| name | option_label | option_value_label | ++---------+--------------+--------------------+ +| T-Shirt | Color | Green | +| T-Shirt | Color | Red | ++---------+--------------+--------------------+ +2 rows in set (0.00 sec) +``` + +*Note: To achieve more advanced behavior, the variants could be "uneven" inside the single product. For instance, you would like to track only t-shirts XL: size separately for some reason (a different price or stock). The example above focused on covering the main case scenario. Still, the approach, overall, is meant to support extending the logic of resolving option values onto a variant under the hood.* +![](https://app.lucidchart.com/publicSegments/view/de9972a8-f630-4400-aacb-d3d9858862cf/image.png) + +### Storefront API + +#### Import API + +* Product import API should accept options as a part of the Product message. +* Because product variant matrix not more belongs to Product message, we have to design import API, which will accept ProductVariant messages. + +#### Read API + +As were mentioned previously, options belong to a product, full options list can be retrieved from a product. + +Variants do not expose at the storefront but help to filter options after one or several variants were selected. + +Such behavior was recently [approved for configurable product](https://github.com/magento/architecture/pull/394). +And since the most complex part of designing API was done the main thing we have to do in the scope of this +chapter - generalize the behavior to support not only configurable products. + +As an input our API has to accept option values which were selected by a shopper. +With the response API has to return: +* List of options and option values that remains avaialble. +* List of images & videos that should be used on PDP. +* List of price identifiers to request actual prices from the price service. +* List of products that were exactly matched by the selected options. + +## Proposal cross references + +This proposal continues the idea of product options unification and aligned with previous design decisions that were made in this area. + +* [Single mutation for adding products to cart](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md) +* [Configurable options selection](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md) +* [Gift Registry](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/gift-registry.md) From 1598d88cbf1c9692dc07d86faa16e931d86a35b7 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 30 Jul 2020 10:42:25 -0500 Subject: [PATCH 206/479] Catalog images tech vision --- design-documents/media/catalog-images.md | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 6c3004ad8..d7f0f1785 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -25,35 +25,36 @@ ![Asset flow](https://app.lucidchart.com/publicSegments/view/21c14319-73c6-4bb4-9d5f-e71a11a58321/image.png) +Detailed steps of the asset flow are described below. + ### Asset Management -1. Asset is uploaded to DAM -2. DAM may perform transformations +1. Step 1: Asset is uploaded to DAM +2. Step 2: DAM may perform transformations ### Assign an image to a product -1. Admin opens product edit page -2. Admin uses asset picker UI (provided as part of DAM integration) to select necessary image -3. Admin clicks "Save" +1. Step 3: Admin opens product edit page +2. Step 4: Admin selects necessary image using asset picker UI (provided as part of DAM integration) and saves the product * Image is linked to the product as provided by DAM * Image path relative to DAM base URL is stored as image path * Asset is assigned to the product in DAM -4. Asset relation is synced to Storefront service in a form of full URL to the asset, as part of product data +3. Step 5: Asset relation is synced to Storefront service in a form of full URL to the asset, as part of product data * URL to the **original** image is synced * Each asset may also include specialized type: `thumbnail`, `small`, etc. The type is not related to the image size or quality and only helps the client understand where the image is supposed to be displayed * The asset itself is not synced to the Storefront and is stored in its original location (in the system responsible for its management: either external DAM or Magento Back Admin) ### Display an image on product details or products list page -1. User opens PDP (product details page) -2. PWA application loads and requests product details from GraphQL application -3. GraphQL application requests product details (including asset URLs) from the SF service. -4. SF service returns full image URL of the **original** image -5. PWA application fetches asset from the CDN by the provided URL +1. Step 6: User opens PDP (product details page) +2. Step 7: PWA application loads and requests product details from GraphQL application +3. Step 8: GraphQL application requests product details (including asset URLs) from the SF service. + * SF service returns full image URL of the **original** image +5. Step 9: PWA application fetches asset from the CDN by the provided URL * PWA may include transformation parameters -6. CDN returns the asset - * The asset is requested from origin if necessary. Origin may perform necessary transformations - * CDN may perform necessary transformations +6. Step 10 (optional): CDN fetches the asset from the origin, if not cached +7. Step 11 (optional): Origin (DAM) may perform necessary transformations +8. Step 12 (optional): CDN may perform necessary transformations #### Asset Transformations From c6314258157c0f6f6ce7245aae8d743baa036f28 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 30 Jul 2020 12:06:48 -0500 Subject: [PATCH 207/479] Propose changes to field types in Add Products to cart mutations --- design-documents/graph-ql/coverage/AddProductsToCart.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls index f3e82899c..f83e2977b 100644 --- a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls +++ b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls @@ -6,12 +6,12 @@ input CartItemInput { sku: String! # already in use quantity: Float # already in use parent_sku: String, # will not be used in deprecated methods - selected_options: [String!] # will not be used in deprecated methods + selected_options: [ID!] # will not be used in deprecated methods entered_options: [EnteredOptionInput!] # will not be used in deprecated methods } input EnteredOptionInput { - id: String! + uid: ID! value: String! } From 56b6efe207b15886923a75d7ca67acb1e34ffb3a Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 30 Jul 2020 13:31:26 -0500 Subject: [PATCH 208/479] removed search --- design-documents/storefront/pricing.md | 22 +++++++++++++++++- .../pricing/integration-option1.png | Bin 0 -> 36676 bytes .../pricing/integration-option2.png | Bin 0 -> 36546 bytes .../pricing/integration-option3.png | Bin 0 -> 35867 bytes 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 design-documents/storefront/pricing/integration-option1.png create mode 100644 design-documents/storefront/pricing/integration-option2.png create mode 100644 design-documents/storefront/pricing/integration-option3.png diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index 26206cb93..021c0d664 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -83,13 +83,32 @@ The following diagram shows the pricing structure for a given product, website a - Monolith calculates prices based on the cache (product matches) - Message broker store prices in price book - Price calculations should be done on the monolith side (see appendix for calculation details). These calculations should be made in runtime based on raw data from the database. The critical part is to have very granular price detection mechanism which should be firing `price_changed` events for specific websites, customer groups, products and sub-products. Example: if product price was changed for single customer group, only this customer group should be present in event and price calculations must be done also for single customer group only. +![Integration option 1](pricing/integration-option1.png) + +#### Other integration options + +__All calculations in message broker__ + +![Integration option 2](pricing/integration-option2.png) + +Notes: +- This approach makes MB stateful. It will hold EAV information that will be used by price rule calculations. + +__All calculations in storefront__ + +![Integration option 3](pricing/integration-option3.png) + +Notes: +- Storefront will handle `cart rule` functionality, so probably we may reuse the same services for catalog rules. +- It's possible to isolate catalog rule calculations and move only them on storefront, other calculations could be done in MB + + ### Price book API ```proto @@ -177,6 +196,7 @@ service PriceBook { rpc delete(PriceBookDeleteInput) returns (PriceBookDeleteResult); rpc assignProducts(PriceBookInput) returns (PriceBookAssignProductsResult); rpc unassignProducts(UnassignProducts) returns (PriceBookUnassignProductsResult); + rpc getPrices(GetPricesInput) returns (GetPricesOutput); } ``` diff --git a/design-documents/storefront/pricing/integration-option1.png b/design-documents/storefront/pricing/integration-option1.png new file mode 100644 index 0000000000000000000000000000000000000000..042e7c6a1e4a18730348f2c96557355bbd3a7cdb GIT binary patch literal 36676 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 #SefXj7A1 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%0DCJu1T4>u!^g#c1c0|Xrp;U!wy=OEo&`?bOl(z z+(0pTq9d1!vvlqv-7{YLiNK5@nQe7%IFBR@0oQP?x=GS5v3Un?{c4;+$!|vz;-9;5 z6F>zLh~yJn7`T{<9H+v5yS z9$?L=hLgHyCEyh(fRhjhk_{jn20RG(-)E)?t$ol{=tu5e}#H1HrVUR$$Y?G0jf#D0IBfVw*3;?5cAAc zULt^msroV)7!ox+)xYx7XWnAjwHR}9(#Bf$BH=|%*Ljvq9NiT0nFQ*+ubk@o;t-&V zpcvNV@*MP`Jj@9F#pkQ(7q2WbLIWvFLm`sEt)H14;M@S$rOIFPJI}O47o7q;lawx6 z;++6F^_h%H-*EA{l(+)vfyKE?BHY6g{%Iol=wBG9bWoQG+NG4WPeReN^ksc3*#zTaKwLu z2JNrY?hfusbOby3kE<_FL|2IXz-my$J%&Q!N6&eo`_71CG8o{1Da6@PreL|0Iq&S- z+q{^WH#me;LJr4gR*er{^7J+v!lvk4W9jQ3CnLHaC;^psZ@23Y;KYb6XoZ>41T(y>}P~y zbXA$t=!AqPngaE_mcDoPC#ei?ja@0(kZuBOi-e@gUzm9rAh#7ZmxS8q-QaNwY)}VG zEFiek7|mCLWYZ;y0RbMDkEYFRa!EdxDje;`_6-YTxJftvE}=*%TD8yg%KJTMMHJPE zBbys5=39xxxk1hHQ#_Nt>+^)J$AGXb^CpEF%E<`h0QcSx5UjrX_-Ok|HGYtiBesGI zpP&g@L#~pnK-@N16g2SGQ12u6b^cqKBT@wx1fJ4F_0NrLtMCea=Op~`eT^FRrQf)L zHrU!PJV^rDM`ulJ?{JgSd^yq`x8GZ>v0`@IcpU}LjSm2uzG(a!du8sO7m5nuD?dI@ zkGYlJ+!VV>b};hT#JHWXSz41m-lEe>V9lDAYXdM%#Gg6UAUcm%+1r+LR&0n z_HJ46DoG}*I(YLBB^@`Y!K~&tyCmt%Y$Q}{`N=Sk2)#UNld%s6DySGA#`5lD>V-P5 zygqbFlD49fejqgXoY$UuGZiNpvTVEf#1FN5+wp)Bhd?g<^RFp62xRkL^N_NT>qq~Z z6I=m#{KKCUY4!i^_BOEE|8m`C3;z}-Nveaq{>N%;U9Q&&BT2%#4-oK$UcahBPBt6| z^$Gub{#BmTo!^6raqfdaK8ADDOg@(fJm-=&EM@&m!^^hyUyUgPwGfqS^~v|67ArE? zV9G7h=N|@A*8F=TgK~s_E8#y;!qdp`fNmWKr1+4@ae{V4P=-m(vdW)3%}q(qT=uOz z`$TC6B3S`ct?U)b%)J<=jz^QSS!xjhRaNv-@UL`V9(nq16h5I6yyhL=h9 z*i?7HLs>=uv3U?&x2(LRrJ`~|NA>u!V%y)eZEqXj=s6T4b96Zb{5*KI6;@;-GnoA*4Uxe)4Ei zeekL50pANb+632u(VyoVeRd{t)Lau3iJW{R9k&M;ODxRwv2O+Tqe9oqO~%nNPCJ6z zWCUlrO_Dl{e@)drFkL#Ct$Sc!tTq`%si)Qi=ts3xo#rlcpe$8U1;r}rA_b;M$#~Lj%Rk2J^}n6>H1z1pGncPZpIT??IuI>fi?eojOG7plIAcMWVDFCT-$Iz ROFFgV2=gP^KcBt%e*hx-DdYeE literal 0 HcmV?d00001 diff --git a/design-documents/storefront/pricing/integration-option2.png b/design-documents/storefront/pricing/integration-option2.png new file mode 100644 index 0000000000000000000000000000000000000000..2c5323cd60f984c6dbf17298780fe4b3e1929013 GIT binary patch literal 36546 zcmeFZcU03$w?7;yQapf&h$2lrigX)As^~#IARt1h5}JzCh|~ZH3MxueI7bl$A%a36 z0Rqw?6!lP)rnDd>p@q;(LJI-%PC(Cnp8Gua{_bDz`~KFtuH{;3GvAp#d-nd!XMcuA zm(4B+2}%k=AP}M7FaCA~0@+T7K)43@w}Vg4Umbu!Aa@|Y|7K_v(lgcX^f1r^DZd(M zA;C8>wAaP_c5dGh8{w>n4{bVyzTCWEf~=@tiB|nsFAbLb1TCj|tdzZjjA(s{?*Xf-Pv}b3VQRNu>qK%o#B}~y0 zjoV;#x&*w62&-lxx>maitS!Kp{%`x2ce)^2opM&%cd$stcf82=t;V!-Z0KZxPEMZKKmLtZMXEGak;_k)bX@w2vXb-5jvI@3Odq*00d7(9xVPw+lBY zkhy@C(HWpt*u=S;$u)?LjHk-AzVwD2YUu6LZ^?K3<8y@54m!_e&{-X8U5NfAHRTK( zVp^T-Ulj6I)2{brlQeuV&=+4k&+JJcuTGgw7C3m12X+aC1_eZmV~=U6O++|^#8CFT zSA|_*Cg(ls>44X@KDG$8wR8{F!pX=?rXK%Jys_X$&!DLXx}CVNR`eRq+`rtGu0 zpOdfylki3oeH&ic`rWF|+S@_1=gupdrE2lthOMHW8nCA02d`R5UDW!v2LiF})i*;3 zqoCePl7_6Ia#yGN>-!UA_O8~eCf4wgvLMLRYr5K1TXhX@bNEy@7o_HVyt&_O z!o&lA>lLHRke~}v{*5-H{Wm|~rpL>?_|83*v6i5!TIBYXn-BGf7Xr~x5}t6p?oYQ= z*>?$**P(WoD{sdaeC;}yxjA$EmtRvM6<)Bu%BKqWc@t#_B+n81{q@>eo$VI-mnS4A zAyYAH5+PFPlO9|z-_h0kZOK>Fwn3aH=}xM_U4INN2fnrzyW{vLM!L~>EyYY-#QD@c z^G~yV6Ne!X?+BP`L%Ss;!*owNVyW<{%DO238Fs4A*2RPB#XV5!S^ zw|{pHYa!}EAj_lFv)KNzaeXQ7N9|8i+*v!!3Q?yuMM{MzepPAZ$D}5|U?{pf3V}6fti5znfk>6`B2PXr>5F zuYcM0q3cr&NozTOTQ|QU`9WfW?DNM_ulS^8*nbVIWb#=D`D`$*Zm`;8S}A_bVJ9_H z-7W~wLI2ZYBmH~&t(Ro1r`F!@AO4^r00Q6yjXHXZm*A0*p9&_SacBna4Xw<$O=%lWxq*2*nYO-rJ0oU!+%vy9S_MO+ zyK6>FyVv$pJ}OpgGTPjOUMwC*W|*dRkNyga)9h>UA<502V5R7fuXMYs=sZH!r==}8 zhCV}LQq*J!nfu&Zm8zC;b-B2FW=4H?xUD3cPHGSKt{VRut4&K^E?KK|3eJfgZqrD) z!Fa}uadT}qjEb5Vt*~nDYml1Umd0jFU_gC*yID+y($Z6< zrQat;lc;4=rr*M+D|7w75&NN1Ra)U8ctyl@N)D?EGh2+Vqcr5I*#L)~Cqvj|Fh_u^es=AG{!(w?F#ut)H{FBTYYhdL=K zSxIM}mnaEMjGtDM++GK~C#$n*E|yR@ zFvCR^CDr<W9v@m)!DwO)@qN;(TO=l1(S!3b-^@9QS{?!?VCE;%hl_QcJBTjn4 zLZ|tJo@L^q@<`%HmF0@HmsIYYK%|b(%Im(4@U98r7qX-UUq?e&r`ODF8N}X1IrvI) zuf8E{qcX5-u0P<}^c(9Z`x~DcX$rZWwLgtaL1LTj>s}$|CyW`N)MK0Fl^!=^jY=#- zNly+z-AKgInhze0%j})NHqE8seJ4b;ZiXb2gec->)!dNBE+FglhMVJCHf|NXY)#qS zkMb*BzJZG)Roa%JmTZi=mfO9UlGQ+OKd5KNIo&J)yAaxY3>C zyGDF65eezN=~3qHLfoSk9y@1F&UFY8nICGNC4N5B|K~T%XW~YX_P`P7^ z5emAXbCaUs;uThnk1TpCjMT{*H{X!#6>e!!I^vTcO~y7R)_Wz{YJkmlq>2=z(ap8x zG*QHbn&{yJ0vt!hA;N2 z?MM#TD;6s$Ecv=*VXE+^(>n5se^PH+#EJkUuNhH>oSa@bH9GEYw4Sv{K#zKPk(%3k zmqfmlydpJScNbzgnkMLGR+uKp&6cMVXwsn++RGt}Z|s)`YxW9o(#P$%gzbrRPu#PZ zYWV(;HV-j#lg^;LTjUeYl&)Xy$k*a&HL5n+dBN5v$GVcua^SWd0TW+G(ron`bbOdh zB9-wKOYwOYKPkLy5}PdB;bU*~H3(Ad#ZF(0h4uF`Z|%AS660o{e}aJ`yt;-xrEcv-V)^Q6{~qwAB(X%bm81 zjSA65jM@Qwm1V2;LV>5ZzGJ?ukK130y_TJ=Rc@~RF<;h@T2ph9lz4K)_CvEdqM5l|BQykx=SDg9c z1?gY-PBJ>JqQ$w}h^JP_&$o{)^lsD}t;0VB2z2&D67jVR^-jB1+EtAPo!OO*fPIC% z+nI=O2H@RX4=*Bb-zH{B5TM@>rx=ighM|seq+sPQ!6D{o7OCK-q~#4X26{iVKD{1x z!&~NHgF~^bDdpQaJ_NGamMO)|_%#0_moX2G zA|^d$jHuS%9<4nFBFJ^yaw&sdBimSKIH>^2i(tM9yKE&}sO-5tMV0hw-%v*)pE&ox zxFAx}^J}x(pAOi@ZJ+eTVmTw;KXJ;{u_U-_9@*28rA|{}%S?Ia{r+?O^x3)Lw2cH? z)tDtquiS~e(&{gS>BO4_Z7a7OPin}D=xPL?E*ibullv`^GMZ68x!3j5rOM`r51EQ@ zbSLK|d#aRL*8KS(=TE4Wz$<(8Disq6&BUjcUa;b(zyt)caXZ3s$)U$AN1=gxDS6A+ z${Zz|Vof4)CFAIKg@@;l^W~6S$**ML$mg)C~~;3m zpE#yrqerg2vwpMUP54dL{kW(X`Xa}Vz)pF)CrpklzIq0MOwoybRf{*mLSHwV)AVR0 zke8F?L2eGL{*_d`j*N{jntmJ4(twXo+C`H|$*$F{Yn47pkh=4nMd;*3jKArEY8;d9 z_~jzArhWH_^z$PE%ny}oa$|_*Ya_*@rwI<*TLT>5w730r?V;RkX6mvJpI(L8P1Ysd z9(@snd5`e>6w;-!7eaK^{rV+)b z+2%O6A;a;-R!@B_O^DA~HHMX%_UJg|STQGJHZPsRRFkB`vJxAeI+}D^by`aBqf=BG zT!(yRamOQQNmlvOmG_3QD#~qWJE7e9eWMzESucbC*!f7JkUBxNL5clh9r5YTDZ;)u zGl|$|X05MKCRH;7u#H|#5_+4DwUtxE=$jjK(Pf>_D1D`gPYu_jh#Bz`R4<+swIkY8 znFVHZQk3h896|T@@|Ghz>udYD+i24A7k$Li5gun>>C2j8qr7@HaLc1_2K~w}4e%hm zqc#&M=qIhTB5@+4e)%_qvi3AU9HiWeqcfP448=P-ib%Hfiz;q{V)1O|kPaj7hIdu& z_k2@>BsE2fAz=bhCR?w*Ug~?g=5oiBGM1#(N^TE#xS>AW;1bQvckb~c=GD~j%LBww z_2(2kLwHyQQlr}1IPCzh!pe)Y4pZGuQp*??pY&^7J`tIUVZf1?g|zt><-;Bx6e`gr z%tnBYTXKkL(>L#Q3oc6uq5O?HRQLM^e>*WyJuonS6?%|-(fDgs^Dy2k_ z{aBOaBz)p>Mk9||T)hTA=iKIaxIsrXG2&7Bc$n)e;57l5$(Y&v_6Nyz3yw&~4#HJR zS9f^+ZwBQ}cDXUEyI$>TvVmpW$=Y>)wI}*$XNTlY#?Z9Kv}U$JrY1PMBq%iW`|YX2 znAw!TZo+5Uw`gfvGoOhRJWD$z$gu#^M$NB~;=}dA%T&68FXc~ELiQ?g4t^xCEyO#k zlbvKjzC;ZUx*m)p5OvgMHtL!H&KduU&hW%`+S#C4>6Rwa0egRqRXn0JE;md}H%IdI z2RJjd!=$MgC+lA}5^8VLYG4=Q5?(_uJ_jM=7L)n zL$4();_|nG@_f8WoQ<+ff#)Tnz00L0CzeGt%^161Z|u1IHb{5pCd^n|$h=Wyr*Cql zpQt6>Zp`8y(pw^qp{%o?rL87Rx1g6SeT%0jH}}kuNaCYYv$tyM zzeI>tH)oHbaJi-n1Gj#~Br_ji;0a{;?me5>sG_a$deyLS!is8&`1;xWz{kxAUK8~i z_1~Ef45}dz%cO)IN0;7W3)J;y4D*wRJw`_%{Q~r*JVbsD06*|bET!%=V-L63K@Qlk zR*RNSYSnq%e8dX^c}}B7>gw@2_wXRXx`}HGmdv7SH_|c9#M+vB{2V%=v$6ScGc(Iw zxJVFk=j_-!EM!W|kSwDyn|HH%Cqab!nEFR7IIJ(;);j5S5&|)4X}oV1Dp*qp9$$zM z9Zi;47THzD>ugRes^Qt>CpJ6GKR9a}27zS0O4yOSu=|dO*oX&N>9w)g%M*j`V4*@i zythM~S&ZAM&r-OK`9$je|Bc0TCJM`Rt+^yCrXR7W7x5a!3qfw~WU$2;N5BX2=!kX* zYiQ0znz>=)c_OfIC0NMnA!K&Qr62s>P7{51Cq(YF`h_c=ZJ>#hf6cwxyBj8yWnxbNsRTW4e)2pK<+ulhmwuTdR(~SCCrNuTo{#-r zEP^Im@8wP?L)Kfy_M_(Utw+<(UzKGoiu6}e=iinmBOuNO(pJ(dp{qWzl&Xa+^S+J2 z0*r<6lfog&tu=?-mux2q)GLvfPoL43`^>8c-Qf{~Z>z&uA5E&U&n5k8tA|&Xm7Ahi zGiyhDrPkOVBG|3QSpuCH_TsEHZ>U)TH-t6bGQKce!#0RuC6$!eMBtt$UaYMicS26+ zQJL&l1OsO-*ZcEI)o}|w-_6Mz(fDfOdUeE_Ij0-h^#M4JOAq_~n!J<*`@Qk#?RlDM zDPzB~6Pt`aE0TWXYJOXBX;||w*ls)Wb!%mKMgnAi;PY0t8*l8_b6MkE3HtOxZfE6_ zY7(mx{N^(jF%SzD8pdz#)^QAmtchUapg@S@6}vXP-FsQtVrDjs35yh=K)#D}C&Q6J3A zFXXFj#8cQ}F40%S2)2F>+P))JgTu{fr{KHg1hx5jw-Xq6x@g4IRTiJ~k=tO*^6 zz7lrXPr7M}vT>gPGfa^aaW<$YRIjg+?nxO>8?JYfRA2uo2=<6strC_$g5d@iyLGfk zei5Fs_Qpscl33|WPL9HIaXNQh%t;pD^iZ>Mc;K?9f#TUPhRQv&Q0*c&te>+}O=<_K zV~5&$ikx|zJRqSJ4mX?wFSvJ(D5UcsUMBRKcUT&xTrrN5N1u(uKHoJWK;JpCji3Jf zN~A96B}UF1v;dnJwBLLIKrCo_xZxy)6EY%o)1S+UfTwSLdEjTi{K;Q`_S&Yq)jjBB zn_PpXG3Z}=JZrN7XCRv&fQLeO5XK6IGH2x%tTu=7Iqj7BkMRIA|I>I3Q@;NG|LR;8 ztju{iqf1dXW^4|VV@$p~TXXo&R`>_&{-3O%0L;gOI4jSwADHW=Aw|w?&e{d$Yhdy7 zse(T&crK3bZwqqTKIb$;{CEM@l+#qMHGBu~8^O&5WOcz{rjWM zz#YHpIdU4_V1Qwe~#zhTLG*Q@SbE&f7_z3L=ve|s#WHz)|h%dZzj_) zc3STy;_AQcjx#;aU4~?j@tQLvPybi_OKC&23nnP{(cUm+j@xXl{@-r%%;-Rar4oq~ zrt}3}z7$Qlom!u&FbGFv)lc~w9B%NsW$VYnb^4Rg7{}#qasVbNRrsD{6>y_S;yONJ zV@Xq%_gAFS3~AAiy%EcPGf!i$?3`8%y4L-Q0U%5N8OZ)L3lp{P8Yn?=b`q z!Yg-nZA?~%H-vh;OlmCsGQ0sA${w6h;}hxUco64*QIVY5hC|VbnMq36{E}PsMymH2 zWV5j$*&W4@a41o*h6sW|a?9@4QoZj}FvX5l^o{sqeeKQlde;WhwDUtS%CS4s6E4Zw zr#r+(at=t1hvL2z$6Qv`yCJxM<0=dfbj*fL(!1xij9Q`L$a4+h6@U)F#i?4$p1<{~ zxXAo+77v0yc_Co3{?u>+yfwzxy-{>z`sTys(R%XImyr1#vg_0f*p8+M8JFIZ)$eeVO_ee#KtHm7getBy-^Q|s#gh&0jt{dz(!8hTcIrwXsqI&(W+!DtV7iiqmVLRv=R)~koAgw*C z>m7B0^@fS!mk#c1I`;t=Q$H?Qo<6x`q|W=uF_Kss5t^-!-C^20P;ls{Z-|djNjWT# zQ$zU>qIE_OB_f_eQGu;uN=t42Bg>000lIY@p-SiT#>Tsw`iDmYjaF^ zQlp5sGlR?b%JU+`gy;t!n~FaztX+F)nY1m?pfq}<^qO{tGv5P6~EyM?N>B} zVR|?7fL{M+Ik;nLiX8k2=`WMYCzf78W#&Xa(G8<98{zh$`76h4o}1O%33nPFXiiqd zgr2k(sB?P`m*~AJKBmr#5d8?%v$o=iH?90)t}YoX+ADtddZ@}b35?Q@Fdj+l>aA0! z$J7?lUkK2zT$I30WQRT7y;O_iIJg>XX5va9eh1yFn~MFc>&--Y?^qQk&8<&t`jw?2 z3^rZ}Er4P!)Z{xD!emhCc`Cd&Eui?BD|4jXLAC2Khiyuz1hxACfvGO!*tOkBfD>aX zoX6*iW^Or^l_BhO2m0i+ltiPWjd@&2MvR=|VsR_35EpYtIf61=sp#YeR=)hJn(ux# zrn+9VjhgDfbDb&pv3xE^wE;bsFLmriNxtG^rRb-Am_kQCR^gRzG~iTdPR5lZs5dm4 zBkY8S%C{q!Zhr|@XS4^`welh&i7D!Ykk?VQ?>2%&$KoA#FDgyYCn?F$wYBWSV`7|4 zT(@f^M{+`d9(1a~#L({bi}w?W{?NPDG7FtKg4uD)RSV22^l89^8E3M#NF%bJ$=6`3Q%=LGIinR>D|VJ=j&gFxWAz5S;NsJA{n$Hh zrMG@Po6TR|gHyfOD2pc!sYDjI`X!gs*XeHvGd;oMp@GolS+NmnD^!?7hemj?dLxOp zrhD%?!k9&iZGl|KuC1kO<1C$5g?^qKyxEY9Lq2g<$;oK6>4);3pvFg-8p7@c&c6)A zHe?HuCuB~F^iSmaB}7)~K5nv^hgKG^(eyEqx^~Y&bl^e6auxHt(L?8{G?`@KlgTsh zZC_g#5EFCF<0NA3#HCQn(Q1?(06Inzw+Rbg4d&-z2%y@7dn`;LS`wVKIwCm|Sh?qk z92_}#(W~>mkB7R$JqN1g%~8@vykK!+WEIrh!VX0)ypp?S>#@ImNi)ZKsvVDOf3y`Q zlD*?>s`PI`L7;K)k0S;6lpavz%3HW98XhP_PgHbLu&IJ~QfS!u?DAX~Q^V2RAdxTT zABLZ-c(xK}>AEC9r`M=%y!K7ts^S-#oWY+LA5qcC-}rXNkR1MqX5h`w&3Ho?fByR1 ztoPCM=T|DMsh5GvyD8DjuYOW>+(DS2MJyJfZDwR6S|XO(u=?LZo>YQ!zTgzSJz^BU z(Y6G5?t>xO6BdH%y&uhs$Y1Rg(t|RuWNM0yOmD;x!>_}50!4qx5D&Fr22PjX^QG3P zTKCnqGU}Vom8izr1y8h=4~&@;*tr^g4#BeVTeyiR3PReOT1=z(pyNGnKV*ffi?c&q z0_N;V8O?Ra>;)5-RPaO1%W=W503AJ=UE}h|?@Np*P-SPBabRHj5>b^GQ4#!vxF#7C zc-Ofc1#^^3K*5Ap6S-;tC#@UsxD!Qf56kG~u=KfqB&5jaoVu!U5A2fkj$bDY1dY2a z75XI82%-b&mX;b&jGUTv1z@%usK+b99~?ZUY>cH`0yVbv2UBl3xEugt%|Wk`MY~Rr zNzjwS;^_8(I`)N&wnfGqZ0~TOq485OTDYXz)7B;UGBH$-_)NLGLIJ0iq5@#HLXlc^ zE$XHb*>^hvagmgcGy7woi___aFXo?Loh<<@^xCO6`8{eP2N{-OxsoIC<~uy099&Or z;zA=smFQho%)sFZGtKYmbck|f$qJkQW7?1(qum@7+Fb*kR_%E1%`+BWj_*zYumN$) zN|>H#O;69uAJ+h9i-vYiOoGqJsVO=JtDPm5PXh11dI)}BPR+?ExMOgsUZupKHL@xI z*UV54k1lU%kgPj64O!|wlB)#tGWno>r*b3vjF+~GyWkow_kIE)1`ScG4uUaHy~J-v zcqFw4y=(Uz-8Q@v@RwSBge;xed_dufcRmqNn#g44sI%rAB#yLWt!ke4rWhE z@~eWa-n)m??WLW59pg1(bhKN(+gqpThK$x8AuI)~(q;w+P(^^QzceWuO0`OaBi6>g zVcgO=fZc}C-&r(@W;wY(TU70_o@-VN;<>@W(lviY5z!Ck%;HMkf^61)VH7)%9a>d* zMK!Vr=M2x}O!+0;vTFSt$OvxlIwQ7Vru_bqTEDvmY3m7Lya+EGKfcZLJ%4GBHPz^5 zycuR6+f-?|jnY|ZfH>2o-oUyayK&o1>ji;pivE&0oIahaV{~KVHFLc!kU@`$vj7&~ z!gf9?4JO$3OG=EPY6^Q^Q_*G`&^P)Gla|=@+0+7=)>lrJgo!&;s9!MjW4TD_!3OT` z^KlY`j@ecQwxb9hsa~L>$W!5}EbgU|u=w&($0|LA4z#!r{;E^%%%V}=3z=$9mW>cS zO%u^jHkD-I$%LBDM*Zt(6D)m48ufeZ^Itc7xZ685)Mqy@ua0OyBw5lU2b3s5R*NXKtcvt!2~? zFjXc8f5lnP4_|DO)AcVr+=VT9Jf|qqoJMc=eDSHg6nPIdA3Tl{g^Wg+fw>SOO-Nm6 z3>BOGcp6=6`_@ZqsSKJa@-thtQUe1{fSX6lNa7Pd z#CT7Lg(wdvIcV+WtjEQ;gj}%#;}C!aFKM-9wHX&?c##9kGGHjva$Gz7ywO1MQ#rM2 z4OsS<$pZU)FU*kNp>Bso^`l!rFcgIKX_z7TDNIdGBzpqqsqVl74_tn7O9=&oy-KiN zi2sSPJ=gAA|*;vtoF@w>^=ISk-RcUOPO*Q&&Qeh>`Qg+U!oBR;gs?!s~r0+K%w0u2XElm|9;FH zr5p{DK`S1odD;rA+^dnQ#Bg1_njM60x2$8+uxXE-k3ctU=flAP5Mz|L#{gabxc+H9 zK3u=6nLcn9vpT#E?bIy2&BC|B}RmO|bdpTszKOKl`0&-3gH|dfsrejh+ z?<#D_<;}GDZ-1V5urO?TO1^P;LP^}fs{#ea zvza5dNSuEpO%xj}J%zR?GiL?;D2se9HM5d^5%PbD{Z}(k1eDJnT!kG{Kpd)|F> zq(B2z;EFg|$+N{CO#vO&C7r(sn{FYH*mw{L4pPcy5`klrdw*~`TSN&r=pmBW&req^ z@OjD7-o7>WAB4sxa{)LNKtphTru*Z|AJhitV-C->MUC9uGo(S^rCOPH6bqeyg|tLG%Au1Wr2U z{uk@pmsM?4O7LfiF2nIagoyyzK>ISq*)kZFRt}b%; zitvpHs@KlF`EM7HOYQI#J3s=3T@meb_kShkbbspY^AV9_NuY0pYPVUIA_{!URf52bHV+)F6TZwCCQ0Z$tW933uw;Tpy9T?CKcEvJUm zj>5@S_G)k=w)1<}0GQC7=@9C3D-KW6zriS}Rv5eN`EfCpvRXH7zW$fj+8&)_hY7D8 z2VPNDQ~>!ZMj)(hV6=?1bOTCY2w zk?Ub<`cf+}%MGCyZ>5=J&V=`f+yO-a+oD5&$JSm1mgOsfN3`#6WJKWS?(gx zO@W1sFW-|fHobKirtlYI$L~{*+g)`?Ioitaua9S;MuiAr0p+IY9i{j+c$kHR!$wuo z**JGQ_Oa1WfxLBYXA8g8O8&*?SB7)jvQDjU*J*5OM8XyVyjg$5PBh0>PA^e(nTF{d z50y5gNm7jqJAQ9(02H@TL3ALL5&N5hdQQ)MHFraqKpkg@_xXDWiBTitX`^KEUY&IP z;n$qPPJr@y<&rd^V?=3>oHX;x{?~Hwk!BqgMQfS@D3jz5AM5>NR>x2V3JN{~c?fyo zNCoxp<%+NCQm8=uDz7JvUO9aKBq((xdOPk$?elQtL5O*f2VFn<;hswdCQpsERLI5R zk{v2}x@YblV??Akv5%`s08ZVuHoxi6@SQ7f3B#_zdkzALhdBdcFF?=iDeE+WK)N@G zS2=~OOGzM|{_p4iYlr{!hyO431=W!J*_F@TJ3iXnzbHU2Bf&VQQur4g$Qg9F?1g{W zpHxno%UbN}%j1R=zLWrgf!EmyL?V$l;Do-JGHYi32Om4DjqrWmcLS8aU9^~Lmzs*W zA!D22LIKi#1h8RuD&@i@In{9&Er2YXzaz>3;M(?515hyArg*o?(gL4AY=$)ctZ@Ti zC*rKORVFvt`xGEG+aQ=KsGV$S{UKq7E2q|4a$p~CfmB@c{%1KGAV}`+9b0AdT@Ma{ zH2Wg?M;#m-$7E_?JGbrw9Fqd6K5y#pVCBghjW$G8t(=5ZR9e1>p-PV(&c46(Q2lmc zV3oFO;Konh^`GVPZo~fzU;W7#a^kAMm5180uUoLQ27^xe@N8E9?|}O?gxqq6V&Ov3 zEjhpgas0Toz`SbV&aL-d!x^UT%=RF!AtVh+6MA}0$iMb3GwfK+ymx}aA%8vo+Mv4eZV2EV(T zr$lTp3rz0^oQp;x8fenI4EmM`gZrQSvT@Qsv{`IAa>Mno0}l+{8EE4sF!h3eC0Dkx z&K;xoDE>Yf(cSoOU7FJrOg`#q)m&Mtv`S>fWY+dz>4_tuI}c=d&3_;_{Sb8gle62x zNbO$QIY$aF4))9N1^Xy1sooDg#{ByM9XyE=So6~;TX*!M2Uk3af%z-?kEHqe93CA0 zvB^qj4+D$K9DE*g%d^#ntWt1gR@vZ?oUb4NaIDMu+=FLkCVsZm$M}Pm zN392ugG`zlk6%>sZN`^z4VM!?w`L?!R6`A8tBWhqMFp_(o!DQeC8}2+U-2AS1ZcDZ zIX$H=JSuFyQkZg_-ho`1$8eRpOuCq3L`OgTx`|yj6*Y&vU{lu|v9u>q z>uObNebXWvtO#pk)66RNFuV^n;3iMyF0XoLbLfXj0!^``*B&CoiSMlu8*H&EA*b0; zQU$6GyW_k82iI@{Rl|NehS_@>ah)SF0Qv_38>5#rP&N?T3K@CjUaGC5Q}F~;i=B7j|db^K0yMkC-O;Vo(21o_vKw>LSc7$syN*U75-#rdQlOP#a|0hJE7v&d zwj{Q=sYQB{iD2G4%Q?-(qVAb{?L`-IZ}*34?@e@t)VoGMpy=?iDakc9go>;;Aao54;p-vcJgDboMbXa(qF7gh=%)1 z$6YY^h*f`V)Uz$B+I` zCdd&6=&p)xF6GlFOY7H;Q!cmXsEvPsby&=+DIFUhzfZ$dqn5tA$4;VTNMHA#=x|W~ zrEgRNJYoQTkf?L|G{O-|I9L1U3^)t z0gkz)$M`Rnx~qpac^2q@^sRW7_^OtAps!QtqZUUOLDUT_i?Gj6xF5#$_rBZz$%l=t z^{n8ZCfk$@sY+m5!Z~^1AS6e(7Aq%`aZIfQlgT-}$iaVWZj|2*Pm!Un6 zCn9}-V4m)*O3)G-!}UshaD451%KB(m4q(?ly!-E$&L7=dz1njhM8ij2n-Vn6NtrR* zzTfF?^XffIsM1o^+!{mT+=H;*U8#=Qx{pcpqj}+j_miNZhZ>@OZ)%*DpHP6X{D~Li zN{0W%q)`=@Kv{WCK<9a4(E7S8+YkXI_mP|{p&xN|@nPsYYGy|JQmuKe_o8z0r7iLu z74jVJ1w}2NiQ{{=rJMRM{CW{-Hrx>wDV(0+d<^XsEYE;}_4?}{*K4-rgXQKv3;nUr78f*flQy+s6C#-P$9`3ZQj_FFGLsYb z8*C{wK~((tyl?2OIFpSn@x$qDb~Cf9T!4P32T?YE?q{qErVV7>IR|*FN`6e&u{|F3 zp`l8k#1vPLJgaFab6H1C6E(J#(<~S>Qo7MF{@|9=Pmbqo(qqd4KooLjn%K9(fmNIPqr!jAYBP?*w&J*(UL}w< z9c~!b{~t|mlM3vU4*)zKxIjA`{WISFhw=9LubgstysbdGw@iE_C#&TD+`{f~xUr$V zWzNZK6@9785iovw7ujKx&YThZ_a<~(p5TyJviDWzoz^pRXW1+JV?%d~M|1%p?)IEr z`i?JVoNP!UlT$9z!o=xFqMptz4$=5u**W?4-X$R2=|=(DiOeY-$9ncs<~!VC$w@1- zdm{@odVV`v)It*-v2Yoao5kG}UTop(3o<09NNa6s`u+>snJHvG9GC&jei;WPEek(j zH|>W7Z(^vo&T#2%pX2bfx}78OXH)B9mamOfakzAcQ@_QLI@q@NCGhjU?`fwcZ}##c z!h!}k{H$Co>z;3w$HyH!Ti#|kc)`$?CxlwfUZ$+lom=g6m^ex|ca zO7`SQkDipF{TqKbcxoy@KmV1e!)K1&J%j|xumIr`2RppVgJ69cGK87JSL2d?E zj7j&=m`=SL<-SgqD;=Qj>l0J|s2*KE5h`6%>cm`PF3xm`zoQ=TBxAat7SqfOT1(0% z88p4wp0}osG*B_N@8zercon;3f@>+IvVBI>4`9bo2gY#=jP4fn(2R$nyJKCNp42R( zN=SO5vhCk^Y&XWu5=gz^oc-x0eB|JB#rN$um&%^PvUe--`OXK6HbA9rA{SNy4X>2Z z=j4lzJN;q(+)Uk0d^Zr{fJ#jJ{XOVo`KsA5BJLQ zFW5q~LqjiuNMJIk;(D1CFegj+Ft`=^Rgk{aqG6VUuqqHb$BRHX2lvg{G8Obi9EuO! z51;Sm^Ktto^K=;+f41`bWke>cj*t zQ`mnfNM5M-*l%$Ol%+SL^YpK(((iPR&OCTh0Nr#LmKEjlY&;oAO?Qp>VZNHi@pZH` zK9L0gb3-a=*T{4TC-x_wjk6KYTg>A3J@{)(O)l{YUh2+one9MoSRZpIQz|>28%J-& z1C`7(T=8*5r{o0dN~%&bF86?~zR_5mA#6dtSV!b)W3N6aDW7dzfp=ydW3=aw_Wn6z zQ!=6Ih|Ze`eFXH-CqcNe)e znPa(iziALgaZ-?8ZY9$ut$a3pGOUt2iSd%&G+W`ZseIBN)%gbGoo>SM(YA$k!oLYpXYF|1^a0*yJZFMW*At zio~U(h^ko8W*bU+e3U76F1tLV@6XiBZnee10#() z|loy=Ey?n5Mrrlqft^;M`RHcpaker zr*?zdBLr0bxj>yMu4FytmUkve6kJ^o^@DcGH=qaJ`3ClYyJbvC|3E|2o|d(Mq;pHh zBXR?JJ`?G8awndrObXM>w?C%Jotm|Q%`dy@=VZviPZ`4U`Q32V)lgo9yShR3nCM9I z^^#hf}AQMxl4)bT^hCU4@bpnUwa;S zeqxSPkFwZEy7RG({mV*lw|-=T;~zG{q%Veco$ABo`b#HB4UMuTsyq%haT0jN#z9wT zXHL`Ojrenwsn)0&+qmGYP@A$?$BEJlalv`~ZdZ<41|}0HU(}aP1O2Sq*WW*BKz-^# z{D11SQF?slb$lp2=Wy8R(bZTl!C4^7c>3i>eBww%iL7;qN*) z_B-~Hx6b+q7ztjmbHdEy`{9O0o8~})+SRLv)SMn^U5iVoHV9M_0J^lb<3Ot#Vu1GM z?Z1pESnGH@{ybewfNpE%c(JSYdi@4XsYg{2h3d)^!Wvb}sfh>=7ru;^E@3HFn19QA z66i7#UJHAQB+4ohlhhZj|1~Qw(Yl|@!RwO%)En<6u;K}cF_wok`Zc)9)~pf~cA9*! zi`!1a7+ZS|_^}gin0gtMe3XV>8jeyFZFZ*V-LewO7gY zJ`9!EGCZYUFojT+C`nesHI{0q0dew!;CKM}7V*{WVT023UP8pQ#X6|`mHk0`Of#Of zS^LgLM;gazoGIeXm2SdUB-be@)ZHy#iAm`i>X`Ue|T9 z5PM_v=1;iE=>QzKLhNz{p~IM*r!F_L#9CiP!{!U<1nu+?YLhkoi z5vt3LPUD7v>X5$O4@X^Gp%*24{|Kc7R2mq{NG}M`U9VqlSJ0TuJsoVfnlih(=5xTZ za{zU<(1|cz(@QR^hXxjvQoLXJ!Qx0dYneHi)kmbht&2UwB~|%0z{p5s@w|yFVzp&G zQKkTg0tdga1@bTba6=hG4xR=zut0|ESwk42(=MOiey~!Wa~*!z@E{m|;MW*2vv@uwvAu@5l@WwWswJA0g|) zIP#ItL)LRjLHXn_g7mSYk%U6m#zic>b1OQpg70wQRt{DFBFzqUBYPv!_~i&>!re(_^ZHi(KK}AkGCi&PaT4&p#WN zU_-p1kB=lC;G7cx6I|g7yQB<+PY;{8Q_B^jLjqlF{>XWOp3Cs1I#tng*9vae*jnB5 zVa62f?(5`5KwV%62Rsm2^*WJ~y6cK3J92;kuB3v9Qwg;db^v?Ksb>jz_^R>IQov-w>7jbvMw;lA%u-kwnzs1;@BO*uKY|A^FD2{Yy5O&{o)Xgy`3z`!6Hr zd!5Rb_YuOHZeNs$l{Sd2Id=c>izO(TT1(TDGPOELs)-U`_gOAQ-%5b{0LU1fsmiY1hI5c8{ofMEp#F1oyVtlZoNXX8q2;>|kks#LE6 z4bC;vM!TA!%&}y!xirJ$Bn)lKkyT%O0FEO@7#Z!gxrQxU!xJ1&r3jw1{uVEbH9z{o z04CGcnKNgCd`N4at+*7Iz<0MX`?uXA)30+$MvpJ6d0dk7AMLe_#tX^C7Qy9R9&AL?# zGb>;x2~u_Gck-pKS?7=^ca78r)h+#I#J`V?A!t8l&q`{qDx05Za4^Z-451Ln<57sy zfrj1+Ioio;COYN=-^)JAYx5#4rWA^WO=mD2lm=!>Yl13t7IcQO!ra3%$(2*7G%<)_f4_vZ2Xu%vm@1$uz6izBk$>p?>m2Z z%_+VW&h;ShjQ5;p1h0IPRHou70FqE6Bn~**=UeCQJ~le*kCLu9=rCVAg)7EE0~PWb z&(-m-w0YKaye&`Z$Z2R)a*CJ#>Y&|F(f_PmA@spN*^eU%s<~E=Foadl9pb{>e$x@4 z-E7K(aKx?Y*9X0@5#576lili}J{bvQ=rL_YlWh|f{EW7&Ge9Z`#ENdUPNC9JEZr)S z7*lQ^&ZohlOF#@d?|+A|d8+0$y}h+$LWszNA-Mv9giY1s)c zu7;~l@6)HCb+lD|gt0Uv~_86c@R@!t54#PT!my?`3ARw6dRx;efia7-K#s3g)!=0W)N zG_|{ao>ToNUje9$?^4ifi4h@*ky>2A?no->F5ceHM)>tmvEP%DvOEBeBhG&Cv?_@@4gdTzv0DBr$U#vCTuHk+kzFI4_WrFL(dj>VtRgg4P z?N_mzs}9+Y`-qdf{Yp-4_JW0|RuD(9z3nC6D+@nGa}22xK+RobDu=ZIe+BZJ)Bp%M z0QTPb%!&Ih0IC?5(G@gI48Qz8?R|GxlUcWK)Nyc}85l>9aiq)yOF$tOq!$Yj8w5mD zT2N6CDUlkI5I`LjiNZudq$>!qAQ9;hhz$uKB18!_Q3!+(ASCnz$lU?-o5FYQxzBx` zbI*6r=N~HU?ES8__S$Q&{ae3xCnd_}G00L8l>`ywiL?DiK@H&HKQv<|dMy`;yt?d{ zCV|&%3ZtHFUHmfVc}BhH3}6bMto!6*Js13cs#{bcdkgayh1>lgBzjSn)utnD?a3M0 zMjdXY1p5#r^Ozm$7T>>rY~QWLrEmg=XE|PrEMHyUGBPC$5&k3`Ue?>WrOQwBA=rNW z>d;1C@Y)t}pzp^cw~(I_3IWUBv+ZHdn%m=rgIj8qOsyV?9;(?{X@Pf11}9FadgFF4 zsG!U}j88rKXF`5~qdv!@FYY8;wzJ6B-`)1hi4g+XoVXc~OM$V!=mFqM^SPqCUJAmh z#JF&xG%*&yNp2TfdDtfEzyBNl`X#^SKX9FlbDd z(f5|-;)bfw(&HlwBb8rZ-Y<_?09@0Oy&K#I-lS_-RB_^ekJ`$ME|S^PpK5o3SB&pe zOnmSVEeYjT;NsWMRWzCJ=hc_=Q>GI?`g}inH(Y{de`G8LC7&;_ayFL9GjxlK{vEu{ z%`{HMZ8Cp`>3oFooWEDVY96tz7%vwPiqfR*+~3eknp#>~28`L0Z%dHM*TVjR5I*g3 zxyKEy?Xr|f#21GY5mmpvHD6!-A`MwD1VD&}msbZ*J}D)Sj(TknHk6xMI~ z2Po|@94%UMf3`{*4BOri#o*2WL^EE!ov2hIw}6twcbXq0Z0U^U&e$C};gy>EW%bUZ zIL+A-!z|zdE=AbWOeIeJayB_wAcJ(5s_(bDH(L#^m=FaNQBulZpQWCdzb4_!feTd5 z&fUC%^}NKQN$8foF6NZ*2#{6otDA#S=0-h63a~T!UA+H3cxN9ScjRMtP|rrp>A%`g zPC-p2e)oN9=g&Cx>0V2;`7{st?I5V$DPD^sHY7bvHSOn(IZmD=1= z3VWz9u`x5tZbfU_M5*d;bA$zFyE(}8{;Ee+GC@Bm_GZT6a>bi=WfPnpEAMpwDp6hh zQ4ZZER{V4ysU)k0eui^JJ*wL4l@}{yW+{I}91HJ#6ukM*@@6=EW#h8ATog3gshcb- zv|77=KxG#j8yn+CN=v`OI7DDZU#&#m_IMH4^QAzr@+`}H3(sc%^)i*i4hnM>?UJlH z{Hehz`yF0@2kyP$-Nv-&ZHp@u!Y5wFym+##GCkN+$#pp&8gb3%THNK_x>_pBATG;3 zWOMbG6`1-DBG8uskf_nh@p-zR>>zE3VAFUgD7n}7DmKXQ~l`|{G zPlyA~-ou{M%NtTdUW~>)|6EM@BS!^H`RvsQvy~^rifF{%RZ5TA51kwP`21oX;vrBnq}4rlp1sgXaEFu(f$M0a|b*j)~4Kn3cf;MFct zI5}0KmHgg-_siywl0C>p$Cv%daM#lqU+zJud=QuyF4<7=wWZ+rRX_g<7@7Xn!i=hy zpq4lNfy~kX{Nhff^8}vps1z*-+jXj}k8obFvmWRFovzV=M;Q^1BMtga{g8@jbPl;d z-Joak+}z+@&(~0SMIS|t-{frNOZNUiRo<@mwffgf_TRZGviX6fI$C*tnLpyUu*hw2 z>iLVf$)K}odAPK4zANRSOFGSN?xai5gp5*MPqB}OKq?QJb+g+OyK(I}hG9;c z5_)2znMIA!j}*I3!=K@_;kTjiEE+~y=Zu-zG+ywH682dZvwQ!BZAaGp&}_FD;}X9y<&9#*Q(pRa3U`**kf)NI@|*?9l*CVr=U4X{=El(Z(1d~N zZ}`+j-Sp_P^wRYM-u_TAaWMz}KHCe6Iqv^>DNnz5)j7-6JyDtTs|C=$tebD>(^&4td&8T{K zIc3@q+>XSk&vp7*ZL%Svb0OOKIXO@_ZN<2F@J6?%z~m zNG!@J}JZncBdWD&QTL}m-jtac#xc_+4!o4vIUHD;b>=c+5fa0~bIHy`HPDW32%dNat| z%HV7gSt~bV#->@7*P1E2R2P~ih&O-6+q{bqL@Xn^PeBFL)jGq@3Bov!J+2c@1_i~M=xSOOBv=&@8-#jbyqO4Sw z6u`#bX3@+XhsLTHZ7I4jtIkOnl4zChvJG3SOnomS(O9FpPuG|%#%lcA5ihDaw<2Qe zg6rg2;*y`j|KxumcG|M&wCLY0f3-nr*SAwV-7BvbJsE|EkWu}E-t0|_j;}|kJ>dmS z=K9^%gdmK)17%+k@cShwfZk+(3V;9a_`f~p+S_&bN2&>K^GsmsSYnwncEjssb5c*b z=wddC#;(uPfJH=&z9ZT~o$K>THEBfO2ei+*&o=M!JC*W<6nx8}dzL0R*XC7N!`lv6 zb8|oG9dBWHsr|Hi?x$?_Rx*>iRR^3m>x``QPLqLMKQ!t$9_zU4u=v@M+GN!5o8pM= zk89!AUV`wG_@}^pY-;u?*tpgP@t^dTHT(Y4=DT-+0akziq$j9kftVU{Uofkc_sIpdQtO*jE9uyRYPIG3 zO#bkd9&Lfm_8Ul1%67or^bT8aU|-|CC=J0HxhHoQ7V9t`SLc=i(aT?O=$NxeN{NT! zv&-`l`cFB|qK~$IGRo;!dL>^vD{ZE}=D=S}?p z-;VqOY~6RKt|lX;)=tvAzS$^Uwl?~hd16I8Z~KmW*OINJC;>joX$^uUuK-^)8K0tm z7;!Hh9^6_o#|kV*_f& zplh^(S&;5zUr}1Z)W>-E!7x#Hg*MbUF&>5Y74_?zZO4wUj2Vx^q9B4jbLkW$$?u(u zG`x3OkTf+!thb(-HXh#~69p$xX*X?Ve9*CnZg&StY5J6s8NNDfrcIz_YO&lJnZQlY zogLl9x{=#quo(I2*F*LRTtx{id)F9JcC3pC>x-@I#2TcRpCjFxN%@I&a%_r01 zVnXtMyM|v6&5*Du3~zD#rnzC>wsG6T#Y5&=MiyFolZ}{bwu6!KUzJbLoM)2Kci$U& zwW8?y#UB;1m&b?TJDrgSzz|_()0bje=^I6gSjZ^uoFm{5-@38;UTKCX8MP8*wu4Wd z$R7^-N4_UvOoeK4>tFzjV?9y1_8vizBr^EaRhY?r`nfGZ6SN^x z8~6Q}&BUAcpJ#Wq%xp5NBh(;srU$2nIhlcC^UBOybpM7obF&0aW3z2C$5}LIZE&JQ zkT#^#?Tlpevs``!0_CtKi(ze@MPs|mTx`jljXv8pRW>Ky`S-3)bt=HwyfE3qWwj%x zn9fRAHaRhQjK<5h7dP%zOB;5<<# z4z8?;F7|q<|0G%;@n)Ef4-#dtr+f!}!f>PHYIBYq`PG5;YrE(d+R-t>gP8Ms-t4+*qVG9fK`jw68m+W~6 zR1qxdg%7EXFwmu*^hTWE%)txqj?)A^!XwjPq^eGXRhtcQ7#AsQ;WW`dOHe5EAZ9Nz z6Hda}u>r)`9Wkm@LJy(Rtzt$(6SFi$fwnLa=S20`;086CCcSY)2?sA9OD?$zMH3LH zW~A95m20@Q5p@6!pDaHeMSb~pz}-_lqdbS*0FGqb|T@oX6p%IspIZ+>~Jql zRGMu2G(|U$(p{uKWD>UJToRv0&=Ns{cT3s{W`uzjJI-|{&iAU?%H)fo!hkfa&00kD zJs9pDED=9VGl=qSfTDZxE666-C*k7T73Q-sHE;kwRLe6946g%LXlrDg$y$e+D#f?f zj2%w09V}2+CO4>Pi7u;c^u5WlWRtMGG)qtYz!4OAt>fhT6j_}8;6}rSynnrZMu`YT z*G5tGYI%-sU>xQeB~>!yJibri!=Ctcg)~ zwG(5t{U1LY>0(I6^3v&T=SUdy$>s_u#VNJ`?o0XJM z*q}qgt9X#a;!g8?-X^s%7qyRN3^!`1Drhf_s3B5)J4>bw0v?d*wSld*6T@R}41QTA zlW-s$zMoup=BzaIh%gp~@g;dZudI6I_KK@y`Hc?lap~BWR;1fR>Y2ft5qbR3KJt6xV^v)+Q6b!O5mTEhj=tp;lS7nd zt!slKP_@GrWUBL$wWycer03h z8TSv+!hII$@waq_b-`$Zomzxz4ov!iZ|_V5IU49yd+v4(nLJSLdNf%pf3?A-GdOnG z5k>5pO`**w}ldD`3I>Z z{&G+95G)YCY-GM2g}$H$r{m1PGk#?rn!a$c?a#qUs7`tm?PM;-aY9R*ny~ z#pPF@!uJ;%>_WF_h+6Z>RoAv|o}SKM+pnfnOxN6*v2283&p8ivKKBH7XMLA466-M>MIG@|P@ZyTc#UY=9URopi{UJ}(+;Hw^6e$H zX3-bAe8B^M$+RrbM4+6~Gs!I~xNCzvO-7r=&bU?R-I*tf&Kx9`jOZo}tO&JT9E_x` zCUd>f+XN%G32FGLaHr$J=43se24?Ewz&I$}OfW%UY8~b*GCs-)q71N5>c>b&>7Lv5 zgIqm@V#ha&9uGCQNiRpt~d;H?Vyq%5}k~LiJ(Uam} zqE1O?eqxl#aW0WM7*;3{KLe{8F7gj`Z3ICh*|*U=<`r*yMl*jS;q|7q2$EbBRTVDk zS+~4sP;*<^rDN>0`i-JSI}cU?eT~sW@E}PT1wTL??3#2?$)qPK#xhc^UOhabDQc{8 z>5il#SiP!R>I#W7jLT}2V?RYj?ls&;LC7{{dQv>>p*Y&{z=5ZCGg^V`6H!Sz+8&J> zO{JbO#}5ef%cET!dn>tl`+sdZ`nrJm`~XG$_N|x!-d<%*QTlyi3_lC6{R~UqElbDy zy;~0n!eOF5BPbe`{xQK`oQ<;psU#2|OrC}%r_$D2$To2Fx!+r3%c(2VW3P{*A}aJ` z(M4596rJ>eLMl2qsbBBMjrj0(!b&22HMDjy#EGdz}0q`(LMyjKpEAB-l-MQ6TvHJa0l>p`dkm zRFGQUvy=u=9AG&lIqy(~RK9K^9(<=IsmNvDfhPtJK|VS)(uY*2h!^sc$wi_49|sf$ zGbo$-52y&4to2d**hdj+vZLY}ZBgg@lsw;YfI$Qed)Q`8_tjHQ!A0OpwMr6-h>WXQ z=EU8n(*XQ&k8a%Wy-O!++|RJfto~~~9glq{FdbMsJyWs3AD1R<-oR+9H?C2L@QqlLNb;8h1D{uoO>?0LbgTs1-Y2o9F zX@*hMz~DS<{6pFK#mR|NrzHUJ$qfVXZa2+Y2mTB0d#GYlu3xw6cjAi@O{nJRiiS9@(~)Z*(3c zVdtTg1m7Jw9^N)%N17*8M%#N%z4g(dzF(y1#BtHF1vz-|9r}S7E#hKr)3e&ka|`o{ zcE`s|k4%+1L9)on-V|(nYRywYWEpHS@t`FK(K+l>`?^9xJwQ@+MYp@+*+jLv`}8G0 zl>??wN7-I1a1zqJZs)bp=lG~9of-4}g$gEkK}*t1+A&cre^7-f;VU zaGU{dReJ>NFFTd5W(=hluK^b!zJKJzGfeIE;Nq;w_e-DXSZv7@j&GLEf4A>p%r4BJG z$n=9WOCi@U-vKvJdiMfFyS7u$))#TUX~tpveUfL%s_<3n>Zg(Ih2=RNVC`f>;o9;N z_osVF%4r8;lRECXN?a;DqD&&LD`I`HnAk2qMQmFF?0Mr>igrW#d!j~HskyT4J5I$O z6t1*Bf3tU%Wm_e8g{4AP4C7$TcY*t@WS)5HJro`tMa&S2SK0Z+wtou`_w-@X`Noyp zm&(<6eKO0L`>%+Lt|plk&{V=SjDV&&H{n9m|bBP83nSHa(Y>fjckgC=v3c*W4)5|Xo0rSe-(Dg zIvaF)`vIf?oc(P$_Q|kE&_}2xI=)O+)x($umSrpsNcYZjQ=7l`Oj*se0wLP(J(z1A zA%C5#B3_oRLTs=k45@@_$I*6pg*Z~)mLjFd%4?n$-b=`%QSXdn#zQJOn2&OEsvsJ zSXp^Y<*xF&gNmm~6G1AYO{U%Mx;riwrIMh}4*+lrOmwgGmF;;R z&zO-M#`T4p8D&g-=x6HtjWM4k6dhYtbz z-z;>o2u!aFdHP_jiTTmlj|b*lWR9RXW2}#(VhfVo9BL#7!jaC6yf;Mtm<*}_@C9#e z4#<z3)&Tbmz@eW-cK~Wpy=NF?l(>Mibq3v8SlN1!VLOode`cYQfBXsn zAe=?B!hn1n;3|+g%xq4+KmBU1VGcZj%>DCEf936#8*Ef}cXmW4+IfS5h~hj#-dD4T zO3Y)&c=a_`=8z{c@#I@#!{xAF00sv(V$@~*UDu&(FOuJ!>Q%jbe~HCsJnhmvjsm3o zg8a-wT7N->5UHc90KhYg!zq9)d=4@CW53U1E{M_n5fGGNYF~iF{}2$Afs9Zj?;fzI zrhyE11e5hy&=;TKtN#OVQtq<`^}`S5twrDc87u^C+vi}fzoDA{H|S(J1^|$@94yG; z|2deZ2&~@{3N}JRnQ2LKA4k1JYrC<$kBq}lE+ zl=AwMOJU69Mc0ZuoF5+iq8{1@BM6O!?;pt=_;7bR;Vl6`AGZFHhkvIKrrdar{5OnW zfR#E<;CVB`QMS~+`qx7tG6<~HW0k?k;rsMQej(#+RiU7~fD9Yu9#vF@F5R#k4-cE2 za8papEWlsSMA-&LDkjsWCL|rBwD8Jk8I0XwZ_JDd73D?AlS-{0@dz3SRKd%#(Xac+ zv?1HmseA8EPNHeX05C&|UtoBiM`dv0{kLH^K~)YAR?@t>uPP?O`nm)mPv1t-B>2>? zWkrgef;4McNuoxcv}?GYQqiC-Om^5oIyyS^wvqOO-zk(zLB_k4n&f8^P%Pn93?bxA z24sg2h`rIiG<+AG>Ke|+T5_){1}dVQmEkkKm?j6y)EOHv^shP$Zc%YqQ%u-!S(4q5 zrnyD1xp3Stx_BVlt@Dht^p}0I$BdQ2BkC@GTg6I}!I6q2RVy~z3;9lF4~UijW2*0i z@mzA+k0jN)YopQ>NNzE?))*$EM&w8&%9Z8LKRQq229djfUJMvoj@S37i1wo zB7_T;E+NOK7Oeh3{z4D!LWv1NfKv`)Zedz?QmPKZth$ac%bCvrXU|>mN0B zd%Ql81yQt84w56I-M7{~khSmo@d)P3EpU6BJ>OsY?CsweRR4Vz*8dE5{3EaL-?j$? zumAoL|KE=J|8BSatL^U_S3FCz4mhI%agtuZ^i>YN9jp5h@Y~M1gn6p~dUb>Rnt!c` z?$#2%?FyrRjoh;%#s^>YjlG%I=J>2Ofwq`)V)ylogKd{{YZJhtCakLmiZ?7*o?xm_*Lk{u=)Nzo)`?bSTno5{k>;M&g zdJoB?;VYZsJkF%J(O__sHXvw%dk6Few>sQRTWz`O%JuO4WzWW11XMHCpQ}4iAHV)wUC>11V*J?7fh}*PdL%R?qFsyvWG! zb1r~nu8Ag3W4CmL?1odOc{A%fc8e_~kZZq|k{`XcooESU!=ja^hF`B(4skQ|_Eolu zR&lC7nQqJuvHR4#M;QWn+L|i+0wzDp1v03(qPZYIk}Quxrq}Z?gNHSL|we&WrCb zXD^f8d%-Q}?p)WKMJHTVf+uWzElxf7_<922U*f0hg#>gioJb?~Gx&~YlQr3%wIiza$0u0D^VF{l71hT{8Ad%AT#k>;(~Z;B3) z;bEwZH#VS~9S3Wr_wwf`YH&gN zBqvNos7{U~RHCRTloN>qDIx*_?}eRn-}ia$`}cW%zxVU}^S+$|?2q@%6}ResU@MM+6X#pRIWF(swtTqUKYO+T#wt`Lhmo0XKVDY-cAKYq1uVgQ;L z>51e?dQUfUgKr;*D}R)AZrieo6U;jsZJU-X|8(xH%KJpG>cn%03@&grEzTWus0bh4 zvX%8de{ceGv+&-Qb<8Wj^}lf`J)oIz&GE+3gnI?oI}a66`RK~#s)MNkAF>5j49Y_M z*Osd2uCw7CmYf&Xayf5v(1+qBfHeQV|0|}BRegxQm#Ix;WNJ4uye|g(tM?JW4K0FR zxZ#{ng@pWk2eP@O66t1CjSL|0KEc^Lx;A>e`0AR^?b^g$6?upMc(RmwGyrG5`es!? zU0ha+_Gs8jo+PY=7xQ7Fa4)8vAn3I(r-fAgAryb#%}DXx)4bzCNPM%l1L4i&EdsB9 z&c1%q_L-*dWy^kPmDh7~`ro^q%ig7XZuv5IUe$P2Lv%;eAA|FEC$A8Cn!!V9dYX1B z)sv;y3kK&>!@typ?aO}xh_Y9;kED>y#ut)^-&UPkNcP;E*ZjdG>WgSKi&I{iLSq~! zyrfNqBBL z)LcpFXu!F*F%@6S;d_;Ov|KSIl`^Vf@9F8I8&t}_D=QT@pLwQn%SKsADY7~3b;Vny zbNC&mIL*|WUby(<>wPLpk4#38?W0ynKOPBbJ$SiZ%T#H?NaWPziSdQcSo?ZEU7>U& z=U%Hw+HL~BpV|%Qu2Q;Y&j~)$B zQhH2%^#pivd!1{AO*nI{(zWjJBE;15MWH9IdC?`@B}x;ZT(F9Tl9J}7NWfURcMWdg zTFgR8^EXZ9-#Tg3l_ngEIO_9`Wcvf1CD)r{IXAt-YEEN!HLSe0;%xq>w{6P^?!=by z2`_l;7GNqKpK+zLttf1@lDrq^Lo$7z(!!~g8bzbvsveKlk!h`O3$H6bGLt14N1C{Af^o$!t? zpkQGq!Dx=L#Gj=m=y6LF16@)ED;!8U;l~1BRp*pvvdM1>iCa6{U+K@uytCf4sbrAKq6<9UF5a{DG zoNm~VV8;5F3@JqS?itm2J0(TWed39*EA9)?s}^Fvefxv6lE9t`pVeFC-w<+Fyb|bm zwd;KcC#1O6?PQ*n8(!viB6cmQL1d@;WHQ{kmi>y;qF#t1uCUQQHM63Qr{yPXs&mt; zlyh#HZQ(At^tfx2clu7QFp9cKxbqTrgYT! z345Ep+Xf0Tkab@(uq86J4{PjWkiWN+F?u0J)Uhup&lq)z#Pajd_-#bx%V#locUTwE zPIC*awi;D~-&(LcEArGjSZ&A5{nx~wxxIy78enAsbx_kvX*hX)pp3&0V-mNO>Euq~ z&-nP^ttkaqA6_NL+yXyQ($Hjs-R(a65R}Y<|DnL2X@)H3tk&&W;wYV0 ziC`_T{5Ws*C?xI<$1Hmd%chE1J=+4nB0(1@h@LE6;bl&FU!CZDQaIlwR7PRbDmkMG zOPEEe;}_&vEmCGyeF^dBNxzfW4-7k_s3h^3xq8q;VceNA^b2*WjnA&h(wI5f`X)es zBqsvAR3~0~@S-yE(_KBOn40JIrpe(a+5N&8dEBu)emkELIkNf0csA9OTR1eG=6 zbCt*oF(}9`v1q(q>{F8$H?U4eiN49xcfC6nl;T~fzkZ3ROCB>4DufEX;%gXtC$F*w z2H!JJZuAxqS>7YVD9?4~a9U%TT}hPrW8%51-_Bqu>mlK6Z)`D%6X{2E%hup zxFAiui`^30i3%OAulx`O>ALt9{tV~Bo8IY}K3*UUZM;e)R`Qr&H~6~?t)#KOw1B37 zCg=?_Lr3Y9<~QbG{7J2s4|N$dOq4r2(xiTBuf_=3mXa%q;UAwJg`!X<9hkYQU~k0A z0*qibNv$X`=Gn`w&dO9?MbeULn*xfVpR@4~Qyz3) zh{M)y-ZdkBI_{~V^!PdpnjhM?8*Q5B|3>sI%4ggrKxGV#NCsEfah`?K&wS!gZ7-J= za~2-1Xz$3LP^ZM=9!NyB4Se|Y#|l#@QW@~0FRNbQu$BXbGsW92E*p)VN!WVGXudBf zZ+6`t4lsGhgXaAflbz!Jx%kH>o&_Mp9oWIuo##%3n0ejE0=DxKy@2z>d)^8}s23TJ@8*Wml<>Fj;k9T#mvydyVbj)nJhCzi*W z#D3dr^b`RefenBst!W=HC9oj&Xr1UnYB=A|JpSypzL=*`Q|%DtQ`)HuWkdZ$uJtrw zgc#5ASoy(*#X+`IygM}uxFcaLbvteO$T~QG8#Sf|5j>(19AJ;PhZV~0d1EK|2#&Xq zeiF{X^9%TKFl>)iuDra2WF6F-?P#xrGbd1A6oMhoVUZSD0Zl(glit_oSC=uJI*nZd zjBkHxyC&?Ld&FE)SwwKn||{m1Y=1M zR%TX9umipJ3*&A3aQvSU%pDi zL4vqNPfT}0c-WoJz1!XQ z1UqUc_b?mqZyI#J6JwnEB2t%5sPC@15whMLU2$>$s$gKhSXfcteFS_wP21?9@9&d! zBh!^UFM9XxVyzq@)bBp+HkLGMAJJY8#bS7%DS zkb;;d8c^YKgz(rBK*xDDMNw0Ym-;vdVWT?RIm%Q^Bjd(XGO;)NTmAUngWff}eG_ry zsWXgnL>^l>o5U}$k@zRcG#7ouc%pb=>(!H@-V-BFLHN92Nk~ZNHswC&mEGkm*s81f z1k8J{)!VDH9=>%L)!TgRsD&~NzkphAH}GPnM4Ft!;-S2lfiU9b2D9L`QX}Kk@9?VrI?;-V+dx5d$A;=XCB26T`}`q%Hk1%Jk0BG5b%KHO@1|_x0ExF53ps zV_)WxFwEM4e!@#Im&?4&6kKT2Jf|+Tb5~uo-Ey3%pA%S9{50${Mk6&i#=OhoERQ(! zbnoz1rV*F70nd0?s)Fs|7^jnUUZY13qrP+O$-KD6a(M5yZHckx;udwY#($~ z7gm!b_M z!NzR9vhX%;<>TvMqK)tN0H~zepSex|$KFmT+n^biW;6LbyaCu1A$o>xNxC0!W_vA z>{6Ey9rUX!s2vx9nbsA9X}KdId7!74COp~5`kV`4Z(eDipS)s+yi)|HTQW1jy_KywVQLOaoKqr0o}L7}e1vC}j?evhd~ zrV_AslDT(97d#;rf0&$v)y!^p2r*^|T3n>gDq8`6c5))&JDKS_J3cUas@0mj$ z8;ZKEDkE})NNhqpX4d6FJZ!Z{)1NJ7Ma5^e-S&Y^E^N)P?qVZ(EIgO9T=b5BRCqAG)Q4K+R?F=GuoL2 zEL!d}INh76GG?-J0s}2j4``A=;G#Uj%Og>@m)w|K%M!)Rkw)^O z*%b=}M5yU5c@RgClI8~k6r=Fe8UYei2XFZPDhb7^r7Qh8a*BHxL))k^hT>Z9wc7}a z<=stmR+fti4D~W@QnoU^wYfoAuq6q z*vs}nw&q(De1M+a^cHX-Al}}(?C3NIex0?(84qSuiKwAiQXIu-8q!Vnvp>G9)y8P9 zFLpF+@JS_DBsYy4L1`7Dx{2|f?-JUhAZIGyV4}--krY`63=O^_WzKbYQ%E@=NBg-S z+eB%O9ira!ShTj$No$RVp!*H@%`NRi{U=6Fd$W#4=~6pDyA-=2#hX8~{9*aqXI@VI~DRRUG9~|qp!Kg2Ocp;J>vGq+PzndYYrM&wR8#)+& zi6!O=gWn|0mmPbxHR(5AuPddX$74x2wlRH1So?745MdMeKB|>qBL6LSJ&SYV*?c{s z&(yHYfSb9jbbS^GihMd$Stzr}JSifLdE%cm?Rphx@e`1YhRrGcWkc8PTD7J*Q8>ZgVYdcWVD2$XlB)V&~Lf?Mf66SbmgTy=!~s z>*@Hu-A0YIp&_J!&vQK^F;Uck6jI>SvkUL^pHm7S*{A%Hssxzh_1`^^iG^T!t|yr19pljJ(XjE&nkQqO z`*gW=*7AC35_I`r+5RfKk)uvhmrit%w!xgzW-R~;m<5M(`&R)E;1*$mvgRkr;w=}Y zkxonBLw!sp&MF;DEOt{y;y9xl-O(X@z*nBlUjnSWa^&hWbjEL8D%X6S0i+-*WQCF< znNWw)0NA!T!ZB_j?R>?>7g{g?%C41us_eOCZne0jGfA0NTQ)YiA6~=9sosF8hNA&YtU_u*;RNISlWF6d)%&u*i6R z2?V{sgGku=VOx{PWAn_D4d)o|Q<)(e*1BVsrZcLtXqkBYX>scsBhn5JuZQ5!Z{9h~ z-KXgVbLcyA2hh;q56PxYpub_fDq5AyBZqf zzL)QAj6Ce0yg7Q%)4X?juJ8Q!E!5QEN*m?%0W(uVBI8hO#?Qk@JnA>doy>Ob-69Qv zX~-7IfP5ON?{RTyQc75A15RC_d1)cxX&h<5_w1^*9>eo|5PH65W;9rV3tc(X{}j2& z0Pb=mV~EC(&BjgDlu#et#i3^&3U%cZDr*(pNRC>J#u#udKem!iXw0O&UuP{_puiS- zi+sxgL%Oa!+628QAD-eJIs!;TmQBi!H{i?-4Y=5K#>$ z;#SAON2S+K27KAb<=u-0zFQ3LEvIhbI^DvpbjM#eRD4KK*l3}TV`6ON3U@bp%1^*Q zL67$F^z2B-=2572Mo82i)}9~=~3$lX-mvC-xx z&PefgBS73mc^LZ~@)uvK3W%Y2!g+ti@y`m)#SaSil^DB5kNf1upp3Xr{u?hM7$ZlM{>QCO23%8!$EtJxF*-M34uGlo z$z4^YS~0hPT!7>SI1N2+Qf#4*vPz-PEnM`6E2`LYL|w@^%rJ%EU=TAb8U zU<`{7FD|*Gy}Rk(=J!Y6j*a?n30wcuGOG!;08_sxfI>h2{xbaaA8Qa^bQbzgU{+u17JK$JrsJjs8i_`PST-Y z>byobf7Bo1X^`Vi7wQ%GQHXykhA%?v8=S6T)HFofm0m|hp)22u%a3LMxla4(>Vitq zLKkFVI=taQG#{juceqJ#KcfF^DD+>Y{2RwND~-4*(i3EGNC*t%GLrW#uP( z3>l?!MDSV1#@*PS?hFuG%)fb-XWBS7se#py02vi9C`vOQaJ_w=c4WD7K)QTnB}}{e zZaG^z7{{`_l#g1d0u>Nt*TU6HXM=3ClGnuK- z=cYTNR<~p2QyQ_`qGj`aC~+4|G{ParahN_aF1Kp8>1d>vi@I2uc=8K6Fm1L6yr_zdSpj^K?;%h1$H6Oit?EA@kf*&HLT-#!vr zPNQuTCV*#>vZ)kaoZx0Ud724}T`1x;y3l`h#{&ids3GbSU$?N)a3UdDeCX~2o@WW3 zEOP>s)QGFy~OTGk{yVXgLES8lIk(kN6(AP{LMS-zN$e zcR!hws?=>iS}xyN2;OG0dp>WCK=WIphJfSLanEFq;oJecm~KCQCIo!tOGthJ<6uzG z2j^JrLKNlaC9r~EpBou z-p`1Bs(Y;fHh+^n;a#%vts9^=j?%YDV#$GGc9aFVDDLs`hP|701Ekm!|@*x?mbHDQyWF<_)we z?6gr>NGdZbue!d(vch(DP`1-!`nh@3sfN6}-*dKcYj=-1e2}*du)J#LlV4n zOz88Iptab!yQSI&lf?x_pbAWBc7BrB&g?#Tk`Z=Xs^*Rzj)Tr-hj)T{AJ_s#H@z8vyem}IhZ;s96KusfCElMyVv!wg)bY+*Yrkfzk}shU~4FZ zr}plU8&6f&uq}0wt&;d#NCqi!#{I*>J=juu}Cbl0u4--=v_zWW&wm%lL->tlc}M>+xP5ogRroXs8B+A9g!8E5rn z=^N1tg$iA+YA_CTqw^|oZZ~Y(X1BHmg|~C|NNQ-v;_$i(ROy7!P^$|(T)6Kn`!qJv z5h>f-$yQS+(guO3mg~Cnej~5AQbXB9$?Ihu;HRTK8iJ4YsS7;Y8`yl71~V!#K5bIB z5~->b>)H z#bKlaRT#8=<9Kl;0}$=QNqY^^X@OOPWgx_en|_$i0cA)fhykJnJ|2$VmNO6?-V?Fp z+&Ab&ickZezcrp2*I~MOnCAx+9o&{we*h`n0`^hUNKzfFiqvX4&t3;1U#jOthBG42 zTD%vVQ25P}&0pyzCF7*kYwGKUi+UYi;TwnxhShqF6wK#4Jtn%?!_s;AA@ECAlYyb7 z`xi)gAo9Y7eHff-uaA!-&Rc2w2wDWaF%zRVj{N6P^F2O4C`96LwCptovkk?JBsO4H z_U>ej4mUQcapF=Gpp*rmcFPPh7tqXEvBcJUOH#`AfWySq?&Oj}aMJg%Wih#Y#R|Dh zWhY@lyQsnf^vRh(c)jyv2^+tY>o8ve|0EzOhQ}k4zKKV@VW}TMQkFEAxj(;xTtnIU zx!|XP17T0RwQ@+@bvAXE+zbXgdF^Y)y^Gim_iDWlwZcqOFY`S8cN>tm-vb|>wh{3T zj7Hm=HYW7=KbsedwgT1bMSCJYnlmgebdl8L)dNs14YAIX8m{Yt&nWI*+<;LJ7-yp$ zr4BTdTgMgnai&W`D^JU~*Jn+udm9FeIenf;OME5^6uvT>@Lu+F`V;t>~vIZB4O z#&7g9q~MfiOZ>%nRE1t@u_>&?F!LWP=5~mluuru&mott>Pm3Zf18s8s%_gDNny5|g zx|A~Sjq{2ic@ecXGm*PFECDe}=JXviLeHzKUr$8Q>_rG*bnF z7q#-Ohbh)y0HbAYn_=W1C|)SrItDY@h2s`@*$w>#EE^Fb$n z>vi4n-nsrhqPPaWj`F*KP%E5{;1}LMdoDHyag+`Njk{qp8Eajr)L5>rBvf<9Gy665 zBWnb3kh`~)nf8(0Am{FqPv1qvnbJh@>DsGai8JvzqH1#_!_Wjc| zbhi;#5@Lu!d%OvFh|6wVD-cy%M%xRT;N}y>m1F+Z$WCr=dMCx$ot%H`6^kCFlHAs^ zXQk_>7*{2u5KLYZ%ewOOs!Alolk37n+|tdF9C&f(m8C68}qD79WA9GmrGcN=*&(8=9Oj>IU4} zY_)D=QB#7U?to6wCtrxu=l&U&K~n>6xQj0BS3Modk;2BwI%FYf-2AxD(N-rU^Uy+k zNF3H}F?3PHHMd_~)=g;dD;XKsPd;b%||J%625g0s&dy>-a+k zn;WLA_tAORakP?`V;HbeY8s+p2LSdnEc6@miv0m4!iSd~WD*7A$>K0@92OJ~1d$*V z>5RZ~Dt8X^zI&GRnZ-|&(7wwgA4Q`M%|<_Cf0aK1 zk%DxD!1S}BUfF8|byrj?SkYhg8`JeX*zXD7GDSaTA5b8?F&84WCC^=Wfq9()2X=xZOvlE^K@mH#>;w+ZT^!IA=$;0IW6V!d|F-9dJ9p<`{%PX)7&z#5N69M} zd=A9ad0zOkrJj96ZE-DrK2k_JK`0pu`P>S`a71^wSbNbv;<^?rIy{RQ`c>C~x^XTZ zIkHf8HQ!uimMRn;wFcsY=Qy|f4N;jrqcw4W7Fw~ z?nm_p69HH2Jf0H6kDyD&-Y_VfYVzcBh>ixwvQTz4D0^Xr@Gu$Jo;j{DLx!EpYP1?g$rRI#4`}dA`53a9JjPUl^S^S)H0&vqYz9ML3#62b zd2EvSWwLl-MPQTjyfcm`01sq`*zrVbo~I#n>D@9^Q+rHa4d3z!VFU#P=jMaP=Et*qn`r za4~kfgbdA<3I)H|^WZI#dMXOS{0;>8?DW2o_*CR!Izm)j8JL^1^{GQzLd0roSn}MX zAfTIOkIgwnYIei=miaP=jrZv61` zf&%q)8XFyRehYcur;GfN5@8;M!O6re=F`CTf5II*TAY6C+crQ2w{IYXnp9fHVg@3$ zyZ{1=R>bPxt>v+Bs)%Gg83a1rQPPt1UXB>oQWhl&`*2dQJ3Cu#c3N{h-e4cFQq}M= zb8=?i$obIekzj|KUjZ*mcP25`hS~f)O~{z``@CHeKaA4M(0vEd7=Thzy(n9A3Sg@a zT_6RqB;|r5Gvv9{8#9qJDH}vhD*$u%0dj41f%>5jYjpS6%eGF!MfgY7 z0EDQ8f~EVI;kSX6dUD;Tw0GcX9#M~M{ULG8|kR0*Rl~HlJQN z?PINrBI#7*Ev%@WmbC}Q^%0!AKn`CPsHHW%f69d3f|#X z`~LoWjJYj8f*wWM4n+JvGWr#o0D6TdC&g~EH4}rK`7O1-wg;jwm57FUgaYIdOSAH7319p2 z`mz>*j7#<)pWe-j_=sa;pHseB#i!-ViUF);5rffDS_FN5U|mX!4U_=bBe8G!kIz5u z{&@5UM79Wur~;1yRu{Eo5q?ox1ecV4-2c&H5sgt+B+dW~^}jR(;GIRh%l^-HKi>4e z>q!C6{cGPpy7(c@pF>&f;%`^}eQ^JN>|T6nKq7$$pHzclrP3v1@=G zIVHyab6Pfm*rIW~xsXK`lqW#eT8_NG<`y&aA`Gen!9`9|G{>zzeaZLiCrig}$B-=hYtVHcAYEadxFs0qC*;o{8bZ~3p-+zRysdj+(4@MuH--r|%a`k<|iHonP)U|7P+l}j^< z;8Zv5%YgT?u$jA^GN<2?voR^^>u)6rkh*oR=Vtb1j#FAyl$kf)#&@O=99r2Enynx9 zvF=j;6-9R8TO$qox^ZXN_aw4k(MOAqgl9HGJvq<44Kw<)llqo(@45gwyYF9EtJgv$ zrT}Tf9iUHs`TmPjQ{f6tfQAmp_hzqD1K>q$2MqMe)!T&^+?7>+l&j9=$$kZDvg-hK z-b*~-UV*#5LTRyj_M5AG`M!L!UjXvbW}hdx)AW7H02}ONfYQbi`O?SKjmrVT!fO1{ zC5>#ZsoNV|Zrmz@Sc7mxkTfV<=z`O|FRUj}72680-P9F#KBM0RP0ozjUeXq*#W8!?ew1%&x@ za}!`_l2o7q{ii8^*@4+0FzWEgL3I%=FQZP{S?TJb&CdZ?CuJ#p(`tW(CI9qKhWoXW z;IniN<<}gaWXb(_s;C^(K1!)0FdtL*G2SKs^CVRium7B6UVv8;)_y9Qd7JlIP zoJE{8C2hpVa)3Xe%w4rO*wxGab+A@DxB!YmJB#wkoNyVA9@sL+3f(|W1P)z#mIIsqg8~Sk`vayyS`oD6VBb(R$$H^o&=;fTs%B1PR!Y>Az zh)J0CZbun(&I>5}v;M>6)G>2aKa^GG#jh4+7La-TG)}fYI?JBdK6S%J*d{w`GT ztu1IC~?7S7-lh`0dVG5_}DyJBT#z9{ujL|sQlR-&Dsq?y21-2 z%=QY4flG7En3!GY#u6NBf_83zW~yX3seT zZV^=kkQM%PyjR=n3&GiA3}qcGdn3V$fy_O?c@Zwn1q^ z-e$bK)V#`gi4qVb7W4r@;+Hq4PXQRuKxglR;iA!okeym#V3)X?zF+%s08?V|FD=W9 z&Nj)oH=(DNd4%jaXxUJZ$v$r7=MVbpuu$4VFS=>wRUh`Zd8_i=Dff9$M_A|BUz~<3 zuRdVp_ogU@1gIbL7OU zM;3%h9g4$NioZ9bSFdKZY+L%{o05`MYoxi?XLgU*Ul)S2s{CK$AhmUWUFflMC?V(c z9{=lmaDiSY`_bHA9bPjwyC~M`Wc_vfm6N!bY&NO#*LTmSL4UjLS6@u7dlmE7?H&`e z8Yl(_srcs=uvrsy#~3eo!(Y!de_1@ci90-_Mz%rq(5PslF~~~vj@f@ zf62V9)ualr3u7r0kHZ7QmEQPdY9yb|uY>=rv~Rp_x?6GP-nO z0%{VFw*#A5%%=nAl8+dK-aNC?PoW4u7sT7=y;+yzJjgWfWy?|>o~-C291qyM#&oKI z4+AFkz9OGmcxjtfYF@0*{{71gR-x73{$Q6o2k9Gd&|E$#=F-DPR{>j7m?^053;<9D$=PD=XOkuH;Ow&1v zkSFhz{aNAxSP=gU-GqE=cYH=PnbVlAD{eI^;DAt)!|9NP7W6i*U!Xezz4jjr4DAPs z#AGqG{z0?Yl`K9x2kqgd^rWU}QhpypQgo5L4h~OmtEAi!8BZ9j%Rmx;bNo@NP*7-q z*;-_9C}=GT@{8xg+j?Jz1e9AANXeM3kKGi&A-ad7%>Zgjy8nPx8!y|%?y19p4%*W< zEn)+IBW3&+=*~8}q_7{-)d@pqTYTl+dxH`~EtkRRV<+?G z4`}XhRF^dN+!Gt+l{>iMdifBm;v+8My+$rta_yq;6rnmfWsOJ_mnKjD)DM4WiPF*d z`7=&750c5?Ll_mo}m>bf%(jksNAfKk_R8At1Kp=TkTBnxcdN$3}hn$tvK%}h9mJ*uVf=PjG1a{4zEAkH=Tg1C8fGLu?Iw55}UW22@ ztUAJdNLaN`&n!&T$)~)Uy_oPPOoERC!LKi5 zn#32nhh(0{#&e(8>E$)BJkN4wVnoXm;^hwmGkJdPzZ!dElfvXGy`~~rF!Lmw**HEe zs)r9^mi-;*T3o6hv>C(+dV(XZDHz&*=Xm<9eQ8*u7!2j}Jo5g)G{R)?@U$1$)Oml) z9!d-!w2kXeb4Pe*;#?o$Y$SN@SbT~5a7KLYllXP9L}AYgkxdo_N=})4XcGT` zSvCi~`J~b|wLN`utw9&2z46y}VHS^nTD~A1xj}ZLXOFAVQX>!XN{Q$LCWAv}W9B)K zg#Vxf3ck~o-wH|a+1MR^l(F-Ja!mW71%h7*hhxw-6f;8KD}s6f((wg2=(GDKJKvKr z>jd6|cE?;zI|;IBcA$2UN8MAQ1~c*-XNyl9?!6mxHyTlcQRR$d$+dcgq|LWsBp;b* zydHl*WUTrp{0h*$6pXkG*XJ1 zXnbT4nTW%~OBXI2fL|`u(@6X;phgF2W4JqHYmL^6(Xbk;WZP^4oQF3Ykz`>)_SCxo zW)>y6C3#UuiZU3O)wyi&IU!zM-}_<`yQCVqt1u|0ZYyl0v1XFrN6^vd1~Feu7P>N) zKb?8E{BMY?#UkRSuzVa@c#Pii6F)+kW_5-Y>D#S$pdNlJlJSj4S~Cb#3*(au{GLu4 zIwBdh%llXkon|MX%#MHta1c%u}zpwE0o390u1j#nRZ**aUIIAdxWaj1|*2XR+n7V+5v zfI^tN$iY(_6HXLw1B#EocKU@FYIRo7eQY(lOMjBh+^G}Ax$qkqA$j#d?$}~tpn7NN z2X{7Txx1IzA?8*oitX5lX?yX5hvr64r;QD6zVhNPT3)O}-tKOsE8y8-KJ>*?Y zGznFgzzGNViDU;=z0 zHFvb!V;iyoPjo%JAK^e}bZ;Yn z%rH4g2ZxKk@r%QX2FfRMvKkSEBx38)=8lVtj0>`RuN1&W=jZduGVxG4YDT>Or6d6& z=JJv1a-AF7dF>lN$6)Xdbl+v}Xh!F8m^vVBZMFi1HFb_i)3DI z;-X1em6Vr0?^-l0@v&ujto=Amoab45CXUAh)`^1de23CrHoBhmd1~c!gb{w3obDH4 zTnZ;Q)whIyu1`3ZK8bPL@coMOIw_PB%}D4=|CNs+6a8nXWG)&lg17qr}mu z$-39r%^rB$J17_uRV^9MCHpX9p^ewUfmV?GHSUl? zoSWzw>?(u5X2@fmK#!Ji_nm2NuYw8qluX+R6s3GT=|;eL?IK<=ziPq@;(3f-a`tOj zlK4V*8A?~T5VhI$U~Ilia%=|NNLKLq(n4+`jq`%tB(_ddQ2W*#5^rquc8$y|e%p4i z5AplPgjsAo4xv6`X73t|oki&b`OK!p!ZL8aqRba9$T#oP94%+0hdVGC!w64JZttM} z0j8aVqxp@Cs`K>v$+~wgrT_WP4w5~n5v~fQ9~6&%PKR3_r?<@E;XFmD z!k)y3%iU|6$fG<(yr-oufJTwfrz>6C7zprDI@W5ka{)MS`fEUnAJ}42IIYJOmYBFo zh9iIy9X%y@KONL-Cl*;iM5)mUM@~5YFl_n#r}7yP1akWLooZfg16W6k5`^hC?D$mR z16dZq?tl(O2eKAS>g*ipEjCSI7h)>Pp76hyXmyRI(>NG*)^?id{b8n2cM-3qlgE$L z5sO7uy7j5exdB+9NRzE5V0dm21gtkhdnQWGnj=pwfV=s{m&)8MKXyrHUONFKJj7`_ zXQ_4D&@nJ4yfWH_cL=PB#^KW0w~1-b&)rEIWxuzRh{RGZeqn}>c6c9cZ)9)c%*N|; zJqD%4ErP~kfNCxMy-*fHDMb0umpv!7jwdAi{_@JeG1zcqTkl%t{V$tJOjyMLdBKVB zpbr{`DZp%8q?EbQYk%h$Vn+RQVkUf_M(>~x1?cq$tt>f>!fZFmqzs&HiFb_gII`SB z{jGN)SOJiJH+~lO)%*%n5_Bb&=*H2u zfo@n}z9~6~*&I+jxpWnv7YG+REW)A)010D;;#d`B@ynPlwAg+>73LA;ft}o-&$0*) zPD~-Hs*scyXOE62U7y}~j!#`AmZJmo6$e;z5i?)V4+|TB-5*Cb?$KPj< zS!cvALz$pD_k^GK!TeGTr6-&-c^3ZD@Jzt7GPh3t@C`6|O9t#vPZo|=>qkN{)dZrr z6k+b%VUo(=4JeZUmF$^|CYAMcJK8&|2})9mN1r@L8zVabE>6*TH?LFOBSe53mNi&s$6K`nr{ussrj4#K=Pz^uOr`B>@DF*E9;yp5TVf3- z&u~WeFr9}K9_4=OtOIZqjZDE`%Z7F|Wht@^!W|*;^CMbv%FiX^c`1qF)ZYQp?XBmC z-+LPa-2v9fJ(kGn`c<#{^CJtBf%Q3k#3_-YyXYiwya;~79*(^?q4?1Rw`A|zvH_V?-9tB8zzbx@e<{#UaO~xCWXE)VE-W@I)@5Bk>pI(KpmWTa6ZNfe>m2Mi4#SbUV{$YdW6FQzp3*1jaO76A0W_`kCLMF&a?CicpDCg zf_uZ)xRa}^fyEwTAFXUYnU5#@y_%g)qJ8mhe{!LV04Nxu*KF{V41MI8J349y> zo*Zg5h)M)V#XHp6YD2e@RDO%_MrJ8USCarGy-*gm4;jil)|q#Tiliia7oyNk$Q(=) z7|8V&69q@+&UuLY;TDcieklXA$yO-^t?BQRe3^KgxYTV;F1| z)-y{CnVsWV8dje#Nvgpjx+R=4@q4iRDunXkIj?HM4*jK#mI^ujgmlRk%d(OdF#%2z zBFnDip=F2DVdV5HoP8fRWd~zZLwQQ=j>$!;fI_3U1I9oMxe#Oai~TKlV&Cbg$>vH* zvRJLtOw;y#D7<|3Ng=8g*de}&EM~|-$@zVGQ@eg=Q{UIC*1diOoajbp(V#Fx=pUH~ zc6@AsAw~|F1CH^=rthOe+Ar04Jy}8;xQ>I37CN-`7KWOXOk2aIZ@e)Zm9qyc!vV{} zn}BT9nN@JQ)X)jZNaWkOJmN^Ku>~b6XHtF*yoF|3wPkOj zB+A`>uG6LMLKJ$UZth=^6w6_cb!ou$hn~g+)z>#9iYg7mFk27tAxhPA%3vw>$H+0|XcL+h&G z?vUA~2V$NS5v>uK1Bd4Bl{D&cRh{Xe4o^@Byul^Ngo{6mLRa0wk=u3R>YwmGoH48P zF=#{r)+v!E;9$^@;=uDW!(^W30140qpfl^`(3s9%ue+wAa|8*<0j1IAtT$nGz(Gczz_ED(#`p2uy?eFjFNq6{N%-K7HLZn)Ar8}9YW3SwR@PJ_ ztMm!59g`qIN2kWlCjb|qO7y*+4%yMy|o=0 z9Y_)s^=eUp$)1N>H<%hlCO5w{%B)0FnBV5OxO&nMM9$hn`nxu=f+vH}y@?_8J^SrMPGe2LUD| z#&0%`c}dvQhGYD7}HuEEC;=kw~XVm!X62|i|IixPO^NXK9KAxYt^n*9YifQFDTCesd{4Z*8ma!+4Jf>QQx{Y~ z_-npn#Cang$`3*6QRclal}VVkIk>#4(0xqvQ+K56CVejdSLJBq)X>S8XsGbiH+-y# z)d!=~SK)*HmCy1ZlYIn7slOu2vti9C*qH*|z0#R5*Y}NbYd*A@g@o{>J zlBgOaa=Hlx(lF0id*DZSIxZUZ?^9hb5X}T}cqtP?BYVyyK+a8(O0xJ}C!Wb(w-uOs zhH01h1WeSMu;}YhRpku5^UsVIK}fI>Vddh14z~M@JIyT8g$+H=e}3uych?Jcw<57e zbarwj!)Oy|0+7rxmEk;d6Yv9o7l}8_31343V7}T({R&raN?;s{-EomQ=eJf|Lvv;> zzZWkP!`ZJN0il~`?iBE|B-oI?Se9MmCSV(Yskbsx!ke7^GqDjFb5?P z<|tDnfkYBQNJzrn2W-E-U*G%P=lkn^_dd<@=o1cSpS{=G>#V)^Z~fMSAt)p=^)<~s z&Nha`keT@qpG}*+-QZ}g-6odtk*B-!CdK;zJ+hv+S*E#t%=ciu?{B!t<)uJHCr>0e zs2;1iQ}ivGh^ct^j_REV9j$h-TcTv6rjqOsaqd&r&0j%g1ko5CcL8}?YC&Z4NL~sD ze)s1~h8~T>a=^*p7MV6G{lM7DhDdb>=rBR7X)x%IbjfVYN~vnVXpb4}sr!%p!LEp* zT8H1Mp8UsN7mS12Zu->g=Nv1Dkbt8lCz*Deh=el{Qz=#SSmuMy8W51Rxu9Zmf#Zx9 zY{4fXv0#OPpP<(_-OW&GaZs_~M=dLh+ES$H(}!)VaHM;_0;~JfX}@EkeT*1P)UdRJ z!Aoe|_YsfC{3^9)(lKubFm-=32gUE4pt_yk~pz~w;(UR z-ou_8V}9PVDzvGurBf1+{3Dg)-uD64?m zl`&pB5364{SLvOa`@T7_iRf+^F+44uHBMBawYb41OiD>Fj6f<(cdxREm_x086ZRu-9}{Z@Z2v%36W8?arC(F=Duu}&ClB{& zPbQtb9ekvU);lKFVjJ8AWgXt~tV~W@$cxY42}~wbbrZ$vqgp+J#1NTyNMs!4j zdlz`%WB8N=yDQR*B==GcD7y6aPK4#b1AAA<28JlCAQ6<)wQ9l~ zVm___x=k%asMP!E?7*eAUMhae-Rj+Uh|Ai}YGFQ5S#x2JCCnhBA)K-5f}M*;fv;a^ z>dE)iG}pfPJaBJ>Oo zC94hq=l272*0$Qm(P>Kl>5Ag}vo3wE9N0M6YZtI3O^7+H%@{w?x~Uw-q!kLK{ocLWUsmso?A~L`)p(EF*z#k`%Ev^? zp;IR;Dtz5K1ED0Y)hStWDBc5q>QQ?lH8?J6b;~8it!%&n?K-l#Nb`FCj0va}_}u-D zgw)$^=m69b(A6`Ywsxv)^ro+n~gJCc;CO@zO|<1?RZsdEcK1& zk2;#$-}-{1MP4}m8-6)!bX{GzJ;-`p>O0lGY*5 z2Hoog{@Xg(!Z_w4&88(uW%Al4BBe6 z(i&m$F}~?1tFAn?oH1mak9+LTp-?XXm$$AHSOB)?i3F05ZgOuwx2iPO^kZk>FVi?? zqbQs?dcAh0KjluLLs$Z`VkdieUrlzX1h=`#BXYK!cKQ^BzXak5q2V{iZUZWG-@5G_ zzg-{+LDt-Ilw?Gyx^TA9AqdnrS|pw-Sj(n%Cb1oiyu%y0Dbejag6-k!xEV~Lr+!gHOB20y9yBr&;y}l>pxkuYIYZy{6o*GN@*1Y z{TH{thvg8@221;lUm}ylU_;<$eAVsT>`92rGE-5(N$=*M~F)V?p9CUg+dv4Gv+f`}7)Bw1?z@z0^fN>(5>byqSE>H;4VYhg8ye=;lOT&zuiaiI@Wp5XknmZV+Y6n}H0?*oW7DgVIs_^4WtmJ9I z9#ew78jcqe$}wBO?EOhtz7xs{QK$iM4cBPPWP~Jsj6}nzpWZp(T4vOXXyTlDcgz%= zBtRyHLy%|BBjA~jGiL^et{s!*^jAi?@F<~o&PyV5)KltEO2?>7( zIC+3gfc$GH%KQhPUQgTufqna@}u3q8LP&dFHvsx{fS(rCVNxtG+e53=&Ef;%QSO{gu4AUzsSt zBgT^OPGBJhO*E)^r~g)6!h!A?Q9b+7rJw?&*g%8?|1^l14jekbn!i-LNFm!Od3!QI zya~((z}Qs)Yah@r{)k|aG|eFj1auKh&bAdqo>a)#2j*^h=r4UUN%)U$3mFA~CIAAR z)~j_f@@W>h6RC&NfIeWZ) zU4S+n6vY5Fb%*pPTac2$RH)rLmC<6)*}7L-(1cd7qYQ5(QqDye?=V2cB4P4RPs9=2 zmyeRb<{Y+9$)^vB=AeFwwBG{panfK;G>zp@MF+FtJC#S~_yP|x;K=W}77my=T0yf=0BG*cjm23b!`ZSRne{F}&GhdoXsw(C1jLxm5HBrno%p>T6Jej&%RC4W)Q^{q9+?fX}*)tWDWQ)ftDF4x6uL zwu3OF(j~qU-acx|dHyrCA-6Gpgo+Fso`N!3AvV$`2Po^XbEGK*aj0t^lt@u6(g9^< z<{YJ?&V<7I(UTIGzBOPvE9n1qz2|?sh-?3t{=<7P$18panqPLt?8=)X(6-|bdi>SLMjc?eDFVd#PEga}yT7v6?aP2b zUqufPzIu2hy~ydW9QiZ9L+`|y3Ay6)%k1cMP-*!;DC|ytAdsryDe_Rrb@HB2{wq=b z;RV1iTDqxbe#sB%zzG1a@+Tet`7yQgB+cjDGL@_2bX9Y(8~6{ld~WYQqWv>#3JU)i z(N*{tjuFL}S z_9{qqxmxsp>8I!OK0N%vv*s-p3PcnyG;o!&e^PA0p6<2r`DQL(U+Mg2`Puu^2RE%h z+wn$G)iwHj*2DcqO&!(;zu9Dy;Ini1?%k{F&XZQE9(tI6%5~*+?RAwmDg)1K)W5)= zp<@NYD%P>%&dyifi_%%wV28PNGFArnaviVGk+AIYPxPe>6mWXrqoFK(eg#bj1^N~C zuRvM;>ZUA&E_WP2E4iwmETQlJ#qH1Z=6Vl3O4``13V7>}y`t%~Qs=^#L8B(wDXbb-UJ+kpVr>Et_<~Q!hrT6NbChpr4o9p_TGqdc{)wCl)B#H*(1< zPJG-$Wtz+^KN$gN{`(F1AMd`NtV~qsnClKM{>bCxstCtcYIiQSp__Z>KOkvlswW~8 z_bRN!T{}Tv`Xt~aRa$`ES}4 z_qFP?MXo!^q3tJkp)B{C>ftt?SJ)(3@)Qipr)$diwEo(E+i>sXtQvK2zbx?0DINA# z%|DNq>|gr4L*+b>^~cTy+33iB(_a@>8smw{l$&}!1v5ALY}zFg6j)I#1zIc{9&5kn zUiHnZhhK!SBmRdFeZE{hw#aScriQel6Oo};15QfxIuw4GUUmv7v-d=xH<~h8{Cvw; zbQw_p{a5e2uPwj&2;LDH`oV03%1oh)RR*TB(^IZ^>@$irH0sibuyE208Oio*}04xPrbnwz!NGrUSXZ*&rip~F=Nq> zRD@Fs*xt=&t+NZ%k3^a&3palH*_~tPu>$pbtof?lK)}E^KQ$Z)oZ!^kzK@`8GTx@m zUg1KA%`Vxp=V97DZh*QFXV}f^T@EH zXiA?P_VL>7ASyFNJ~lh*VPS}$L=siv4+>tUI>r)%*+o+}yqdfx`B?8d(%=%yG_RSL zjH@;<{CTM(VZLG38Lk;e5b>k79X*eij8ssWvUgd-N`y~`oL%A;dwm;JlRe*vgQ+p4 zgC)Z}ihO34NDzB~2JU7!tn?`hZz3PLp@iKqbLrqubR`m**$f-uZXPg!vj}45yzaIQ zqv;}dOZY^pdEAQwXw0>OzME9>9IuX5+pk8f-wJ*Y$J80gCEJNtR>m&$x#_wzkXuWi zs}RYZt#w;Ek)gP7#-~fhTOS%O2e~G*=vksnvf@XF_{dIlmpRX2;h|_eZhkfawrd|s z_lS)PFOQ`MsritDzV!YX<&f_j;zQSCyDRE%0e%6QTv$h@Se|^1HjcE9m+tHqm+IvuJN=f~dmF=P)lVw2wFu=dOc7g$iP65`}$C0VT_f zl=Kll`)8DGbIO)m$YbD=qF_|rve2uY*W1ZHxBPz%s|8e;km92ad!v{;-Z_ z`RLdQx)S)y7o%2DOPtPb3K@HV3C(l@bG{*RB^s!@#?R}JmcvoS(B$U(s$size?TpFp@Z7Hz>T!29&cv? z*3ZWV(4Y>Svy_F}Y{>l9qUoN%17(3z8xYM3UAXXt7bN_nydx)1>{>WrZ3az0b+o?8QuhbnTeM# z2sRvkWOe<8-QWAliCt&J7clDsau|XVkNFQ$VFS|58D(jw!*)Cc?@StQVt9kOmVC!g zs&H4E=Z>n-*3WZO7Ocp0QH(ZKpc~n8>Hc_*GIuz5!{bQc|Wr+I6Je1;y1 zn@|?&)sc}tqxo6Skg6ZW zoOR)~BbtaCEvh(bImG**aC4q(eF%C(xX-5fWXM--5UO2C-yjT- z`!^{{W<}b~+b4$*%`-u7Gr(#(H}18a82>s6aeJjAN{2T7L>93^6uV zo%Vnf3*NT+&!l?ZC6S3_EFw?S96s*!h*up!!vBiGnWRgrPFrFE^Sli*f)2^LvS%?$ zW_eDLBH1~Ki{!Lr+-@9eipun+Ey2xAA@HF?zuwHLBjLZr)=&qApmkA&`{~5jNPnuM zdTVI1baT5dy9Tk*fA5k0!!V{^vdB*|8@%9kHwYPKD$JDDl$uD%&pWxl(iEo);?Vsc z@q#d#yLP*v7y)Nzo#%y`qjRYm;&B>k3NeX`m>w{I@f^dgw!k?!J+NS`ymy%~bi|N3 z5E&_Hj+ETj=5!La<5OrK^+GW5zhO=ks3b~b_r4hI_9d?adE*Z%cmy9!3pMKSx4=OqBns+)7zSo5;`EL5v zi5K`iA?A5c?@`>kz+W2)=I^m-TVW&VzhL>sZ}+deeI;+CmB)K|`9uEhXoAqwhm`3^ zFd`i7zRV59OYNj#W*J_CS_y459Fe6rGaEQ%M7keiIKZmvL=rsSseX>Kc;TmeeOREo zfi+c~TQ@MxdgrL?T2AKaO7rR*P<2%C%h9-)tW^XL<2o{ID6Ob~RF4Zhq&rywUJfnp z1>P=Z%{`S#biYR~*)ifeLe?Xld`-!#8675;v_FbN5-A1RVw^psyDmKT+LFM)NQpMJ zlNj?3Aq%Z_l?b(m1aTg^m3ehB!FB#g_^`Xp%r~WmY{7M7x#j$=g9#cOLF+E&gf#3m z8sR`&i(FJ)A-xtQI}tclAgtf#~ir$#F+5z(Av?FYDP!4I_u8o~E?EJ05mBf)X@}){llUHRNto z{$8ipu?UhybKgv#&qk@vwOX>{>kw&RS6NgAkutFj7A4)hxa9S8gR)#&2BO5DdUvsn z_lCT+q)~T181~N#!}qs(0nENYT3=r;fl(VuX+U;hwI|Sx~!$;<7FUBF&;GKvQ zxo&5us(f)v1HmRal_*2dOa*QzT=;P2`0U{u7UPo*b0P0Z2-#UH*A>K9(Bh@+M29~q zb*5v^@b8&(b17_9ksxtG>?RezH4-OK+p9|`Jbi-q`(5k?aVIu(1*e?*S7`L#kgRf*r>t^;qXxw$fXxOb!)T{XUwOksM_#n zU60X~F2<)nxS`t8^kN4vsLA#))=_+h^spnpgk2FDl%dQ)g~bZffGAkOCf2vv|>u>o5})2-<9p71CqJ2Fpxl33Vb z1$h*f1(?-WTk#Z`!`*WMS~!OB*U$zqV_TGD#zD6PSPlD@iV)}6TRlNS#8w|>byURt z?vrcLG;u{{X${1aTnDc>CF@!HdMdS)7Kcgd1>?Z34rjEq>x$-@=fFDS-oR>#YZwou zWR((U@N=%Nc4#L4-fovmas1{Tgpa`p=sZZrF1s2I&DGav?f|4g-zW7 zZyAoA)#eBq+@{B4#RW$#k+)dISP@*Hwe$}`T_O(F;j$fSK65297r?P8Z95(%sV9Ed z$fOo{_!8Zicu?Vhag-kCm1A0Y@aOtyKzJjK6=UhdHjs8!H~4l0XX)pbFtB0wm7Ede z38}=%XWltdB5vWiMAX#QaLs6TpF?=PG{SB;J#PncwH9T#Hobt6sxmHSS=Y9szT*`X zTc&FeiA*AqLmX@elV^q?r&o4~Rb?6WL0ra^KOl#75fgQgDf+~5@hQ@)+tXEYDq z%vGe8-2diiIKG?{OTvdZ?yPAAEneXU&DnyAPAoMfqkhOTlB@kbD~-lGLTP2$dh}$t zdxZ9lQv{ep!0>3aI*eItFF1zXIp*BX3u=8CNnd5BOC8o1g?NljGUvq>EeQP1?q=@NxpVGFqZ#bCm^))(rqjZ4XKd!0?P@Ph$_6Qoia?E_J?dHTWR z_-cvvyUrqGbQeufoFqOu;+T>OE8Cu7zwu=1arHn$;UrM#u8<~69(8AIZiLovZHi(q zW#Lpn5>7+hGc)Y>!|;(_^>_RW?G5!g zct$w>m^~wb{w8F4is_#uv4GJEDAZE(2?0ZG+1&9+$uu@500Ey&a-*|sW*dyv!u0{d zMv9WSjMkFsua-t}U9iP0uh9EautOd8%QKe_t9>Qt*MKqi+G30|)EU%ci=Gs0!z!)n znqnzUypwUiAO_krZ_0JK?1-YAu1}uq4my3*^nQ_~P+RdHu zKZp$s+`Mq|-wK`UvJm%}kC;NELXZx*uenfEi;T%6ujsA>tDyCV^KUD%$jsW2(J$e#Khk7}1VF+RWT~3O3nr5{LzcJos z5s^>Q)FzRMqDlk`hl;pv!37Irux3s$BYrP1tWFK-H$|BCw0U{nZBH<20t* zC^%%*o1x?`7T{w*$FrMIJ}~c& zJoBEcHN>EE;p(XMgq_8$asIV~Nk^DtK`Ps!#-w=qk`2QlfqxTuCDSx>Y03m;L0-;rlV*>BkLx6cL zwoq7rR)Em|a^+9B?N2EH#AVoeQ*!1W0QLYDW88aS6l5EFX&Zcbus)`I%l&;&jEa{h!IF$bF1*NzKUz|4B>kUSRrq7vO0ub1L0^|OU zah=Zm0>;Ide!=iO|C`MKlDL3%eQEaU`k&C>f7zSWf@WF)vI8`5TG{oxF9_br%)cXe z75f)a%VnD(kPYbm;UfI!yntvzGAfq-6)IYUzBZm;&{CXWcWI zQ9oawFG79kb`j5908))0E&mIU3gMZMEo?j=xO_pVu#0;kc=PN)$j3IV-N1kpr=J&0 z%0z=%i`d}8BjQQ?E{r@IAfYRB$&}CgMF1%yH7Kj@&j2{S^Z>Up$sM4A)u;NzWwMsj zg*pbP<#&1b@wZZ6_9F-?X1q&7$0tk|N#z{OF6fuwJ#C}hB8vB%ZzlsV%mE_+5rQn} ziJHOZjR~23q8w)kJnwya*^eL`Dgreh6~xn8hzx?mfWWI`$>gaNL*0Ofh(u$GC?EP| z#5|*<@FTT;Ws2biI6JPhPx=Wbie`rJj-zsIUq@gX%Mu@Gc!X{M0gvAY>IHhu<~wq2 z2gKtE%uC4jxNz0d{WXbW(UB9QFd9J6G8>g2OS@BR4q@XM9S>3rPo72-e*QH?s^)R5 zE9zJZ1oHEcjRTFakt4XC;{YCiQjzMxG+AertaH>G526-Oq~exfe0Rq~!wco_&mwov zHm!gm;C$tjZNgD#?{iPB(cKcoH~KDwgyRHlJ9o&D0`8~=()5Re!c5N_Kr zfP&Euysb_cEUFueKCnSJ^3g!#dkb80gzNOT5vxdg+C-5wX zLGZ=a`n$IK)DZ8^)6i2V$UF`cU~g?4Rt)(6ee6uA=QrZAJqB?`hj_ptBzj+;k z_iKyg-HiS5?7*&IxvJvcvFM+>F`<{N*Tnxmw9{&#jr7J93sru?l`Fp~C@6LvwmxtS zcVNw9ttlr%n}p)kP++1mNbA)@YDj!_z@8o$6q+#bUfDErmTD?A*>ZFsX2n|vvjRq% z!7K?;u}}OV!nOyB*t{98wM{R24yoYRso~w*yr>Q<#-d}2d_d?{wh(%Jwm;QS;dvT* z>z~2Mf3E!hqK!v&P0P%ZrREB$obMH*)T$N(+@As-LNoqH0XHZ_{y*wd)*FV@N|>lp zS5TY$`>_3Gs|RVp_m(CtJ(Cqv@~l#Wu}&crruLv^Y^A~v-uc)54Aes_kiwaE^4))~ z0NTPo+W5y376~l>H!NZLpoV?%aDc+a(i?hum-@=SzB)*KW0~{{I3e)b{*U{d$th>j z+Q0A9U-A4a#uTrPq>TfRq6{)#+7nIWSBAXd{m+P{4kJ*x+u^3MR}q*d4Ez{ zpiQL0LqlD_V){aM`RB_23);A^BD4XM|7Nob#Cm$RE$qv*xoRt%GjFYTCswOFC&D{` zfha7+_E%nOfebmitk-ANT66txpC2-7sXxK1QMaqhxDDM>?x(faTjBY>Hr4G(XXRUm zy5@QU4l5WnUm9-D_cFie*6Eh?_0?<5aaNXrrmfJTb&@z!MWsSdLXb^o&V`ikWbBzg zRED{#@Ixu}<#1QpM=oZ;vbyr!zJ9%Bt@m%JTis zP3TU{sl!?saaY`!317J}H5e7u9P?~d&rgP~{pufLF@d*CU!{HY?-1_uerO1uxlO;j z$nnh5-$;Y30@^^#Y5gJJ67#|nbMqa^G=C2(ybJ@hCb*8lv^Ro~A9rqPbepFV35pwS*i9kDJrc;@>50^DEShX4Qo literal 0 HcmV?d00001 From dd9d25ae5e982952ad9e1bf11f071a4799b04b64 Mon Sep 17 00:00:00 2001 From: Andrii Konosov Date: Thu, 30 Jul 2020 15:21:09 -0500 Subject: [PATCH 209/479] removed search --- design-documents/storefront/pricing.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/design-documents/storefront/pricing.md b/design-documents/storefront/pricing.md index 021c0d664..826f6d9ec 100644 --- a/design-documents/storefront/pricing.md +++ b/design-documents/storefront/pricing.md @@ -91,21 +91,38 @@ group, only this customer group should be present in event and price calculation ![Integration option 1](pricing/integration-option1.png) +Pros: +- Simplicity +- Use existing Magento EAV and catalog rule storages + +Cons: +- Hard to reuse `Catalog Rules` functionality with third-party PIMs + #### Other integration options __All calculations in message broker__ ![Integration option 2](pricing/integration-option2.png) -Notes: -- This approach makes MB stateful. It will hold EAV information that will be used by price rule calculations. +Pros: +- Easy to integrate `Catalog Rule` functionality with third-party PIMs + +Cons: +- Stateful message broker (includes EAV data, catalog rules and matched product cache) +- One more copy of catalog will take some system resources +- Dependencies between asynchronous tasks in message broker. Not necessary a bad thing, but definitely introduces additional complexity __All calculations in storefront__ ![Integration option 3](pricing/integration-option3.png) -Notes: +Pros: - Storefront will handle `cart rule` functionality, so probably we may reuse the same services for catalog rules. + +Cons: +- Storefront is designed to be lightweight. Additional functionality there may reduce performance of storefront. + +Notes: - It's possible to isolate catalog rule calculations and move only them on storefront, other calculations could be done in MB From 4271bf1fce1165da0c8b2fe49b30334c3c003ce7 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 30 Jul 2020 16:17:22 -0500 Subject: [PATCH 210/479] Catalog images tech vision - added image transformation features by different providers --- design-documents/media/catalog-images.md | 51 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index d7f0f1785..9287bf627 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -56,7 +56,7 @@ Detailed steps of the asset flow are described below. 7. Step 11 (optional): Origin (DAM) may perform necessary transformations 8. Step 12 (optional): CDN may perform necessary transformations -#### Asset Transformations +## Asset Transformations Asset transformation is responsibility of either DAM or CDN, depending on the system setup. Both services may provide some level of transformations. @@ -71,6 +71,55 @@ In the first phase, this is not going to be supported. This may cause performance issues on pages with many assets loaded (such as product listing), but it is assumed that production systems should use CDN with image transformation support. Scenario with no CDN is assumed to be a development workflow and loading unresized images is considered less critical in this situation, especially assuming [Images Upload Configuration](./img/images-upload-config.png) allows to cap image size. +### Magento Supported Image Transformations + +Magento supports the following transformations for images: + +1. resize +2. rotate +3. watermark +4. set/change quality +5. set background + +See `\Magento\Catalog\Model\Product\Image` for details. + +### Fastly Image Transformations + +Fastly provides image transformation features with [Fastly IO](https://www.fastly.com/io): + +1. Convert format +2. Rotation +3. Crop +4. Trim +5. Padding +6. Set background color +7. Image overlay +8. Change brightness +9. Change contrast +10. Change saturation +11. Sharpen +12. Blur +13. Set quality +14. Montage (Combine up to four images into a single displayed image.) + +See https://docs.fastly.com/en/guides/image-optimization-api for detailed supported parameters. + +Provided features fully cover Magento capabilities. +Watermarking can be implemented using Overlay functionality. +Overlay must be specified via `x-fastly-imageopto-overlay` header rather than via a URL parameter, which allows the server control it. +To make sure UX is acceptable, the workflow should be described in more details, taking into account Magento scopes. + +### AEM Assets Image Transformations + +AEM Assets work in integration with Dynamic Media (DM) to deliver asstes, and DM provides asset transformation capabilities. +DM uses Akamai as CDN. Does it provide additional image transformation capabilities? Are those even needed taking into account that MD provides broad range of features? + +https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html +https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html + +Watermarking - https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html +Scoping? + ## Risks This section summarizes potential issues with certain scenarios. From 94f8765b96d862c43c6dab840a00f2153361418f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 30 Jul 2020 19:30:50 -0500 Subject: [PATCH 211/479] Propose changes to avoid dependencies in GQL Schema between QuoteGQL and WishlistGQL --- design-documents/graph-ql/coverage/AddProductsToCart.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls index f83e2977b..329c82e8b 100644 --- a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls +++ b/design-documents/graph-ql/coverage/AddProductsToCart.graphqls @@ -10,6 +10,7 @@ input CartItemInput { entered_options: [EnteredOptionInput!] # will not be used in deprecated methods } +# Place this input type in GraphQl Module to avoid dependency between WishlistGraphQl and QuoteGraphQl (And possible cross dependencies in the future). input EnteredOptionInput { uid: ID! value: String! From 797443d44348796821ef42b03403c960ce526935 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 31 Jul 2020 00:39:14 -0500 Subject: [PATCH 212/479] added rpc --- .../catalog/product-options-and-variants.md | 214 +++++++++++------- 1 file changed, 129 insertions(+), 85 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index ca60a589a..1b8673f0b 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -129,6 +129,7 @@ message ProductOption { message ProductVariant { repeated string optionValueId = 1; string id = 2; + int32 weight = 3; string productIdentifierInPricing = 500; #* string productIdentifierInInventory = 600; #* } @@ -146,38 +147,11 @@ The example proposed to show the relations and operations that we have in the do ```sql create table products ( object_id char(36) not null, - name varchar(128) not null, + data json not null, primary key (object_id) ); ``` Table `products` stores registry of products. -```sql - -create table product_options ( - option_id char(36) not null, - object_id char(36) not null, - label varchar(64), - primary key (option_id) -); - -alter table product_options - add foreign key fk_product_options_object_id (object_id) references products(object_id); -``` -Table `product_options` - represents registry of characteristics that allows customization, - for instance, for apparel items, it could be "Color" and "Size". -```sql -create table product_option_values ( - value_id char(36) not null, - option_id char(36) not null, - label varchar(64) not null, - primary key (value_id) -); - -alter table product_option_values - add foreign key fk_product_options_product_id (option_id) references product_options(option_id); -``` -Table `product_option_values` - actual values that could be used to customize products, categorized by options. - ```sql create table product_variant_matrix ( @@ -195,46 +169,79 @@ alter table product_variant_matrix Field `product_variant_matrix.weight` - says how many options should match to match the whole variant. The following script models data from the picture above. -Starting here I will use fancy values for primary keys instead of UUID to make further scripts more readable. -I assume that the human eye cannot efficiently analyze tens UUID signatures. + ```sql -insert into products (object_id, name) -values ('t-shirt', 'T-Shirt'); -insert into product_options (option_id, object_id, label) -values - ('t-shirt/color', 't-shirt', 'Color'), - ('t-shirt/size', 't-shirt', 'Size') -; -insert into product_option_values (value_id, option_id, label) +set @product_data := ' +{ + "id": "t-shirt", + "options": { + "color": { + "label": "Color", + "values": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + } + } + }, + "size" : { + "label": "Size", + "values": { + "m": { + "label": "M" + }, + "l": { + "label": "L" + } + } + } + } +} +'; + +insert into products (object_id, data) values ('t-shirt', @product_data); + +insert into product_variant_matrix (value, object_id, weight) values - ('t-shirt/color/red', 't-shirt/color', 'Red'), - ('t-shirt/color/green', 't-shirt/color', 'Green'), - ('t-shirt/size/l', 't-shirt/size', 'L'), - ('t-shirt/size/m', 't-shirt/size', 'M') + ('t-shirt:options.size.values.l', 'l-red', 2), ('t-shirt:options.color.values.red', 'l-red', 2), + ('t-shirt:options.size.values.m', 'm-red', 2), ('t-shirt:options.color.values.red', 'm-red', 2), + ('t-shirt:options.size.values.m', 'm-green', 2), ('t-shirt:options.color.values.green', 'm-green', 2) ; -insert into product_variant_matrix (value_id, object_id, weight) -values - ('t-shirt/size/l', 'l-red', 2), ('t-shirt/color/red', 'l-red', 2), - ('t-shirt/size/m', 'm-red', 2), ('t-shirt/color/red', 'm-red', 2), - ('t-shirt/size/m', 'm-green', 2), ('t-shirt/color/green', 'm-green', 2); ``` So far, all looks pretty nice with such an approach product may return information for all available options with the single request. ```sql -mysql> select p.name, po.label as option_label, pov.label as option_value_label - -> from products p - -> inner join product_options po on p.object_id = po.object_id - -> inner join product_option_values pov on po.option_id = pov.option_id - -> where p.object_id = 't-shirt'; -+---------+--------------+--------------------+ -| name | option_label | option_value_label | -+---------+--------------+--------------------+ -| T-Shirt | Color | Green | -| T-Shirt | Size | L | -| T-Shirt | Size | M | -| T-Shirt | Color | Red | -+---------+--------------+--------------------+ -4 rows in set (0.01 sec) +mysql> select + -> json_pretty(data->>'$.options.*') as options + -> from products p where p.object_id = 't-shirt'\G +*************************** 1. row *************************** +options: [ + { + "label": "Size", + "values": { + "l": { + "label": "L" + }, + "m": { + "label": "M" + } + } + }, + { + "label": "Color", + "values": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + } + } + } +] +1 row in set (0.00 sec) ``` Let's assume that we have chosen one option value from the list. @@ -242,15 +249,16 @@ Starting this point we can look into variants to analyze remaining options. From the proposed example, we have chosen "Size": "M". ```sql -mysql> select object_id, value_id, weight +mysql> select + -> object_id, value, weight -> from product_variant_matrix - -> where value_id in ('t-shirt/size/m'); -+-----------+----------+--------+ -| object_id | value_id | weight | -+-----------+----------+--------+ -| m-green | m | 2 | -| m-red | m | 2 | -+-----------+----------+--------+ + -> where value in ('t-shirt:options.size.values.m'); ++-----------+-------------------------------+--------+ +| object_id | value | weight | ++-----------+-------------------------------+--------+ +| m-green | t-shirt:options.size.values.m | 2 | +| m-red | t-shirt:options.size.values.m | 2 | ++-----------+-------------------------------+--------+ 2 rows in set (0.01 sec) ``` @@ -262,29 +270,48 @@ which means that we can request the remaining options, that correspond to our current selection. ```sql -select distinct value_id -from product_variant_matrix pvm -where pvm.object_id in ('m-green', 'm-red') and value_id not in ('t-shirt/size/m'); +mysql> select distinct value + -> from product_variant_matrix pvm + -> where pvm.object_id in ('m-green', 'm-red') + -> and value not in ('t-shirt:options.size.values.m'); ++------------------------------------+ +| value | ++------------------------------------+ +| t-shirt:options.color.values.green | +| t-shirt:options.color.values.red | ++------------------------------------+ +2 rows in set (0.01 sec) ``` The remaining option values could be found in values assigned to the matched variants minus values that we selected at the previous step. ```sql -mysql> select p.name, po.label as option_label, pov.label as option_value_label - -> from products p - -> inner join product_options po on p.object_id = po.object_id - -> inner join product_option_values pov on po.option_id = pov.option_id - -> where p.object_id = 't-shirt' and pov.value_id in ('t-shirt/color/red', 't-shirt/color/green'); -+---------+--------------+--------------------+ -| name | option_label | option_value_label | -+---------+--------------+--------------------+ -| T-Shirt | Color | Green | -| T-Shirt | Color | Red | -+---------+--------------+--------------------+ -2 rows in set (0.00 sec) +mysql> select + -> json_pretty( + -> json_extract( + -> data, + -> '$.options.color.label', + -> '$.options.color.values.green', + -> '$.options.color.values.red' + -> ) + -> ) as options + -> from products\G +*************************** 1. row *************************** +options: [ + "Color", + { + "label": "Green" + }, + { + "label": "Red" + } +] +1 row in set (0.00 sec) ``` -*Note: To achieve more advanced behavior, the variants could be "uneven" inside the single product. For instance, you would like to track only t-shirts XL: size separately for some reason (a different price or stock). The example above focused on covering the main case scenario. Still, the approach, overall, is meant to support extending the logic of resolving option values onto a variant under the hood.* +*Note: To achieve more advanced behavior, the variants could be "uneven" inside the single product. + They may have different weight. + For instance, you would like to track only t-shirts XL: size separately for some reason (a different price or stock). The example above focused on covering the main case scenario. Still, the approach, overall, is meant to support extending the logic of resolving option values onto a variant under the hood.* ![](https://app.lucidchart.com/publicSegments/view/de9972a8-f630-4400-aacb-d3d9858862cf/image.png) ### Storefront API @@ -310,6 +337,23 @@ With the response API has to return: * List of images & videos that should be used on PDP. * List of price identifiers to request actual prices from the price service. * List of products that were exactly matched by the selected options. +```proto +message OptionSelection +{ + string productId = 1; + repeated string values = 2; +} +message OptionResponse { + repeated ProductOption options = 1; + repeated MediaGallery gallery = 2; + repeated ProductVarinat matchedVariants = 3; +} + +service SearchService { + rpc GetOptions(OptionSelection) returns (OptionResponse); +} +``` + ## Proposal cross references From 08d48494b57cba038cc6216d1e3e939f80d3a446 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 31 Jul 2020 00:40:21 -0500 Subject: [PATCH 213/479] changed rpc --- .../storefront/catalog/product-options-and-variants.md | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 1b8673f0b..a1f471596 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -335,7 +335,6 @@ As an input our API has to accept option values which were selected by a shopper With the response API has to return: * List of options and option values that remains avaialble. * List of images & videos that should be used on PDP. -* List of price identifiers to request actual prices from the price service. * List of products that were exactly matched by the selected options. ```proto message OptionSelection From 1bd47e2752a33e0ce406690ac317a2938cbda117 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 31 Jul 2020 12:20:17 -0500 Subject: [PATCH 214/479] options and customizations --- .../storefront/catalog/product-options-and-variants.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index a1f471596..c8ab9c78b 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -354,6 +354,13 @@ service SearchService { ``` +## Other ways to customize products + +This document distinguishes product customization made through a shopper input from options. +The shopper input based customization has a different structure often coupled with input type (text, image, amount). It does not have variants, can not be associated with product or inventory, association with the price made on a completely different level than options do. + +I was not able to find similar features in Shopify or Commerce tools, Shopify allows to install custom extensions to add this behavior. I believe that such limitation caused by the complexity of expressing such customizations through the variants, the way how these systems manage options. + ## Proposal cross references This proposal continues the idea of product options unification and aligned with previous design decisions that were made in this area. From 2758ceda61e61c4261d20c69408a332b7eda61b8 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 31 Jul 2020 12:26:45 -0500 Subject: [PATCH 215/479] options and customizations --- .../storefront/catalog/product-options-and-variants.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index c8ab9c78b..accdbb6fd 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -357,9 +357,15 @@ service SearchService { ## Other ways to customize products This document distinguishes product customization made through a shopper input from options. -The shopper input based customization has a different structure often coupled with input type (text, image, amount). It does not have variants, can not be associated with product or inventory, association with the price made on a completely different level than options do. +The shopper input based customization has a different structure often coupled with input type (text, image, amount). +It does not have variants, can not be associated with product or inventory, association with the price made on a completely different level than options do. -I was not able to find similar features in Shopify or Commerce tools, Shopify allows to install custom extensions to add this behavior. I believe that such limitation caused by the complexity of expressing such customizations through the variants, the way how these systems manage options. +I was not able to find similar features in Shopify or Commerce tools, Shopify allows to install custom extensions to add this behavior. +I believe that such limitation caused by the complexity of expressing such customizations through the variants, the way how these systems manage options. + +From my standpoint, that means that idea to segregate options and customization based on a shopper input on two different entities makes sense. +It could significantly simplify the structure of Magento catalog. +The same how it worked for checkout APIs. ## Proposal cross references From 1c13cb47add8daa04f28bd0025ea003f3388c81b Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Fri, 31 Jul 2020 14:29:35 -0500 Subject: [PATCH 216/479] Catalog images tech vision - watermarks - placeholders --- design-documents/media/catalog-images.md | 76 +++++++++++++++++++----- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 9287bf627..9287aeb09 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -71,15 +71,16 @@ In the first phase, this is not going to be supported. This may cause performance issues on pages with many assets loaded (such as product listing), but it is assumed that production systems should use CDN with image transformation support. Scenario with no CDN is assumed to be a development workflow and loading unresized images is considered less critical in this situation, especially assuming [Images Upload Configuration](./img/images-upload-config.png) allows to cap image size. +Note: watermarking is a special case of asset transformation. See next section for its coverage. + ### Magento Supported Image Transformations Magento supports the following transformations for images: 1. resize 2. rotate -3. watermark -4. set/change quality -5. set background +3. set/change quality +4. set background See `\Magento\Catalog\Model\Product\Image` for details. @@ -93,33 +94,78 @@ Fastly provides image transformation features with [Fastly IO](https://www.fastl 4. Trim 5. Padding 6. Set background color -7. Image overlay -8. Change brightness -9. Change contrast -10. Change saturation -11. Sharpen -12. Blur -13. Set quality -14. Montage (Combine up to four images into a single displayed image.) +7. Change brightness +8. Change contrast +9. Change saturation +10. Sharpen +11. Blur +12. Set quality +13. Montage (Combine up to four images into a single displayed image.) See https://docs.fastly.com/en/guides/image-optimization-api for detailed supported parameters. Provided features fully cover Magento capabilities. -Watermarking can be implemented using Overlay functionality. -Overlay must be specified via `x-fastly-imageopto-overlay` header rather than via a URL parameter, which allows the server control it. -To make sure UX is acceptable, the workflow should be described in more details, taking into account Magento scopes. ### AEM Assets Image Transformations AEM Assets work in integration with Dynamic Media (DM) to deliver asstes, and DM provides asset transformation capabilities. DM uses Akamai as CDN. Does it provide additional image transformation capabilities? Are those even needed taking into account that MD provides broad range of features? +1. DefaultImage - it allows the client to specify default image. Might be useful for placeholder im + https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html -Watermarking - https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html +## Watermarking + +Watermarking is a special case of image transformation. +It is special in a way that the **server** decides when and which watermark to apply. +All other transformations are initiated by the client. + +### Watermarking in Magento + +Applied during image transformations. +See `\Magento\Catalog\Model\Product\Image` for details. + +### Watermarking by Fastly + +See "Overlay" in https://docs.fastly.com/en/guides/image-optimization-api + +Overlay must be specified via `x-fastly-imageopto-overlay` header rather than via a URL parameter, which allows the server control it. +To make sure UX is acceptable, the workflow should be described in more details, taking into account Magento scopes. + +### Watermarking with AEM Assets + +Watermarking +- DM - https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html +- Akamai - https://blogs.akamai.com/2019/10/watermarking-a-content-owners-mark-to-prevent-piracy.html + - requires integration with a watermark provider (confirm it is the only option with Akamai) + Scoping? +## Placeholder Asset + +What should happen when requested asset is not available? +Use cases: + +1. The asset has been removed/renamed, and the change is not synced to Storefront service yet +2. The asset has been removed/renamed by mistake or due to server issues + +While we can hope that such things should not happen, it still should not be the visitor fault and there should be a good handling of such situations. +Currently, Magento handles it by loading a placeholder image. + +With new architecture, CDN or DAM should be able to provide such a placeholder. + +TBD: check how this can be supported with Fastly, AEM Assets/DM, Akamai. + +### Fastly + +### AEM Assets + DM + +DefaultImage - allows client to specify default image. + +See https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html + ## Risks This section summarizes potential issues with certain scenarios. From c8f0fa5b0fbfc5b342b6c0a1a1ce2f9dd7456a10 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 31 Jul 2020 18:37:04 -0500 Subject: [PATCH 217/479] review fixes --- .../catalog/product-options-and-variants.md | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index accdbb6fd..1950495fc 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -37,22 +37,6 @@ Both of approaches could be used together. Example: configurable product with customizable option. - -## Competitors analysis -During my research, I analyzed documentation on how options are covered by the competitors. -commercetools and Shopify attack this domain with product variants that represent all the possible options. -Despite the domain simplification by reducing the number of entities both systems constrained with the size of a product which makes it impossible to compose complex business cases that we frequently may face in the Magento ecosystem. - -[Commercetools: Modeling Products](https://docs.commercetools.com/product-modeling-products) - -[Shopify: Variants](https://help.shopify.com/en/manual/products/variants) - -Bigcommerce has a more complex option representation then Shopify or Commercetools. -in some way, I would compare their options features with Magento 2 x functionalities. -Bigcommerce segregates options from variants and allows to associate intersection of options with a new product, and as a result, I believe, struggles from the same disease as Magento does - redundancy in the variant data. - -[Bigcommerce: Product Options](https://support.bigcommerce.com/s/article/Options-SKUs-Rules) - ## Goals * Eliminate hard dependency on prices from options to make possible use promotions and B2B prices for customizable options, bundle, and downloadable products. @@ -85,7 +69,6 @@ So, a variant can request data from the different domains if such data assigned. Such a decision brings us unseen before the level of the flexibility, since there is no difference between product price and variant price, and as the consequence variant price, could be included in B2B pricing. We do not have such behaviors either Magento 1 or 2 and as a result, the whole layer of product types such as bundle and downloadable do not support B2B prices or special prices for options. -Great gap especially taking into account that [Shopify supports advanced price management for variants](https://help.shopify.com/en/manual/sell-online/wholesale/channel/price-lists-customers#choose-a-price-list-type). The link on a product that exists in the same domain could be done through the shared ID, which as for me makes sense since I can see a possible scenario when variation that initially had only a price will be "promoted" to a product. For example due to integration reasons, to pass additional attributes data along with variant to Google Merchant Center. @@ -360,9 +343,6 @@ This document distinguishes product customization made through a shopper input f The shopper input based customization has a different structure often coupled with input type (text, image, amount). It does not have variants, can not be associated with product or inventory, association with the price made on a completely different level than options do. -I was not able to find similar features in Shopify or Commerce tools, Shopify allows to install custom extensions to add this behavior. -I believe that such limitation caused by the complexity of expressing such customizations through the variants, the way how these systems manage options. - From my standpoint, that means that idea to segregate options and customization based on a shopper input on two different entities makes sense. It could significantly simplify the structure of Magento catalog. The same how it worked for checkout APIs. From 4fd2e77505a886bd04478964000c0850e0fd1954 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 31 Jul 2020 19:15:11 -0500 Subject: [PATCH 218/479] updates based on feedback --- design-documents/graph-ql/mutation-error-design.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/mutation-error-design.md b/design-documents/graph-ql/mutation-error-design.md index 002acb92a..2d20e93f5 100644 --- a/design-documents/graph-ql/mutation-error-design.md +++ b/design-documents/graph-ql/mutation-error-design.md @@ -1,6 +1,6 @@ # Mutation Error Design -Mutations are _the_ single way to trigger side-effects in a GraphQL API. Side-effects are where the large majority of our errors are going to happen, as a result of changing some underlying data. +Mutations are _the_ single way to modify application state in a GraphQL API. Mutations are where the large majority of our errors are going to happen, as a result of changing some underlying data. Today, the Magento 2 GraphQL schema does not do a great job of describing what could go wrong as a result of running a mutation. We should fix that! @@ -14,7 +14,7 @@ type Mutation { } ``` -This schema only describes one side-effect that can happen as a result of adding an item to the cart: it succeeds! But some things can go wrong when adding an item to the cart, and we know what many of those things are: +This schema only describes one the happy path of adding an item to the cart. But some things can go wrong when adding an item to the cart, and we know what many of those things are: - Item went out of stock since the product page was loaded - Merchant disabled the product since the product page was loaded @@ -42,8 +42,9 @@ This has some problems: 1. It's unreasonable to expect a UI developer to exercise every possible input to a resolver to find the error states manually 2. These errors are not enforced by the schema, so they're not versioned with the schema. 3. If the UI wants to customize the message for a specific error state, they'd have to match on the exact response string, because there's no concept of error codes +4. Internationalization becomes challenging because the error states/strings are not known ahead of time -Until recently, we didn't need to think quite as carefully about modeling errors and versioning them, because we owned the single reference theme for the application (Luma). In the world of headless UIs, it's critical that our errors are versioned and discoverable. +In the world of headless UIs, it's going to be critical for our errors to be discoverable, translatable, and versioned ## Solution: Design Mutation errors into the schema From c8fd7c386d30966e5a70a9d0d42eda338c30bfe0 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 3 Aug 2020 09:53:38 -0500 Subject: [PATCH 219/479] Update order schema with unique naming and additional item types --- .../graph-ql/coverage/customer-orders.md | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index b748aa2d9..a79db05ad 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -114,13 +114,6 @@ type BundleOrderItem implements OrderItemInterface { bundle_options: [ItemSelectedBundleOption] @doc("A list of bundle options that are assigned to the bundle product") } -type GiftCardOrderItem implements OrderItemInterface { - gift_card_amount: Money! @doc("Amount of value on gift card") - gift_card_sender: String @doc("Name of gift card sender") - gift_card_recipient: String @doc("Name of gift card recipient") - gift_card_message: String @doc("Message accompanying gift card") -} - type ItemSelectedBundleOption { id: ID! @doc(description: "The unique identifier of the option") label: String! @doc(description: "The label of the option") @@ -135,6 +128,24 @@ type ItemSelectedBundleOptionValue { price: Money! @doc("Option value price. price for single quantity") } +type DownloadableOrderItem implements OrderItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") +} +type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { + title: String @doc(description: "The display name of the link") + sort_order: Int @doc(description: "A number indicating the sort order") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. +} + +type GiftCardOrderItem implements OrderItemInterface { + gift_card: GiftCardItem @doc(description: "Selected gift card properties for an order item") @resolver(class: "Magento\\GiftCardGraphQl\\Model\\Resolver\\OrderItem\\GiftCardItem") +} +type GiftCardItem { + sender_name: String @doc(description: "Entered gift card sender name and email") + recipient_name: String @doc(description: "Entered gift card recipient name and email") + message: String @doc(description: "Entered gift card message intended for the recipient") +} + @doc("Represents order item options like selected or entered") type OrderItemOption { id: String! @doc("name of the option") @@ -147,7 +158,7 @@ To provide more customization for different payment solutions, the payment metho ```graphql @doc("Payment method used to pay for the order") -type PaymentMethod { +type OrderPaymentMethod { name: String! @doc("payment method name for e.g Braintree, Authorize etc.") type: String! @doc("payment method type used to pay for the order for e.g Credit Card, PayPal etc.") additional_data: [KeyValue] @doc("additional data per payment method type") @@ -179,7 +190,11 @@ type ShippingHandling { amount_including_tax: Money @doc("shipping amount including tax") amount_excluding_tax: Money @doc("shipping amount excluding tax") taxes: [TaxItem] @doc("shipping taxes details") - discounts: [Discount] @doc("The applied discounts to the shipping) + discounts: [ShippingDiscount] @doc("The applied discounts to the shipping) +} + +type ShippingDiscount @doc(description:"Defines an individual shipping discount. This discount can be applied to shipping.") { + amount: Money! @doc(description:"The amount of the discount") } @doc("Tax item details") @@ -316,10 +331,9 @@ type ShipmentTracking { } ``` - ## CommentItem type ```graphql -type CommentItem { +type SalesCommentItem { timestamp: String! @doc("The timestamp of the comment") message: String! @doc("the comment message") } @@ -345,8 +359,6 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information suffix: String @doc(description: "A value such as Sr., Jr., or III") vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") } - - ``` ## Additional Types From 5a7f96f5e310d88baeb43081152ab5d176e5d0a7 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 3 Aug 2020 14:17:04 -0500 Subject: [PATCH 220/479] Catalog images tech vision - added more details on image transformation and watermarking --- design-documents/media/catalog-images.md | 55 ++++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 9287aeb09..309ac64fb 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -91,30 +91,40 @@ Fastly provides image transformation features with [Fastly IO](https://www.fastl 1. Convert format 2. Rotation 3. Crop -4. Trim -5. Padding -6. Set background color -7. Change brightness -8. Change contrast -9. Change saturation -10. Sharpen -11. Blur -12. Set quality -13. Montage (Combine up to four images into a single displayed image.) - -See https://docs.fastly.com/en/guides/image-optimization-api for detailed supported parameters. +4. Set background color +5. Set quality +6. Montage (Combine up to four images into a single displayed image.) + +and others. See https://docs.fastly.com/en/guides/image-optimization-api for detailed supported parameters. Provided features fully cover Magento capabilities. ### AEM Assets Image Transformations -AEM Assets work in integration with Dynamic Media (DM) to deliver asstes, and DM provides asset transformation capabilities. -DM uses Akamai as CDN. Does it provide additional image transformation capabilities? Are those even needed taking into account that MD provides broad range of features? +AEM Assets work in integration with Dynamic Media (DM) to deliver assets, and DM provides asset transformation capabilities. +DM uses Akamai as CDN. Does it provide additional image transformation capabilities? Are those even needed taking into account that DM provides broad range of features? + +1. DefaultImage - it allows the client to specify default image. Might be useful for placeholder implementation. +2. Resize: crop, scale, size, etc +3. Rotate and flip +4. Quality +5. Background color +6. Change image format + +and many others. +See [Dynamic Media Image Serving and Rendering API - Command reference](https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html) for more details. + +Also, [Image Presets](https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html) with image adjustments can be created in advance and then requested by the client. -1. DefaultImage - it allows the client to specify default image. Might be useful for placeholder im +In addition, Akamai CDN provides ability to setup [Image Manager Policies](https://learn.akamai.com/pdf/ImageManager-CS.pdf) with the following parameters: -https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html -https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html +1. Background color +2. Crop +3. Resize +4. Rotate +5. Scale + +and others. ## Watermarking @@ -138,11 +148,18 @@ To make sure UX is acceptable, the workflow should be described in more details, Watermarking - DM - https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html -- Akamai - https://blogs.akamai.com/2019/10/watermarking-a-content-owners-mark-to-prevent-piracy.html - - requires integration with a watermark provider (confirm it is the only option with Akamai) Scoping? +#### Watermarking with Akamai + +Akamai CDN also provides Watermarking feature using their [Image Manager policies](https://learn.akamai.com/pdf/ImageManager-CS.pdf). +Watermark can be applied to images based on client or server-based rules. +Potentially this may help implement Magento-style scoping for watermark images. + +Also, see [WATERMARKING: A CONTENT OWNER'S MARK TO PREVENT PIRACY](https://blogs.akamai.com/2019/10/watermarking-a-content-owners-mark-to-prevent-piracy.html). +Looks like, it requires integration with a watermark provider. + ## Placeholder Asset What should happen when requested asset is not available? From 7c9a8e70b36e5aeb6b577caf3ca609c090137761 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 3 Aug 2020 14:26:07 -0500 Subject: [PATCH 221/479] Catalog images tech vision - added more details on image transformation and watermarking --- design-documents/media/catalog-images.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 309ac64fb..9ba09cc7a 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -147,7 +147,7 @@ To make sure UX is acceptable, the workflow should be described in more details, ### Watermarking with AEM Assets Watermarking -- DM - https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html +- [AEM Assets Watermarking](https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html) Scoping? From 561042202de377712d3ab431d3d89727125322b8 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Mon, 3 Aug 2020 14:45:24 -0500 Subject: [PATCH 222/479] Catalog images tech vision - added TOC - added introduction --- design-documents/media/catalog-images.md | 63 +++++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 9ba09cc7a..09e106cf6 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -1,8 +1,42 @@ # Catalog Images -## Asset Delivery - High-Level Vision +The document provides the vision for the desired state of assets management in Magento. +It covers currently existing managed Magento environments, or the ones envisioned in the future: -### Terminology +* Magento Cloud with Fastly CDN +* Magento Cloud + AEM Assets as DAM provider + * Including Dynamic Media with Akamai CDN + +Disclaimer: Other types of setup (e.g., on-prem + different CDN/DAM) can be supported in the similar fashion. +Current document just covers the above systems in more details to not grow in size exponentially. +There is no intention to limit usage of other types of setup. + +- [Terminology](#terminology) +- [Scenarios](#scenarios) + * [Asset Management](#asset-management) + * [Assign an image to a product](#assign-an-image-to-a-product) + * [Display an image on product details or products list page](#display-an-image-on-product-details-or-products-list-page) +- [Asset Transformations](#asset-transformations) + * [Magento: Image Transformations](#magento-image-transformations) + * [Fastly: Image Transformations](#fastly-image-transformations) + * [AEM Assets: Image Transformations](#aem-assets-image-transformations) +- [Watermarking](#watermarking) + * [Magento: Watermarking](#magento-watermarking) + * [Fastly: Watermarking](#fastly-watermarking) + * [AEM Assets: Watermarking](#aem-assets-watermarking) + + [Watermarking with Akamai](#watermarking-with-akamai) +- [Placeholders](#placeholders) + * [Magento: Placeholders](#magento-placeholders) + * [Fastly: Placeholders](#fastly-placeholders) + * [AEM Assets: Placeholders](#aem-assets-placeholders) +- [Risks](#risks) + * [Sync full image URL from Magento Admin to Store Front](#sync-full-image-url-from-magento-admin-to-store-front) + * [Full offload of image transformations to DAM/CDN](#full-offload-of-image-transformations-to-dam-cdn) + * [Storefront application provides only original URL](#storefront-application-provides-only-original-url) +- [Questions](#questions) +- [Breaking Changes](#breaking-changes) + +## Terminology * **Asset** - anything that exists in a binary format and comes with the right to use. * **Image**, **video** are types of assets @@ -73,7 +107,7 @@ Scenario with no CDN is assumed to be a development workflow and loading unresiz Note: watermarking is a special case of asset transformation. See next section for its coverage. -### Magento Supported Image Transformations +### Magento: Image Transformations Magento supports the following transformations for images: @@ -84,7 +118,7 @@ Magento supports the following transformations for images: See `\Magento\Catalog\Model\Product\Image` for details. -### Fastly Image Transformations +### Fastly: Image Transformations Fastly provides image transformation features with [Fastly IO](https://www.fastly.com/io): @@ -99,7 +133,7 @@ and others. See https://docs.fastly.com/en/guides/image-optimization-api for det Provided features fully cover Magento capabilities. -### AEM Assets Image Transformations +### AEM Assets: Image Transformations AEM Assets work in integration with Dynamic Media (DM) to deliver assets, and DM provides asset transformation capabilities. DM uses Akamai as CDN. Does it provide additional image transformation capabilities? Are those even needed taking into account that DM provides broad range of features? @@ -132,19 +166,19 @@ Watermarking is a special case of image transformation. It is special in a way that the **server** decides when and which watermark to apply. All other transformations are initiated by the client. -### Watermarking in Magento +### Magento: Watermarking Applied during image transformations. See `\Magento\Catalog\Model\Product\Image` for details. -### Watermarking by Fastly +### Fastly: Watermarking See "Overlay" in https://docs.fastly.com/en/guides/image-optimization-api Overlay must be specified via `x-fastly-imageopto-overlay` header rather than via a URL parameter, which allows the server control it. To make sure UX is acceptable, the workflow should be described in more details, taking into account Magento scopes. -### Watermarking with AEM Assets +### AEM Assets: Watermarking Watermarking - [AEM Assets Watermarking](https://docs.adobe.com/content/help/en/experience-manager-65/assets/administer/watermarking.html) @@ -160,7 +194,7 @@ Potentially this may help implement Magento-style scoping for watermark images. Also, see [WATERMARKING: A CONTENT OWNER'S MARK TO PREVENT PIRACY](https://blogs.akamai.com/2019/10/watermarking-a-content-owners-mark-to-prevent-piracy.html). Looks like, it requires integration with a watermark provider. -## Placeholder Asset +## Placeholders What should happen when requested asset is not available? Use cases: @@ -175,9 +209,14 @@ With new architecture, CDN or DAM should be able to provide such a placeholder. TBD: check how this can be supported with Fastly, AEM Assets/DM, Akamai. -### Fastly +### Magento: Placeholders + +Magento validates whether the file is present on the disk at the moment of URL generation, and provides URL to a placeholder image if the file is absent. +This limits the system to using local storage for the assets, and makes it difficult to fetch images from external DAM systems. + +### Fastly: Placeholders -### AEM Assets + DM +### AEM Assets: Placeholders DefaultImage - allows client to specify default image. @@ -244,7 +283,7 @@ url = origUrl + '?' + transformationParams[width] + '=100&' + transformationPara > https://my.dam.com/catalog/product/my/product.jpg?w=100&h=100&q=80 ``` -## Questions: +## Questions 1. Do we sync full image URL to SF or provide Base CDN URL as configuration for the store? 1. What do wee do with secure/unsecure URLs in case of full URL? From 924e802a5d327550331a976d2522e71f949d4b02 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Mon, 3 Aug 2020 23:25:44 -0500 Subject: [PATCH 223/479] review fixes --- .../catalog/product-options-and-variants.md | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 1950495fc..21e8fb76f 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -1,12 +1,30 @@ -## Domain Overview +## Overview -Magento allows customizing a product before adding the product to the cart. -Customization happens through the customizable options defined by the merchant. -Options can represent customization, other products, digital goods so on. +Product options are a powerful instrument that allows a shopper to customize and (or) personalize a product before adding the product to the cart. -All Magento product types represented through the options. +Introduction of the storefront API brings us a unique opportunity to revisit the options management, address the existing limitation, and address known issues: +* Reduce redundancy caused by variants matrix creation. Magento creates variant and simple product for each intersection of options and option values selected to represent the configurable product. Not all the cases in real require a product creation, so extracting the variant as an entity could reduce system load caused by the number of products. + +* Allow to set up a dependency between the different options of a simple product based on previously selected option values. +Currently, simple and bundle products do not support dependencies between option values. +An example, bundle product that represents a computer in your store may need to correlate the list of the available motherboards with the socket of the selected processor. + +* Support grouped B2B prices at the options level. There is no way to specify a special price for a customer group for simple, downloadable, and some of the bundled options of the product. + +* Although all the options which represent product variants are pretty similar and most of them have similar properties, the options not generalized in Magento. As a result, it makes the option management more complicated, and for some cases such as option displaying, causes frontend rendering logic duplication. The ultimate goal after the options API revision is to build a generalized view that should to significantly reduce the complexity of the options domain for the storefront application. + +### Taxonomy of the options + +Due to their origin and structure options segregates on two main subtypes. + +***Product Option Variants*** & ***Shopper Input Options***. + +The first subtype - ***Product Option Variants***, is much often used and represents product configuration, which was predefined by a merchant. And so, product customization could be described by the selection of these predefined options, which, in their turn, creates product variants, where each variant represents the selection of one or many option values. +This document will be focused on the first subtype, Product Option Variants, it just briefly covers the reasons for the options separation, due to the intention do not overload this document with information about the domain. + +*Note: Most of the logic that we use to associate with Magento product types (configurable, bundle, downloadable, etc) in fact is the logic of the different options.* ### Definitions From 09fae688e79121df04687b8e04ff2d0efc2eaa2d Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 4 Aug 2020 00:03:32 -0500 Subject: [PATCH 224/479] change of the structure --- .../catalog/product-options-and-variants.md | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 21e8fb76f..e0eaf6943 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -1,5 +1,5 @@ -## Overview +## Document purpose Product options are a powerful instrument that allows a shopper to customize and (or) personalize a product before adding the product to the cart. @@ -19,11 +19,13 @@ An example, bundle product that represents a computer in your store may need to Due to their origin and structure options segregates on two main subtypes. -***Product Option Variants*** & ***Shopper Input Options***. +**Product Option Variants** & **Shopper Input Options**. -The first subtype - ***Product Option Variants***, is much often used and represents product configuration, which was predefined by a merchant. And so, product customization could be described by the selection of these predefined options, which, in their turn, creates product variants, where each variant represents the selection of one or many option values. +* The first subtype - **Product Option Variants**, is much often used and represents product configuration, which was predefined by a merchant. And so, product customization could be described by the selection of these predefined options, which, in their turn, creates product variants, where each variant represents the selection of one or many option values. This document will be focused on the first subtype, Product Option Variants, it just briefly covers the reasons for the options separation, due to the intention do not overload this document with information about the domain. +* Another subtype **Shopper Input Option**s represent an approach to personalizing a product before adding it to the cart by adding custom images, entering text so on. Gift Cards with customer-defined amounts could be treated as one of the cases the Shopper Input Options. These subtype of options do not provide any predefined values, it provides constraints for the input instead like, max number of symbols, a range for amount, or allowed extensions for files. Shopper Input options do not have variants, could not be associated with a product or inventory record, but may have a reference on price. Due to the excessive list of differences from product options variants, this option subtype is out of the document. The document just points out that the options segregation by the mentioned above criteria should happen, especially to respect checkout API that we released recently. Anyway, Shopper Input Options should have their own representation as a product top-level property. + *Note: Most of the logic that we use to associate with Magento product types (configurable, bundle, downloadable, etc) in fact is the logic of the different options.* ### Definitions @@ -37,8 +39,8 @@ Option value could by display label, also one or may values could be pre-selecte Depends on the business scenario, a particular product variant could be linked with an existing product, with a price, or an inventory record, or no be linked to any. Even with no entities associated with the variant, it still has great value for a catalog because the presence of the variant says that such composition of options and their values described by the variant makes sense so that it can be purchased. - -### Actual usages + +## Actual usages The application distinguishes two approaches to manage options: @@ -55,16 +57,6 @@ Both of approaches could be used together. Example: configurable product with customizable option. -## Goals - -* Eliminate hard dependency on prices from options to make possible use promotions and B2B prices for customizable options, bundle, and downloadable products. -* Eliminate variants redundancy - * Catalog should not have a variant per each option values combination, only if necessary. - * Catalog should not create a product per variant if this product never used. -* Hide the variant matrix from storefront client, use it only in the context of operations. -* Align a similar option with a single data structure to manage them together. - - ## Modeling Options and Variants Data Objects The great advantage of Magento 2 - modularity was not a real thing for the product options during Magento the lifetime. From cc5b5e784b078dddcfb41254697f1674b224ed15 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 4 Aug 2020 00:07:30 -0500 Subject: [PATCH 225/479] change of the structure --- .../catalog/product-options-and-variants.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index e0eaf6943..6f8a48d5d 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -346,17 +346,6 @@ service SearchService { } ``` - -## Other ways to customize products - -This document distinguishes product customization made through a shopper input from options. -The shopper input based customization has a different structure often coupled with input type (text, image, amount). -It does not have variants, can not be associated with product or inventory, association with the price made on a completely different level than options do. - -From my standpoint, that means that idea to segregate options and customization based on a shopper input on two different entities makes sense. -It could significantly simplify the structure of Magento catalog. -The same how it worked for checkout APIs. - ## Proposal cross references This proposal continues the idea of product options unification and aligned with previous design decisions that were made in this area. From e27455616de7a2f28f727014b9b0dae95460930d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 4 Aug 2020 11:17:24 -0500 Subject: [PATCH 226/479] MC-35159: Login as Customer Schema Design - Organized GraphQL-related proposals - Updated README.md to reflect current situation --- README.md | 30 ------------------ .../{ => best-practice}/extensibility.md | 0 .../id-fields-object-types.md | 0 .../lightweight-resolver.md | 0 .../{ => best-practice}/nullability.md | 0 .../{ => best-practice}/versioning.md | 0 .../coverage/{ => b2b}/company-credit.md | 0 .../graph-ql/coverage/{ => b2b}/company.md | 0 .../{ => b2b}/negotiableQuotes.graphqls | 0 .../{ => b2b}/requisitionList.graphqls | 0 .../{ => cart}/AddProductsToCart.graphqls | 0 .../coverage/{ => cart}/Cart.graphqls | 0 .../{ => cart}/CartAddressOperations.graphqls | 0 .../coverage/{ => cart}/CartPrices.graphqls | 0 .../{ => cart}/CartPromotions.graphqls | 0 .../{ => cart}/CouponOperations.graphqls | 0 .../add-items-to-cart-single-mutation.md | 0 .../coverage/{ => cart}/shared-cart.md | 0 .../coverage/{ => catalog}/Swatches.graphqls | 0 .../coverage/{ => catalog}/gift-card.md | 0 .../coverage/{ => catalog}/inconsistency.md | 0 .../custom-attributes}/attributes-metadata.md | 0 .../custom-attributes-container.md | 0 .../SubscribeEmailToNewsletter.graphqls | 0 .../coverage/{ => customer}/Wishlist.graphqls | 0 .../customer-email-password-update.md | 0 .../{ => customer}/customer-orders.md | 0 .../{ => customer}/customer-password-reset.md | 0 .../{ => customer}/customer-reorder.graphqls | 0 .../{ => customer}/gift-registry.graphqls | 0 .../coverage/{ => customer}/gift-registry.md | 0 .../coverage/{ => customer}/gift-wrapping.md | 0 .../{ => customer}/reward-points.graphqls | 0 .../coverage/{ => customer}/reward-points.md | 0 .../coverage/{ => customer}/wishlist.md | 0 .../{ => payment}/payflowpro-vault.md | 0 .../{ => routing}/available-stores.md | 0 .../{ => routing}/storefront-route.md | 0 .../{ => routing}/url-resolver-identifier.md | 0 .../layered_navigation.png | Bin .../product_filter_and_search_changes.md | 0 .../{ => coverage/search}/storefront-api.md | 0 .../{ => shipping}/store-pickup.graphqls | 0 .../{ => framework}/batch-resolver.md | 0 44 files changed, 30 deletions(-) rename design-documents/graph-ql/{ => best-practice}/extensibility.md (100%) rename design-documents/graph-ql/{ => best-practice}/id-fields-object-types.md (100%) rename design-documents/graph-ql/{ => best-practice}/lightweight-resolver.md (100%) rename design-documents/graph-ql/{ => best-practice}/nullability.md (100%) rename design-documents/graph-ql/{ => best-practice}/versioning.md (100%) rename design-documents/graph-ql/coverage/{ => b2b}/company-credit.md (100%) rename design-documents/graph-ql/coverage/{ => b2b}/company.md (100%) rename design-documents/graph-ql/coverage/{ => b2b}/negotiableQuotes.graphqls (100%) rename design-documents/graph-ql/coverage/{ => b2b}/requisitionList.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/AddProductsToCart.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/Cart.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/CartAddressOperations.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/CartPrices.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/CartPromotions.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/CouponOperations.graphqls (100%) rename design-documents/graph-ql/coverage/{ => cart}/add-items-to-cart-single-mutation.md (100%) rename design-documents/graph-ql/coverage/{ => cart}/shared-cart.md (100%) rename design-documents/graph-ql/coverage/{ => catalog}/Swatches.graphqls (100%) rename design-documents/graph-ql/coverage/{ => catalog}/gift-card.md (100%) rename design-documents/graph-ql/coverage/{ => catalog}/inconsistency.md (100%) rename design-documents/graph-ql/{framework => coverage/custom-attributes}/attributes-metadata.md (100%) rename design-documents/graph-ql/{ => coverage/custom-attributes}/custom-attributes-container.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/SubscribeEmailToNewsletter.graphqls (100%) rename design-documents/graph-ql/coverage/{ => customer}/Wishlist.graphqls (100%) rename design-documents/graph-ql/coverage/{ => customer}/customer-email-password-update.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/customer-orders.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/customer-password-reset.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/customer-reorder.graphqls (100%) rename design-documents/graph-ql/coverage/{ => customer}/gift-registry.graphqls (100%) rename design-documents/graph-ql/coverage/{ => customer}/gift-registry.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/gift-wrapping.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/reward-points.graphqls (100%) rename design-documents/graph-ql/coverage/{ => customer}/reward-points.md (100%) rename design-documents/graph-ql/coverage/{ => customer}/wishlist.md (100%) rename design-documents/graph-ql/coverage/{ => payment}/payflowpro-vault.md (100%) rename design-documents/graph-ql/coverage/{ => routing}/available-stores.md (100%) rename design-documents/graph-ql/coverage/{ => routing}/storefront-route.md (100%) rename design-documents/graph-ql/coverage/{ => routing}/url-resolver-identifier.md (100%) rename design-documents/graph-ql/coverage/{ => search}/layered-navigation-filter-names-change/layered_navigation.png (100%) rename design-documents/graph-ql/coverage/{ => search}/product_filter_and_search_changes.md (100%) rename design-documents/graph-ql/{ => coverage/search}/storefront-api.md (100%) rename design-documents/graph-ql/coverage/{ => shipping}/store-pickup.graphqls (100%) rename design-documents/graph-ql/{ => framework}/batch-resolver.md (100%) diff --git a/README.md b/README.md index 51151ccec..c843cd939 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,6 @@ As a result, the `master` branch must contain content approved by Magento archit * [Approved proposals](https://github.com/magento/architecture/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged+) are represented by merged PRs. * [Declined proposals](https://github.com/magento/architecture/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aunmerged+is%3Aclosed) are represented by closed (not merged) PRs. -## Glossary - -author -: a Magento core engineer, or any community member - -facilitator -: a Magento architect who makes sure the process is followed (Olga Koplyova @buskamuza) - ## The Workflow 1. Fork the repository and add or edit a document in your branch. @@ -48,7 +40,6 @@ Contributions are expected from Magento core engineers mostly, although the comm * Review the entire document by specified due date (if any) * If it is impossible, find a replacement - * Contact the facilitator in case you can't find a replacement * If the due date is unreasonable for the size of the document, agree on another due date with the author * Include a detailed feedback * Ensure the feedback is objective @@ -60,24 +51,3 @@ The implementation process is out of scope in this project. After approval of the document, a new discussion may be raised basing on the issues occurred during implementation. It is also possible in case of new informational updates that discover hidden sides of the future implementation. If it is the case, a new PR should be opened to update existing document. The PR should include explained reasons for the proposed change. - -## Architectural Discussions - -Architectural Discussions are public meetings open to anyone and taking place on a regular basis (**bi-weekly**). Topics for the Architectural Discussions should be proposed in advance. If no topics are proposed prior to the meeting, it may be canceled. - -### Meeting notes and topics - -Meeting notes and topics are available as [meeting notes issues](https://github.com/magento/architecture/issues?q=is%3Aissue+is%3Aopen+label%3A%22meeting+notes%22). - -Prior to November 14th, 2018, meeting minutes are available at sidebar of the [wiki](https://github.com/magento/architecture/wiki) - -### How to propose a topic? - -To propose a topic: -1. Find an issue for the next available date in the list of [meeting notes issues](https://github.com/magento/architecture/issues?q=is%3Aissue+is%3Aopen+label%3A%22meeting+notes%22) - 1. Add your topic as a comment in format described in the issue -2. If there is no such issue, please create one from the "Meeting Notes" template - 1. Calculate the date based on the last meeting note. Don't worry if the date is incorrect, one of the maintainers will fix it if necessary. Just make sure that it is a future date, so it's not missed - 1. Add your topic as a comment in format described in the issue template - -During the meeting, expect that the topics will be discussed in the order they are requested. Also, the discussions may be interrupted if requested time elapses. Please, include time for the discussion in the requested duration for your topic. diff --git a/design-documents/graph-ql/extensibility.md b/design-documents/graph-ql/best-practice/extensibility.md similarity index 100% rename from design-documents/graph-ql/extensibility.md rename to design-documents/graph-ql/best-practice/extensibility.md diff --git a/design-documents/graph-ql/id-fields-object-types.md b/design-documents/graph-ql/best-practice/id-fields-object-types.md similarity index 100% rename from design-documents/graph-ql/id-fields-object-types.md rename to design-documents/graph-ql/best-practice/id-fields-object-types.md diff --git a/design-documents/graph-ql/lightweight-resolver.md b/design-documents/graph-ql/best-practice/lightweight-resolver.md similarity index 100% rename from design-documents/graph-ql/lightweight-resolver.md rename to design-documents/graph-ql/best-practice/lightweight-resolver.md diff --git a/design-documents/graph-ql/nullability.md b/design-documents/graph-ql/best-practice/nullability.md similarity index 100% rename from design-documents/graph-ql/nullability.md rename to design-documents/graph-ql/best-practice/nullability.md diff --git a/design-documents/graph-ql/versioning.md b/design-documents/graph-ql/best-practice/versioning.md similarity index 100% rename from design-documents/graph-ql/versioning.md rename to design-documents/graph-ql/best-practice/versioning.md diff --git a/design-documents/graph-ql/coverage/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md similarity index 100% rename from design-documents/graph-ql/coverage/company-credit.md rename to design-documents/graph-ql/coverage/b2b/company-credit.md diff --git a/design-documents/graph-ql/coverage/company.md b/design-documents/graph-ql/coverage/b2b/company.md similarity index 100% rename from design-documents/graph-ql/coverage/company.md rename to design-documents/graph-ql/coverage/b2b/company.md diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/negotiableQuotes.graphqls rename to design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls diff --git a/design-documents/graph-ql/coverage/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/requisitionList.graphqls rename to design-documents/graph-ql/coverage/b2b/requisitionList.graphqls diff --git a/design-documents/graph-ql/coverage/AddProductsToCart.graphqls b/design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/AddProductsToCart.graphqls rename to design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls diff --git a/design-documents/graph-ql/coverage/Cart.graphqls b/design-documents/graph-ql/coverage/cart/Cart.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/Cart.graphqls rename to design-documents/graph-ql/coverage/cart/Cart.graphqls diff --git a/design-documents/graph-ql/coverage/CartAddressOperations.graphqls b/design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/CartAddressOperations.graphqls rename to design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls diff --git a/design-documents/graph-ql/coverage/CartPrices.graphqls b/design-documents/graph-ql/coverage/cart/CartPrices.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/CartPrices.graphqls rename to design-documents/graph-ql/coverage/cart/CartPrices.graphqls diff --git a/design-documents/graph-ql/coverage/CartPromotions.graphqls b/design-documents/graph-ql/coverage/cart/CartPromotions.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/CartPromotions.graphqls rename to design-documents/graph-ql/coverage/cart/CartPromotions.graphqls diff --git a/design-documents/graph-ql/coverage/CouponOperations.graphqls b/design-documents/graph-ql/coverage/cart/CouponOperations.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/CouponOperations.graphqls rename to design-documents/graph-ql/coverage/cart/CouponOperations.graphqls diff --git a/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md b/design-documents/graph-ql/coverage/cart/add-items-to-cart-single-mutation.md similarity index 100% rename from design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md rename to design-documents/graph-ql/coverage/cart/add-items-to-cart-single-mutation.md diff --git a/design-documents/graph-ql/coverage/shared-cart.md b/design-documents/graph-ql/coverage/cart/shared-cart.md similarity index 100% rename from design-documents/graph-ql/coverage/shared-cart.md rename to design-documents/graph-ql/coverage/cart/shared-cart.md diff --git a/design-documents/graph-ql/coverage/Swatches.graphqls b/design-documents/graph-ql/coverage/catalog/Swatches.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/Swatches.graphqls rename to design-documents/graph-ql/coverage/catalog/Swatches.graphqls diff --git a/design-documents/graph-ql/coverage/gift-card.md b/design-documents/graph-ql/coverage/catalog/gift-card.md similarity index 100% rename from design-documents/graph-ql/coverage/gift-card.md rename to design-documents/graph-ql/coverage/catalog/gift-card.md diff --git a/design-documents/graph-ql/coverage/inconsistency.md b/design-documents/graph-ql/coverage/catalog/inconsistency.md similarity index 100% rename from design-documents/graph-ql/coverage/inconsistency.md rename to design-documents/graph-ql/coverage/catalog/inconsistency.md diff --git a/design-documents/graph-ql/framework/attributes-metadata.md b/design-documents/graph-ql/coverage/custom-attributes/attributes-metadata.md similarity index 100% rename from design-documents/graph-ql/framework/attributes-metadata.md rename to design-documents/graph-ql/coverage/custom-attributes/attributes-metadata.md diff --git a/design-documents/graph-ql/custom-attributes-container.md b/design-documents/graph-ql/coverage/custom-attributes/custom-attributes-container.md similarity index 100% rename from design-documents/graph-ql/custom-attributes-container.md rename to design-documents/graph-ql/coverage/custom-attributes/custom-attributes-container.md diff --git a/design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls b/design-documents/graph-ql/coverage/customer/SubscribeEmailToNewsletter.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/SubscribeEmailToNewsletter.graphqls rename to design-documents/graph-ql/coverage/customer/SubscribeEmailToNewsletter.graphqls diff --git a/design-documents/graph-ql/coverage/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/Wishlist.graphqls rename to design-documents/graph-ql/coverage/customer/Wishlist.graphqls diff --git a/design-documents/graph-ql/coverage/customer-email-password-update.md b/design-documents/graph-ql/coverage/customer/customer-email-password-update.md similarity index 100% rename from design-documents/graph-ql/coverage/customer-email-password-update.md rename to design-documents/graph-ql/coverage/customer/customer-email-password-update.md diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer/customer-orders.md similarity index 100% rename from design-documents/graph-ql/coverage/customer-orders.md rename to design-documents/graph-ql/coverage/customer/customer-orders.md diff --git a/design-documents/graph-ql/coverage/customer-password-reset.md b/design-documents/graph-ql/coverage/customer/customer-password-reset.md similarity index 100% rename from design-documents/graph-ql/coverage/customer-password-reset.md rename to design-documents/graph-ql/coverage/customer/customer-password-reset.md diff --git a/design-documents/graph-ql/coverage/customer-reorder.graphqls b/design-documents/graph-ql/coverage/customer/customer-reorder.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/customer-reorder.graphqls rename to design-documents/graph-ql/coverage/customer/customer-reorder.graphqls diff --git a/design-documents/graph-ql/coverage/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/gift-registry.graphqls rename to design-documents/graph-ql/coverage/customer/gift-registry.graphqls diff --git a/design-documents/graph-ql/coverage/gift-registry.md b/design-documents/graph-ql/coverage/customer/gift-registry.md similarity index 100% rename from design-documents/graph-ql/coverage/gift-registry.md rename to design-documents/graph-ql/coverage/customer/gift-registry.md diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/customer/gift-wrapping.md similarity index 100% rename from design-documents/graph-ql/coverage/gift-wrapping.md rename to design-documents/graph-ql/coverage/customer/gift-wrapping.md diff --git a/design-documents/graph-ql/coverage/reward-points.graphqls b/design-documents/graph-ql/coverage/customer/reward-points.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/reward-points.graphqls rename to design-documents/graph-ql/coverage/customer/reward-points.graphqls diff --git a/design-documents/graph-ql/coverage/reward-points.md b/design-documents/graph-ql/coverage/customer/reward-points.md similarity index 100% rename from design-documents/graph-ql/coverage/reward-points.md rename to design-documents/graph-ql/coverage/customer/reward-points.md diff --git a/design-documents/graph-ql/coverage/wishlist.md b/design-documents/graph-ql/coverage/customer/wishlist.md similarity index 100% rename from design-documents/graph-ql/coverage/wishlist.md rename to design-documents/graph-ql/coverage/customer/wishlist.md diff --git a/design-documents/graph-ql/coverage/payflowpro-vault.md b/design-documents/graph-ql/coverage/payment/payflowpro-vault.md similarity index 100% rename from design-documents/graph-ql/coverage/payflowpro-vault.md rename to design-documents/graph-ql/coverage/payment/payflowpro-vault.md diff --git a/design-documents/graph-ql/coverage/available-stores.md b/design-documents/graph-ql/coverage/routing/available-stores.md similarity index 100% rename from design-documents/graph-ql/coverage/available-stores.md rename to design-documents/graph-ql/coverage/routing/available-stores.md diff --git a/design-documents/graph-ql/coverage/storefront-route.md b/design-documents/graph-ql/coverage/routing/storefront-route.md similarity index 100% rename from design-documents/graph-ql/coverage/storefront-route.md rename to design-documents/graph-ql/coverage/routing/storefront-route.md diff --git a/design-documents/graph-ql/coverage/url-resolver-identifier.md b/design-documents/graph-ql/coverage/routing/url-resolver-identifier.md similarity index 100% rename from design-documents/graph-ql/coverage/url-resolver-identifier.md rename to design-documents/graph-ql/coverage/routing/url-resolver-identifier.md diff --git a/design-documents/graph-ql/coverage/layered-navigation-filter-names-change/layered_navigation.png b/design-documents/graph-ql/coverage/search/layered-navigation-filter-names-change/layered_navigation.png similarity index 100% rename from design-documents/graph-ql/coverage/layered-navigation-filter-names-change/layered_navigation.png rename to design-documents/graph-ql/coverage/search/layered-navigation-filter-names-change/layered_navigation.png diff --git a/design-documents/graph-ql/coverage/product_filter_and_search_changes.md b/design-documents/graph-ql/coverage/search/product_filter_and_search_changes.md similarity index 100% rename from design-documents/graph-ql/coverage/product_filter_and_search_changes.md rename to design-documents/graph-ql/coverage/search/product_filter_and_search_changes.md diff --git a/design-documents/graph-ql/storefront-api.md b/design-documents/graph-ql/coverage/search/storefront-api.md similarity index 100% rename from design-documents/graph-ql/storefront-api.md rename to design-documents/graph-ql/coverage/search/storefront-api.md diff --git a/design-documents/graph-ql/coverage/store-pickup.graphqls b/design-documents/graph-ql/coverage/shipping/store-pickup.graphqls similarity index 100% rename from design-documents/graph-ql/coverage/store-pickup.graphqls rename to design-documents/graph-ql/coverage/shipping/store-pickup.graphqls diff --git a/design-documents/graph-ql/batch-resolver.md b/design-documents/graph-ql/framework/batch-resolver.md similarity index 100% rename from design-documents/graph-ql/batch-resolver.md rename to design-documents/graph-ql/framework/batch-resolver.md From df3f0b6b82616d7ac37235177a2f426e1be1753f Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 4 Aug 2020 12:22:34 -0500 Subject: [PATCH 227/479] MC-35159: Login as Customer Schema Design --- .../customer/login-as-customer.graphqls | 13 ++++++++ .../coverage/customer/login-as-customer.md | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 design-documents/graph-ql/coverage/customer/login-as-customer.graphqls create mode 100644 design-documents/graph-ql/coverage/customer/login-as-customer.md diff --git a/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls b/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls new file mode 100644 index 000000000..2da837f62 --- /dev/null +++ b/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls @@ -0,0 +1,13 @@ +type Mutation { + generateCustomerTokenAsAdmin(input: GenerateCustomerTokenAsAdminInput!): GenerateCustomerTokenAsAdminOutput +} + +input GenerateCustomerTokenAsAdminInput +{ + customer_email: String! +} + +type GenerateCustomerTokenAsAdminOutput +{ + customer_token: String! +} diff --git a/design-documents/graph-ql/coverage/customer/login-as-customer.md b/design-documents/graph-ql/coverage/customer/login-as-customer.md new file mode 100644 index 000000000..3d08d345c --- /dev/null +++ b/design-documents/graph-ql/coverage/customer/login-as-customer.md @@ -0,0 +1,32 @@ +## Use cases + +# Admin user obtains customer token + +Admin user is expected to be authenticated using admin token. While there is no GraphQL Mutation for retrieving the admin token, REST should be used (see [example](https://devdocs.magento.com/guides/v2.4/graphql/queries/index.html#staging)). + +Admin bearer token must be provided in the `Authorization` header along with the following mutation. The admin user associated with that token must have `Login as Customer` (`Magento_LoginAsCustomer::login`) permissions. As usual, the store context is determined based on `Store` header in the request. + +```graphql +mutation { + generateCustomerTokenAsAdmin( + input: { + customer_email: "customer@example.com" + } + ) { + customer_token + } +} +``` + +### Additional requirements + 1. A new `LoginAsCustomerGraphQL` module should be created. + 2. `Magento_LoginAsCustomer::login` permission must be declared in `LoginAsCustomer` module. + 3. The following store config settings must be honored, but should not be exposed as part of `StoreConfig` GraphQL query: +```xml + + + 0 + 60 + + +``` From 4ecb78a6249c6e888d7b73e14502a319017e3642 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Tue, 4 Aug 2020 15:18:46 -0500 Subject: [PATCH 228/479] Update design-documents/graph-ql/mutation-error-design.md Co-authored-by: Alex Paliarush --- design-documents/graph-ql/mutation-error-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/mutation-error-design.md b/design-documents/graph-ql/mutation-error-design.md index 2d20e93f5..572237b19 100644 --- a/design-documents/graph-ql/mutation-error-design.md +++ b/design-documents/graph-ql/mutation-error-design.md @@ -14,7 +14,7 @@ type Mutation { } ``` -This schema only describes one the happy path of adding an item to the cart. But some things can go wrong when adding an item to the cart, and we know what many of those things are: +This schema only describes the happy path of adding an item to the cart. But some things can go wrong when adding an item to the cart, and we know what many of those things are: - Item went out of stock since the product page was loaded - Merchant disabled the product since the product page was loaded From 61758e083446d20f8fdb8b0ba716a7e345f627c5 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Wed, 5 Aug 2020 18:09:53 -0500 Subject: [PATCH 229/479] MC-36347: ID Improvement Plan --- .../graph-ql/id-improvement-plan.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 design-documents/graph-ql/id-improvement-plan.md diff --git a/design-documents/graph-ql/id-improvement-plan.md b/design-documents/graph-ql/id-improvement-plan.md new file mode 100644 index 000000000..b5bd00144 --- /dev/null +++ b/design-documents/graph-ql/id-improvement-plan.md @@ -0,0 +1,227 @@ +# ID Improvement Plan + +We've recently agreed on some standardization to how object identifiers are represented in Magento GraphQL schemas, which will require some changes to existing code to reach the desired state. + +This work _must not introduce any breaking changes_. + +## Approved Proposals + +- [Propose renaming id_v2 to something more permanent, and change type #396](https://github.com/magento/architecture/pull/396) +- [Add document with suggestions for ID fields #395](https://github.com/magento/architecture/pull/395) + +Between these 2 proposals, agreement was reached on the following guidelines: + +- Identifier fields and arguments _must_ use the `ID` scalar type +- Identifier fields in Object Types _must_ be non-nullable (`!`) +- Identifier fields in Object Types _must_ have the field name `uid` +- Arguments that accept a `uid` value (either `Query` or `Mutation`) _must_ have `uid` in the argument name_ +- All objects representing entities that _can_ be addressed by ID _must_ have a `uid` field +- All `ID` values _must_ be unique to their type (no collisions on IDs) + +## Terminology +- **Primary Identifier**: An ID owned by the current Object Type (ex: `Product.id`) +- **Foreign Identifier**: An ID referencing another Object Type (ex: `PaymentMethodInput.code`) + +## Work that needs to get done + +### Object Types and Interfaces with a _Primary Identifier_ + +I found 51 Objects/Interfaces in the Open-Source schema that will need these changes. + +#### Changes needed +- Add a new, non-nullable field named `uid` with type `ID` +- Resolve the `uid` type to the same value as the object's existing _primary identifier_ field +- Deprecate the existing _primary identifier_ field, with a message directing developers to the `uid` field + +#### Example Change +```diff +interface ProductInterface { +- id: Int ++ id: Int @deprecated(reason: "Use the 'uid' field instead") ++ uid: ID! +} +``` + +### Fields with 1 or more _Foreign Identifier_ argument + +I found 10 arguments across 8 fields in the Open-Source schema that will need these changes. + +#### Changed Needed +- For each argument that's a _foreign identifier_ + - Add a new argument with a name of the format `{ForeignObject}_uid`, where `ForeignObject` is the name of the Object Type with the matching `uid` field + - Deprecate the existing argument, with a message directing developers to the new argument + - Update the resolver to use _either_ the new or old argument, but throw a validation error when both are used + - The new argument must have the same nullability as the old argument + +#### Example Change +```diff +type Query { +- cart(cart_id: String!): Cart ++ cart( ++ cart_id: String! @deprecated(reason: "Use the 'cart_uid' argument instead") ++ cart_uid: ID! @doc(description: "ID from the Cart.uid field") ++ ): Cart +} +``` + +### Input Object Types with 1 or more _Foreign Identifier_ fields + +I found 49 fields across 44 Input Object Types in the Open-Source schema that will need these changes. + +#### Changes Needed +- For each field that's a _foreign identifier_ + - Add a new field with a name of the format `{ForeignObject}_uid`, where `ForeignObject` is the name of the Object Type with the matching `uid` field + - Deprecate the existing field, with a message directing developers to the new field + - Update all related resolvers to use _either_ the new or old field, but throw a validation error when both are used + - The new field must have the same nullability as the old field + +#### Example Change +```diff +input CartItemInput { +- sku: String! ++ sku: String! @deprecated(reason: "Use the CartItemInput.product_uid field instead") ++ product_uid: ID! @doc(description: "ID from the ProductInterface.uid field") +} +``` + +## Suggested Grouping of Work + +Ideally, whether this work is done all at once or incrementally, we'll make sure that changes are made in groups. That is, anytime a _primary identifier_ field changes on an Object/Interface, the same changeset should include changes to all places the matching _foreign identifier_ field/argument is used. + +For example, when the `uid` field is added to the `Cart` type in the `Magento/QuoteGraphQl` module, all input objects and arguments that reference the old `cart_id` should be updated to include the new `cart_uid` field. + +### Example Breakdown of Work +

+Click to Expand + +| Object/Interface | Primary Identifier Field | References | +|-------------------|--------------------------|------------------------------------| +| Cart | id | Mutation.mergeCarts | +| | | Query.cart | +| | | AddBundleProductsToCartInput | +| | | AddConfigurableProductsToCartInput | +| | | AddDownloadableProductsToCartInput | +| | | AddSimpleProductsToCartInput | +| | | AddVirtualProductsToCartInput | +| | | ApplyCouponToCartInput | +| | | ApplyGiftCardToCartInput | +| | | ApplyStoreCreditToCartInput | +| | | createEmptyCartInput | +| | | HostedProUrlInput | +| | | PayflowLinkTokenInput | +| | | PayflowProResponseInput | +| | | PayflowProTokenInput | +| | | PaypalExpressTokenInput | +| | | PlaceOrderInput | +| | | RemoveCouponFromCartInput | +| | | RemoveGiftCardFromCartInput | +| | | RemoveItemFromCartInput | +| | | RemoveStoreCreditFromCartInput | +| | | SetBillingAddressOnCartInput | +| | | SetGuestEmailOnCartInput | +| | | SetPaymentMethodAndPlaceOrderInput | +| | | SetPaymentMethodOnCartInput | +| | | SetShippingAddressesOnCartInput | +| | | SetShippingMethodsOnCartInput | +| | | UpdateCartItemsInput | +| | | | +| ProductInterface | id | CartItemInput | +| | | ProductSortInput | +| | | SendEmailToFriendInput | +| | | ConfigurableProductCartItemInput | +| | | ProductFilterInput | +| | | BundleProduct | +| | | ConfigurableProduct | +| | | DownloadableProduct | +| | | GiftCardProduct | +| | | GroupedProduct | +| | | SimpleProduct | +| | | VirtualProduct | +| | | ProductAttributeFilterInput | +| | | ConfigurableProductOptions | +| | | CustomizableAreaOption | +| | | BundleItem | +| | | CustomizableAreaValue | +| | | CustomizableCheckboxValue | +| | | CustomizableDateOption | +| | | CustomizableDateValue | +| | | CustomizableDropDownValue | +| | | CustomizableFieldOption | +| | | CustomizableFieldValue | +| | | CustomizableFileOption | +| | | CustomizableFileValue | +| | | CustomizableMultipleValue | +| | | CustomizableRadioValue | +| | | ProductLinks | +| | | ProductLinksInterface | +| | | | +| | | | +| CategoryInterface | id | CategoryTree | +| | | ProductFilterInput | +| | | CategoryFilterInput | +| | | StoreConfig | +| | | Breadcrumb | +| | | ProductAttributeFilterInput | +| | | | +| CartItemInterface | id | ConfigurableCartItem | +| | | DownloadableCartItem | +| | | SimpleCartItem | +| | | VirtualCartItem | +| | | BundleCartItem | +| | | CartItemUpdateInput | +| | | RemoveItemFromCartInput | + +
+ + +## Breaking Change Risks + +The following changes need to be avoided to ensure we're not breaking backwards compatibility: +- The nullability of existing fields/arguments _must not change_ +- The return type of existing fields _must not change_ +- Existing fields/arguments _must not be removed_ + +## Current State Breakdown (Open-Source) +
+Click to Expand + +- 51 Object Types and Interfaces with a _primary identifier_ + | Primary Identifier Field Name | Count | + |-------------------------------|-------| + | id | 34 | + | option_id | 10 | + | option_type_id | 4 | + | order_number | 1 | + | value_id | 1 | + | agreement_id | 1 | + +- 10 arguments across 8 different fields take a _foreign identifier_ + | Foreign Identifier Argument Name | Count | + |----------------------------------|-------| + | id | 4 | + | cart_id | 1 | + | identifier | 1 | + | identifiers | 1 | + | source_cart_id | 1 | + | destination_cart_id | 1 | + | orderNumber | 1 | + +- 49 Input Object fields across 44 different Input Objects take a _foreign identifier_ + | Foreign Identifier Field Name | Count | + |-------------------------------|-------| + | cart_id | 26 | + | sku | 3 | + | attribute_code | 2 | + | cart_item_id | 2 | + | code | 2 | + | customer_address_id | 2 | + | carrier_code | 1 | + | category_id | 1 | + | link_id | 1 | + | method_code | 1 | + | parent_sku | 1 | + | product_id | 1 | + | region_code | 1 | + | variant_sku | 1 | + +
\ No newline at end of file From b031037e8168b22905a60f633b8b82affe8681b0 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 6 Aug 2020 12:17:11 -0500 Subject: [PATCH 230/479] Giftwrapping changes on schema --- .../graph-ql/coverage/gift-wrapping.md | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index 897ac4098..2c3d9a2cb 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -45,6 +45,11 @@ type SalesItemInterface { gift_message: GiftMessage @doc(description: "The entered gift message for the order item") } +interface OrderItemInterface { + gift_wrapping: GiftWrapping @resolver(class: "Magento\\GiftWrappingGraphQl\\Model\\Resolver\\Order\\Item\\GiftWrapping") @doc(description: "The selected gift wrapping for the order item") +} + + type CustomerOrder { gift_wrapping: GiftWrapping @doc(description: "The selected gift wrapping for the order") printed_card_included: Boolean! @doc(description: "Whether customer requested printed card for the order") @@ -56,25 +61,40 @@ type CartPrices { gift_options: GiftOptionsPrices @doc(description: "The list of prices for the selected gift options") } -type SalesTotalsInterface { - gift_options: GiftOptionsPrices @doc(description: "The list of prices for the selected gift options") -} ###### End: Extending existing types ###### -###### Begin: Defining new types ###### -type GiftWrapping { - id: ID! @doc(description: "Gift wrapping unique identifier") - design: String! @doc(description: "Gift wrapping design name") - price: Money! @doc(description: "Gift wrapping price") - image: GiftWrappingImage! @doc(description: "Gift wrapping preview image") +###### StoreConfig ###### +type StoreConfig { + allow_gift_wrapping_on_order: String @doc(description: "Allow Gift Wrapping on Order Level") + allow_gift_wrapping_on_order_items: String @doc(description: "Allow Gift Wrapping for Order Items") + allow_gift_receipt: String @doc(description: "Allow Gift Receipt") + allow_printed_card: String @doc(description: "Allow Printed Card") + printed_card_price: String @doc(description: "Default Price for Printed Card") + cart_gift_wrapping: String @doc(description: "Display Gift Wrapping Prices") + cart_printed_card: String @doc(description: "Display Printed Card Prices") + sales_gift_wrapping: String @doc(description: "Display Gift Wrapping Prices") + sales_printed_card: String @doc(description: "Display Printed Card Prices") } +###### End ###### + + + + +###### Begin: Defining new types ###### type GiftWrappingImage { label: String! @doc(description: "Gift wrapping preview image label") url: String! @doc(description: "Gift wrapping preview image URL") } +type GiftWrapping { + id: ID! @doc(description: "Gift wrapping unique identifier") + design: String! @doc(description: "Gift wrapping design name") + price: Money! @doc(description: "Gift wrapping price") + image: GiftWrappingImage @doc(description: "Gift wrapping preview image") +} + type GiftOptionsPrices { gift_wrapping_for_order: Money @doc(description: "Price of the gift wrapping for the whole order") gift_wrapping_for_items: Money @doc(description: "Price of the gift wrapping for all individual order items") From a761d6416e8d11c71278fdb763b758b2a9f8cc77 Mon Sep 17 00:00:00 2001 From: Olga Kopylova Date: Thu, 6 Aug 2020 13:49:14 -0500 Subject: [PATCH 231/479] Catalog images tech vision - watermarking --- design-documents/media/catalog-images.md | 65 +++++++++++++----------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/design-documents/media/catalog-images.md b/design-documents/media/catalog-images.md index 09e106cf6..58cf2c54a 100644 --- a/design-documents/media/catalog-images.md +++ b/design-documents/media/catalog-images.md @@ -24,7 +24,6 @@ There is no intention to limit usage of other types of setup. * [Magento: Watermarking](#magento-watermarking) * [Fastly: Watermarking](#fastly-watermarking) * [AEM Assets: Watermarking](#aem-assets-watermarking) - + [Watermarking with Akamai](#watermarking-with-akamai) - [Placeholders](#placeholders) * [Magento: Placeholders](#magento-placeholders) * [Fastly: Placeholders](#fastly-placeholders) @@ -148,17 +147,7 @@ DM uses Akamai as CDN. Does it provide additional image transformation capabilit and many others. See [Dynamic Media Image Serving and Rendering API - Command reference](https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html) for more details. -Also, [Image Presets](https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html) with image adjustments can be created in advance and then requested by the client. - -In addition, Akamai CDN provides ability to setup [Image Manager Policies](https://learn.akamai.com/pdf/ImageManager-CS.pdf) with the following parameters: - -1. Background color -2. Crop -3. Resize -4. Rotate -5. Scale - -and others. +In addition, [Image Presets](https://docs.adobe.com/content/help/en/experience-manager-65/assets/dynamic/managing-image-presets.html) with image adjustments can be created in advance and then requested by the client. ## Watermarking @@ -185,40 +174,54 @@ Watermarking Scoping? -#### Watermarking with Akamai - -Akamai CDN also provides Watermarking feature using their [Image Manager policies](https://learn.akamai.com/pdf/ImageManager-CS.pdf). -Watermark can be applied to images based on client or server-based rules. -Potentially this may help implement Magento-style scoping for watermark images. +## Placeholders -Also, see [WATERMARKING: A CONTENT OWNER'S MARK TO PREVENT PIRACY](https://blogs.akamai.com/2019/10/watermarking-a-content-owners-mark-to-prevent-piracy.html). -Looks like, it requires integration with a watermark provider. +Placeholders are used in the following cases: -## Placeholders +1. A product has no image assigned to it. +2. An image assigned to the product is unavailable (due to delays caused by distributed architecture or accidentally) -What should happen when requested asset is not available? -Use cases: +Expectations from the placeholder delivery: -1. The asset has been removed/renamed, and the change is not synced to Storefront service yet -2. The asset has been removed/renamed by mistake or due to server issues +1. Client application can distinguish a placeholder from a real product image. This gives the client an ability to control what (if anything) should be displayed as placeholder. +2. Placeholder image should be possible to cache on the client side and reuse in different places. +3. If possible, the system (Magento?) should provide placeholder URL, so the client can use it if desired (not all clients may want to use admin-configured placeholders). -While we can hope that such things should not happen, it still should not be the visitor fault and there should be a good handling of such situations. -Currently, Magento handles it by loading a placeholder image. +Based on the above, Catalog service should not handle placeholders and try to provide them if product image is absent. +Instead, product image data should be empty, and system configuration GraphQL API should provide URL for placeholders by type. +To fully support external DAM systems, it should be possible to specify placeholder URLs in Magento system configuration, in addition to existing "upload file" option. +Configuration API should return full URL to the placeholder when requested by the client. This can be: -With new architecture, CDN or DAM should be able to provide such a placeholder. +1. URL pointing to Magento application for default placeholder image (example: `https://static.base.url/catalog/product/placeholder.jpg`) +2. URL pointing to uploaded placeholder (example: `https://media.base.url/catalog/product/placeholder.jpg`) +3. URL pointing to external DAM/CDN (example: `https://external.dam.com/my-magento/catalog/product/placeholder.jpg`) -TBD: check how this can be supported with Fastly, AEM Assets/DM, Akamai. +The client should not assume the placeholder base URL is the same as Media Base URL. +It is responsibility of the client application to handle situation where an image assigned to a product is absent in the storage and handle this situation gracefully. ### Magento: Placeholders +Magento allows to specify a placeholder image for each type of the product image (base, small, thumbnail, swatch) per store view. + Magento validates whether the file is present on the disk at the moment of URL generation, and provides URL to a placeholder image if the file is absent. -This limits the system to using local storage for the assets, and makes it difficult to fetch images from external DAM systems. +This covers two different use cases: + +1. The image has never been specified for the product, so there is no image available and no file is actually validated for presence. +2. The imaghe has been assigned to the product, but then has disappeared from the storage (due to a mistake, failure or otherwise). -### Fastly: Placeholders +Existing behavior limits the system to using local storage for the assets as it has a hard dependency on the local file system, and makes it difficult to fetch images from external DAM systems. +As mentioned before, placeholder URLs should be supported. ### AEM Assets: Placeholders -DefaultImage - allows client to specify default image. +When integrating with AEM Assets or another external DAM system: + +1. Content author uploads and publishes placeholder asset in the DAM system +2. Magento admin references the published placeholder URL in Magento system configuration +3. Client application requests the placeholder URL via Magento GraphQL API to place where a product image can't be loaded + +In addition, Dynamic Media supports "DefaultImage", which allows the client to specify default image. +This can be used as another fallback path in case an image is absent. See https://docs.adobe.com/content/help/en/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html From 53137481cf42e6f0be4c9d9998d1bd57bb7d07ca Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 6 Aug 2020 15:48:42 -0500 Subject: [PATCH 232/479] Added changes from review to Giftwrapping schema --- design-documents/graph-ql/coverage/gift-wrapping.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index 2c3d9a2cb..68a8468a0 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -119,11 +119,11 @@ The following gift options need to be whitelisted in the `storeConfig` query. Se
- + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno From 599fdbfa9a80be34e4fcaf6e665b3d24b983f82d Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Thu, 6 Aug 2020 18:10:55 -0500 Subject: [PATCH 233/479] Remove giftwrapping on cart level and on item level changes on schema --- design-documents/graph-ql/coverage/gift-wrapping.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index 68a8468a0..b85fd4927 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -196,10 +196,16 @@ type CartItemUpdateInput { gift_wrapping_id: ID @doc(description: "The unique identifier of the gift wrapping to be used for the cart item") gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") } +type RemoveItemFromCartInput { + gift_wrapping_id: ID @doc(description: "The unique identifier of the gift wrapping to be used for removing from the cart item") +} type Mutation { setGiftOptionsOnCart(cart_id: String!, gift_message: GiftMessageInput, gift_wrapping_id: ID, gift_receipt_included: Boolean, printed_card_included: Boolean): SetGiftOptionsOnCartOutput @doc(description: "Set gift options like gift wrapping or gift message for the entire cart") + removeGiftOptionsOnCart(cart_id: String!, gift_wrapping_id: ID, gift_receipt_included: Boolean, printed_card_included: Boolean): RemoveGiftOptionsOnCartOutput @doc(description: "Remove gift options gift wrapping for the entire cart") } + + ###### End: Extending existing types ###### @@ -207,6 +213,10 @@ type Mutation { type SetGiftOptionsOnCartOutput { cart: Cart! @doc(description: "The modified cart object") } +type RemoveGiftOptionsOnCartOutput { + cart: Cart! @doc(description: "The modified cart object") +} + type GiftMessageInput { to: String! @doc(description: "Recepient name") From 3542972192ba3cf27c46060198c7a9891ed48c38 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 7 Aug 2020 13:33:15 -0500 Subject: [PATCH 234/479] MC-36365: Schema for B2B purchase order details --- .../graph-ql/coverage/b2b/purchase-order.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 design-documents/graph-ql/coverage/b2b/purchase-order.md diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md new file mode 100644 index 000000000..e143594ed --- /dev/null +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -0,0 +1,67 @@ +## Use Cases + +### View "My Purchase Orders" list + +### View "Requires My Approval" list of purchase orders + +Should support pagination. + +### View "Company Purchase Orders" list + +Should support pagination. + +### View purchase order details + +Should support pagination. + +#### Items + +Should support pagination. + +#### Basic details +#### Approval Flow +#### Comments +#### History Log +#### Totals +#### Shipping Address +#### Billing Address +#### Payment Method +#### Shipping Method + +### Add items to cart from purchase order + +### Add purchase order comment + +### Reject purchase order + +### Cancel purchase order + +### Approve purchase order + +### Get a list of available actions + +### Store config + +Consider combining the following settings and exposing as a single field: +- Check if purchase order is enabled on global level +- If purchase order enabled on company level +- If comapnies are enabled on global level + +### View a list of approval rules + +Should support pagination. + +### View approval rule details + +### Create new approval rule + +#### Get list of "Applies to" users + +#### Get list of "Requires approval from" users + +### Update approval rule + +### Delete approval rule + + + From cd181bc6ddb6b245bcdad6e271dd3d6a9ef632c4 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 7 Aug 2020 17:04:12 -0500 Subject: [PATCH 235/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 36 +++++++ .../graph-ql/coverage/b2b/purchase-order.md | 99 ++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 design-documents/graph-ql/coverage/b2b/purchase-order.graphqls diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls new file mode 100644 index 000000000..1a11024f1 --- /dev/null +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -0,0 +1,36 @@ +type Customer { + purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders +} + +input PurchaseOrdersFilterInput { + status: PurchaseOrderStatus + createdBy: FilterStringTypeInput + createdDate: FilterRangeTypeInput +} + +type PurchaseOrders { + items: [PurchaseOrder]! + page_info: SearchResultPageInfo + total_count: Int +} + +type PurchaseOrder { + uid: ID! + number: String! + order: CustomerOrder + created_at: String! + created_by: String! + status: PurchaseOrderStatus! + total: Money! +} + +enum PurchaseOrderStatus { + PENDING + APPROVAL_REQUIRED + APPROVED + ORDER_IN_PROGRESS + ORDER_PLACED + ORDER_FAILED + REJECTED + CANCELED +} diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index e143594ed..bdd293fea 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -2,14 +2,105 @@ ### View "My Purchase Orders" list +```graphql +{ + customer { + purchase_orders( + filter: {createdBy: {eq: "Active User Name"}}, + currentPage: 1, + pageSize: 10 + ) { + items { + uid + number + order { + number + } + created_at + created_by + status + total { + currency + value + } + } + total_count + page_info { + current_page + page_size + total_pages + } + } + } +} +``` ### View "Requires My Approval" list of purchase orders -Should support pagination. - +```graphql +{ + customer { + purchase_orders( + filter: {status: APPROVAL_REQUIRED}, + currentPage: 1, + pageSize: 10 + ) { + items { + uid + number + order { + number + } + created_at + created_by + status + total { + currency + value + } + } + total_count + page_info { + current_page + page_size + total_pages + } + } + } +} +``` ### View "Company Purchase Orders" list -Should support pagination. - +```graphql +{ + customer { + purchase_orders( + currentPage: 1, + pageSize: 10 + ) { + items { + uid + number + order { + number + } + created_at + created_by + status + total { + currency + value + } + } + total_count + page_info { + current_page + page_size + total_pages + } + } + } +} +``` ### View purchase order details Should support pagination. From 86f6ec524d27bacce77466cbca8af699b79cc947 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Fri, 7 Aug 2020 18:29:25 -0500 Subject: [PATCH 236/479] Cleaned up schema --- .../graph-ql/coverage/gift-wrapping.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index b85fd4927..5e8f200b4 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -196,15 +196,18 @@ type CartItemUpdateInput { gift_wrapping_id: ID @doc(description: "The unique identifier of the gift wrapping to be used for the cart item") gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") } -type RemoveItemFromCartInput { - gift_wrapping_id: ID @doc(description: "The unique identifier of the gift wrapping to be used for removing from the cart item") -} type Mutation { - setGiftOptionsOnCart(cart_id: String!, gift_message: GiftMessageInput, gift_wrapping_id: ID, gift_receipt_included: Boolean, printed_card_included: Boolean): SetGiftOptionsOnCartOutput @doc(description: "Set gift options like gift wrapping or gift message for the entire cart") - removeGiftOptionsOnCart(cart_id: String!, gift_wrapping_id: ID, gift_receipt_included: Boolean, printed_card_included: Boolean): RemoveGiftOptionsOnCartOutput @doc(description: "Remove gift options gift wrapping for the entire cart") + setGiftOptionsOnCart(input: SetGiftOptionsOnCartInput): SetGiftOptionsOnCartOutput @doc(description: "Set gift options like gift wrapping or gift message for the entire cart") } +input SetGiftOptionsOnCartInput{ + cart_id: String! @doc(description:"The unique ID that identifies the shopper's cart") + gift_message: GiftMessageInput @doc(description: "Gift message details for the cart") + gift_wrapping_id: ID @doc(description: "The unique identifier of the gift wrapping to be used for the cart") + printed_card_included: Boolean! @doc(description: "Whether customer requested printed card for the cart") + gift_receipt_included: Boolean! @doc(description: "Whether customer requested gift receipt for the cart") +} ###### End: Extending existing types ###### @@ -213,12 +216,10 @@ type Mutation { type SetGiftOptionsOnCartOutput { cart: Cart! @doc(description: "The modified cart object") } -type RemoveGiftOptionsOnCartOutput { - cart: Cart! @doc(description: "The modified cart object") -} -type GiftMessageInput { + +input GiftMessageInput { to: String! @doc(description: "Recepient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") From e1c7b39ab01fe0b4dc456c28f007f37707ee40a2 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Fri, 7 Aug 2020 18:38:34 -0500 Subject: [PATCH 237/479] removed gift_wrapping from salesItemInterface on schema --- design-documents/graph-ql/coverage/gift-wrapping.md | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index 5e8f200b4..cb5954742 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -41,7 +41,6 @@ type GiftCardCartItem { } type SalesItemInterface { - gift_wrapping: GiftWrapping @doc(description: "The selected gift wrapping for the order item") gift_message: GiftMessage @doc(description: "The entered gift message for the order item") } From 665f6002b52199bdc192deeee152733ecfe97919 Mon Sep 17 00:00:00 2001 From: Anusha Vattam Date: Fri, 7 Aug 2020 21:29:36 -0500 Subject: [PATCH 238/479] merge conflicts --- design-documents/graph-ql/coverage/gift-wrapping.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/gift-wrapping.md b/design-documents/graph-ql/coverage/gift-wrapping.md index cb5954742..85ffbc3b6 100644 --- a/design-documents/graph-ql/coverage/gift-wrapping.md +++ b/design-documents/graph-ql/coverage/gift-wrapping.md @@ -48,7 +48,6 @@ interface OrderItemInterface { gift_wrapping: GiftWrapping @resolver(class: "Magento\\GiftWrappingGraphQl\\Model\\Resolver\\Order\\Item\\GiftWrapping") @doc(description: "The selected gift wrapping for the order item") } - type CustomerOrder { gift_wrapping: GiftWrapping @doc(description: "The selected gift wrapping for the order") printed_card_included: Boolean! @doc(description: "Whether customer requested printed card for the order") @@ -216,8 +215,6 @@ type SetGiftOptionsOnCartOutput { cart: Cart! @doc(description: "The modified cart object") } - - input GiftMessageInput { to: String! @doc(description: "Recepient name") from: String! @doc(description: "Sender name") From 6c1ff15c311a0fb57a6a225053f8a164cee76222 Mon Sep 17 00:00:00 2001 From: oleksandrkravchuk Date: Sat, 8 Aug 2020 15:50:05 +0300 Subject: [PATCH 239/479] GraphQl Schema actualization with latest released changes. Add ProductInfo to the schema for intesection search. --- .../graph-ql/coverage/store-pickup.graphqls | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/store-pickup.graphqls b/design-documents/graph-ql/coverage/store-pickup.graphqls index 772484150..a4db6f882 100644 --- a/design-documents/graph-ql/coverage/store-pickup.graphqls +++ b/design-documents/graph-ql/coverage/store-pickup.graphqls @@ -1,12 +1,15 @@ type Query { pickupLocations ( - filter: PickupLocationFilterInput, + area: AreaInput, + filters: PickupLocationFilterInput, + sort: PickupLocationSortInput, pageSize: Int = 20, - currentPage: Int = 1 + currentPage: Int = 1, + productsInfo: [ProductInfoInput] ): PickupLocations } -input PickupLocationFilterInput { +input AreaInput { # This type is added for extensibility search_term: String! # Depending on the distance calculation algorithm selected in the admin, this field will require ZIP code (for offline mode) or arbitrary part of the address (for Google mode). IMPORTANT: Current mode must be exposed as part of storeConfig query and used on the client to display different hints for the input field radius: Int # This field is not part of MVP and can be added later. IMPORTANT: Radius units must be exposed as part of storeConfig query and displayed on the client @@ -18,12 +21,55 @@ type PickupLocations { total_count: Int } +input PickupLocationFilterInput { + name: FilterTypeInput + pickup_location_code: FilterTypeInput + country_id: FilterTypeInput + postcode: FilterTypeInput + region: FilterTypeInput + region_id: FilterTypeInput + city: FilterTypeInput + street: FilterTypeInput +} + +input PickupLocationSortInput { + name: SortEnum + pickup_location_code: SortEnum + distance: SortEnum + country_id: SortEnum + region: SortEnum + region_id: SortEnum + city: SortEnum + street: SortEnum + postcode: SortEnum + longitude: SortEnum + latitude: SortEnum + email: SortEnum + fax: SortEnum + phone: SortEnum + contact_name: SortEnum + description: SortEnum +} + type PickupLocation { + pickup_location_code: String name: String! # In the admin is called Frontend Name description: String! # In the admin is called Frontend Description - country: String! - region: String! - city: String! - street: String! - postcode: String! + email: String + fax: String + contact_name: String + latitude: Float + longitude: Float + country_id: String + region_id: Int + region: String + city: String + street: String + postcode: String + phone: String +} + +# Used in products assignment intersection search - select Pickup Locations which can be used to deliver all products in the request. +input ProductInfoInput { + sku: String! } From 68e693e6ac00367a06bac32c2f2ca072a3da6476 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 10 Aug 2020 10:14:22 -0500 Subject: [PATCH 240/479] Add additional product types --- .../graph-ql/coverage/customer-orders.md | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index a79db05ad..e6129f034 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -77,7 +77,7 @@ type CustomerOrder { billing_address: OrderAddress @doc("billing address for the order") carrier: String @doc("shipping carrier for the order delivery") shipping_method: String @doc("shipping method for the order") - comments: [CommentItem] @doc("comments on the order") + comments: [SalesCommentItem] @doc("comments on the order") } ``` @@ -129,20 +129,23 @@ type ItemSelectedBundleOptionValue { } type DownloadableOrderItem implements OrderItemInterface { - downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") } + type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "A string that encodes option details.") } type GiftCardOrderItem implements OrderItemInterface { - gift_card: GiftCardItem @doc(description: "Selected gift card properties for an order item") @resolver(class: "Magento\\GiftCardGraphQl\\Model\\Resolver\\OrderItem\\GiftCardItem") + gift_card: GiftCardItem @doc(description: "Selected gift card properties for an order item") } type GiftCardItem { - sender_name: String @doc(description: "Entered gift card sender name and email") - recipient_name: String @doc(description: "Entered gift card recipient name and email") + sender_name: String @doc(description: "Entered gift card sender name") + sender_email: String @doc(description: "Entered gift card sender email") + recipient_name: String @doc(description: "Entered gift card recipient name") + recipient_email: String @doc(description: "Entered gift card recipient email") message: String @doc(description: "Entered gift card message intended for the recipient") } @@ -216,7 +219,7 @@ type Invoice { number: String! @doc("sequential invoice number") total: InvoiceTotal @doc("invoice total amount details") items: [InvoiceItemInterface] @doc("invoiced product details") - comments: [CommentItem] @doc("comments on the invoice") + comments: [SalesCommentItem] @doc("comments on the invoice") } @doc("Invoice item details") @@ -237,6 +240,14 @@ type BundleInvoiceItem implements InvoiceItemInterface { bundle_options: [ItemSelectedBundleOption] @doc("A list of bundle options that are assigned to the bundle product") } +type DownloadableInvoiceItem implements InvoiceItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are invoiced from the downloadable product") +} + +type GiftCardInvoiceItem implements InvoiceItemInterface { + gift_card: GiftCardItem @doc(description: "Selected gift card properties for an invoice item") +} + @doc("Invoice total amount details") type InvoiceTotal { subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") @@ -253,16 +264,16 @@ type InvoiceTotal { ## Refund Type Schema The credit memo entity will have the similar to the order and invoice schema: -The `id` will be a `base64_encode_encode(increment_id)` which in future can be replaced by UUID. +The `id` will be a `base64encode(increment_id)` which in future can be replaced by UUID. ```graphql @doc("Credit memo details") type CreditMemo { id: ID! @doc("the ID of the credit memo, used for API purposes") number: String! @doc("sequential credit memo number") - items: [CreditMemoItem] @doc("items refunded") + items: [CreditMemoItemInterface] @doc("items refunded") total: CreditMemoTotal @doc("refund total amount details") - comments: [CommentItem] @doc("comments on the credit memo") + comments: [SalesCommentItem] @doc("comments on the credit memo") } @doc("Credit memo item details") @@ -283,6 +294,14 @@ type BundleCreditMemoItem implements CreditMemoIntemInterface { bundle_options: [ItemSelectedBundleOption] } +type DownloadableCreditMemoItem implements CreditMemoItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are refunded from the downloadable product") +} + +type GiftCardCreditMemoItem implements CreditMemoItemInterface { + gift_card: GiftCardItem @doc(description: "Selected gift card properties for a credit memo item") +} + @doc("Credit memo price details") type CreditMemoTotal { subtotal: Money! @doc("subtotal amount excluding, shipping, discounts and tax") @@ -302,8 +321,8 @@ type OrderShipment { id: ID! @doc("the ID of the shipment, used for API purposes") number: String! @doc("sequential credit shipment number") tracking: [ShipmentTracking] @doc("shipment tracking details") - items: [ShipmentItem] @doc("items included in the shipment") - comments: [CommentItem] @doc("comments on the shipment") + items: [ShipmentItemInterface] @doc("items included in the shipment") + comments: [SalesCommentItem] @doc("comments on the shipment") } @doc("Order shipment item details") @@ -323,6 +342,10 @@ type BundleShipmentItem implements ShipmentItemInterface { bundle_options: [ItemSelectedBundleOption] } +type GiftCardShipmentItem implements ShipmentItemInterface { + gift_card: GiftCardItem @doc(description: "Selected gift card properties for a shipment item") +} + @doc("Order shipment tracking details") type ShipmentTracking { title: String! @doc("shipment tracking title") @@ -331,7 +354,7 @@ type ShipmentTracking { } ``` -## CommentItem type +## SalesCommentItem type ```graphql type SalesCommentItem { timestamp: String! @doc("The timestamp of the comment") From aabf21febf3f056f6821fa6960c09cb6fb2a54d3 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 10 Aug 2020 10:44:04 -0500 Subject: [PATCH 241/479] Fix payment_methods field --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index e6129f034..9242f256c 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -72,7 +72,7 @@ type CustomerOrder { invoices: [Invoice] @doc("invoice list for the order") credit_memos: [CreditMemo] @doc("credit memo list for the order") shipments: [OrderShipment] @doc("shipment list for the order") - payment_methods: [PaymentMethod] @doc("payment details for the order") + payment_methods: [OrderPaymentMethod] @doc("payment details for the order") shipping_address: OrderAddress @doc("shipping address for the order") billing_address: OrderAddress @doc("billing address for the order") carrier: String @doc("shipping carrier for the order delivery") From f1bedde301c8909190e8f8e4e775658dbb809c03 Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 10 Aug 2020 11:06:54 -0500 Subject: [PATCH 242/479] Fix type for order item id --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 9242f256c..2f66f05b4 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -151,7 +151,7 @@ type GiftCardItem { @doc("Represents order item options like selected or entered") type OrderItemOption { - id: String! @doc("name of the option") + id: ID! @doc("name of the option") value: String! @doc("value of the option") } ``` From 46a863ac45d06cdb094eab332c8615e946223caa Mon Sep 17 00:00:00 2001 From: Daniel Renaud Date: Mon, 10 Aug 2020 11:41:38 -0500 Subject: [PATCH 243/479] Fix order item option type --- design-documents/graph-ql/coverage/customer-orders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-orders.md b/design-documents/graph-ql/coverage/customer-orders.md index 2f66f05b4..c11b40e04 100644 --- a/design-documents/graph-ql/coverage/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer-orders.md @@ -151,7 +151,7 @@ type GiftCardItem { @doc("Represents order item options like selected or entered") type OrderItemOption { - id: ID! @doc("name of the option") + label: String! @doc("name of the option") value: String! @doc("value of the option") } ``` From a2d90a0c275826e81d248266db20c6d4f273c92d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 12:00:52 -0500 Subject: [PATCH 244/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 49 ++++++++++++++++--- .../graph-ql/coverage/b2b/purchase-order.md | 7 ++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 1a11024f1..02a083ea9 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -15,13 +15,48 @@ type PurchaseOrders { } type PurchaseOrder { - uid: ID! - number: String! - order: CustomerOrder - created_at: String! - created_by: String! - status: PurchaseOrderStatus! - total: Money! + uid: ID! @doc(description: "Unique identifier for the purcahse order") + number: String! @doc(description: "The purchase order number") + order: CustomerOrder @doc(description: "The reference to the order placed based on the purchase order") + purchase_order_date: String! @doc(description: "The date the purchase order was created") + created_by: String! @doc(description: "The name of the user who created the purchase order") + status: PurchaseOrderStatus! @doc(description: "The current status of the purcahse order") + total: PurchaseOrderTotal @doc(description: "Contains details about the calculated totals for the purchase order") + items: [PurchaseOrderItem] @doc(description: "An array containing the items in this purchase order") + payment_methods: [PaymentMethod] @doc(description: "Payment details for the purchase order") + shipping_address: CustomerAddress @doc(description: "The shipping address for the purchase order") + billing_address: CustomerAddress @doc(description: "The billing address for the purchase order") + carrier: String @doc(description: "The shipping carrier for the purchase order delivery") + shipping_method: String @doc(description: "The delivery method for the purchase order") +} + +type PurchaseOrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { + subtotal: Money! @doc(description: "The subtotal of the purchase order, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the purchase order") + estimated_total_tax: Money! @doc(description: "The amount of tax applied to the purchase order") + estimated_taxes: [TaxItem] @doc(description: "The purchase order tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the order") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the purchase purchase order") +} + +type PurchaseOrderItem @doc(description: "Purchase order item details") { + uid: ID! @doc(description: "The unique identifier of the purchase order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_url_key: String @doc(description: "URL key of the base product") + product_type: String @doc(description: "The type of product, such as simple, configurable, or bundle") + product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options") + discounts: [Discount] @doc(description: "The final discount information for the product") + selected_options: [PurchaseOrderItemOption] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [PurchaseOrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image") + quantity: Float @doc(description: "The number of units for this item") +} + +type PurchaseOrderItemOption @doc(description: "Represents purcahse order item options like selected or entered") { + uid: ID! @doc(description: "The name of the option") + value: String! @doc(description: "The value of the option") } enum PurchaseOrderStatus { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index bdd293fea..a4deb714b 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -103,8 +103,6 @@ ``` ### View purchase order details -Should support pagination. - #### Items Should support pagination. @@ -155,4 +153,9 @@ Should support pagination. ### Delete approval rule +## Implementation details + +The purchase order GraphQL schema depends on Sales and Customer GraphQL schemas. + + From 6400074dbd6206411e14c9d0545eebbbdcc4c1b0 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 14:06:27 -0500 Subject: [PATCH 245/479] MC-36365: Schema for B2B purchase order details - Documented existing GraphQL schema for customer --- .../coverage/customer/customer.graphqls | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 design-documents/graph-ql/coverage/customer/customer.graphqls diff --git a/design-documents/graph-ql/coverage/customer/customer.graphqls b/design-documents/graph-ql/coverage/customer/customer.graphqls new file mode 100644 index 000000000..2577d29cd --- /dev/null +++ b/design-documents/graph-ql/coverage/customer/customer.graphqls @@ -0,0 +1,395 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type StoreConfig { + required_character_classes_number : String @doc(description: "The number of different character classes required in a password (lowercase, uppercase, digits, special characters).") + minimum_password_length : String @doc(description: "The minimum number of characters required for a valid password.") + autocomplete_on_storefront : Boolean @doc(description: "Enable autocomplete on login and forgot password forms") +} + +type Query { + customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") @cache(cacheable: false) + isEmailAvailable ( + email: String! @doc(description: "The new customer email") + ): IsEmailAvailableOutput @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\IsEmailAvailable") +} + +type Mutation { + generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") + changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") + createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") + updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") + deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") + requestPasswordResetEmail(email: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RequestPasswordResetEmail") @doc(description: "Request an email with a reset password token for the registered customer identified by the specified email.") + resetPassword(email: String!, resetPasswordToken: String!, newPassword: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ResetPassword") @doc(description: "Reset a customer's password using the reset password token that the customer received in an email after requesting it using requestPasswordResetEmail.") +} + +input CustomerAddressInput { + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + street: [String] @doc(description: "An array of strings that define the street number and name") + city: String @doc(description: "The city or town") + region: CustomerAddressRegionInput @doc(description: "An object containing the region name, region code, and region ID") + postcode: String @doc(description: "The customer's ZIP or postal code") + country_id: CountryCodeEnum @doc(description: "Deprecated: use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") + default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") + default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + fax: String @doc(description: "The fax number") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.") +} + +input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { + region_code: String @doc(description: "The address region code") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "The unique ID for a pre-defined region") +} + +input CustomerAddressAttributeInput { + attribute_code: String! @doc(description: "Attribute code") + value: String! @doc(description: "Attribute value") +} + +type CustomerToken { + token: String @doc(description: "The customer token") +} + +input CustomerInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +type CustomerOutput { + customer: Customer! +} + +type RevokeCustomerTokenOutput { + result: Boolean! +} + +type Customer @doc(description: "Customer defines the customer name and address and other details") { + created_at: String @doc(description: "Timestamp indicating when the account was created") + group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + default_billing: String @doc(description: "The ID assigned to the billing address") + default_shipping: String @doc(description: "The ID assigned to the shipping address") + dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") + addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") +} + +type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ + id: Int @doc(description: "The ID assigned to the address object") + customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") + region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") + region_id: Int @doc(description: "The unique ID for a pre-defined region") + country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") + street: [String] @doc(description: "An array of strings that define the street number and name") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + fax: String @doc(description: "The fax number") + postcode: String @doc(description: "The customer's ZIP or postal code") + city: String @doc(description: "The city or town") + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") + default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into container") + extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes") +} + +type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { + region_code: String @doc(description: "The address region code") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "The unique ID for a pre-defined region") +} + +type CustomerAddressAttribute { + attribute_code: String @doc(description: "Attribute code") + value: String @doc(description: "Attribute value") +} + +type IsEmailAvailableOutput { + is_email_available: Boolean @doc(description: "Is email availabel value") +} + +enum CountryCodeEnum @doc(description: "The list of countries codes") { + AF @doc(description: "Afghanistan") + AX @doc(description: "Åland Islands") + AL @doc(description: "Albania") + DZ @doc(description: "Algeria") + AS @doc(description: "American Samoa") + AD @doc(description: "Andorra") + AO @doc(description: "Angola") + AI @doc(description: "Anguilla") + AQ @doc(description: "Antarctica") + AG @doc(description: "Antigua & Barbuda") + AR @doc(description: "Argentina") + AM @doc(description: "Armenia") + AW @doc(description: "Aruba") + AU @doc(description: "Australia") + AT @doc(description: "Austria") + AZ @doc(description: "Azerbaijan") + BS @doc(description: "Bahamas") + BH @doc(description: "Bahrain") + BD @doc(description: "Bangladesh") + BB @doc(description: "Barbados") + BY @doc(description: "Belarus") + BE @doc(description: "Belgium") + BZ @doc(description: "Belize") + BJ @doc(description: "Benin") + BM @doc(description: "Bermuda") + BT @doc(description: "Bhutan") + BO @doc(description: "Bolivia") + BA @doc(description: "Bosnia & Herzegovina") + BW @doc(description: "Botswana") + BV @doc(description: "Bouvet Island") + BR @doc(description: "Brazil") + IO @doc(description: "British Indian Ocean Territory") + VG @doc(description: "British Virgin Islands") + BN @doc(description: "Brunei") + BG @doc(description: "Bulgaria") + BF @doc(description: "Burkina Faso") + BI @doc(description: "Burundi") + KH @doc(description: "Cambodia") + CM @doc(description: "Cameroon") + CA @doc(description: "Canada") + CV @doc(description: "Cape Verde") + KY @doc(description: "Cayman Islands") + CF @doc(description: "Central African Republic") + TD @doc(description: "Chad") + CL @doc(description: "Chile") + CN @doc(description: "China") + CX @doc(description: "Christmas Island") + CC @doc(description: "Cocos (Keeling) Islands") + CO @doc(description: "Colombia") + KM @doc(description: "Comoros") + CG @doc(description: "Congo-Brazzaville") + CD @doc(description: "Congo-Kinshasa") + CK @doc(description: "Cook Islands") + CR @doc(description: "Costa Rica") + CI @doc(description: "Côte d’Ivoire") + HR @doc(description: "Croatia") + CU @doc(description: "Cuba") + CY @doc(description: "Cyprus") + CZ @doc(description: "Czech Republic") + DK @doc(description: "Denmark") + DJ @doc(description: "Djibouti") + DM @doc(description: "Dominica") + DO @doc(description: "Dominican Republic") + EC @doc(description: "Ecuador") + EG @doc(description: "Egypt") + SV @doc(description: "El Salvador") + GQ @doc(description: "Equatorial Guinea") + ER @doc(description: "Eritrea") + EE @doc(description: "Estonia") + ET @doc(description: "Ethiopia") + FK @doc(description: "Falkland Islands") + FO @doc(description: "Faroe Islands") + FJ @doc(description: "Fiji") + FI @doc(description: "Finland") + FR @doc(description: "France") + GF @doc(description: "French Guiana") + PF @doc(description: "French Polynesia") + TF @doc(description: "French Southern Territories") + GA @doc(description: "Gabon") + GM @doc(description: "Gambia") + GE @doc(description: "Georgia") + DE @doc(description: "Germany") + GH @doc(description: "Ghana") + GI @doc(description: "Gibraltar") + GR @doc(description: "Greece") + GL @doc(description: "Greenland") + GD @doc(description: "Grenada") + GP @doc(description: "Guadeloupe") + GU @doc(description: "Guam") + GT @doc(description: "Guatemala") + GG @doc(description: "Guernsey") + GN @doc(description: "Guinea") + GW @doc(description: "Guinea-Bissau") + GY @doc(description: "Guyana") + HT @doc(description: "Haiti") + HM @doc(description: "Heard & McDonald Islands") + HN @doc(description: "Honduras") + HK @doc(description: "Hong Kong SAR China") + HU @doc(description: "Hungary") + IS @doc(description: "Iceland") + IN @doc(description: "India") + ID @doc(description: "Indonesia") + IR @doc(description: "Iran") + IQ @doc(description: "Iraq") + IE @doc(description: "Ireland") + IM @doc(description: "Isle of Man") + IL @doc(description: "Israel") + IT @doc(description: "Italy") + JM @doc(description: "Jamaica") + JP @doc(description: "Japan") + JE @doc(description: "Jersey") + JO @doc(description: "Jordan") + KZ @doc(description: "Kazakhstan") + KE @doc(description: "Kenya") + KI @doc(description: "Kiribati") + KW @doc(description: "Kuwait") + KG @doc(description: "Kyrgyzstan") + LA @doc(description: "Laos") + LV @doc(description: "Latvia") + LB @doc(description: "Lebanon") + LS @doc(description: "Lesotho") + LR @doc(description: "Liberia") + LY @doc(description: "Libya") + LI @doc(description: "Liechtenstein") + LT @doc(description: "Lithuania") + LU @doc(description: "Luxembourg") + MO @doc(description: "Macau SAR China") + MK @doc(description: "Macedonia") + MG @doc(description: "Madagascar") + MW @doc(description: "Malawi") + MY @doc(description: "Malaysia") + MV @doc(description: "Maldives") + ML @doc(description: "Mali") + MT @doc(description: "Malta") + MH @doc(description: "Marshall Islands") + MQ @doc(description: "Martinique") + MR @doc(description: "Mauritania") + MU @doc(description: "Mauritius") + YT @doc(description: "Mayotte") + MX @doc(description: "Mexico") + FM @doc(description: "Micronesia") + MD @doc(description: "Moldova") + MC @doc(description: "Monaco") + MN @doc(description: "Mongolia") + ME @doc(description: "Montenegro") + MS @doc(description: "Montserrat") + MA @doc(description: "Morocco") + MZ @doc(description: "Mozambique") + MM @doc(description: "Myanmar (Burma)") + NA @doc(description: "Namibia") + NR @doc(description: "Nauru") + NP @doc(description: "Nepal") + NL @doc(description: "Netherlands") + AN @doc(description: "Netherlands Antilles") + NC @doc(description: "New Caledonia") + NZ @doc(description: "New Zealand") + NI @doc(description: "Nicaragua") + NE @doc(description: "Niger") + NG @doc(description: "Nigeria") + NU @doc(description: "Niue") + NF @doc(description: "Norfolk Island") + MP @doc(description: "Northern Mariana Islands") + KP @doc(description: "North Korea") + NO @doc(description: "Norway") + OM @doc(description: "Oman") + PK @doc(description: "Pakistan") + PW @doc(description: "Palau") + PS @doc(description: "Palestinian Territories") + PA @doc(description: "Panama") + PG @doc(description: "Papua New Guinea") + PY @doc(description: "Paraguay") + PE @doc(description: "Peru") + PH @doc(description: "Philippines") + PN @doc(description: "Pitcairn Islands") + PL @doc(description: "Poland") + PT @doc(description: "Portugal") + QA @doc(description: "Qatar") + RE @doc(description: "Réunion") + RO @doc(description: "Romania") + RU @doc(description: "Russia") + RW @doc(description: "Rwanda") + WS @doc(description: "Samoa") + SM @doc(description: "San Marino") + ST @doc(description: "São Tomé & Príncipe") + SA @doc(description: "Saudi Arabia") + SN @doc(description: "Senegal") + RS @doc(description: "Serbia") + SC @doc(description: "Seychelles") + SL @doc(description: "Sierra Leone") + SG @doc(description: "Singapore") + SK @doc(description: "Slovakia") + SI @doc(description: "Slovenia") + SB @doc(description: "Solomon Islands") + SO @doc(description: "Somalia") + ZA @doc(description: "South Africa") + GS @doc(description: "South Georgia & South Sandwich Islands") + KR @doc(description: "South Korea") + ES @doc(description: "Spain") + LK @doc(description: "Sri Lanka") + BL @doc(description: "St. Barthélemy") + SH @doc(description: "St. Helena") + KN @doc(description: "St. Kitts & Nevis") + LC @doc(description: "St. Lucia") + MF @doc(description: "St. Martin") + PM @doc(description: "St. Pierre & Miquelon") + VC @doc(description: "St. Vincent & Grenadines") + SD @doc(description: "Sudan") + SR @doc(description: "Suriname") + SJ @doc(description: "Svalbard & Jan Mayen") + SZ @doc(description: "Swaziland") + SE @doc(description: "Sweden") + CH @doc(description: "Switzerland") + SY @doc(description: "Syria") + TW @doc(description: "Taiwan") + TJ @doc(description: "Tajikistan") + TZ @doc(description: "Tanzania") + TH @doc(description: "Thailand") + TL @doc(description: "Timor-Leste") + TG @doc(description: "Togo") + TK @doc(description: "Tokelau") + TO @doc(description: "Tonga") + TT @doc(description: "Trinidad & Tobago") + TN @doc(description: "Tunisia") + TR @doc(description: "Turkey") + TM @doc(description: "Turkmenistan") + TC @doc(description: "Turks & Caicos Islands") + TV @doc(description: "Tuvalu") + UG @doc(description: "Uganda") + UA @doc(description: "Ukraine") + AE @doc(description: "United Arab Emirates") + GB @doc(description: "United Kingdom") + US @doc(description: "United States") + UY @doc(description: "Uruguay") + UM @doc(description: "U.S. Outlying Islands") + VI @doc(description: "U.S. Virgin Islands") + UZ @doc(description: "Uzbekistan") + VU @doc(description: "Vanuatu") + VA @doc(description: "Vatican City") + VE @doc(description: "Venezuela") + VN @doc(description: "Vietnam") + WF @doc(description: "Wallis & Futuna") + EH @doc(description: "Western Sahara") + YE @doc(description: "Yemen") + ZM @doc(description: "Zambia") + ZW @doc(description: "Zimbabwe") +} From 57f33eac58b74a4bbd1d0e3b744880f8eb3b0598 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 14:11:34 -0500 Subject: [PATCH 246/479] MC-36365: Schema for B2B purchase order details - Introduced `country` field in `CustomerAddress` to be able to fetch country name from the customer address - Deprecated `region_id` and `country_code` --- .../graph-ql/coverage/customer/customer.graphqls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/customer.graphqls b/design-documents/graph-ql/coverage/customer/customer.graphqls index 2577d29cd..42075d0f8 100644 --- a/design-documents/graph-ql/coverage/customer/customer.graphqls +++ b/design-documents/graph-ql/coverage/customer/customer.graphqls @@ -110,9 +110,10 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform id: Int @doc(description: "The ID assigned to the address object") customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") - region_id: Int @doc(description: "The unique ID for a pre-defined region") + region_id: Int @doc(description: "The unique ID for a pre-defined region") @deprecated(reason: "Use `region` instead.") country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") - country_code: CountryCodeEnum @doc(description: "The customer's country") + country_code: CountryCodeEnum @doc(description: "The customer's country") @deprecated(reason: "Use `country` instead.") + country: Country @doc(description: "The customer's country") street: [String] @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's company") telephone: String @doc(description: "The telephone number") From 34dc68d2b47bd4238f6ae7707d2041a470cf6e8b Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 14:38:19 -0500 Subject: [PATCH 247/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 6 + .../graph-ql/coverage/b2b/purchase-order.md | 154 +++++++++++++++--- .../catalog/configurable-options-selection.md | 2 +- 3 files changed, 139 insertions(+), 23 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 02a083ea9..2251bb142 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -1,5 +1,6 @@ type Customer { purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders + purchase_order(uid: ID!): PurchaseOrder } input PurchaseOrdersFilterInput { @@ -69,3 +70,8 @@ enum PurchaseOrderStatus { REJECTED CANCELED } + +type CustomerAddress { + # This field must be added to the CustomerAddress type definition directly in CustomerGraphQl module + country: Country @doc(description: "The customer's country") +} diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index a4deb714b..130d71220 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -16,12 +16,14 @@ order { number } - created_at + purchase_order_date created_by status total { - currency - value + grand_total { + currency + value + } } } total_count @@ -50,12 +52,14 @@ order { number } - created_at + purchase_order_date created_by status total { - currency - value + grand_total { + currency + value + } } } total_count @@ -83,12 +87,14 @@ order { number } - created_at + purchase_order_date created_by status total { - currency - value + grand_total { + currency + value + } } } total_count @@ -103,19 +109,123 @@ ``` ### View purchase order details -#### Items - -Should support pagination. - -#### Basic details -#### Approval Flow -#### Comments -#### History Log -#### Totals -#### Shipping Address -#### Billing Address -#### Payment Method -#### Shipping Method +The query should allow to fetch the following data: + - Items with pagination + - Basic details + - Approval Flow + - Comments + - History Log + - Totals + - Shipping Address + - Billing Address + - Payment Method + - Shipping Method + +```graphql +{ + customer { + purchase_order(uid: "abc234hsasdfa") { + uid + created_by + purchase_order_date + number + order { + number + } + status + total { + subtotal { + currency + value + } + estimated_taxes { + amount { + currency + value + } + rate + title + } + grand_total { + currency + value + } + shipping_handling { + total_amount { + currency + value + } + } + } + items { + uid + product_name + product_sku + product_url_key + product_type + product_sale_price { + currency + value + } + quantity + selected_options { + uid + value + } + entered_options { + uid + value + } + discounts { + amount { + currency + value + } + label + } + } + payment_methods { + name + type + additional_data { + name + value + } + } + billing_address { + firstname + lastname + street + city + region { + region + } + postcode + country { + full_name_locale + } + telephone + } + carrier + shipping_method + shipping_address { + firstname + lastname + street + city + region { + region + } + postcode + country { + full_name_locale + } + telephone + } + } + } +} +``` ### Add items to cart from purchase order diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 74b1c5880..6fdeddf52 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -178,7 +178,7 @@ In case when the facet filter was used on the category page, for example to sear products(filter: {category_id: {eq: "shorts category ID"}}) { items { name - sku + sku ... on ConfigurableProduct { configurable_options_selection_metadata( selectedConfigurableOptionValues: ["hash from selected red color option"] From cca39016db25fb16eda034d3bab9cb5b248d3f03 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 14:57:23 -0500 Subject: [PATCH 248/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 8 +++- .../graph-ql/coverage/b2b/purchase-order.md | 48 +++++++++++-------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 2251bb142..a26ddc542 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -23,7 +23,7 @@ type PurchaseOrder { created_by: String! @doc(description: "The name of the user who created the purchase order") status: PurchaseOrderStatus! @doc(description: "The current status of the purcahse order") total: PurchaseOrderTotal @doc(description: "Contains details about the calculated totals for the purchase order") - items: [PurchaseOrderItem] @doc(description: "An array containing the items in this purchase order") + items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") payment_methods: [PaymentMethod] @doc(description: "Payment details for the purchase order") shipping_address: CustomerAddress @doc(description: "The shipping address for the purchase order") billing_address: CustomerAddress @doc(description: "The billing address for the purchase order") @@ -31,6 +31,12 @@ type PurchaseOrder { shipping_method: String @doc(description: "The delivery method for the purchase order") } +type PurchaseOrderItems { + items: [PurchaseOrderItem]! + page_info: SearchResultPageInfo + total_count: Int +} + type PurchaseOrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { subtotal: Money! @doc(description: "The subtotal of the purchase order, excluding shipping, discounts, and taxes") discounts: [Discount] @doc(description: "The applied discounts to the purchase order") diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 130d71220..e2f5c7828 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -157,31 +157,39 @@ The query should allow to fetch the following data: } } } - items { - uid - product_name - product_sku - product_url_key - product_type - product_sale_price { - currency - value + items(currentPage: 1, pageSize: 10) { + total_count + page_info { + current_page + page_size + total_pages } - quantity - selected_options { + items { uid - value - } - entered_options { - uid - value - } - discounts { - amount { + product_name + product_sku + product_url_key + product_type + product_sale_price { currency value } - label + quantity + selected_options { + uid + value + } + entered_options { + uid + value + } + discounts { + amount { + currency + value + } + label + } } } payment_methods { From a2d7f0ee1e79fc0feb0a267690de2836656dc1e8 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 10 Aug 2020 16:52:12 -0500 Subject: [PATCH 249/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 14 +++++++++++++ .../graph-ql/coverage/b2b/purchase-order.md | 21 ++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index a26ddc542..0ef8832b0 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -24,6 +24,7 @@ type PurchaseOrder { status: PurchaseOrderStatus! @doc(description: "The current status of the purcahse order") total: PurchaseOrderTotal @doc(description: "Contains details about the calculated totals for the purchase order") items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") + comments(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderComments @doc(description: "Purchase order comments") payment_methods: [PaymentMethod] @doc(description: "Payment details for the purchase order") shipping_address: CustomerAddress @doc(description: "The shipping address for the purchase order") billing_address: CustomerAddress @doc(description: "The billing address for the purchase order") @@ -31,6 +32,19 @@ type PurchaseOrder { shipping_method: String @doc(description: "The delivery method for the purchase order") } +type PurchaseOrderComments { + items: [PurchaseOrderComment]! + page_info: SearchResultPageInfo + total_count: Int +} + +type PurchaseOrderComment { + uid: ID! + creation_date_and_time: String! @doc(description: "The date and time when the comment was created") + author: String! @doc(description: "The name of the user who left the comment") + text: String! @doc(description: "The text of the comment") +} + type PurchaseOrderItems { items: [PurchaseOrderItem]! page_info: SearchResultPageInfo diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index e2f5c7828..edb46a2af 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -112,14 +112,14 @@ The query should allow to fetch the following data: - Items with pagination - Basic details - - Approval Flow - - Comments - - History Log - Totals - Shipping Address - Billing Address - Payment Method - Shipping Method + - Comments + - History Log + - Approval Flow ```graphql { @@ -230,9 +230,24 @@ The query should allow to fetch the following data: } telephone } + comments(currentPage: 2, pageSize: 10) { + page_info { + current_page + page_size + total_pages + } + total_count + items { + uid + creation_date_and_time + author + text + } + } } } } + ``` ### Add items to cart from purchase order From eae40005c37939891b83a0a78ab9916f61a3649f Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 11 Aug 2020 11:28:23 -0500 Subject: [PATCH 250/479] change of the structure --- .../catalog/product-options-and-variants.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 6f8a48d5d..134ac2f60 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -159,7 +159,6 @@ alter table product_variant_matrix ``` `product_variant_matrix` - Stores the correlation between option values and variants, by using this correlation, we can say which of option combination is real. -Field `product_variant_matrix.weight` - says how many options should match to match the whole variant. The following script models data from the picture above. @@ -196,11 +195,11 @@ set @product_data := ' insert into products (object_id, data) values ('t-shirt', @product_data); -insert into product_variant_matrix (value, object_id, weight) +insert into product_variant_matrix (value, object_id) values - ('t-shirt:options.size.values.l', 'l-red', 2), ('t-shirt:options.color.values.red', 'l-red', 2), - ('t-shirt:options.size.values.m', 'm-red', 2), ('t-shirt:options.color.values.red', 'm-red', 2), - ('t-shirt:options.size.values.m', 'm-green', 2), ('t-shirt:options.color.values.green', 'm-green', 2) + ('t-shirt:options.size.values.l', 'l-red'), ('t-shirt:options.color.values.red', 'l-red'), + ('t-shirt:options.size.values.m', 'm-red'), ('t-shirt:options.color.values.red', 'm-red'), + ('t-shirt:options.size.values.m', 'm-green'), ('t-shirt:options.color.values.green', 'm-green') ; ``` @@ -242,9 +241,12 @@ Starting this point we can look into variants to analyze remaining options. From the proposed example, we have chosen "Size": "M". ```sql -mysql> select - -> object_id, value, weight - -> from product_variant_matrix +mysql> select t.* + -> from ( + -> select + -> object_id, value, count(1) over (partition by object_id) as weight + -> from product_variant_matrix + -> ) as t -> where value in ('t-shirt:options.size.values.m'); +-----------+-------------------------------+--------+ | object_id | value | weight | @@ -252,7 +254,8 @@ mysql> select | m-green | t-shirt:options.size.values.m | 2 | | m-red | t-shirt:options.size.values.m | 2 | +-----------+-------------------------------+--------+ -2 rows in set (0.01 sec) +2 rows in set (0.00 sec) + ``` As you may see, our selection has matched two variants. @@ -332,7 +335,6 @@ With the response API has to return: ```proto message OptionSelection { - string productId = 1; repeated string values = 2; } message OptionResponse { From 221cc85a612d749f0dfb603a7875d15c12bf9ceb Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 11 Aug 2020 12:00:19 -0500 Subject: [PATCH 251/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 17 +++++++++++++++-- .../graph-ql/coverage/b2b/purchase-order.md | 14 +++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 0ef8832b0..254e7d635 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -23,13 +23,14 @@ type PurchaseOrder { created_by: String! @doc(description: "The name of the user who created the purchase order") status: PurchaseOrderStatus! @doc(description: "The current status of the purcahse order") total: PurchaseOrderTotal @doc(description: "Contains details about the calculated totals for the purchase order") - items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") comments(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderComments @doc(description: "Purchase order comments") payment_methods: [PaymentMethod] @doc(description: "Payment details for the purchase order") shipping_address: CustomerAddress @doc(description: "The shipping address for the purchase order") billing_address: CustomerAddress @doc(description: "The billing address for the purchase order") carrier: String @doc(description: "The shipping carrier for the purchase order delivery") shipping_method: String @doc(description: "The delivery method for the purchase order") + items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") + history_log(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderHistoryLog @doc(description: "The log of the events related to the purchase order") } type PurchaseOrderComments { @@ -39,12 +40,24 @@ type PurchaseOrderComments { } type PurchaseOrderComment { - uid: ID! + uid: ID! @doc(description: "Unique identifier of the comment.") creation_date_and_time: String! @doc(description: "The date and time when the comment was created") author: String! @doc(description: "The name of the user who left the comment") text: String! @doc(description: "The text of the comment") } +type PurchaseOrderHistoryLog { + items: [PurchaseOrderHistoryItem]! + page_info: SearchResultPageInfo + total_count: Int +} + +type PurchaseOrderHistoryItem { + uid: ID! @doc(description: "Unique identifier of the purchase rder history item.") + date_and_time: String! @doc(description: "The date and time when the event happened.") + description: String! @doc(description: "Description of the event.") +} + type PurchaseOrderItems { items: [PurchaseOrderItem]! page_info: SearchResultPageInfo diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index edb46a2af..c3e7394c8 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -244,10 +244,22 @@ The query should allow to fetch the following data: text } } + history_log(currentPage: 1, pageSize: 10) { + page_info { + current_page + page_size + total_pages + } + total_count + items { + uid + date_and_time + description + } + } } } } - ``` ### Add items to cart from purchase order From 6b3c1a1c9435aeb72abfc76be00dd2d9a37c1860 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 11 Aug 2020 13:34:55 -0500 Subject: [PATCH 252/479] removed weight --- .../storefront/catalog/product-options-and-variants.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 134ac2f60..85abe6ab9 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -122,7 +122,6 @@ message ProductOption { message ProductVariant { repeated string optionValueId = 1; string id = 2; - int32 weight = 3; string productIdentifierInPricing = 500; #* string productIdentifierInInventory = 600; #* } @@ -150,7 +149,6 @@ Table `products` stores registry of products. create table product_variant_matrix ( value_id char(36) not null, object_id char(36) not null, - weight tinyint not null, primary key (value_id, object_id) ); @@ -306,7 +304,7 @@ options: [ ``` *Note: To achieve more advanced behavior, the variants could be "uneven" inside the single product. - They may have different weight. + They may have different "weight". For instance, you would like to track only t-shirts XL: size separately for some reason (a different price or stock). The example above focused on covering the main case scenario. Still, the approach, overall, is meant to support extending the logic of resolving option values onto a variant under the hood.* ![](https://app.lucidchart.com/publicSegments/view/de9972a8-f630-4400-aacb-d3d9858862cf/image.png) From 322585eef7dd1eb6a201639d94e0af6f67ee1fc3 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 11 Aug 2020 14:00:14 -0500 Subject: [PATCH 253/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 18 ++++++++++++++++++ .../graph-ql/coverage/b2b/purchase-order.md | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 254e7d635..70be87d49 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -31,6 +31,24 @@ type PurchaseOrder { shipping_method: String @doc(description: "The delivery method for the purchase order") items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") history_log(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderHistoryLog @doc(description: "The log of the events related to the purchase order") + approval_flow: PurchaseOrderApprovalFlow @doc(description: "The log of the events related to the purchase order approval flow") +} + +type PurchaseOrderApprovalFlow { + items: [PurchaseOrderApprovalFlowItem]! +} + +type PurchaseOrderApprovalFlowItem { + uid: ID! @doc(description: "Unique identifier of the purchase order flow item.") + title: String! @doc(description: "Summary of the event related to purchase order approval flow") + description: String! @doc(description: "Description of the event related to purchase order approval flow") + status: PurchaseOrderApprovalFlowItemStatus! @doc(description: "Status associated with the event related to purchase order approval flow") +} + +enum PurchaseOrderApprovalFlowItemStatus { + PENDING + APPROVED + REJECTED } type PurchaseOrderComments { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index c3e7394c8..7cb3ca0b2 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -257,6 +257,14 @@ The query should allow to fetch the following data: description } } + approval_flow { + items { + uid + status + title + description + } + } } } } From 848ac17e3d0db6df70330cebc8f14cffebdf907a Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Tue, 11 Aug 2020 18:07:22 -0500 Subject: [PATCH 254/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 27 ++++- .../graph-ql/coverage/b2b/purchase-order.md | 112 +++++++++++++++++- .../coverage/cart/AddProductsToCart.graphqls | 1 + .../coverage/customer/gift-registry.md | 2 +- 4 files changed, 133 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 70be87d49..c780b7bc0 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -59,7 +59,7 @@ type PurchaseOrderComments { type PurchaseOrderComment { uid: ID! @doc(description: "Unique identifier of the comment.") - creation_date_and_time: String! @doc(description: "The date and time when the comment was created") + timestamp: String! @doc(description: "The date and time when the comment was created") author: String! @doc(description: "The name of the user who left the comment") text: String! @doc(description: "The text of the comment") } @@ -72,7 +72,7 @@ type PurchaseOrderHistoryLog { type PurchaseOrderHistoryItem { uid: ID! @doc(description: "Unique identifier of the purchase rder history item.") - date_and_time: String! @doc(description: "The date and time when the event happened.") + timestamp: String! @doc(description: "The date and time when the event happened.") description: String! @doc(description: "Description of the event.") } @@ -93,7 +93,7 @@ type PurchaseOrderTotal @doc(description: "Contains details about the sales tota shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the purchase purchase order") } -type PurchaseOrderItem @doc(description: "Purchase order item details") { +interface PurchaseOrderItemInterface @doc(description: "Purchase order item details") { uid: ID! @doc(description: "The unique identifier of the purchase order item") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") @@ -103,11 +103,21 @@ type PurchaseOrderItem @doc(description: "Purchase order item details") { discounts: [Discount] @doc(description: "The final discount information for the product") selected_options: [PurchaseOrderItemOption] @doc(description: "The selected options for the base product, such as color or size") entered_options: [PurchaseOrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image") - quantity: Float @doc(description: "The number of units for this item") + quantity: Float! @doc(description: "The number of units for this item") } -type PurchaseOrderItemOption @doc(description: "Represents purcahse order item options like selected or entered") { - uid: ID! @doc(description: "The name of the option") +type PurchaseOrderItem implements PurchaseOrderItemInterface { + +} + +type PurchaseOrderBundleItem implements PurchaseOrderItemInterface { + parent_product_sku: String! @doc(description: "SKU of the bundle itself") + parent_product_quantity: Float! @doc(description: "Quantity of the bundle itself") +} + +type PurchaseOrderItemOption @doc(description: "Represents purcahse order item options") { + uid: ID! @doc(description: "The unique ID of the option") + label: String! @doc(description: "The label of the option") value: String! @doc(description: "The value of the option") } @@ -126,3 +136,8 @@ type CustomerAddress { # This field must be added to the CustomerAddress type definition directly in CustomerGraphQl module country: Country @doc(description: "The customer's country") } + +type CartItemInput { + # This field must be added to the CartItemInput type definition directly in QuoteGraphQl module + parent_quantity: Float @doc(description: "Parent quantity can be used when adding complex product to cart. For example bundle products") +} diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 7cb3ca0b2..cd073a7bc 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -177,10 +177,12 @@ The query should allow to fetch the following data: quantity selected_options { uid + label value } entered_options { uid + label value } discounts { @@ -239,7 +241,7 @@ The query should allow to fetch the following data: total_count items { uid - creation_date_and_time + timestamp author text } @@ -253,7 +255,7 @@ The query should allow to fetch the following data: total_count items { uid - date_and_time + timestamp description } } @@ -272,6 +274,112 @@ The query should allow to fetch the following data: ### Add items to cart from purchase order +Get details about purchase items order items that need to be added to cart: + +```graphql +{ + customer { + purchase_order(uid: "abc234hsasdfa") { + uid + items(currentPage: 1, pageSize: 1000) { + items { + uid + product_sku + quantity + selected_options { + uid + } + entered_options { + uid + value + } + } + } + } + } +} +``` + +Using the results from the previous request, execute the following mutation to add items to cart. This will work for all product types except for bundle. + +```graphql +mutation { + addProductsToCart( + cartId: "existing-cart-id-id", + cartItems: [ + { + sku: "simple-hat", + quantity: 2, + selected_options: [ + "hash based on custom option for the select type goes here" + ], + entered_options: [ + { + uid: "hash from custom phrase option ID", + value: "Custom Hat" + } + ] + } + ] + ) { + cart { + items { + id + } + } + } +} +``` + +Bundle product is added to cart by specifying `parent_sku` and `parent_quantity`. +`CartItemInput` needs to be extended with a new field `parent_quantity` directly in `QuoteGraphQl` module. + +```graphql +mutation { + addProductsToCart( + cartId: "existing-cart-id-id", + cartItems: [ + { + sku: "fan-kit-hat", + parent_sku: "fan-kit", + quantity: 2, + parent_quantity: 3, + selected_options: [ + "hash based on custom option for the select type goes here. Must be identical for all bundle children" + ], + entered_options: [ + { + uid: "hash based on custom phrase option goes here. Must be identical for all bundle children", + value: "Custom Text" + } + ] + }, + { + sku: "fan-kit-scarf", + parent_sku: "fan-kit", + quantity: 1, + parent_quantity: 3, + selected_options: [ + "hash based on custom option for the select type goes here. Must be identical for all bundle children" + ], + entered_options: [ + { + uid: "hash based on custom phrase option goes here. Must be identical for all bundle children", + value: "Custom Text" + } + ] + } + ] + ) { + cart { + items { + id + } + } + } +} +``` + ### Add purchase order comment ### Reject purchase order diff --git a/design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls b/design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls index 329c82e8b..7d64c1882 100644 --- a/design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls +++ b/design-documents/graph-ql/coverage/cart/AddProductsToCart.graphqls @@ -6,6 +6,7 @@ input CartItemInput { sku: String! # already in use quantity: Float # already in use parent_sku: String, # will not be used in deprecated methods + parent_quantity: Float, # will not be used in deprecated methods selected_options: [ID!] # will not be used in deprecated methods entered_options: [EnteredOptionInput!] # will not be used in deprecated methods } diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.md b/design-documents/graph-ql/coverage/customer/gift-registry.md index 1de1e857c..3e85ada60 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.md +++ b/design-documents/graph-ql/coverage/customer/gift-registry.md @@ -364,7 +364,7 @@ It is critical to have ability to avoid the hash generation on the client, that } ``` -#### Getting details wishlist item which needs to be added to gift registry +#### Getting details about wishlist item which needs to be added to gift registry Note that if the item is not fully configured, the user must be redirected to the product page to complete selections before the item can be added to the gift registry. From 07ba6cca0a74194d8e9b5ecd5dd5f4ad2ed502ee Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 12 Aug 2020 13:55:01 -0500 Subject: [PATCH 255/479] MC-36365: Schema for B2B purchase order details --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 2 +- design-documents/graph-ql/coverage/b2b/purchase-order.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index c780b7bc0..0430a68cf 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -77,7 +77,7 @@ type PurchaseOrderHistoryItem { } type PurchaseOrderItems { - items: [PurchaseOrderItem]! + items: [PurchaseOrderItemInterface]! page_info: SearchResultPageInfo total_count: Int } diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index cd073a7bc..49091459c 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -293,6 +293,10 @@ Get details about purchase items order items that need to be added to cart: uid value } + ... on PurchaseOrderBundleItem { + parent_product_sku + parent_product_quantity + } } } } From a74ea2a034687224c29cf35172ea3c7d33f780fe Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Wed, 12 Aug 2020 17:17:51 -0500 Subject: [PATCH 256/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 48 +++++++++++++++ .../graph-ql/coverage/b2b/purchase-order.md | 58 ++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 0430a68cf..710b7c967 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -1,3 +1,44 @@ +type Mutation { + addPurchaseOrderComment(input: AddPurchaseOrderCommentInput!): AddPurchaseOrderCommentOutput + approvePurchaseOrder(input: ApprovePurchaseOrderInput!): ApprovePurchaseOrderOutput + cancelPurchaseOrder(input: CancelPurchaseOrderInput!): CancelPurchaseOrderOutput + rejectPurchaseOrder(input: RejectPurchaseOrderInput!): RejectPurchaseOrderOutput +} + +input ApprovePurchaseOrderInput { + purchase_order_uid: ID! +} + +type ApprovePurchaseOrderOutput { + purchase_order: PurchaseOrder +} + +input CancelPurchaseOrderInput { + purchase_order_uid: ID! +} + +type CancelPurchaseOrderOutput { + purchase_order: PurchaseOrder +} + +input RejectPurchaseOrderInput { + purchase_order_uid: ID! +} + +type RejectPurchaseOrderOutput { + purchase_order: PurchaseOrder +} + +input AddPurchaseOrderCommentInput { + purchase_order_uid: ID! + comment: String! +} + +type AddPurchaseOrderCommentOutput { + purchase_order: PurchaseOrder + comment: PurchaseOrderComment +} + type Customer { purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders purchase_order(uid: ID!): PurchaseOrder @@ -32,6 +73,13 @@ type PurchaseOrder { items(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderItems @doc(description: "Items that belong to the purchase order") history_log(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderHistoryLog @doc(description: "The log of the events related to the purchase order") approval_flow: PurchaseOrderApprovalFlow @doc(description: "The log of the events related to the purchase order approval flow") + available_actions: [PurchaseOrderAction] @doc(description: "Purcahse order actions available to the customer. Can be used to display action buttons on the client") +} + +enum PurchaseOrderAction { + REJECT + CANCEL + APPROVE } type PurchaseOrderApprovalFlow { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 49091459c..b160d5df7 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -120,6 +120,7 @@ The query should allow to fetch the following data: - Comments - History Log - Approval Flow + - Available actions (actions, customer can execute on purchase order) ```graphql { @@ -267,6 +268,7 @@ The query should allow to fetch the following data: description } } + available_actions } } } @@ -386,13 +388,67 @@ mutation { ### Add purchase order comment +In the mutation response, it is possible to request just created comment, or the whole purchase order, depending on the client needs. + +```graphql +mutation { + addPurchaseOrderComment( + input: { + purchase_order_uid: "h2l1k23gpw", + comment: "Purchase order comment" + } + ) { + comment { + uid + author + text + timestamp + } + purchase_order { + uid + } + } +} +``` + ### Reject purchase order +```graphql +mutation { + rejectPurchaseOrder(input: {purchase_order_uid: "asdghwl324a"}) { + purchase_order { + uid + status + } + } +} +``` + ### Cancel purchase order +```graphql +mutation { + cancelPurchaseOrder(input: {purchase_order_uid: "asdghwl324a"}) { + purchase_order { + uid + status + } + } +} +``` + ### Approve purchase order -### Get a list of available actions +```graphql +mutation { + approvePurchaseOrder(input: {purchase_order_uid: "asdghwl324a"}) { + purchase_order { + uid + status + } + } +} +``` ### Store config From ac9b41a8f33847c16b09b9ac0f9c9fb0becc00b6 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 14 Aug 2020 10:51:49 -0500 Subject: [PATCH 257/479] MC-36365: Schema for B2B purchase order details --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 1 + .../graph-ql/coverage/b2b/purchase-order.md | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 710b7c967..08756032a 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -42,6 +42,7 @@ type AddPurchaseOrderCommentOutput { type Customer { purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders purchase_order(uid: ID!): PurchaseOrder + purchase_orders_enabled: Boolean! @doc(description: "Whether purchase orders functionality is enabled for current customer. Takes into account global and company-level settings.") } input PurchaseOrdersFilterInput { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index b160d5df7..0b4208184 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -452,10 +452,18 @@ mutation { ### Store config -Consider combining the following settings and exposing as a single field: -- Check if purchase order is enabled on global level +The following settings and combined and exposed as a single customer field: +- If purchase order is enabled on global level - If purchase order enabled on company level -- If comapnies are enabled on global level +- If companies are enabled on global level + +```graphql +{ + customer { + purchase_orders_enabled + } +} +``` ### View a list of approval rules From 5ef23f3aa653135c6302ff2c5099ec20da5cdfc1 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 14 Aug 2020 14:30:45 -0500 Subject: [PATCH 258/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 32 +++++++++++++++++-- .../graph-ql/coverage/b2b/purchase-order.md | 27 +++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 08756032a..1e6cc43cf 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -40,8 +40,9 @@ type AddPurchaseOrderCommentOutput { } type Customer { - purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders - purchase_order(uid: ID!): PurchaseOrder + purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders @doc(description: "A list of purchase orders visible to the customer") + purchase_order(uid: ID!): PurchaseOrder @doc(description: "Purchase order details") + purchase_order_approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") purchase_orders_enabled: Boolean! @doc(description: "Whether purchase orders functionality is enabled for current customer. Takes into account global and company-level settings.") } @@ -57,6 +58,33 @@ type PurchaseOrders { total_count: Int } +type PurchaseOrderApprovalRules { + items: [PurchaseOrderApprovalRule]! + page_info: SearchResultPageInfo + total_count: Int +} + +type PurchaseOrderApprovalRule { + uid: ID! @doc(description: "Unique identifier for the purcahse order approval rule") + name: String! @doc(description: "Name of the purcahse order approval rule") + status: PurchaseOrderApprovalRuleStatus! @doc(description: "Status of the purcahse order approval rule") + type: PurchaseOrderApprovalRuleType! @doc(description: "Type of the purcahse order approval rule") + created_by: String! @doc(description: "The name of the user who created the purcahse order approval rule") + applies_to: String! @doc(description: "The name of the user(s) affected by the the purcahse order approval rule") + approver: String! @doc(description: "The name of the user who needs to approve purchase orders that trigger the approval rule") +} + +enum PurchaseOrderApprovalRuleStatus { + ENABLED + DISABLED +} + +enum PurchaseOrderApprovalRuleType { + ORDER_TOTAL + SHIPPING_COST + NUMBER_OF_SKUS +} + type PurchaseOrder { uid: ID! @doc(description: "Unique identifier for the purcahse order") number: String! @doc(description: "The purchase order number") diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 0b4208184..96cf8689f 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -467,7 +467,32 @@ The following settings and combined and exposed as a single customer field: ### View a list of approval rules -Should support pagination. +```graphql +{ + customer { + purchase_order_approval_rules( + currentPage: 1, + pageSize: 10 + ) { + items { + uid + name + status + type + created_by + applies_to + approver + } + total_count + page_info { + current_page + page_size + total_pages + } + } + } +} +``` ### View approval rule details From 0442a8142e9260b033b0792ec99f82027e205125 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Fri, 14 Aug 2020 17:13:45 -0500 Subject: [PATCH 259/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 23 ++++++++++++++- .../graph-ql/coverage/b2b/purchase-order.md | 28 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 1e6cc43cf..58f058c62 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -43,7 +43,8 @@ type Customer { purchase_orders(filter: PurchaseOrdersFilterInput, currentPage: Int = 1, pageSize: Int = 20): PurchaseOrders @doc(description: "A list of purchase orders visible to the customer") purchase_order(uid: ID!): PurchaseOrder @doc(description: "Purchase order details") purchase_order_approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") - purchase_orders_enabled: Boolean! @doc(description: "Whether purchase orders functionality is enabled for current customer. Takes into account global and company-level settings.") + purchase_order_approval_rule(uid: ID!): PurchaseOrderApprovalRule @doc(description: "Purchase order approval rule details") + purchase_orders_enabled: Boolean! @doc(description: "Whether purchase orders functionality is enabled for current customer. Takes into account global and company-level settings") } input PurchaseOrdersFilterInput { @@ -72,6 +73,26 @@ type PurchaseOrderApprovalRule { created_by: String! @doc(description: "The name of the user who created the purcahse order approval rule") applies_to: String! @doc(description: "The name of the user(s) affected by the the purcahse order approval rule") approver: String! @doc(description: "The name of the user who needs to approve purchase orders that trigger the approval rule") + condition: PurchaseOrderApprovalRuleConditionInterface! @doc(description: "Condition which triggers the approval rule") +} + +interface PurchaseOrderApprovalRuleConditionInterface { + operator: PurchaseOrderApprovalRuleConditionOperator! @doc(description: "The operator to be used for evaluation of the approval rule condition") +} + +enum PurchaseOrderApprovalRuleConditionOperator { + MORE_THAN + LESS_THAN + MORE_THAN_OR_EQUAL_TO + LESS_THAN_OR_EQUAL_TO +} + +type PurchaseOrderApprovalRuleConditionAmount implements PurchaseOrderApprovalRuleConditionInterface { + amount: Money! @doc(description: "The amount to be be used for evaluation of the approval rule condition") +} + +type PurchaseOrderApprovalRuleConditionQuantity implements PurchaseOrderApprovalRuleConditionInterface { + quantity: Int! @doc(description: "The quantity to be be used for evaluation of the approval rule condition") } enum PurchaseOrderApprovalRuleStatus { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 96cf8689f..1d1f9df10 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -496,6 +496,34 @@ The following settings and combined and exposed as a single customer field: ### View approval rule details +```graphql +{ + customer { + purchase_order_approval_rule(uid: "abc2710fsdlfh") { + uid + name + status + type + created_by + applies_to + approver + condition { + operator + ... on PurchaseOrderApprovalRuleConditionAmount { + amount { + value + currency + } + } + ... on PurchaseOrderApprovalRuleConditionQuantity { + quantity + } + } + } + } +} +``` + ### Create new approval rule #### Get list of "Applies to" users From da5b67b7a7355b3f367b0bd882bbf65f352b26a0 Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Mon, 17 Aug 2020 19:45:49 +0530 Subject: [PATCH 260/479] negotiable-quote-graphql-schema-improvement --- .../graph-ql/coverage/negotiableQuotes.graphqls | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 0670837ee..2aca8e08a 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -172,7 +172,7 @@ enum NegotiableQuoteCommentCreatorType { type NegotiableQuote { id: ID! name: String! - items: [NegotiableQuoteItem!] + items: [CartItemInterface!] # Attachment Support is dependent on headless File Upload design # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] @@ -198,11 +198,6 @@ enum NegotiableQuoteStatus { EXPIRED } -type NegotiableQuoteItem @doc(description: "A line item on a Negotiable Quote") { - id: ID! @doc(description: "ID of the line item (not product) in a Negotiable Quote") - item: CartItemInterface @doc(description: "Product assigned to line item, with selected configurations") -} - input NegotiableQuoteFilterInput { ids: FilterEqualTypeInput @doc(description: "Filter by Negotiable Quote ID(s)") name: FilterMatchTypeInput @doc(description: "Filter by Negotiable Quote name") @@ -247,8 +242,8 @@ type NegotiableQuoteHistoryChanges { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L40-L73 type NegotiableQuoteHistoryStatusChange { - old_status: NegotiableQuoteStatus @doc(description: "Will be null for the first history entry on a Negotiable Quote") - new_status: NegotiableQuoteStatus! + old_status: String @doc(description: "Will be null for the first history entry on a Negotiable Quote") + new_status: String! @doc(description: "Negotiable Quote History New Status.") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L48 From 206b759bf974f0e3ba30635cea77257f786b8c38 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 17 Aug 2020 10:45:18 -0500 Subject: [PATCH 261/479] MC-36365: Schema for B2B purchase order details --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 7 +++++++ .../graph-ql/coverage/b2b/purchase-order.md | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 58f058c62..d93aebef6 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -44,9 +44,16 @@ type Customer { purchase_order(uid: ID!): PurchaseOrder @doc(description: "Purchase order details") purchase_order_approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") purchase_order_approval_rule(uid: ID!): PurchaseOrderApprovalRule @doc(description: "Purchase order approval rule details") + purchase_order_approval_rule_metadata: PurchaseOrderApprovalRuleMetadata @doc(description: "Purchase order approval rule metadata which is can be used for rule edit form rendering") purchase_orders_enabled: Boolean! @doc(description: "Whether purchase orders functionality is enabled for current customer. Takes into account global and company-level settings") } +type PurchaseOrderApprovalRuleMetadata { + available_applies_to: [String]! @doc(description: "A list of B2B user roles that the rule can be applied to") + available_condition_currencies: [CurrencyEnum]! @doc(description: "A list of currencies that can be used to create approval rules based on ammounts, for example shipping cost rules") + available_requires_approval_from: [String]! @doc(description: "A list of B2B user roles that can be specified as approvers for the approval rules") +} + input PurchaseOrdersFilterInput { status: PurchaseOrderStatus createdBy: FilterStringTypeInput diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 1d1f9df10..363f89d2e 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -526,9 +526,19 @@ The following settings and combined and exposed as a single customer field: ### Create new approval rule -#### Get list of "Applies to" users +To render the rule creation and update forms, some metadata is required: -#### Get list of "Requires approval from" users +```graphql +{ + customer { + purchase_order_approval_rule_metadata { + available_applies_to + available_requires_approval_from + available_condition_currencies + } + } +} +``` ### Update approval rule From f77a11eb22d848ca366c44fc10e579e2b6a66ae1 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 17 Aug 2020 11:58:03 -0500 Subject: [PATCH 262/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index d93aebef6..3cae0e142 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -1,8 +1,49 @@ type Mutation { - addPurchaseOrderComment(input: AddPurchaseOrderCommentInput!): AddPurchaseOrderCommentOutput - approvePurchaseOrder(input: ApprovePurchaseOrderInput!): ApprovePurchaseOrderOutput - cancelPurchaseOrder(input: CancelPurchaseOrderInput!): CancelPurchaseOrderOutput - rejectPurchaseOrder(input: RejectPurchaseOrderInput!): RejectPurchaseOrderOutput + addPurchaseOrderComment(input: AddPurchaseOrderCommentInput!): AddPurchaseOrderCommentOutput @doc(description: "Add a comment to an existing purchase order") + approvePurchaseOrder(input: ApprovePurchaseOrderInput!): ApprovePurchaseOrderOutput @doc(description: "Approve purchase order") + cancelPurchaseOrder(input: CancelPurchaseOrderInput!): CancelPurchaseOrderOutput @doc(description: "Cancel purchase order") + rejectPurchaseOrder(input: RejectPurchaseOrderInput!): RejectPurchaseOrderOutput @doc(description: "Reject purchase order") + createPurchaseOrderApprovalRule(input: CreatePurchaseOrderApprovalRuleInput!): CreatePurchaseOrderApprovalRuleOutput @doc(description: "Create purchase order approval rule") + updatePurchaseOrderApprovalRule(input: UpdatePurchaseOrderApprovalRuleInput!): UpdatePurchaseOrderApprovalRuleOutput @doc(description: "Update purchase order approval rule") +} + +type CreatePurchaseOrderApprovalRuleOutput { + approval_rule: PurchaseOrderApprovalRule @doc(description: "Created purchase order approval rule") + approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") +} + +type UpdatePurchaseOrderApprovalRuleOutput { + approval_rule: PurchaseOrderApprovalRule @doc(description: "Updated purchase order approval rule") + approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") +} + +input CreatePurchaseOrderApprovalRuleInput { + approval_rule: PurchaseOrderApprovalRuleInput! @doc(description: "Purchase order approval rule data") +} + +input UpdatePurchaseOrderApprovalRuleInput { + approval_rule_uid: ID! @doc(description: "Purchase order approval rule ID") + approval_rule: PurchaseOrderApprovalRuleInput! @doc(description: "Purchase order approval rule data") +} + +input PurchaseOrderApprovalRuleInput { + name: String! @doc(description: "Purchase order approval rule name") + description: String @doc(description: "Purchase order approval rule description") + applies_to: [String!]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied") + type: PurchaseOrderApprovalRuleType! @doc(description: "Purchase order approval rule type") + condition: CreatePurchaseOrderApprovalRuleConditionInput! @doc(description: "Purchase order approval rule condition") + requires_approval_from: [String!]! @doc(description: "A list of B2B user roles that can approve this purchase order approval rule") +} + +input CreatePurchaseOrderApprovalRuleConditionInput { + operator: PurchaseOrderApprovalRuleConditionOperator! @doc(description: "Purchase order approval rule condition operator") + amount: CreatePurchaseOrderApprovalRuleConditionAmountInput @doc(description: "Purchase order approval rule condition ammount. Is mutually exclusive with condition quantity") + quantity: Int @doc(description: "Purchase order approval rule condition quantity. Is mutually exclusive with condition amount") +} + +input CreatePurchaseOrderApprovalRuleConditionAmountInput { + value: Float! @doc(description: "Purchase order approval rule condition ammount value") + currency: CurrencyEnum! @doc(description: "Purchase order approval rule condition ammount currency") } input ApprovePurchaseOrderInput { From c322933dffb5a0cb084d0321d97d451f8f46c14d Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 17 Aug 2020 13:27:11 -0500 Subject: [PATCH 263/479] MC-35159: Login as Customer Schema Design --- .../coverage/customer/login-as-customer.graphqls | 15 +++++++++++++++ .../coverage/customer/login-as-customer.md | 10 +++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls b/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls index 2da837f62..9437478db 100644 --- a/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls +++ b/design-documents/graph-ql/coverage/customer/login-as-customer.graphqls @@ -11,3 +11,18 @@ type GenerateCustomerTokenAsAdminOutput { customer_token: String! } + +type Customer +{ + allow_remote_shopping_assistance: Boolean! +} + +input CustomerCreateInput +{ + allow_remote_shopping_assistance: Boolean +} + +input CustomerUpdateInput +{ + allow_remote_shopping_assistance: Boolean +} diff --git a/design-documents/graph-ql/coverage/customer/login-as-customer.md b/design-documents/graph-ql/coverage/customer/login-as-customer.md index 3d08d345c..416aa491f 100644 --- a/design-documents/graph-ql/coverage/customer/login-as-customer.md +++ b/design-documents/graph-ql/coverage/customer/login-as-customer.md @@ -1,6 +1,6 @@ ## Use cases -# Admin user obtains customer token +### Admin user obtains customer token Admin user is expected to be authenticated using admin token. While there is no GraphQL Mutation for retrieving the admin token, REST should be used (see [example](https://devdocs.magento.com/guides/v2.4/graphql/queries/index.html#staging)). @@ -18,7 +18,11 @@ mutation { } ``` -### Additional requirements +### Customer can manage and view "Allow remote shopping assistance" flag + +This flag should be added to `Customer`, `CustomerCreateInput` and `CustomerUpdateInput`. + +## Additional requirements 1. A new `LoginAsCustomerGraphQL` module should be created. 2. `Magento_LoginAsCustomer::login` permission must be declared in `LoginAsCustomer` module. 3. The following store config settings must be honored, but should not be exposed as part of `StoreConfig` GraphQL query: @@ -26,7 +30,7 @@ mutation { 0 - 60 ``` + 4. Customer field `allow_remote_shopping_assistance` must be taken into account From b6acd069b7e491602e7d8aa12484d3fd573a0f2b Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 17 Aug 2020 16:47:39 -0500 Subject: [PATCH 264/479] MC-36365: Schema for B2B purchase order details --- .../coverage/b2b/purchase-order.graphqls | 25 +++-- .../graph-ql/coverage/b2b/purchase-order.md | 96 ++++++++++++++++++- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index 3cae0e142..a3557f30d 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -4,7 +4,8 @@ type Mutation { cancelPurchaseOrder(input: CancelPurchaseOrderInput!): CancelPurchaseOrderOutput @doc(description: "Cancel purchase order") rejectPurchaseOrder(input: RejectPurchaseOrderInput!): RejectPurchaseOrderOutput @doc(description: "Reject purchase order") createPurchaseOrderApprovalRule(input: CreatePurchaseOrderApprovalRuleInput!): CreatePurchaseOrderApprovalRuleOutput @doc(description: "Create purchase order approval rule") - updatePurchaseOrderApprovalRule(input: UpdatePurchaseOrderApprovalRuleInput!): UpdatePurchaseOrderApprovalRuleOutput @doc(description: "Update purchase order approval rule") + updatePurchaseOrderApprovalRule(input: UpdatePurchaseOrderApprovalRuleInput!): UpdatePurchaseOrderApprovalRuleOutput @doc(description: "Update existing purchase order approval rule") + deletePurchaseOrderApprovalRule(input: DeletePurchaseOrderApprovalRuleInput!): DeletePurchaseOrderApprovalRuleOutput @doc(description: "Delete existing purchase order approval rule") } type CreatePurchaseOrderApprovalRuleOutput { @@ -17,6 +18,10 @@ type UpdatePurchaseOrderApprovalRuleOutput { approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") } +type DeletePurchaseOrderApprovalRuleOutput { + approval_rules(currentPage: Int = 1, pageSize: Int = 20): PurchaseOrderApprovalRules @doc(description: "A list of purchase order approval rules visible to the customer") +} + input CreatePurchaseOrderApprovalRuleInput { approval_rule: PurchaseOrderApprovalRuleInput! @doc(description: "Purchase order approval rule data") } @@ -26,13 +31,17 @@ input UpdatePurchaseOrderApprovalRuleInput { approval_rule: PurchaseOrderApprovalRuleInput! @doc(description: "Purchase order approval rule data") } +input DeletePurchaseOrderApprovalRuleInput { + approval_rule_uid: ID! @doc(description: "Purchase order approval rule ID") +} + input PurchaseOrderApprovalRuleInput { name: String! @doc(description: "Purchase order approval rule name") description: String @doc(description: "Purchase order approval rule description") - applies_to: [String!]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied") + applies_to: [ID]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied. In case when empty array is provided, the rule will be applied to all user roles in the system, including those created in the future") type: PurchaseOrderApprovalRuleType! @doc(description: "Purchase order approval rule type") condition: CreatePurchaseOrderApprovalRuleConditionInput! @doc(description: "Purchase order approval rule condition") - requires_approval_from: [String!]! @doc(description: "A list of B2B user roles that can approve this purchase order approval rule") + requires_approval_from: [ID!]! @doc(description: "A list of B2B user roles that can approve this purchase order approval rule") } input CreatePurchaseOrderApprovalRuleConditionInput { @@ -90,15 +99,15 @@ type Customer { } type PurchaseOrderApprovalRuleMetadata { - available_applies_to: [String]! @doc(description: "A list of B2B user roles that the rule can be applied to") + available_applies_to: [CompanyRole]! @doc(description: "A list of B2B user roles that the rule can be applied to") available_condition_currencies: [CurrencyEnum]! @doc(description: "A list of currencies that can be used to create approval rules based on ammounts, for example shipping cost rules") - available_requires_approval_from: [String]! @doc(description: "A list of B2B user roles that can be specified as approvers for the approval rules") + available_requires_approval_from: [CompanyRole]! @doc(description: "A list of B2B user roles that can be specified as approvers for the approval rules") } input PurchaseOrdersFilterInput { - status: PurchaseOrderStatus - createdBy: FilterStringTypeInput - createdDate: FilterRangeTypeInput + status: PurchaseOrderStatus @doc(description: "Filter by the status of the purchase order") + createdBy: FilterStringTypeInput @doc(description: "Filter by the name of the user who created the purchase order") + createdDate: FilterRangeTypeInput @doc(description: "Filter by the creation date of the purchase order") } type PurchaseOrders { diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index 363f89d2e..df56eafa2 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -532,22 +532,108 @@ To render the rule creation and update forms, some metadata is required: { customer { purchase_order_approval_rule_metadata { - available_applies_to - available_requires_approval_from + available_applies_to { + id + name + } + available_requires_approval_from { + id + name + } available_condition_currencies } } } ``` +Create a new rule with the condition based on shipping cost. In the response it is possible to request the rule just created, or a list of all rules available to the current user for viewing. + +```graphql +mutation { + createPurchaseOrderApprovalRule( + input: { + approval_rule: { + name: "Junior Buyer Orders" + description: "The rule applies to junior buyers" + applies_to: ["vah234gwy3"] + type: SHIPPING_COST + condition: { + operator: MORE_THAN_OR_EQUAL_TO + amount: { + currency: USD + value: 1000.50 + } + } + requires_approval_from: ["ghwldsfh237s", "fhsk23h49kl"] + } + } + ) { + approval_rule { + uid + } + approval_rules(currentPage: 1, pageSize: 10) { + items { + uid + } + } + } +} +``` + ### Update approval rule -### Delete approval rule +Similarly to creation of the rule, metadata for the form rendering needs to be fetched first. +The next mutation demonstrates updating of an existing rule with the condition based on the number of SKUs. -## Implementation details +```graphql +mutation { + updatePurchaseOrderApprovalRule( + input: { + approval_rule_uid: "gasdgfhwlr234sdfla" + approval_rule: { + name: "Junior Buyer Orders" + description: "The rule applies to junior buyers" + applies_to: ["vah234gwy3"] + type: NUMBER_OF_SKUS + condition: { + operator: MORE_THAN + quantity: 100 + } + requires_approval_from: ["ghwldsfh237s"] + } + } + ) { + approval_rule { + uid + } + approval_rules(currentPage: 1, pageSize: 10) { + items { + uid + } + } + } +} +``` -The purchase order GraphQL schema depends on Sales and Customer GraphQL schemas. +### Delete approval rule +```graphql +mutation { + deletePurchaseOrderApprovalRule( + input: { + approval_rule_uid: "gabh572kfhs" + } + ) { + approval_rules(currentPage: 1, pageSize: 10) { + items { + uid + } + } + } +} +``` +## Implementation details +The purchase order GraphQL schema depends on Sales and Customer GraphQL schemas. From 3fd19a2f7f8bde148e0caa7b2ad921203f36bb77 Mon Sep 17 00:00:00 2001 From: Alex Paliarush Date: Mon, 17 Aug 2020 16:58:23 -0500 Subject: [PATCH 265/479] MC-36365: Schema for B2B purchase order details --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 11 ++++++++--- .../graph-ql/coverage/b2b/purchase-order.md | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index a3557f30d..b8ab3b243 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -209,16 +209,21 @@ enum PurchaseOrderApprovalFlowItemStatus { type PurchaseOrderComments { items: [PurchaseOrderComment]! page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } type PurchaseOrderComment { uid: ID! @doc(description: "Unique identifier of the comment.") timestamp: String! @doc(description: "The date and time when the comment was created") - author: String! @doc(description: "The name of the user who left the comment") + author: PurchaseOrderCommentAuthor! @doc(description: "The name of the user who left the comment") text: String! @doc(description: "The text of the comment") } +type PurchaseOrderCommentAuthor { + firstname: String! @doc(description: "First name of the user who left the purchase order comment") + lastname: String! @doc(description: "Last name of the user who left the purchase order comment") +} + type PurchaseOrderHistoryLog { items: [PurchaseOrderHistoryItem]! page_info: SearchResultPageInfo @@ -292,7 +297,7 @@ type CustomerAddress { country: Country @doc(description: "The customer's country") } -type CartItemInput { +input CartItemInput { # This field must be added to the CartItemInput type definition directly in QuoteGraphQl module parent_quantity: Float @doc(description: "Parent quantity can be used when adding complex product to cart. For example bundle products") } diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.md b/design-documents/graph-ql/coverage/b2b/purchase-order.md index df56eafa2..13ea3a9c9 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.md +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.md @@ -243,7 +243,10 @@ The query should allow to fetch the following data: items { uid timestamp - author + author { + firstname + lastname + } text } } @@ -400,7 +403,10 @@ mutation { ) { comment { uid - author + author { + firstname + lastname + } text timestamp } From 64c29c3c04982db3a274299cd622ffa83cf50e95 Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Tue, 18 Aug 2020 16:11:14 +0530 Subject: [PATCH 266/479] schema file modify --- .../graph-ql/coverage/negotiableQuotes.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 2aca8e08a..7a4345fd0 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -242,8 +242,8 @@ type NegotiableQuoteHistoryChanges { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L40-L73 type NegotiableQuoteHistoryStatusChange { - old_status: String @doc(description: "Will be null for the first history entry on a Negotiable Quote") - new_status: String! @doc(description: "Negotiable Quote History New Status.") + old_status: NegotiableQuoteStatus @doc(description: "Will be null for the first history entry on a Negotiable Quote") + new_status: NegotiableQuoteStatus! @doc(description: "Negotiable Quote History New Status.") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L48 @@ -261,8 +261,8 @@ type NegotiableQuoteHistoryCommentChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L259-L294 type NegotiableQuoteHistoryTotalChange { - old_price: Money! - new_price: Money! + old_price: Money + new_price: Money } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L74-L103 From 3046c6f98d86dcc9eb2c866269197da91f1bc61a Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Fri, 21 Aug 2020 14:19:42 +0530 Subject: [PATCH 267/479] remove buyer update item from the storefront schema --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 5 ----- 1 file changed, 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 7a4345fd0..0894f5106 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -42,11 +42,6 @@ type Mutation { input: RemoveNegotiableQuoteItemsInput! ): RemoveNegotiableQuoteItemsOutput @doc(description: "Remove 1 or more products from a Negotiable Quote") - # Covers "buyer can add items" of https://devdocs.magento.com/guides/v2.4/b2b/negotiable-update.html#add-a-new-quote-item-to-the-negotiable-quote - addNegotiableQuoteItems( - input: AddNegotiableQuoteItemsInput! - ): AddNegotiableQuoteItemsOutput @doc(description: "Add 1 or more products to a Negotiable Quote") - # Covers first half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request closeNegotiableQuotes( input: CloseNegotiableQuotesInput! From 1c77ea3813b692ae5a5832b3bf403686596df7aa Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 14:58:53 -0500 Subject: [PATCH 268/479] Copy over Magento_QuoteGraphQl schema from 2.4-develop --- .../graph-ql/coverage/quote.graphqls | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 design-documents/graph-ql/coverage/quote.graphqls diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls new file mode 100644 index 000000000..cf311354d --- /dev/null +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -0,0 +1,391 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + cart(cart_id: String!): Cart @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart") @doc(description:"Returns information about shopping cart") @cache(cacheable: false) + customerCart: Cart! @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CustomerCart") @doc(description:"Returns information about the customer shopping cart") @cache(cacheable: false) +} + +type Mutation { + createEmptyCart(input: createEmptyCartInput): String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") + addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") + removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart") + updateCartItems(input: UpdateCartItemsInput): UpdateCartItemsOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\UpdateCartItems") + removeItemFromCart(input: RemoveItemFromCartInput): RemoveItemFromCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveItemFromCart") + setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart") + setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetBillingAddressOnCart") + setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") + setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") + setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") + setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") + mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts") + placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") + addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput @doc(description:"Add any type of product to the cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddProductsToCart") +} + +input createEmptyCartInput { + cart_id: String +} + +input AddSimpleProductsToCartInput { + cart_id: String! + cart_items: [SimpleProductCartItemInput!]! +} + +input SimpleProductCartItemInput { + data: CartItemInput! + customizable_options:[CustomizableOptionInput!] +} + +input AddVirtualProductsToCartInput { + cart_id: String! + cart_items: [VirtualProductCartItemInput!]! +} + +input VirtualProductCartItemInput { + data: CartItemInput! + customizable_options:[CustomizableOptionInput!] +} + +input CartItemInput { + sku: String! + quantity: Float! + parent_sku: String @doc(description: "For child products, the SKU of its parent product") + selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [EnteredOptionInput!] @doc(description: "An array of entered options for the base product, such as personalization text") +} + +input CustomizableOptionInput { + id: Int! + value_string: String! +} + +input ApplyCouponToCartInput { + cart_id: String! + coupon_code: String! +} + +input UpdateCartItemsInput { + cart_id: String! + cart_items: [CartItemUpdateInput!]! +} + +input CartItemUpdateInput { + cart_item_id: Int! + quantity: Float + customizable_options: [CustomizableOptionInput!] +} + +input RemoveItemFromCartInput { + cart_id: String! + cart_item_id: Int! +} + +input SetShippingAddressesOnCartInput { + cart_id: String! + shipping_addresses: [ShippingAddressInput!]! +} + +input ShippingAddressInput { + customer_address_id: Int # If provided then will be used address from address book + address: CartAddressInput + customer_notes: String +} + +input SetBillingAddressOnCartInput { + cart_id: String! + billing_address: BillingAddressInput! +} + +input BillingAddressInput { + customer_address_id: Int + address: CartAddressInput + use_for_shipping: Boolean @doc(description: "Deprecated: use `same_as_shipping` field instead") + same_as_shipping: Boolean @doc(description: "Set billing address same as shipping") +} + +input CartAddressInput { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: String + region_id: Int + postcode: String + country_code: String! + telephone: String! + save_in_address_book: Boolean @doc(description: "Determines whether to save the address in the customer's address book. The default value is true") +} + +input SetShippingMethodsOnCartInput { + cart_id: String! + shipping_methods: [ShippingMethodInput!]! +} + +input ShippingMethodInput { + carrier_code: String! + method_code: String! +} + +input SetPaymentMethodAndPlaceOrderInput { + cart_id: String! + payment_method: PaymentMethodInput! +} + +input PlaceOrderInput { + cart_id: String! +} + +input SetPaymentMethodOnCartInput { + cart_id: String! + payment_method: PaymentMethodInput! +} + +input PaymentMethodInput { + code: String! @doc(description:"Payment method code") + purchase_order_number: String @doc(description:"Purchase order number") +} + +input SetGuestEmailOnCartInput { + cart_id: String! + email: String! +} + +type CartPrices { + grand_total: Money + subtotal_including_tax: Money + subtotal_excluding_tax: Money + discount: CartDiscount @deprecated(reason: "Use discounts instead ") + subtotal_with_discount_excluding_tax: Money + applied_taxes: [CartTaxItem] + discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts") +} + +type CartTaxItem { + amount: Money! + label: String! +} + +type CartDiscount { + amount: Money! + label: [String!]! +} + +type SetPaymentMethodOnCartOutput { + cart: Cart! +} + +type SetBillingAddressOnCartOutput { + cart: Cart! +} + +type SetShippingAddressesOnCartOutput { + cart: Cart! +} + +type SetShippingMethodsOnCartOutput { + cart: Cart! +} + +type ApplyCouponToCartOutput { + cart: Cart! +} + +type PlaceOrderOutput { + order: Order! +} + +type Cart { + id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The ID of the cart.") + items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") + email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") + shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") + billing_address: BillingCartAddress @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") + available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") + selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") + prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") + total_quantity: Float! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartTotalQuantity") + is_virtual: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartIsVirtual") +} + +interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: CartAddressRegion + postcode: String + country: CartAddressCountry! + telephone: String! +} + +type ShippingCartAddress implements CartAddressInterface { + available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\AvailableShippingMethods") + selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") + customer_notes: String + items_weight: Float @deprecated(reason: "This information shoud not be exposed on frontend") + cart_items: [CartItemQuantity] @deprecated(reason: "`cart_items_v2` should be used instead") + cart_items_v2: [CartItemInterface] +} + +type BillingCartAddress implements CartAddressInterface { + customer_notes: String @deprecated (reason: "The field is used only in shipping address") +} + +type CartItemQuantity @doc(description:"Deprecated: `cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") { + cart_item_id: Int! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") + quantity: Float! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") +} + +type CartAddressRegion { + code: String + label: String + region_id: Int +} + +type CartAddressCountry { + code: String! + label: String! +} + +type SelectedShippingMethod { + carrier_code: String! + method_code: String! + carrier_title: String! + method_title: String! + amount: Money! + base_amount: Money @deprecated(reason: "The field should not be used on the storefront") +} + +type AvailableShippingMethod { + carrier_code: String! + carrier_title: String! + method_code: String @doc(description: "Could be null if method is not available") + method_title: String @doc(description: "Could be null if method is not available") + error_message: String + amount: Money! + base_amount: Money @deprecated(reason: "The field should not be used on the storefront") + price_excl_tax: Money! + price_incl_tax: Money! + available: Boolean! +} + +type AvailablePaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") +} + +type SelectedPaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") + purchase_order_number: String @doc(description: "The purchase order number.") +} + +type AppliedCoupon { + code: String! +} + +input RemoveCouponFromCartInput { + cart_id: String! +} + +type RemoveCouponFromCartOutput { + cart: Cart +} + +type AddSimpleProductsToCartOutput { + cart: Cart! +} + +type AddVirtualProductsToCartOutput { + cart: Cart! +} + +type UpdateCartItemsOutput { + cart: Cart! +} + +type RemoveItemFromCartOutput { + cart: Cart! +} + +type SetGuestEmailOnCartOutput { + cart: Cart! +} + +type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") { + customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") +} + +type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") { + customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") +} + +interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { + id: String! + quantity: Float! + prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") + product: ProductInterface! +} + +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") { + amount: Money! @doc(description:"The amount of the discount") + label: String! @doc(description:"A description of the discount") +} + +type CartItemPrices { + price: Money! + row_total: Money! + row_total_including_tax: Money! + discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") + total_item_discount: Money @doc(description:"The total of all discounts applied to the item") +} + +type SelectedCustomizableOption { + id: Int! + label: String! + is_required: Boolean! + values: [SelectedCustomizableOptionValue!]! + sort_order: Int! +} + +type SelectedCustomizableOptionValue { + id: Int! + label: String! + value: String! + price: CartItemSelectedOptionValuePrice! +} + +type CartItemSelectedOptionValuePrice { + value: Float! + units: String! + type: PriceTypeEnum! +} + +type Order { + order_number: String! + order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") +} + +type CartUserInputError @doc(description:"An error encountered while adding an item to the the cart.") { + message: String! @doc(description: "A localized error message") + code: CartUserInputErrorType! @doc(description: "Cart-specific error code") +} + +type AddProductsToCartOutput { + cart: Cart! @doc(description: "The cart after products have been added") + user_errors: [CartUserInputError!]! @doc(description: "An error encountered while adding an item to the cart.") +} + +enum CartUserInputErrorType { + PRODUCT_NOT_FOUND + NOT_SALABLE + INSUFFICIENT_STOCK + UNDEFINED +} From 78c4f59840da7f3a5398152ad835f0ee7c57bd05 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:23:04 -0500 Subject: [PATCH 269/479] Copy over Magento_CatalogGraphQl schema from 2.4-develop --- .../graph-ql/coverage/catalog.graphqls | 494 ++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 design-documents/graph-ql/coverage/catalog.graphqls diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls new file mode 100644 index 000000000..f510637f4 --- /dev/null +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -0,0 +1,494 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + products ( + search: String @doc(description: "Performs a full-text search using the specified key words."), + filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."), + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") + ): Products + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") + category ( + id: Int @doc(description: "Id of the category.") + ): CategoryTree + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + categoryList( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") + categories ( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional.") + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1.") + ): CategoryResult @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoriesQuery") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") +} + +type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") { + amount: Money @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "The price of a product plus a three-letter currency code.") + adjustments: [PriceAdjustment] @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.") +} + +type PriceAdjustment @doc(description: "PriceAdjustment is deprecated. Taxes will be included or excluded in the price. The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") { + amount: Money @doc(description: "The amount of the price adjustment and its currency code.") + code: PriceAdjustmentCodesEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.") + description: PriceAdjustmentDescriptionEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.") +} + +enum PriceAdjustmentCodesEnum @doc(description: "PriceAdjustment.code is deprecated. This enumeration contains values defined in modules other than the Catalog module.") { +} + +enum PriceAdjustmentDescriptionEnum @doc(description: "PriceAdjustmentDescriptionEnum is deprecated. This enumeration states whether a price adjustment is included or excluded.") { + INCLUDED + EXCLUDED +} + +enum PriceTypeEnum @doc(description: "This enumeration the price type.") { + FIXED + PERCENT + DYNAMIC +} + +type ProductPrices @doc(description: "ProductPrices is deprecated, replaced by PriceRange. The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { + minimalPrice: Price @deprecated(reason: "Use PriceRange.minimum_price.") @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") + maximalPrice: Price @deprecated(reason: "Use PriceRange.maximum_price.") @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") + regularPrice: Price @deprecated(reason: "Use regular_price from PriceRange.minimum_price or PriceRange.maximum_price.") @doc(description: "The base price of a product.") +} + +type PriceRange @doc(description: "Price range for a product. If the product has a single price, the minimum and maximum price will be the same."){ + minimum_price: ProductPrice! @doc(description: "The lowest possible price for the product.") + maximum_price: ProductPrice @doc(description: "The highest possible price for the product.") +} + +type ProductPrice @doc(description: "Represents a product price.") { + regular_price: Money! @doc(description: "The regular price of the product.") + final_price: Money! @doc(description: "The final price of the product after discounts applied.") + discount: ProductDiscount @doc(description: "The price discount. Represents the difference between the regular and final price.") +} + +type ProductDiscount @doc(description: "A discount applied to a product price.") { + percent_off: Float @doc(description: "The discount expressed a percentage.") + amount_off: Float @doc(description: "The actual value of the discount.") +} + +type ProductLinks implements ProductLinksInterface @doc(description: "ProductLinks is an implementation of ProductLinksInterface.") { +} + +interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductLinkTypeResolverComposite") @doc(description:"ProductLinks contains information about linked products, including the link type and product type of each item.") { + sku: String @doc(description: "The identifier of the linked product.") + link_type: String @doc(description: "One of related, associated, upsell, or crosssell.") + linked_product_sku: String @doc(description: "The SKU of the linked product.") + linked_product_type: String @doc(description: "The type of linked product (simple, virtual, bundle, downloadable, grouped, configurable).") + position: Int @doc(description: "The position within the list of product links.") +} + +interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { + id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + name: String @doc(description: "The product name. Customers use this name to identify the product.") + sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") + description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") + short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") + special_price: Float @doc(description: "The discounted price of the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\SpecialPrice") + special_from_date: String @doc(description: "The beginning date that a product has a special price.") + special_to_date: String @doc(description: "The end date that a product has a special price.") + attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") + meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") + meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines.") + meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") + image: ProductImage @doc(description: "The relative path to the main image on the product page.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") + small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") + thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") + new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") + options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") + created_at: String @doc(description: "Timestamp indicating when the product was created.") + updated_at: String @doc(description: "Timestamp indicating when the product was updated.") + country_of_manufacture: String @doc(description: "The product's country of origin.") + type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") @deprecated(reason: "Use __typename instead.") + websites: [Website] @doc(description: "An array of websites in which the product is available.") @deprecated(reason: "The field should not be used on the storefront.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") + product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\BatchProductLinks") + media_gallery_entries: [MediaGalleryEntry] @deprecated(reason: "Use product's `media_gallery` instead") @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries") + price: ProductPrices @deprecated(reason: "Use price_range for product price information.") @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") + price_range: PriceRange! @doc(description: "A PriceRange object, indicating the range of prices for the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\PriceRange") + gift_message_available: String @doc(description: "Indicates whether a gift message is available.") + manufacturer: Int @doc(description: "A number representing the product's manufacturer.") + categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") + canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") + media_gallery: [MediaGalleryInterface] @doc(description: "An array of Media Gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") +} + +interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "PhysicalProductInterface contains attributes specific to tangible products.") { + weight: Float @doc(description: "The weight of the item, in units defined by the store.") +} + +type CustomizableAreaOption implements CustomizableOptionInterface @doc(description: "CustomizableAreaOption contains information about a text area that is defined as part of a customizable option.") { + value: CustomizableAreaValue @doc(description: "An object that defines a text area.") + product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") +} + +type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price and sku of a product whose page contains a customized text area.") { + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. +} + +type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { + children: [CategoryTree] @doc(description: "Child categories tree.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") +} + +type CategoryResult @doc(description: "A collection of CategoryTree objects and pagination information.") { + items: [CategoryTree] @doc(description: "A list of categories that match the filter criteria.") + page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") + total_count: Int @doc(description: "The total number of categories that match the criteria.") +} + +type CustomizableDateOption implements CustomizableOptionInterface @doc(description: "CustomizableDateOption contains information about a date picker that is defined as part of a customizable option.") { + value: CustomizableDateValue @doc(description: "An object that defines a date field in a customizable option.") + product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") +} + +type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price and sku of a product whose page contains a customized date picker.") { + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. +} + +type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { + value: [CustomizableDropDownValue] @doc(description: "An array that defines the set of options for a drop down menu.") +} + +type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defines the price and sku of a product whose page contains a customized drop down menu.") { + option_type_id: Int @doc(description: "The ID assigned to the value.") + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + title: String @doc(description: "The display name for this option.") + sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. +} + +type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { + value: [CustomizableMultipleValue] @doc(description: "An array that defines the set of options for a multiselect.") +} + +type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defines the price and sku of a product whose page contains a customized multiselect.") { + option_type_id: Int @doc(description: "The ID assigned to the value.") + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + title: String @doc(description: "The display name for this option.") + sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") +} + +type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { + value: CustomizableFieldValue @doc(description: "An object that defines a text field.") + product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") +} + +type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines the price and sku of a product whose page contains a customized text field.") { + price: Float @doc(description: "The price of the custom value.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. +} + +type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { + value: CustomizableFileValue @doc(description: "An object that defines a file value.") + product_sku: String @doc(description: "The Stock Keeping Unit of the base product.") +} + +type CustomizableFileValue @doc(description: "CustomizableFileValue defines the price and sku of a product whose page contains a customized file picker.") { + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + file_extension: String @doc(description: "The file extension to accept.") + image_size_x: Int @doc(description: "The maximum width of an image.") + image_size_y: Int @doc(description: "The maximum height of an image.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. +} + +interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { + url: String @doc(description: "The URL of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Url") + label: String @doc(description: "The label of the product image or video.") + position: Int @doc(description: "The media item's position after it has been sorted.") + disabled: Boolean @doc(description: "Whether the image is hidden from view.") +} + +type ProductImage implements MediaGalleryInterface @doc(description: "Product image information. Contains the image URL and label.") { +} + +type ProductVideo implements MediaGalleryInterface @doc(description: "Contains information about a product video.") { + video_content: ProductMediaGalleryEntriesVideoContent @doc(description: "Contains a ProductMediaGalleryEntriesVideoContent object.") +} + +interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CustomizableOptionTypeResolver") @doc(description: "The CustomizableOptionInterface contains basic information about a customizable option. It can be implemented by several types of configurable options.") { + title: String @doc(description: "The display name for this option.") + required: Boolean @doc(description: "Indicates whether the option is required.") + sort_order: Int @doc(description: "The order in which the option is displayed.") + option_id: Int @doc(description: "Option ID.") +} + +interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "CustomizableProductInterface contains information about customizable product options.") { + options: [CustomizableOptionInterface] @doc(description: "An array of options for a customizable product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Options") +} + +interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search.") { + id: Int @doc(description: "An ID that uniquely identifies the category.") + description: String @doc(description: "An optional description of the category.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") + name: String @doc(description: "The display name of the category.") + path: String @doc(description: "Category Path.") + path_in_store: String @doc(description: "Category path in store.") + url_key: String @doc(description: "The url key assigned to the category.") + url_path: String @doc(description: "The url path assigned to the category.") + canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Categories' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CanonicalUrl") + position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.") + level: Int @doc(description: "Indicates the depth of the category within the tree.") + created_at: String @doc(description: "Timestamp indicating when the category was created.") + updated_at: String @doc(description: "Timestamp indicating when the category was updated.") + product_count: Int @doc(description: "The number of products in the category that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\ProductsCount") + default_sort_by: String @doc(description: "The attribute to use for sorting.") + products( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.") + ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") + breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") +} + +type Breadcrumb @doc(description: "Breadcrumb item."){ + category_id: Int @doc(description: "Category ID.") + category_name: String @doc(description: "Category name.") + category_level: Int @doc(description: "Category level.") + category_url_key: String @doc(description: "Category URL key.") + category_url_path: String @doc(description: "Category URL path.") +} + +type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option.") { + value: [CustomizableRadioValue] @doc(description: "An array that defines a set of radio buttons.") +} + +type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines the price and sku of a product whose page contains a customized set of radio buttons.") { + option_type_id: Int @doc(description: "The ID assigned to the value.") + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + title: String @doc(description: "The display name for this option.") + sort_order: Int @doc(description: "The order in which the radio button is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. +} + +type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { + value: [CustomizableCheckboxValue] @doc(description: "An array that defines a set of checkbox values.") +} + +type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values.") { + option_type_id: Int @doc(description: "The ID assigned to the value.") + price: Float @doc(description: "The price assigned to this option.") + price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") + sku: String @doc(description: "The Stock Keeping Unit for this option.") + title: String @doc(description: "The display name for this option.") + sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. +} + +type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { +} + +type SimpleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "A simple product is tangible and are usually sold as single units or in fixed quantities.") +{ +} + +type Products @doc(description: "The Products object is the top-level object returned in a product search.") { + items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.") + page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") + total_count: Int @doc(description: "The number of products that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") + filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") + aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") + sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") +} + +type CategoryProducts @doc(description: "The category products object returned in the Category query.") { + items: [ProductInterface] @doc(description: "An array of products that are assigned to the category.") + page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") + total_count: Int @doc(description: "The number of products in the category that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") +} + +input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { + category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") +} + +input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") +{ + ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") + url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category.") + name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") + url_path: FilterEqualTypeInput @doc(description: "Filter by the URL path for the category.") +} + +input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { + name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") + sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") + description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") + short_description: FilterTypeInput @doc(description: "A short description of the product. Its use depends on the theme.") + price: FilterTypeInput @doc(description: "The price of an item.") + special_price: FilterTypeInput @doc(description: "The discounted price of the product. Do not include the currency code.") + special_from_date: FilterTypeInput @doc(description: "The beginning date that a product has a special price.") + special_to_date: FilterTypeInput @doc(description: "The end date that a product has a special price.") + weight: FilterTypeInput @doc(description: "The weight of the item, in units defined by the store.") + manufacturer: FilterTypeInput @doc(description: "A number representing the product's manufacturer.") + meta_title: FilterTypeInput @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") + meta_keyword: FilterTypeInput @doc(description: "A comma-separated list of keywords that are visible only to search engines.") + meta_description: FilterTypeInput @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") + image: FilterTypeInput @doc(description: "The relative path to the main image on the product page.") + small_image: FilterTypeInput @doc(description: "The relative path to the small image, which is used on catalog pages.") + thumbnail: FilterTypeInput @doc(description: "The relative path to the product's thumbnail image.") + tier_price: FilterTypeInput @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") + news_from_date: FilterTypeInput @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") + news_to_date: FilterTypeInput @doc(description: "The end date for new product listings.") + custom_layout_update: FilterTypeInput @doc(description: "XML code that is applied as a layout update to the product page.") + min_price: FilterTypeInput @doc(description:"The numeric minimal price of the product. Do not include the currency code.") + max_price: FilterTypeInput @doc(description:"The numeric maximal price of the product. Do not include the currency code.") + category_id: FilterTypeInput @doc(description: "Category ID the product belongs to.") + options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page.") + required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options.") + has_options: FilterTypeInput @doc(description: "Indicates whether additional attributes have been created for the product.") + image_label: FilterTypeInput @doc(description: "The label assigned to a product image.") + small_image_label: FilterTypeInput @doc(description: "The label assigned to a product's small image.") + thumbnail_label: FilterTypeInput @doc(description: "The label assigned to a product's thumbnail image.") + created_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was created.") + updated_at: FilterTypeInput @doc(description: "Timestamp indicating when the product was updated.") + country_of_manufacture: FilterTypeInput @doc(description: "The product's country of origin.") + custom_layout: FilterTypeInput @doc(description: "The name of a custom layout.") + gift_message_available: FilterTypeInput @doc(description: "Indicates whether a gift message is available.") + or: ProductFilterInput @doc(description: "The keyword required to perform a logical OR comparison.") +} + +type ProductMediaGalleryEntriesContent @doc(description: "ProductMediaGalleryEntriesContent contains an image in base64 format and basic information about the image.") { + base64_encoded_data: String @doc(description: "The image in base64 format.") + type: String @doc(description: "The MIME type of the file, such as image/png.") + name: String @doc(description: "The file name of the image.") +} + +type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalleryEntriesVideoContent contains a link to a video file and basic information about the video.") { + media_type: String @doc(description: "Must be external-video.") + video_provider: String @doc(description: "Describes the video source.") + video_url: String @doc(description: "The URL to the video.") + video_title: String @doc(description: "The title of the video.") + video_description: String @doc(description: "A description of the video.") + video_metadata: String @doc(description: "Optional data about the video.") +} + +input ProductSortInput @doc(description: "ProductSortInput is deprecated, use @ProductAttributeSortInput instead. ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { + name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") + sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") + description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") + short_description: SortEnum @doc(description: "A short description of the product. Its use depends on the theme.") + price: SortEnum @doc(description: "The price of the item.") + special_price: SortEnum @doc(description: "The discounted price of the product.") + special_from_date: SortEnum @doc(description: "The beginning date that a product has a special price.") + special_to_date: SortEnum @doc(description: "The end date that a product has a special price.") + weight: SortEnum @doc(description: "The weight of the item, in units defined by the store.") + manufacturer: SortEnum @doc(description: "A number representing the product's manufacturer.") + meta_title: SortEnum @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") + meta_keyword: SortEnum @doc(description: "A comma-separated list of keywords that are visible only to search engines.") + meta_description: SortEnum @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") + image: SortEnum @doc(description: "The relative path to the main image on the product page.") + small_image: SortEnum @doc(description: "The relative path to the small image, which is used on catalog pages.") + thumbnail: SortEnum @doc(description: "The relative path to the product's thumbnail image.") + tier_price: SortEnum @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") + news_from_date: SortEnum @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") + news_to_date: SortEnum @doc(description: "The end date for new product listings.") + custom_layout_update: SortEnum @doc(description: "XML code that is applied as a layout update to the product page.") + options_container: SortEnum @doc(description: "If the product has multiple options, determines where they appear on the product page.") + required_options: SortEnum @doc(description: "Indicates whether the product has required options.") + has_options: SortEnum @doc(description: "Indicates whether additional attributes have been created for the product.") + image_label: SortEnum @doc(description: "The label assigned to a product image.") + small_image_label: SortEnum @doc(description: "The label assigned to a product's small image.") + thumbnail_label: SortEnum @doc(description: "The label assigned to a product's thumbnail image.") + created_at: SortEnum @doc(description: "Timestamp indicating when the product was created.") + updated_at: SortEnum @doc(description: "Timestamp indicating when the product was updated.") + country_of_manufacture: SortEnum @doc(description: "The product's country of origin.") + custom_layout: SortEnum @doc(description: "The name of a custom layout.") + gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") +} + +input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option") +{ + relevance: SortEnum @doc(description: "Sort by the search relevance score (default).") + position: SortEnum @doc(description: "Sort by the position assigned to each product.") +} + +type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { + id: Int @doc(description: "The identifier assigned to the object.") + media_type: String @doc(description: "image or video.") + label: String @doc(description: "The alt text displayed on the UI when the user points to the image.") + position: Int @doc(description: "The media item's position after it has been sorted.") + disabled: Boolean @doc(description: "Whether the image is hidden from view.") + types: [String] @doc(description: "Array of image types. It can have the following values: image, small_image, thumbnail.") + file: String @doc(description: "The path of the image on the server.") + content: ProductMediaGalleryEntriesContent @doc(description: "Contains a ProductMediaGalleryEntriesContent object.") + video_content: ProductMediaGalleryEntriesVideoContent @doc(description: "Contains a ProductMediaGalleryEntriesVideoContent object.") +} + +type LayerFilter { + name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead.") + request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead.") + filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead.") + filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead.") +} + +interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") { + label: String @doc(description: "Filter label.") @deprecated(reason: "Use AggregationOption.label instead.") + value_string: String @doc(description: "Value for filter request variable to be used in query.") @deprecated(reason: "Use AggregationOption.value instead.") + items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.") +} + +type LayerFilterItem implements LayerFilterItemInterface { + +} + +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { + count: Int @doc(description: "The number of options in the aggregation group.") + label: String @doc(description: "The aggregation display name.") + attribute_code: String! @doc(description: "Attribute code of the aggregation group.") + options: [AggregationOption] @doc(description: "Array of options for the aggregation.") +} + +interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") { + count: Int @doc(description: "The number of items that match the aggregation option.") + label: String @doc(description: "Aggregation option display label.") + value: String! @doc(description: "The internal ID that represents the value of the option.") +} + +type AggregationOption implements AggregationOptionInterface { + +} + +type SortField { + value: String @doc(description: "Attribute code of sort field.") + label: String @doc(description: "Label of sort field.") +} + +type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields.") { + default: String @doc(description: "Default value of sort fields.") + options: [SortField] @doc(description: "Available sort fields.") +} + +type StoreConfig @doc(description: "The type contains information about a store config.") { + product_url_suffix : String @doc(description: "Product URL Suffix.") + category_url_suffix : String @doc(description: "Category URL Suffix.") + title_separator : String @doc(description: "Page Title Separator.") + list_mode : String @doc(description: "List Mode.") + grid_per_page_values : String @doc(description: "Products per Page on Grid Allowed Values.") + list_per_page_values : String @doc(description: "Products per Page on List Allowed Values.") + grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") + list_per_page : Int @doc(description: "Products per Page on List Default Value.") + catalog_default_sort_by : String @doc(description: "Default Sort By.") + root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") +} \ No newline at end of file From b83b5384e57658ee6e374a23a44b6738d40f9a62 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:36:31 -0500 Subject: [PATCH 270/479] Copy over Magento_CustomerGraphQl schema from 2.4-develop --- .../graph-ql/coverage/customer.graphqls | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 design-documents/graph-ql/coverage/customer.graphqls diff --git a/design-documents/graph-ql/coverage/customer.graphqls b/design-documents/graph-ql/coverage/customer.graphqls new file mode 100644 index 000000000..63c5bc6a3 --- /dev/null +++ b/design-documents/graph-ql/coverage/customer.graphqls @@ -0,0 +1,426 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type StoreConfig { + required_character_classes_number : String @doc(description: "The number of different character classes required in a password (lowercase, uppercase, digits, special characters).") + minimum_password_length : String @doc(description: "The minimum number of characters required for a valid password.") + autocomplete_on_storefront : Boolean @doc(description: "Enable autocomplete on login and forgot password forms") +} + +type Query { + customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") @cache(cacheable: false) + isEmailAvailable ( + email: String! @doc(description: "The new customer email") + ): IsEmailAvailableOutput @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\IsEmailAvailable") +} + +type Mutation { + generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") + changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + createCustomerV2 (input: CustomerCreateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Deprecated. Use UpdateCustomerV2 instead.") + updateCustomerV2 (input: CustomerUpdateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") + createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") + updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") + deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") + requestPasswordResetEmail(email: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RequestPasswordResetEmail") @doc(description: "Request an email with a reset password token for the registered customer identified by the specified email.") + resetPassword(email: String!, resetPasswordToken: String!, newPassword: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ResetPassword") @doc(description: "Reset a customer's password using the reset password token that the customer received in an email after requesting it using requestPasswordResetEmail.") + updateCustomerEmail(email: String!, password: String!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerEmail") @doc(description: "") +} + +input CustomerAddressInput { + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + street: [String] @doc(description: "An array of strings that define the street number and name") + city: String @doc(description: "The city or town") + region: CustomerAddressRegionInput @doc(description: "An object containing the region name, region code, and region ID") + postcode: String @doc(description: "The customer's ZIP or postal code") + country_id: CountryCodeEnum @doc(description: "Deprecated: use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") + default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") + default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + fax: String @doc(description: "The fax number") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.") +} + +input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { + region_code: String @doc(description: "The address region code") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "The unique ID for a pre-defined region") +} + +input CustomerAddressAttributeInput { + attribute_code: String! @doc(description: "Attribute code") + value: String! @doc(description: "Attribute value") +} + +type CustomerToken { + token: String @doc(description: "The customer token") +} + +input CustomerInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +input CustomerCreateInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String! @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String! @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String! @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +input CustomerUpdateInput { + date_of_birth: String @doc(description: "The customer's date of birth") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + firstname: String @doc(description: "The customer's first name") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") + lastname: String @doc(description: "The customer's family name") + middlename: String @doc(description: "The customer's middle name") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") +} + +type CustomerOutput { + customer: Customer! +} + +type RevokeCustomerTokenOutput { + result: Boolean! +} + +type Customer @doc(description: "Customer defines the customer name and address and other details") { + created_at: String @doc(description: "Timestamp indicating when the account was created") + group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + default_billing: String @doc(description: "The ID assigned to the billing address") + default_shipping: String @doc(description: "The ID assigned to the shipping address") + dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") + addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") +} + +type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ + id: Int @doc(description: "The ID assigned to the address object") + customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") + region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") + region_id: Int @doc(description: "The unique ID for a pre-defined region") + country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") + country_code: CountryCodeEnum @doc(description: "The customer's country") + street: [String] @doc(description: "An array of strings that define the street number and name") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + fax: String @doc(description: "The fax number") + postcode: String @doc(description: "The customer's ZIP or postal code") + city: String @doc(description: "The city or town") + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") + default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") + default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into container") + extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes") +} + +type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { + region_code: String @doc(description: "The address region code") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "The unique ID for a pre-defined region") +} + +type CustomerAddressAttribute { + attribute_code: String @doc(description: "Attribute code") + value: String @doc(description: "Attribute value") +} + +type IsEmailAvailableOutput { + is_email_available: Boolean @doc(description: "Is email availabel value") +} + +enum CountryCodeEnum @doc(description: "The list of countries codes") { + AF @doc(description: "Afghanistan") + AX @doc(description: "Åland Islands") + AL @doc(description: "Albania") + DZ @doc(description: "Algeria") + AS @doc(description: "American Samoa") + AD @doc(description: "Andorra") + AO @doc(description: "Angola") + AI @doc(description: "Anguilla") + AQ @doc(description: "Antarctica") + AG @doc(description: "Antigua & Barbuda") + AR @doc(description: "Argentina") + AM @doc(description: "Armenia") + AW @doc(description: "Aruba") + AU @doc(description: "Australia") + AT @doc(description: "Austria") + AZ @doc(description: "Azerbaijan") + BS @doc(description: "Bahamas") + BH @doc(description: "Bahrain") + BD @doc(description: "Bangladesh") + BB @doc(description: "Barbados") + BY @doc(description: "Belarus") + BE @doc(description: "Belgium") + BZ @doc(description: "Belize") + BJ @doc(description: "Benin") + BM @doc(description: "Bermuda") + BT @doc(description: "Bhutan") + BO @doc(description: "Bolivia") + BA @doc(description: "Bosnia & Herzegovina") + BW @doc(description: "Botswana") + BV @doc(description: "Bouvet Island") + BR @doc(description: "Brazil") + IO @doc(description: "British Indian Ocean Territory") + VG @doc(description: "British Virgin Islands") + BN @doc(description: "Brunei") + BG @doc(description: "Bulgaria") + BF @doc(description: "Burkina Faso") + BI @doc(description: "Burundi") + KH @doc(description: "Cambodia") + CM @doc(description: "Cameroon") + CA @doc(description: "Canada") + CV @doc(description: "Cape Verde") + KY @doc(description: "Cayman Islands") + CF @doc(description: "Central African Republic") + TD @doc(description: "Chad") + CL @doc(description: "Chile") + CN @doc(description: "China") + CX @doc(description: "Christmas Island") + CC @doc(description: "Cocos (Keeling) Islands") + CO @doc(description: "Colombia") + KM @doc(description: "Comoros") + CG @doc(description: "Congo-Brazzaville") + CD @doc(description: "Congo-Kinshasa") + CK @doc(description: "Cook Islands") + CR @doc(description: "Costa Rica") + CI @doc(description: "Côte d’Ivoire") + HR @doc(description: "Croatia") + CU @doc(description: "Cuba") + CY @doc(description: "Cyprus") + CZ @doc(description: "Czech Republic") + DK @doc(description: "Denmark") + DJ @doc(description: "Djibouti") + DM @doc(description: "Dominica") + DO @doc(description: "Dominican Republic") + EC @doc(description: "Ecuador") + EG @doc(description: "Egypt") + SV @doc(description: "El Salvador") + GQ @doc(description: "Equatorial Guinea") + ER @doc(description: "Eritrea") + EE @doc(description: "Estonia") + ET @doc(description: "Ethiopia") + FK @doc(description: "Falkland Islands") + FO @doc(description: "Faroe Islands") + FJ @doc(description: "Fiji") + FI @doc(description: "Finland") + FR @doc(description: "France") + GF @doc(description: "French Guiana") + PF @doc(description: "French Polynesia") + TF @doc(description: "French Southern Territories") + GA @doc(description: "Gabon") + GM @doc(description: "Gambia") + GE @doc(description: "Georgia") + DE @doc(description: "Germany") + GH @doc(description: "Ghana") + GI @doc(description: "Gibraltar") + GR @doc(description: "Greece") + GL @doc(description: "Greenland") + GD @doc(description: "Grenada") + GP @doc(description: "Guadeloupe") + GU @doc(description: "Guam") + GT @doc(description: "Guatemala") + GG @doc(description: "Guernsey") + GN @doc(description: "Guinea") + GW @doc(description: "Guinea-Bissau") + GY @doc(description: "Guyana") + HT @doc(description: "Haiti") + HM @doc(description: "Heard & McDonald Islands") + HN @doc(description: "Honduras") + HK @doc(description: "Hong Kong SAR China") + HU @doc(description: "Hungary") + IS @doc(description: "Iceland") + IN @doc(description: "India") + ID @doc(description: "Indonesia") + IR @doc(description: "Iran") + IQ @doc(description: "Iraq") + IE @doc(description: "Ireland") + IM @doc(description: "Isle of Man") + IL @doc(description: "Israel") + IT @doc(description: "Italy") + JM @doc(description: "Jamaica") + JP @doc(description: "Japan") + JE @doc(description: "Jersey") + JO @doc(description: "Jordan") + KZ @doc(description: "Kazakhstan") + KE @doc(description: "Kenya") + KI @doc(description: "Kiribati") + KW @doc(description: "Kuwait") + KG @doc(description: "Kyrgyzstan") + LA @doc(description: "Laos") + LV @doc(description: "Latvia") + LB @doc(description: "Lebanon") + LS @doc(description: "Lesotho") + LR @doc(description: "Liberia") + LY @doc(description: "Libya") + LI @doc(description: "Liechtenstein") + LT @doc(description: "Lithuania") + LU @doc(description: "Luxembourg") + MO @doc(description: "Macau SAR China") + MK @doc(description: "Macedonia") + MG @doc(description: "Madagascar") + MW @doc(description: "Malawi") + MY @doc(description: "Malaysia") + MV @doc(description: "Maldives") + ML @doc(description: "Mali") + MT @doc(description: "Malta") + MH @doc(description: "Marshall Islands") + MQ @doc(description: "Martinique") + MR @doc(description: "Mauritania") + MU @doc(description: "Mauritius") + YT @doc(description: "Mayotte") + MX @doc(description: "Mexico") + FM @doc(description: "Micronesia") + MD @doc(description: "Moldova") + MC @doc(description: "Monaco") + MN @doc(description: "Mongolia") + ME @doc(description: "Montenegro") + MS @doc(description: "Montserrat") + MA @doc(description: "Morocco") + MZ @doc(description: "Mozambique") + MM @doc(description: "Myanmar (Burma)") + NA @doc(description: "Namibia") + NR @doc(description: "Nauru") + NP @doc(description: "Nepal") + NL @doc(description: "Netherlands") + AN @doc(description: "Netherlands Antilles") + NC @doc(description: "New Caledonia") + NZ @doc(description: "New Zealand") + NI @doc(description: "Nicaragua") + NE @doc(description: "Niger") + NG @doc(description: "Nigeria") + NU @doc(description: "Niue") + NF @doc(description: "Norfolk Island") + MP @doc(description: "Northern Mariana Islands") + KP @doc(description: "North Korea") + NO @doc(description: "Norway") + OM @doc(description: "Oman") + PK @doc(description: "Pakistan") + PW @doc(description: "Palau") + PS @doc(description: "Palestinian Territories") + PA @doc(description: "Panama") + PG @doc(description: "Papua New Guinea") + PY @doc(description: "Paraguay") + PE @doc(description: "Peru") + PH @doc(description: "Philippines") + PN @doc(description: "Pitcairn Islands") + PL @doc(description: "Poland") + PT @doc(description: "Portugal") + QA @doc(description: "Qatar") + RE @doc(description: "Réunion") + RO @doc(description: "Romania") + RU @doc(description: "Russia") + RW @doc(description: "Rwanda") + WS @doc(description: "Samoa") + SM @doc(description: "San Marino") + ST @doc(description: "São Tomé & Príncipe") + SA @doc(description: "Saudi Arabia") + SN @doc(description: "Senegal") + RS @doc(description: "Serbia") + SC @doc(description: "Seychelles") + SL @doc(description: "Sierra Leone") + SG @doc(description: "Singapore") + SK @doc(description: "Slovakia") + SI @doc(description: "Slovenia") + SB @doc(description: "Solomon Islands") + SO @doc(description: "Somalia") + ZA @doc(description: "South Africa") + GS @doc(description: "South Georgia & South Sandwich Islands") + KR @doc(description: "South Korea") + ES @doc(description: "Spain") + LK @doc(description: "Sri Lanka") + BL @doc(description: "St. Barthélemy") + SH @doc(description: "St. Helena") + KN @doc(description: "St. Kitts & Nevis") + LC @doc(description: "St. Lucia") + MF @doc(description: "St. Martin") + PM @doc(description: "St. Pierre & Miquelon") + VC @doc(description: "St. Vincent & Grenadines") + SD @doc(description: "Sudan") + SR @doc(description: "Suriname") + SJ @doc(description: "Svalbard & Jan Mayen") + SZ @doc(description: "Swaziland") + SE @doc(description: "Sweden") + CH @doc(description: "Switzerland") + SY @doc(description: "Syria") + TW @doc(description: "Taiwan") + TJ @doc(description: "Tajikistan") + TZ @doc(description: "Tanzania") + TH @doc(description: "Thailand") + TL @doc(description: "Timor-Leste") + TG @doc(description: "Togo") + TK @doc(description: "Tokelau") + TO @doc(description: "Tonga") + TT @doc(description: "Trinidad & Tobago") + TN @doc(description: "Tunisia") + TR @doc(description: "Turkey") + TM @doc(description: "Turkmenistan") + TC @doc(description: "Turks & Caicos Islands") + TV @doc(description: "Tuvalu") + UG @doc(description: "Uganda") + UA @doc(description: "Ukraine") + AE @doc(description: "United Arab Emirates") + GB @doc(description: "United Kingdom") + US @doc(description: "United States") + UY @doc(description: "Uruguay") + UM @doc(description: "U.S. Outlying Islands") + VI @doc(description: "U.S. Virgin Islands") + UZ @doc(description: "Uzbekistan") + VU @doc(description: "Vanuatu") + VA @doc(description: "Vatican City") + VE @doc(description: "Venezuela") + VN @doc(description: "Vietnam") + WF @doc(description: "Wallis & Futuna") + EH @doc(description: "Western Sahara") + YE @doc(description: "Yemen") + ZM @doc(description: "Zambia") + ZW @doc(description: "Zimbabwe") +} \ No newline at end of file From d6cfdb06e806f05554f8fa5e44fd08319ae94283 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:17:31 -0500 Subject: [PATCH 271/479] Add "uid" and deprecate "id" in "CartItemInterface" --- design-documents/graph-ql/coverage/quote.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index cf311354d..5a4977132 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -328,7 +328,8 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car } interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { - id: String! + id: String! @deprecated(reason: "Use CartItemInterface.uid instead") + uid: ID! @doc(description: "Unique identifier for a Cart Item") quantity: Float! prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") product: ProductInterface! From 12a6c102bc387ff6d008da95deceacbbafe957ee Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:03:37 -0500 Subject: [PATCH 272/479] Add "cart_item_uid" and deprecate "cart_item_id" in "CartItemUpdateInput" --- design-documents/graph-ql/coverage/quote.graphqls | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 5a4977132..8c5821ff9 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -73,7 +73,10 @@ input UpdateCartItemsInput { } input CartItemUpdateInput { - cart_item_id: Int! + # Implementation Note: The resolver should accept either `cart_item_id` or `cart_item_uid`, + # but _must_ return an error when both fields are provided in a request + cart_item_id: Int! @deprecated(reason: "Use `CartItemUpdateInput.cart_item_uid` instead") + cart_item_uid: ID! @doc(description: "Unique Identifier from objects implementing `CartItemInterface`") quantity: Float customizable_options: [CustomizableOptionInput!] } From 838e4188f7c13ecd9e029178768a42bbb1aa3584 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:05:58 -0500 Subject: [PATCH 273/479] Add "cart_item_uid" and deprecate "cart_item_id" in "RemoveItemFromCartInput" --- design-documents/graph-ql/coverage/quote.graphqls | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 8c5821ff9..5e7890d16 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -83,7 +83,10 @@ input CartItemUpdateInput { input RemoveItemFromCartInput { cart_id: String! - cart_item_id: Int! + # Implementation Note: The resolver should accept either `cart_item_id` or `cart_item_uid`, + # but _must_ return an error when both fields are provided in a request + cart_item_id: Int! @deprecated(reason: "Use `RemoveItemFromCartInput.cart_item_uid` instead") + cart_item_uid: ID! @doc(description: "Unique Identifier from objects implementing `CartItemInterface`") } input SetShippingAddressesOnCartInput { From 970fc37c5c8f6c7a67aeba6e786a294ff2039049 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:10:43 -0500 Subject: [PATCH 274/479] Fix incorrect deprecation (wrong directive) for "CartItemQuantity" --- design-documents/graph-ql/coverage/quote.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 5e7890d16..4e31845ad 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -244,7 +244,7 @@ type BillingCartAddress implements CartAddressInterface { customer_notes: String @deprecated (reason: "The field is used only in shipping address") } -type CartItemQuantity @doc(description:"Deprecated: `cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") { +type CartItemQuantity @deprecated(description:"Deprecated: `cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") { cart_item_id: Int! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") quantity: Float! @deprecated(reason: "`cart_items` field of `ShippingCartAddress` returns now `CartItemInterface` instead of `CartItemQuantity`") } From 7b551cae979a59e2669817b140926b336e075764 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:23:44 -0500 Subject: [PATCH 275/479] Add "uid" and deprecate "id" in "CategoryInterface" --- design-documents/graph-ql/coverage/catalog.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index f510637f4..c1377905c 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -239,7 +239,8 @@ interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGra } interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search.") { - id: Int @doc(description: "An ID that uniquely identifies the category.") + id: Int @deprecated(reason: "Use `CategoryInterface.uid instead") + uid: ID! @doc(description: "Unique identifier for a Category") description: String @doc(description: "An optional description of the category.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") name: String @doc(description: "The display name of the category.") path: String @doc(description: "Category Path.") From 7a110931234fa7133a66958e62ca99707d29e234 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:29:01 -0500 Subject: [PATCH 276/479] Add "categoryInterfaceUID" argument and deprecate "id" argument in "Query.category" --- design-documents/graph-ql/coverage/catalog.graphqls | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index c1377905c..901d8bdcd 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -11,7 +11,10 @@ type Query { ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( - id: Int @doc(description: "Id of the category.") + # Implementation Note: The resolver should accept either `id` or `categoryInterfaceUID`, + # but _must_ return an error when both fields are provided in a request + id: Int @deprecated(reason: "Use the `categoryInterfaceUID` argument instead") + categoryInterfaceUID: ID @doc(description: "Unique identifier from objects implementing `CategoryInterface`") ): CategoryTree @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") categoryList( From 6f2c6937d091308a4f7258576d21986fda6291f9 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:32:21 -0500 Subject: [PATCH 277/479] Add "uid" and deprecate "id" in "ProductInterface" --- design-documents/graph-ql/coverage/catalog.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index 901d8bdcd..a0b6c2367 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -86,7 +86,8 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M } interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { - id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + id: Int @deprecated(description: "Use the `uid` field instead") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + uid: ID! @doc(description: "Unique identifier for objects implementing `ProductInterface`") name: String @doc(description: "The product name. Customers use this name to identify the product.") sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") From 00cbb7161a4126bea6610a0e6d47355dcd4bfdfe Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:35:00 -0500 Subject: [PATCH 278/479] Add "category_interface_uid" and deprecate "category_id" in "Breadcrumb" --- design-documents/graph-ql/coverage/catalog.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index a0b6c2367..63174c147 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -267,7 +267,8 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model } type Breadcrumb @doc(description: "Breadcrumb item."){ - category_id: Int @doc(description: "Category ID.") + category_id: Int @deprecated(reason: "Use the `category_interface_uid` field instead") + category_interface_uid: ID! @doc(description: "Unique identifier from objects implementing CategoryInterface") category_name: String @doc(description: "Category name.") category_level: Int @doc(description: "Category level.") category_url_key: String @doc(description: "Category URL key.") From 9846887a5263a58ebf9ce20f4d74e582192365e3 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 21 Aug 2020 15:38:26 -0500 Subject: [PATCH 279/479] Add "uid" field to "Customer" type --- design-documents/graph-ql/coverage/customer.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer.graphqls b/design-documents/graph-ql/coverage/customer.graphqls index 63c5bc6a3..959c93483 100644 --- a/design-documents/graph-ql/coverage/customer.graphqls +++ b/design-documents/graph-ql/coverage/customer.graphqls @@ -131,7 +131,8 @@ type Customer @doc(description: "Customer defines the customer name and address dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") date_of_birth: String @doc(description: "The customer's date of birth") taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") - id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") + id: Int @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") + uid: ID! @doc(description: "Unique identifier for a Customer") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") From 37a84caeaca4948f32cbe780b753aa37443304ae Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Sun, 23 Aug 2020 14:08:52 +0530 Subject: [PATCH 280/479] close negotiable quote status change --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 0894f5106..b9b851b16 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -111,7 +111,7 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - closed_quotes: [NegotiableQuote]! @doc(description: "Quotes that were just closed") + closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed") negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, From 7dbbf50c2805bd25ad758035babe12d8f4082884 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Mon, 24 Aug 2020 12:50:30 -0500 Subject: [PATCH 281/479] Update GraphQL Object ID Guidance document to use new "uid" --- design-documents/graph-ql/id-fields-object-types.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/id-fields-object-types.md b/design-documents/graph-ql/id-fields-object-types.md index 82f8b6927..a4097fd32 100644 --- a/design-documents/graph-ql/id-fields-object-types.md +++ b/design-documents/graph-ql/id-fields-object-types.md @@ -8,8 +8,9 @@ When a GraphQL Object Type is used to represent an entity that can be referenced - Use the `ID` scalar type - Make the `ID` field non-nullable (syntax: `ID!`) -- Use the field name `id` or `_id` - - Most GraphQL client libs automatically use this field for caching. Any other field name will require manual caching logic on the client +- Use the field name `uid` + - [Historical context for the field name `uid`](id-improvement-plan.md) + - Most GraphQL client libs will need to be made aware of this field name for caching - https://www.apollographql.com/docs/react/caching/cache-configuration/#assigning-unique-identifiers - https://formidable.com/open-source/urql/docs/graphcache/normalized-caching/#key-generation - Include an ID field anytime an Object Type _could_ have an ID field @@ -18,7 +19,7 @@ When a GraphQL Object Type is used to represent an entity that can be referenced - Include info in the client-facing description describing how the field is encoded/decoded (should be opaque) - Example: The client shouldn't know if an `ID` is a base64-encoded integer - Use `String` or `Int` type -- Use duplicate IDs across a concrete type. In other words, if the client wants to produce a cache key, the concatenation of a `__typename` + `id` field should _always_ be unique +- Use duplicate IDs across a concrete type. In other words, if the client wants to produce a cache key, the concatenation of a `__typename` + `uid` field should _always_ be unique ## Examples where an ID is not helpful From 0e26c98f79c108359c3c2059b1faffe1bb6754da Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Tue, 25 Aug 2020 19:34:19 +0530 Subject: [PATCH 282/479] negotiable-quote-modify --- .../graph-ql/coverage/negotiableQuotes.graphqls | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index b9b851b16..192c59271 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -99,11 +99,7 @@ type DeleteNegotiableQuotesOutput { # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin - negotiable_quotes( - filter: NegotiableQuoteFilterInput, - pageSize: Int = 20, - currentPage: Int = 1 - ): NegotiableQuotesOutput @doc(description: "List of negotiable quotes available to customer") + quote: Boolean } input CloseNegotiableQuotesInput { @@ -112,11 +108,6 @@ input CloseNegotiableQuotesInput { type CloseNegotiableQuotesOutput { closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed") - negotiable_quotes( - filter: NegotiableQuoteFilterInput, - pageSize: Int = 20, - currentPage: Int = 1 - ): NegotiableQuotesOutput @doc(description: A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") } input RemoveNegotiableQuoteItemsInput { From a855d7eff180ba2be223fe715c92512931bc036d Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Tue, 25 Aug 2020 20:19:14 -0500 Subject: [PATCH 283/479] magento2/issues/29251 configurable options corrections --- .../configurable-options-selection.graphqls | 5 ++-- .../catalog/configurable-options-selection.md | 24 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 2131630b7..01403ec31 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -10,12 +10,11 @@ type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata correspond } type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { - available_value_ids: [ID!]! @doc(description: "Configurable option values available for further selection.") + option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") } # Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart type ConfigurableProductOptionsValues { - id: ID! - is_available_for_selection: Boolean! + uid: ID! } diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 74b1c5880..43ca99a18 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -17,8 +17,7 @@ User navigates to the configurable product page. Option values available for sel attribute_code label values { - id - is_available_for_selection + uid value_index label swatch_data { @@ -29,8 +28,8 @@ User navigates to the configurable product page. Option values available for sel } configurable_options_selection_metadata { options_available_for_selection { + option_value_uids attribute_code - available_value_ids } media_gallery { url @@ -55,11 +54,11 @@ The images and videos relevant for the selection are also updated. items { ... on ConfigurableProduct { configurable_options_selection_metadata( - selectedConfigurableOptionValues: ["hash from selected option value"] + selectedConfigurableOptionValues: ["hash from selected option value compatible with single mutation"] ) { options_available_for_selection { + option_value_uids attribute_code - available_value_ids } media_gallery { url @@ -102,8 +101,7 @@ Then the product data along with available selections can be requested in a sing attribute_code label values { - id - is_available_for_selection + uid value_index label swatch_data { @@ -113,11 +111,11 @@ Then the product data along with available selections can be requested in a sing } } configurable_options_selection_metadata( - selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + selectedConfigurableOptionValues: ["hash from selected option value compatible with single mutation", "hash from another option value compatible with single mutation"] ) { options_available_for_selection { + option_value_uids attribute_code - available_value_ids } media_gallery { url @@ -145,11 +143,11 @@ After the user makes final selection, the corresponding simple product data beco items { ... on ConfigurableProduct { configurable_options_selection_metadata( - selectedConfigurableOptionValues: ["hash from selected option value", "hash from another option value"] + selectedConfigurableOptionValues: ["hash from selected option value compatible with single mutation", "hash from another option value compatible with single mutation"] ) { options_available_for_selection { + option_value_uids attribute_code - available_value_ids } media_gallery { url @@ -181,11 +179,11 @@ In case when the facet filter was used on the category page, for example to sear sku ... on ConfigurableProduct { configurable_options_selection_metadata( - selectedConfigurableOptionValues: ["hash from selected red color option"] + selectedConfigurableOptionValues: ["hash from selected red color option compatible with single mutation"] ) { options_available_for_selection { + option_value_uids attribute_code - available_value_ids } media_gallery { url From 6c7013bf18320c01d70e498c542cd67bdd87541f Mon Sep 17 00:00:00 2001 From: OMelnyk Date: Thu, 27 Aug 2020 10:53:19 +0300 Subject: [PATCH 284/479] magento2/issues/29479 Returns : graphql implementation --- design-documents/graph-ql/coverage/returns.graphqls | 2 ++ design-documents/graph-ql/coverage/returns.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 5e5f1d690..35b030afc 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -13,6 +13,8 @@ type Mutation { } input RequestReturnInput { + order_id: ID! + customer_custom_email: String! items: [RequestReturnItemInput!]! comment_text: String } diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 98e2a0c45..79ad46068 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -211,6 +211,8 @@ Existing schema of `Attribute` and `AttributeOption` must be extended to provide mutation { requestReturn( input: { + order_id: 12345 + customer_custom_email: "returnemail@magento.com" items: [ { order_item_id: "absdfj2l3415", From 2fe91b7373900defa955cd069b8c89a328da2ce3 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 27 Aug 2020 11:56:17 -0500 Subject: [PATCH 285/479] categoryInterfaceUID arg changed to uid on Query.category, based on feedback --- design-documents/graph-ql/coverage/catalog.graphqls | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index 63174c147..562444582 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -11,10 +11,11 @@ type Query { ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( - # Implementation Note: The resolver should accept either `id` or `categoryInterfaceUID`, - # but _must_ return an error when both fields are provided in a request - id: Int @deprecated(reason: "Use the `categoryInterfaceUID` argument instead") - categoryInterfaceUID: ID @doc(description: "Unique identifier from objects implementing `CategoryInterface`") + # Implementation Note: For back-compat reasons, this query must: + # - Accept _either_ `uid` or `id` + # - Set a field error when both `uid` or `id` are used + id: Int @deprecated(reason: "Use the `uid` argument instead") + uid: ID @doc(description: "Unique identifier from objects implementing `CategoryInterface`") ): CategoryTree @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") categoryList( From a66f16687c053f43926be93bfed9a288dcf4c13c Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 27 Aug 2020 12:09:47 -0500 Subject: [PATCH 286/479] Fix bug where non-nullable requirement would fail query at validation time (whoops) --- .../graph-ql/coverage/quote.graphqls | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 4e31845ad..a76da9fb7 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -73,20 +73,40 @@ input UpdateCartItemsInput { } input CartItemUpdateInput { - # Implementation Note: The resolver should accept either `cart_item_id` or `cart_item_uid`, - # but _must_ return an error when both fields are provided in a request - cart_item_id: Int! @deprecated(reason: "Use `CartItemUpdateInput.cart_item_uid` instead") - cart_item_uid: ID! @doc(description: "Unique Identifier from objects implementing `CartItemInterface`") + # Implementation Note: For back-compat reasons, the following rules should be applied to + # a resolver handling `CartItemUpdateInput` (see https://github.com/magento/architecture/pull/424) + # + # 1. Either `cart_item_id` or `cart_item_uid` _must_ be provided. If both are absent, raise + # a field error advising the client that `cart_item_uid` is required + # 2. If _both_ `cart_item_id` and `cart_item_uid` are provided, raise a field error advising + # the client to only use `cart_item_uid` + # + # GraphQL does not provide a way for an `input` field to allow use of only 1 field or another, + # which is why we're enforcing non-nullability in the resolver, instead of via the schema language. + # When `cart_item_id` is removed in a future version, we can mark `cart_item_uid` as non-nullable + # with minimal disruption + cart_item_id: Int @deprecated(reason: "Use the `cart_item_uid` field instead") + cart_item_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") quantity: Float customizable_options: [CustomizableOptionInput!] } input RemoveItemFromCartInput { cart_id: String! - # Implementation Note: The resolver should accept either `cart_item_id` or `cart_item_uid`, - # but _must_ return an error when both fields are provided in a request - cart_item_id: Int! @deprecated(reason: "Use `RemoveItemFromCartInput.cart_item_uid` instead") - cart_item_uid: ID! @doc(description: "Unique Identifier from objects implementing `CartItemInterface`") + # Implementation Note: For back-compat reasons, the following rules should be applied to + # a resolver handling `RemoveItemFromCartInput` (see https://github.com/magento/architecture/pull/424) + # + # 1. Either `cart_item_id` or `cart_item_uid` _must_ be provided. If both are absent, raise + # a field error advising the client that `cart_item_uid` is required + # 2. If _both_ `cart_item_id` and `cart_item_uid` are provided, raise a field error advising + # the client to only use `cart_item_uid` + # + # GraphQL does not provide a way for an `input` field to allow use of only 1 field or another, + # which is why we're enforcing non-nullability in the resolver, instead of via the schema language. + # When `cart_item_id` is removed in a future version, we can mark `cart_item_uid` as non-nullable + # with minimal disruption + cart_item_id: Int @deprecated(reason: "Use the `cart_item_uid` field instead") + cart_item_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") } input SetShippingAddressesOnCartInput { From e74ffbc0347096e0d532343066407b8635888d4e Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Thu, 27 Aug 2020 14:13:49 -0500 Subject: [PATCH 287/479] Document existing GraphQL naming conventions --- .../graph-ql/naming-conventions.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 design-documents/graph-ql/naming-conventions.md diff --git a/design-documents/graph-ql/naming-conventions.md b/design-documents/graph-ql/naming-conventions.md new file mode 100644 index 000000000..eed50237d --- /dev/null +++ b/design-documents/graph-ql/naming-conventions.md @@ -0,0 +1,44 @@ +# GraphQL Naming Conventions + +The following naming guidelines should be followed when adding or modifying schemas in the Magento platform. + +These guidelines are based on the conventions already used within the schema. Because of this, there will be places where the style diverges from more common styles found in the GraphQL community. + +## Object/Interface Types + +Object/Interface names should always be _PascalCase_. + +```graphql +# PascalCase +type TwoWords {} +interface TwoWords {} +``` + +## Object/Input Field Names + +Object/Input field names should always be _snake_case_. + +```graphql +type Query { + # snake_case + two_words: String +} + +input Data { + # snake_case + two_words: String +} +``` + +## Arguments + +Argument names should always be _camelCase_. + +```graphql +type Query { + example( + # camelCase + twoWords: String + ): String +} +``` \ No newline at end of file From a091ceba4eddbc079bbec1badfcdb77977be3524 Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Thu, 27 Aug 2020 15:14:42 -0500 Subject: [PATCH 288/479] Apply suggestions from code review Co-authored-by: Andrew Levine --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index b8ab3b243..f12d45816 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -38,7 +38,7 @@ input DeletePurchaseOrderApprovalRuleInput { input PurchaseOrderApprovalRuleInput { name: String! @doc(description: "Purchase order approval rule name") description: String @doc(description: "Purchase order approval rule description") - applies_to: [ID]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied. In case when empty array is provided, the rule will be applied to all user roles in the system, including those created in the future") + applies_to: [ID!]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied. In case when empty array is provided, the rule will be applied to all user roles in the system, including those created in the future") type: PurchaseOrderApprovalRuleType! @doc(description: "Purchase order approval rule type") condition: CreatePurchaseOrderApprovalRuleConditionInput! @doc(description: "Purchase order approval rule condition") requires_approval_from: [ID!]! @doc(description: "A list of B2B user roles that can approve this purchase order approval rule") @@ -214,7 +214,7 @@ type PurchaseOrderComments { type PurchaseOrderComment { uid: ID! @doc(description: "Unique identifier of the comment.") - timestamp: String! @doc(description: "The date and time when the comment was created") + created_at: String! @doc(description: "The date and time when the comment was created") author: PurchaseOrderCommentAuthor! @doc(description: "The name of the user who left the comment") text: String! @doc(description: "The text of the comment") } @@ -232,7 +232,7 @@ type PurchaseOrderHistoryLog { type PurchaseOrderHistoryItem { uid: ID! @doc(description: "Unique identifier of the purchase rder history item.") - timestamp: String! @doc(description: "The date and time when the event happened.") + created_at: String! @doc(description: "The date and time when the event happened.") description: String! @doc(description: "Description of the event.") } From a3627dd9c856ff2fbf341b74dcfd94e22ebe59d7 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 27 Aug 2020 15:16:15 -0500 Subject: [PATCH 289/479] Apply suggestions from code review --- .../graph-ql/coverage/b2b/purchase-order.graphqls | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls index b8ab3b243..f6f8c826f 100644 --- a/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls +++ b/design-documents/graph-ql/coverage/b2b/purchase-order.graphqls @@ -1,5 +1,6 @@ type Mutation { addPurchaseOrderComment(input: AddPurchaseOrderCommentInput!): AddPurchaseOrderCommentOutput @doc(description: "Add a comment to an existing purchase order") + validatePurchaseOrder(input: ValidatePurchaseOrderInput!): ValidatePurchaseOrderOutput @doc(description: "Validate purchase order") approvePurchaseOrder(input: ApprovePurchaseOrderInput!): ApprovePurchaseOrderOutput @doc(description: "Approve purchase order") cancelPurchaseOrder(input: CancelPurchaseOrderInput!): CancelPurchaseOrderOutput @doc(description: "Cancel purchase order") rejectPurchaseOrder(input: RejectPurchaseOrderInput!): RejectPurchaseOrderOutput @doc(description: "Reject purchase order") @@ -40,6 +41,7 @@ input PurchaseOrderApprovalRuleInput { description: String @doc(description: "Purchase order approval rule description") applies_to: [ID]! @doc(description: "A list of B2B user roles to which this purchase order approval rule should be applied. In case when empty array is provided, the rule will be applied to all user roles in the system, including those created in the future") type: PurchaseOrderApprovalRuleType! @doc(description: "Purchase order approval rule type") + status: PurchaseOrderApprovalRuleStatus! @doc(description: "Purchase order approval rule status") condition: CreatePurchaseOrderApprovalRuleConditionInput! @doc(description: "Purchase order approval rule condition") requires_approval_from: [ID!]! @doc(description: "A list of B2B user roles that can approve this purchase order approval rule") } @@ -55,6 +57,14 @@ input CreatePurchaseOrderApprovalRuleConditionAmountInput { currency: CurrencyEnum! @doc(description: "Purchase order approval rule condition ammount currency") } +input ValidatePurchaseOrderInput { + purchase_order_uid: ID! +} + +type ValidatePurchaseOrderOutput { + purchase_order: PurchaseOrder +} + input ApprovePurchaseOrderInput { purchase_order_uid: ID! } @@ -186,6 +196,7 @@ type PurchaseOrder { enum PurchaseOrderAction { REJECT CANCEL + VALIDATE APPROVE } @@ -239,7 +250,7 @@ type PurchaseOrderHistoryItem { type PurchaseOrderItems { items: [PurchaseOrderItemInterface]! page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } type PurchaseOrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { @@ -258,7 +269,6 @@ interface PurchaseOrderItemInterface @doc(description: "Purchase order item deta product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") product_url_key: String @doc(description: "URL key of the base product") - product_type: String @doc(description: "The type of product, such as simple, configurable, or bundle") product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options") discounts: [Discount] @doc(description: "The final discount information for the product") selected_options: [PurchaseOrderItemOption] @doc(description: "The selected options for the base product, such as color or size") @@ -279,6 +289,7 @@ type PurchaseOrderItemOption @doc(description: "Represents purcahse order item o uid: ID! @doc(description: "The unique ID of the option") label: String! @doc(description: "The label of the option") value: String! @doc(description: "The value of the option") + sort_order: Int! @doc(description: "The sort order of the option") } enum PurchaseOrderStatus { From e2927742eb0aabf2de4ff5566e0fce7586ffc5b8 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 27 Aug 2020 16:19:42 -0500 Subject: [PATCH 290/479] Apply suggestions from code review --- .../coverage/catalog/configurable-options-selection.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 01403ec31..b77985033 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -17,4 +17,5 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt # Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart type ConfigurableProductOptionsValues { uid: ID! + is_available_for_selection: Boolean! } From faf6ae3001a2ac81f5ddddbba8b3fa56f6b90f5c Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 27 Aug 2020 16:21:53 -0500 Subject: [PATCH 291/479] Apply suggestions from code review --- .../coverage/catalog/configurable-options-selection.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md index 43ca99a18..ca2aa37f0 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md @@ -18,6 +18,7 @@ User navigates to the configurable product page. Option values available for sel label values { uid + is_available_for_selection value_index label swatch_data { @@ -101,7 +102,8 @@ Then the product data along with available selections can be requested in a sing attribute_code label values { - uid + uid + is_available_for_selection value_index label swatch_data { From 3e1031f2b58cb5641b622db192549003cf0bde4a Mon Sep 17 00:00:00 2001 From: OMelnyk Date: Fri, 28 Aug 2020 10:51:49 +0300 Subject: [PATCH 292/479] magento2/issues/29479 Returns : graphql implementation - implemented requested changes --- design-documents/graph-ql/coverage/returns.graphqls | 2 +- design-documents/graph-ql/coverage/returns.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 35b030afc..97957e6c0 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -14,7 +14,7 @@ type Mutation { input RequestReturnInput { order_id: ID! - customer_custom_email: String! + contact_email: String items: [RequestReturnItemInput!]! comment_text: String } diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 79ad46068..40290236e 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -212,7 +212,7 @@ mutation { requestReturn( input: { order_id: 12345 - customer_custom_email: "returnemail@magento.com" + contact_email: "returnemail@magento.com" items: [ { order_item_id: "absdfj2l3415", From fd27084fc8da47612d48a28ba6ca2b7c3a326487 Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Fri, 28 Aug 2020 17:54:18 +0530 Subject: [PATCH 293/479] schema update graphql --- .../graph-ql/coverage/negotiableQuotes.graphqls | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 192c59271..bf5f86be7 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -99,7 +99,11 @@ type DeleteNegotiableQuotesOutput { # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin - quote: Boolean + negotiableQuotes( + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 + ): NegotiableQuotesOutput @resolver(class: "\\Magento\\NegotiableQuoteGraphQl\\Model\\Resolver\\GetNegotiableQuotes") @doc(description: "List of negotiable quotes available to customer") } input CloseNegotiableQuotesInput { @@ -108,6 +112,12 @@ input CloseNegotiableQuotesInput { type CloseNegotiableQuotesOutput { closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed") + #optionally display all negotiable quotes + negotiableQuotes( + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 + ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") } input RemoveNegotiableQuoteItemsInput { From e05a91f9dd5c663950440eeb267990284ba5e4f2 Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Fri, 28 Aug 2020 17:55:29 +0530 Subject: [PATCH 294/479] schema update graphql --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index bf5f86be7..6a86bc23f 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -103,7 +103,7 @@ type DeleteNegotiableQuotesOutput { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): NegotiableQuotesOutput @resolver(class: "\\Magento\\NegotiableQuoteGraphQl\\Model\\Resolver\\GetNegotiableQuotes") @doc(description: "List of negotiable quotes available to customer") + ): NegotiableQuotesOutput @doc(description: "List of negotiable quotes available to customer") } input CloseNegotiableQuotesInput { From 1588af4fb9da4cc92902f4b3744c1bc19b9c4558 Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Fri, 28 Aug 2020 19:12:20 +0530 Subject: [PATCH 295/479] snack case for the negotiable_quote schema --- design-documents/graph-ql/coverage/negotiableQuotes.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls index 6a86bc23f..af0ec3dbc 100644 --- a/design-documents/graph-ql/coverage/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/negotiableQuotes.graphqls @@ -99,7 +99,7 @@ type DeleteNegotiableQuotesOutput { # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin - negotiableQuotes( + negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 @@ -113,7 +113,7 @@ input CloseNegotiableQuotesInput { type CloseNegotiableQuotesOutput { closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed") #optionally display all negotiable quotes - negotiableQuotes( + negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 From f13c39759437750046808d45fe22cac8f8427435 Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Fri, 28 Aug 2020 08:55:56 -0500 Subject: [PATCH 296/479] Apply suggestions from code review --- design-documents/graph-ql/coverage/returns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 40290236e..8b75bd040 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -211,7 +211,7 @@ Existing schema of `Attribute` and `AttributeOption` must be extended to provide mutation { requestReturn( input: { - order_id: 12345 + order_id: 000000001 contact_email: "returnemail@magento.com" items: [ { From 99b83fc8942e5b276ad1b3a1361e3cc41cf5ebaa Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Fri, 28 Aug 2020 10:45:47 -0500 Subject: [PATCH 297/479] Revert a change made for the example --- design-documents/graph-ql/coverage/returns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/returns.md b/design-documents/graph-ql/coverage/returns.md index 8b75bd040..40290236e 100644 --- a/design-documents/graph-ql/coverage/returns.md +++ b/design-documents/graph-ql/coverage/returns.md @@ -211,7 +211,7 @@ Existing schema of `Attribute` and `AttributeOption` must be extended to provide mutation { requestReturn( input: { - order_id: 000000001 + order_id: 12345 contact_email: "returnemail@magento.com" items: [ { From 2be5c8a6723c173d0b79cedd23cd5f49b545c9c7 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Fri, 28 Aug 2020 11:14:21 -0500 Subject: [PATCH 298/479] Add section on top-level fields on Query and Mutation --- design-documents/graph-ql/naming-conventions.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/naming-conventions.md b/design-documents/graph-ql/naming-conventions.md index eed50237d..55a7f0dae 100644 --- a/design-documents/graph-ql/naming-conventions.md +++ b/design-documents/graph-ql/naming-conventions.md @@ -41,4 +41,18 @@ type Query { twoWords: String ): String } -``` \ No newline at end of file +``` + +## Top-level Fields on Query and Mutation + +Top-level fields on Query and Mutation should always be _camelCase_. + +``` +type Query { + twoWords: TwoWords +} + +type Mutation { + twoWords(input: TwoWordsInput!): TwoWordsOutput +} +``` From 893d41626602b9dda7e51ece9efc3576498a754b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 28 Aug 2020 11:52:48 -0500 Subject: [PATCH 299/479] add section about ID fields --- .../graph-ql/naming-conventions.md | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/naming-conventions.md b/design-documents/graph-ql/naming-conventions.md index 55a7f0dae..526ce4676 100644 --- a/design-documents/graph-ql/naming-conventions.md +++ b/design-documents/graph-ql/naming-conventions.md @@ -47,12 +47,40 @@ type Query { Top-level fields on Query and Mutation should always be _camelCase_. -``` +```graphql type Query { + # camelCase twoWords: TwoWords } type Mutation { + # camelCase twoWords(input: TwoWordsInput!): TwoWordsOutput } ``` + +## ID Fields + +These rules should apply to any field assigned the scalar type `ID`, both in Object Types and Input Object Types. + +There are two types of `ID` fields: + +- **Primary Identifier** : An ID owned by the current Object Type +- **Foreign Identifier**: An ID referencing another Object Type + +A _Primary Identifier_ should _always_ be given the field name `uid`. +A _Foreign Identifier_ should _always_ be given the field name `source_object_uid`, where `source_object` is a _snake_case_ string referring to the type that owns the `ID` (either an Object or an Interface) + +```graphql +type ProductInterface { + # Good, ProductInterface owns `uid` + uid: ID! +} + +type ConfigurableProductOptions { + # Good, field refers to ID on `ProductInterface` + product_interface_uid: ID! +} +``` + +For additional context on ID naming requirements, see [the ID Improvement Plan](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/id-improvement-plan.md_) \ No newline at end of file From 5d60f8ff64158755c8268596dccfc06ad8abd3e9 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Mon, 31 Aug 2020 16:55:53 -0500 Subject: [PATCH 300/479] Remove dynamic attributes from ProductInterface --- .../catalog/remove-product-attributes.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 design-documents/graph-ql/coverage/catalog/remove-product-attributes.md diff --git a/design-documents/graph-ql/coverage/catalog/remove-product-attributes.md b/design-documents/graph-ql/coverage/catalog/remove-product-attributes.md new file mode 100644 index 000000000..fc6b032db --- /dev/null +++ b/design-documents/graph-ql/coverage/catalog/remove-product-attributes.md @@ -0,0 +1,45 @@ +# Remove product dynamic attributes from ProductInterface + +## Current state + +Currently, Magento exposes EAV attributes as part of the product schema as top-level fields of ProductInterface. As consequences: +* The merchant can change the GraphQL from the admin, which makes it hard to cache GraphQL schema. +* Regular product interface overloaded with fields and almost unreadable. +* It is quite often practice to create product programmatically, so the number of attributes could be really excessive with tens thousands of attributes. +* Two Magento instances do not have the same GraphQL schema. As a result, it is near impossible to build a simple client/SDK, which could be easily transferred from one instance to another. +* It is impossible to aggregate the several Magento instances behind the common [BFF](https://docs.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends). + +## Proposed solution + +We should to remove attributes dynamic attributes, +except system attributes which makes sense for the storefront, +from the product top level and use a [dynamic container](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/custom-attributes-container.md) instead. + + +```graphql +## ProductInteface with system EAV attributes +type ProductInterface { + uid: ID + name: String + sku: String + attribute_set_id: Int + description: ComplexTextValue + short_description: ComplexTextValue + meta_title: String + meta_keyword: String + meta_description: String + created_at: String + updated_at: String + custom_attributes: [CustomAttribute] + ... +} +``` + +## System EAV attributes that should be deprecated at the storefront + +* `special_price: Float` These fields should not be used a the source of the discount since the discount also could be caused by group price or rule price, which are not reflected in this field. +The same is true for `special_from_date: String` and `special_to_date`. + +* `options_container: String` - this field just an exposure of `catalog_product_entity` table field, information about product options could be explicitly retrieved from the corresponding field. +* `manufacturer: Int` - just a sample attribute from Magento 1.x, we can move it to the custom attributes container. +the same is true for `country_of_manufacture: String` From c532ad779fedaf1d17fc4055d3cdfac12079d2fc Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Mon, 31 Aug 2020 20:17:33 -0500 Subject: [PATCH 301/479] Apply suggestions from code review --- design-documents/graph-ql/coverage/quote.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index a76da9fb7..296f2ee88 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -86,7 +86,7 @@ input CartItemUpdateInput { # When `cart_item_id` is removed in a future version, we can mark `cart_item_uid` as non-nullable # with minimal disruption cart_item_id: Int @deprecated(reason: "Use the `cart_item_uid` field instead") - cart_item_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") + cart_item_interface_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") quantity: Float customizable_options: [CustomizableOptionInput!] } @@ -106,7 +106,7 @@ input RemoveItemFromCartInput { # When `cart_item_id` is removed in a future version, we can mark `cart_item_uid` as non-nullable # with minimal disruption cart_item_id: Int @deprecated(reason: "Use the `cart_item_uid` field instead") - cart_item_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") + cart_item_interface_uid: ID @doc(description: "Required field. Unique Identifier from objects implementing `CartItemInterface`") } input SetShippingAddressesOnCartInput { From 9a1fd86b0d44b0403308aeb592632dcf07f3f46a Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 2 Sep 2020 19:14:03 -0500 Subject: [PATCH 302/479] Updated GraphQl Compare List Proposal --- .../graph-ql/coverage/catalog/compare-list.md | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index 1307b54cc..c6cc5a925 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -1,11 +1,26 @@ # Use cases -## Guest scenario +## User scenario (Guest/ Logged in) + +Use createCompareList mutation to create a new compare list. The server should create a new list with the items added and return the list_id. The clients can use this list_id for futher operations. + +# Create Compare List +```graphql +{ + mutation { + createCompareList(items: ["100123", "234567", "874321"]) #items optional + } { + list_id + items { + sku + } + } +} +``` +* For a guest user, new list will be created +* For a logged in user, exisiting list_id will be returned (Can be extended in future ee - to support multiple compare list per customer) + -* A buyer can create a new compare list for the selected -products by calling mutation `addItemsToCompareList` with -the list of product ids and ID which will identify the list. -Compare list ID is client generated identifier. ```graphql { mutation { @@ -18,7 +33,7 @@ Compare list ID is client generated identifier. { query { customer { - compare_list { + compare_list(id) { # id filter is optional. Will return an array of compare lists (Future extensibility) list_id items { sku @@ -29,10 +44,11 @@ Compare list ID is client generated identifier. } ``` * If the registered customer does not have an active list then null will be returned. -This means the client has to generate and send a new ID if the compare list functionality requested. -* If the buyer calls addItemsToCompareList with a new ID the previous list will be abandoned. -For the registered user an active compare list will be replaced with a new one. +``` +assignCompareListToCustomer(customerId: ID!, listId: ID!): CompareList +``` +mutation can be used to assign a guest compare list to a registered customer. * A buyer can modify the existing list by calling mutation: * `addItemsToCompareList` to add new items to compare list. @@ -74,6 +90,10 @@ preconfigured at the backoffice. * Compare list could be assigned to the registered customer after login or account creation. +## Removing stale comparison list +* Introduce a mutation to removeComparisonList(id: ID!): Boolean, which clients can use to remove the list once the session expires +* A cron job + ![compare-list.graphqls](compare-list/compare-list.png) # Non functional requirements: @@ -83,3 +103,25 @@ preconfigured at the backoffice. Guest compare list business logic not implemented yet. Additional development required. +## Current limitations + +Existing table structure for compare list +``` +catalog_compare_item +------------------------------------------------------------------------------ +| catalog_compare_item_id | visitor_id | customer_id | product_id | store_id | +============================================================================== +``` +The visitor_id (non nullable) is tied to session \Magento\Customer\Model\Visitor in Luma. GraphQl layer should not be tied to Luma's session. So Compare-lists via GraphQl will have to differ/deviate from Luma. + +This dependency can be solved by managing the compare list state in a new table +``` +catalog_compare_list +------------------------------------------------------------------------------ +| entity_id (int) (primary)| masked_id (varchar)(indexed) | customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) +============================================================================== +``` +* On first client request, a row will be populated with an entity_id, server generated masked_id, customer_id (? if logged in), product_id and store_id. +* masked_id will be used for client communications +* product_ids is a json field for reducing storage complexity. +* introducing entity_id for the list will enable customers to have more than one compare list (future extensibility) From d5bac1414bf217a13cdb0bca3cc2f63d7e546621 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 2 Sep 2020 19:32:33 -0500 Subject: [PATCH 303/479] minor fix. --- design-documents/graph-ql/coverage/catalog/compare-list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index c6cc5a925..d6fcfeb02 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -92,7 +92,7 @@ preconfigured at the backoffice. ## Removing stale comparison list * Introduce a mutation to removeComparisonList(id: ID!): Boolean, which clients can use to remove the list once the session expires -* A cron job +* A cron job to remove staled entries beyond certain time. ![compare-list.graphqls](compare-list/compare-list.png) From d3d26425873fc9ea63c19c583d0eb863629d58d8 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 3 Sep 2020 10:54:19 -0500 Subject: [PATCH 304/479] Removing the additional id field, based on Anton's suggestion. --- design-documents/graph-ql/coverage/catalog/compare-list.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index d6fcfeb02..ac9bd6ed4 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -118,10 +118,10 @@ This dependency can be solved by managing the compare list state in a new table ``` catalog_compare_list ------------------------------------------------------------------------------ -| entity_id (int) (primary)| masked_id (varchar)(indexed) | customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) +| entity_id (int) (primary)| customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) ============================================================================== ``` * On first client request, a row will be populated with an entity_id, server generated masked_id, customer_id (? if logged in), product_id and store_id. -* masked_id will be used for client communications +* encoded entity_id will be used for client communications * product_ids is a json field for reducing storage complexity. * introducing entity_id for the list will enable customers to have more than one compare list (future extensibility) From 45fb01d46ad77fdd67a6a0683fa3a4b71c5dfb42 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 3 Sep 2020 13:47:00 -0500 Subject: [PATCH 305/479] Update compare-list.md --- design-documents/graph-ql/coverage/catalog/compare-list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index ac9bd6ed4..b59a92162 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -118,7 +118,7 @@ This dependency can be solved by managing the compare list state in a new table ``` catalog_compare_list ------------------------------------------------------------------------------ -| entity_id (int) (primary)| customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) +| entity_id (int) (primary)| customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) | additional_data (json) (nullable) ============================================================================== ``` * On first client request, a row will be populated with an entity_id, server generated masked_id, customer_id (? if logged in), product_id and store_id. From 97fa6b271cd408f44200fa5791feae47e05f874d Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 3 Sep 2020 13:58:33 -0500 Subject: [PATCH 306/479] Reflecting schema changes based on the new proposal. --- .../graph-ql/coverage/catalog/compare-list.graphqls | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index ce856dd26..37371d4ca 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -10,24 +10,25 @@ type ComparableItem { type ComparableAttribute { code: String! @doc(description: "Attribute code ") - title: String! @doc(description: "Addibute display title") + title: String! @doc(description: "Attribute display title") } type CompareList { - list_id: ID! @doc(description: " Compare list id") + list_id: ID! @doc(description: "Compare list id") items: [ComparableItem] @doc(description: "Comparable products") attributes: [ComparableAttribute] @doc(description: "Comparable attributes, provides codes and titles for the attributes") } type Customer { - compare_list: CompareList @doc(description: "Active customers compare list") + compareList(id: ID): [CompareList] @doc(description: "Active customers compare list") #id is optional. Introduces the possibility of more than 1 compare list. } type Query { - compareList(id: ID!): CompareList @doc(description: "Compare list") + compareList(id: ID!): [CompareList] @doc(description: "Compare list") } type Mutation { + createCompareList(items: [ID!]): CompareList @doc(description: "Creates a new compare list. For a logged in user, the created list is assigned to the user") addItemsToCompareList( id: ID! items: [ID!] From 2868872075cfec8d0b206a1dd38c6a16585954f2 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Fri, 4 Sep 2020 10:40:24 -0500 Subject: [PATCH 307/479] Update compare-list.graphqls --- .../coverage/catalog/compare-list.graphqls | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index 37371d4ca..816e1cbb6 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -14,30 +14,35 @@ type ComparableAttribute { } type CompareList { - list_id: ID! @doc(description: "Compare list id") + uid: ID! @doc(description: "Compare list id") items: [ComparableItem] @doc(description: "Comparable products") attributes: [ComparableAttribute] @doc(description: "Comparable attributes, provides codes and titles for the attributes") } type Customer { - compareList(id: ID): [CompareList] @doc(description: "Active customers compare list") #id is optional. Introduces the possibility of more than 1 compare list. + compareList(uid: ID): [CompareList] @doc(description: "Active customers compare list") #id is optional. Introduces the possibility of more than 1 compare list. } type Query { - compareList(id: ID!): [CompareList] @doc(description: "Compare list") + compareList(uid: ID!): CompareList @doc(description: "Compare list") } type Mutation { - createCompareList(items: [ID!]): CompareList @doc(description: "Creates a new compare list. For a logged in user, the created list is assigned to the user") + createCompareList(input: CreateCompareListInput): CompareList @doc(description: "Creates a new compare list. For a logged in user, the created list is assigned to the user") addItemsToCompareList( - id: ID! + uid: ID!, items: [ID!] ): CompareList removeItemsFromCompareList( - id: ID! + uid: ID!, items: [ID!] ): CompareList - assignCompareListToCustomer(customerId: ID!, listId: ID!): Boolean + assignCompareListToCustomer(customerId: ID!, uid: ID!): Boolean! + deleteCompareList(uid: ID!): Boolean! +} + +input CreateCompareListInput { + items: [ID!] } schema { From f36d236e50b6e7052469066653929e78bb06fd14 Mon Sep 17 00:00:00 2001 From: Damien Retzinger Date: Fri, 4 Sep 2020 15:21:48 -0400 Subject: [PATCH 308/479] feat(message-queue): adds design for first-class queue configuration objects --- .../First_Class_Queue_Configuration.md | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 design-documents/message-queue/First_Class_Queue_Configuration.md diff --git a/design-documents/message-queue/First_Class_Queue_Configuration.md b/design-documents/message-queue/First_Class_Queue_Configuration.md new file mode 100644 index 000000000..0ff0115be --- /dev/null +++ b/design-documents/message-queue/First_Class_Queue_Configuration.md @@ -0,0 +1,73 @@ +# First Class Queue Configuration + +This proposal exposes a first class "queue" object in `queue_topology.xml` and provides backwards compatability for prior versions of Magento before this change. + +## Overview and Introduction +The Message Broker, RabbitMQ supports arguments that are defined at Queue creation time which dictate certain behaviors of the queue in question. Currently (as of v2.4.0), Magento infers what queues will be created from properties of the `exchange.bindings` defined in any given module's `queue_toplogy.xml`. + +Unfortunately, the current implementation of `queue_topology.xml` doesn't expose a queue object to specify arguments at queue creation time, preventing the configurable behavior of RabbitMQ. + +## Use Cases +* As a developer, I would like to configure my queues with additional arguments. +* As a developer, I would like to achieve High Availability with my message Queueing Behavior. + +### The Scenario +In order to support High Availability message processing, we would like to utilize RabbitMQ's [`x-dead-letter-exchange`](https://www.rabbitmq.com/dlx.html) and [`x-message-ttl`](https://www.rabbitmq.com/ttl.html) argument for queues. This would require two queues: + +1. A main queue which processes messages. +2. A "retry" queue with a defined `x-message-ttl` and a `x-dead-letter-exchange`. + +The first queue's consumers attempt to process its messages, and then upon failure conditions, publish these messsages (via an exchange) into the "retry" queue. + +After the retry queues TTL passes, these messages are then expire (**without being consumed**) and move the messages as a dead-letter to the origin queue. + +In the end, this results in messages being retryable in whatever mechanism a developer wishes, based upon whatever failure conditions a developer desires. + +## How it would work +An additional object would need to be added to the `queue_topology.xml` API, called a `queue`. This `queue` supports the following properties: + +```txt +name +connection +durable +autoDelete +arguments +``` + +These arguments are consistent with the existing arguments for exchanges and bindings, and should feel very familiar to developers accustomed to the current syntax. + +### A Sample Configuration +```xml + + + + + + + + arg-value + my-dlx + + + +``` + +This configuration would result in a single queue: `some-queue` which is configured by the queue object. + +## Backwards Compatability +Backwards compatability is achieved via the following mechanism: + +1. If no `queue`s are defined, behavior is as before. +2. If a `queue` is defined with a connection that is different from the `exchange` that would bind to it, two queues are created, one in each connection. + * We should warn people about this. +3. The older bindings -> queue generator mechansim should not attempt to create queues where the `queue` with the same name and connection exists. + +## Open Questions + +1. What about `autoDelete` differences? + I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. +2. What about `durable` differences? + I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. \ No newline at end of file From 6769608543aebd08795de278466422c26779616a Mon Sep 17 00:00:00 2001 From: Damien Retzinger Date: Sun, 6 Sep 2020 13:12:09 -0400 Subject: [PATCH 309/479] style(message-queue): clean up English grammer and include some precision around intended behaviors --- .../First_Class_Queue_Configuration.md | 73 ---------- .../first-class-queue-configuration.md | 132 ++++++++++++++++++ 2 files changed, 132 insertions(+), 73 deletions(-) delete mode 100644 design-documents/message-queue/First_Class_Queue_Configuration.md create mode 100644 design-documents/message-queue/first-class-queue-configuration.md diff --git a/design-documents/message-queue/First_Class_Queue_Configuration.md b/design-documents/message-queue/First_Class_Queue_Configuration.md deleted file mode 100644 index 0ff0115be..000000000 --- a/design-documents/message-queue/First_Class_Queue_Configuration.md +++ /dev/null @@ -1,73 +0,0 @@ -# First Class Queue Configuration - -This proposal exposes a first class "queue" object in `queue_topology.xml` and provides backwards compatability for prior versions of Magento before this change. - -## Overview and Introduction -The Message Broker, RabbitMQ supports arguments that are defined at Queue creation time which dictate certain behaviors of the queue in question. Currently (as of v2.4.0), Magento infers what queues will be created from properties of the `exchange.bindings` defined in any given module's `queue_toplogy.xml`. - -Unfortunately, the current implementation of `queue_topology.xml` doesn't expose a queue object to specify arguments at queue creation time, preventing the configurable behavior of RabbitMQ. - -## Use Cases -* As a developer, I would like to configure my queues with additional arguments. -* As a developer, I would like to achieve High Availability with my message Queueing Behavior. - -### The Scenario -In order to support High Availability message processing, we would like to utilize RabbitMQ's [`x-dead-letter-exchange`](https://www.rabbitmq.com/dlx.html) and [`x-message-ttl`](https://www.rabbitmq.com/ttl.html) argument for queues. This would require two queues: - -1. A main queue which processes messages. -2. A "retry" queue with a defined `x-message-ttl` and a `x-dead-letter-exchange`. - -The first queue's consumers attempt to process its messages, and then upon failure conditions, publish these messsages (via an exchange) into the "retry" queue. - -After the retry queues TTL passes, these messages are then expire (**without being consumed**) and move the messages as a dead-letter to the origin queue. - -In the end, this results in messages being retryable in whatever mechanism a developer wishes, based upon whatever failure conditions a developer desires. - -## How it would work -An additional object would need to be added to the `queue_topology.xml` API, called a `queue`. This `queue` supports the following properties: - -```txt -name -connection -durable -autoDelete -arguments -``` - -These arguments are consistent with the existing arguments for exchanges and bindings, and should feel very familiar to developers accustomed to the current syntax. - -### A Sample Configuration -```xml - - - - - - - - arg-value - my-dlx - - - -``` - -This configuration would result in a single queue: `some-queue` which is configured by the queue object. - -## Backwards Compatability -Backwards compatability is achieved via the following mechanism: - -1. If no `queue`s are defined, behavior is as before. -2. If a `queue` is defined with a connection that is different from the `exchange` that would bind to it, two queues are created, one in each connection. - * We should warn people about this. -3. The older bindings -> queue generator mechansim should not attempt to create queues where the `queue` with the same name and connection exists. - -## Open Questions - -1. What about `autoDelete` differences? - I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. -2. What about `durable` differences? - I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. \ No newline at end of file diff --git a/design-documents/message-queue/first-class-queue-configuration.md b/design-documents/message-queue/first-class-queue-configuration.md new file mode 100644 index 000000000..3c19a712c --- /dev/null +++ b/design-documents/message-queue/first-class-queue-configuration.md @@ -0,0 +1,132 @@ +# First Class Queue Configuration + +This proposal intends to expose a first-class "queue" object in `queue_topology.xml` of a module and provides backward compatibility considerations for prior versions of Magento before this change. + +## Overview +The Message Broker, RabbitMQ, supports `arguments` that are defined at queue creation time that dictate certain behaviors of the resulting queue. Currently (as of v2.4.0), Magento infers what queues will be created from properties of the `exchange.binding` defined in any given module's `queue_topology.xml`. + +Unfortunately, the current implementation of `queue_topology.xml` does not expose a queue object to specify arguments at queue creation time, preventing utilization of the configurable behavior of RabbitMQ queues. + +## Use Cases +* As a developer, I would like to configure my queues with additional arguments. +* As a developer, I would like to achieve High Availability with my queueing system. + +### The Scenario +In order to support High Availability message processing, we would like to utilize RabbitMQ's [`x-dead-letter-exchange`](https://www.rabbitmq.com/dlx.html) and [`x-message-ttl`](https://www.rabbitmq.com/ttl.html) arguments for queues. Achieveing High Availabilty with these two arguments would require two queues: + +1. A primary queue that processes messages. +2. A secondary "retry" queue with a defined `x-message-ttl` and `x-dead-letter-exchange` arguments. + +The first queue's consumers attempt to process its messages, and then upon failure conditions, publish "retryable" messages (via an exchange) into the "retry" queue. + +After the retry queue's TTL passes, the now "expired" message is then moved (**without being consumed**) as a dead-letter back to the primary queue (via the x-dead-letter-exchange). + +In the end, this results in messages being retryable in whatever mechanism a developer wishes, based upon whatever failure conditions a developer desires. + +## Design +An additional object would need to be added to the `queue_topology.xml` API, called a `queue`. A `queue` supports the following properties: + +```txt +name +connection +durable +autoDelete +arguments +``` + +These arguments are consistent with the existing arguments for exchanges and bindings and should feel very familiar to developers accustomed to the current syntax. + +### Matching Behavior +Since there is pre-existing behavior defined for queue creation from `exchange.binding` there must be a matching mechanisms which determines which mechanism of queue-creation wins. + +A queue is considered a "match" (and therefore overrules the prior `exchange.binding` behavior) when the first-class queues `name` and `connection` exactly match (string comparison, case-sensitive) a queue which would be created by `exchange.binding`. That is to say, the bindings -> queue generator mechanism should not attempt to create queues where the `queue` with the same name and connection exists. + +### Sample Configurations + +* [A Simple Configuration](./#a-simple-configuration) +* [First-Class Queue Priority](./#first-class-queue-prioritization) +* [Queue-Connection Mismatch](./#queue-connection-mismatch) + +#### A Simple Configuration +This configuration would result in a single queue: `some-queue` which is configured by the queue object. + +```xml + + +    +        +           arg-value +           my-dlx +        +    + +``` + +#### First-Class Queue Priority +When a `queue` and an existing `exchange.binding` collide via the described matching mechanism, a single queue results: `some-queue` as described in the below case. + +```xml + + + + + +    +        +           arg-value +           my-dlx +        +    + +``` + +#### Queue-Connection Mismatch +If a `queue` is defined with a connection that is different from the `exchange` that would bind to it via the old queue-creation mechanism, two queues are created, one in each connection. We should likely warn in this scenario, as likely this is an unintentional behavior. A suggested message (appearing during `setup:upgrade`) is: + +> `some-queue` is defined in two connections: `amqp` and `db`. If this intentional, this message can be ignored. + +```xml + + + + + +    +        +           arg-value +           my-dlx +        +    + +``` + +## Backwards Compatability +Backward compatibility is well-defined and should be as follows: + +### No Queue Defined +If no `queue` is defined, the behavior is as before, a single queue is created in the defined connection. + +```xml + + + + + + +``` + +## Open Questions + +1. What about `autoDelete` differences? +    I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant.   +2. What about `durable` differences? +    I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. From 28acfd164d0d978597971c77898f50eabd0812f8 Mon Sep 17 00:00:00 2001 From: Damien Retzinger Date: Sun, 6 Sep 2020 13:16:03 -0400 Subject: [PATCH 310/479] style(message-queue): clean up English grammer and include some precision around intended behaviors --- .../first-class-queue-configuration.md | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/design-documents/message-queue/first-class-queue-configuration.md b/design-documents/message-queue/first-class-queue-configuration.md index 3c19a712c..691a161b6 100644 --- a/design-documents/message-queue/first-class-queue-configuration.md +++ b/design-documents/message-queue/first-class-queue-configuration.md @@ -3,27 +3,31 @@ This proposal intends to expose a first-class "queue" object in `queue_topology.xml` of a module and provides backward compatibility considerations for prior versions of Magento before this change. ## Overview -The Message Broker, RabbitMQ, supports `arguments` that are defined at queue creation time that dictate certain behaviors of the resulting queue. Currently (as of v2.4.0), Magento infers what queues will be created from properties of the `exchange.binding` defined in any given module's `queue_topology.xml`. + +The Message Broker, RabbitMQ, supports `arguments` that are defined at queue creation time that dictate certain behaviors of the resulting queue. Currently (as of v2.4.0), Magento infers what queues will be created from properties of the `exchange.binding` defined in any given module's `queue_topology.xml`. Unfortunately, the current implementation of `queue_topology.xml` does not expose a queue object to specify arguments at queue creation time, preventing utilization of the configurable behavior of RabbitMQ queues. ## Use Cases -* As a developer, I would like to configure my queues with additional arguments. -* As a developer, I would like to achieve High Availability with my queueing system. + +- As a developer, I would like to configure my queues with additional arguments. +- As a developer, I would like to achieve High Availability with my queueing system. ### The Scenario -In order to support High Availability message processing, we would like to utilize RabbitMQ's [`x-dead-letter-exchange`](https://www.rabbitmq.com/dlx.html) and [`x-message-ttl`](https://www.rabbitmq.com/ttl.html) arguments for queues. Achieveing High Availabilty with these two arguments would require two queues: + +In order to support High Availability message processing, we would like to utilize RabbitMQ's [`x-dead-letter-exchange`](https://www.rabbitmq.com/dlx.html) and [`x-message-ttl`](https://www.rabbitmq.com/ttl.html) arguments for queues. Achieving High Availability with these two arguments would require two queues: 1. A primary queue that processes messages. -2. A secondary "retry" queue with a defined `x-message-ttl` and `x-dead-letter-exchange` arguments. +2. A secondary "retry" queue with a defined `x-message-ttl` and `x-dead-letter-exchange` arguments. -The first queue's consumers attempt to process its messages, and then upon failure conditions, publish "retryable" messages (via an exchange) into the "retry" queue. +The first queue's consumers attempt to process its messages, and then upon failure conditions, publish "retryable" messages (via an exchange) into the "retry" queue. After the retry queue's TTL passes, the now "expired" message is then moved (**without being consumed**) as a dead-letter back to the primary queue (via the x-dead-letter-exchange). In the end, this results in messages being retryable in whatever mechanism a developer wishes, based upon whatever failure conditions a developer desires. -## Design +## Design + An additional object would need to be added to the `queue_topology.xml` API, called a `queue`. A `queue` supports the following properties: ```txt @@ -37,17 +41,19 @@ arguments These arguments are consistent with the existing arguments for exchanges and bindings and should feel very familiar to developers accustomed to the current syntax. ### Matching Behavior -Since there is pre-existing behavior defined for queue creation from `exchange.binding` there must be a matching mechanisms which determines which mechanism of queue-creation wins. + +Since there is pre-existing behavior defined for queue creation from `exchange.binding` there must be a matching mechanism that determines which mechanism of queue-creation wins. A queue is considered a "match" (and therefore overrules the prior `exchange.binding` behavior) when the first-class queues `name` and `connection` exactly match (string comparison, case-sensitive) a queue which would be created by `exchange.binding`. That is to say, the bindings -> queue generator mechanism should not attempt to create queues where the `queue` with the same name and connection exists. ### Sample Configurations -* [A Simple Configuration](./#a-simple-configuration) -* [First-Class Queue Priority](./#first-class-queue-prioritization) -* [Queue-Connection Mismatch](./#queue-connection-mismatch) +- [A Simple Configuration](./#a-simple-configuration) +- [First-Class Queue Priority](./#first-class-queue-prioritization) +- [Queue-Connection Mismatch](./#queue-connection-mismatch) #### A Simple Configuration + This configuration would result in a single queue: `some-queue` which is configured by the queue object. ```xml @@ -63,17 +69,18 @@ This configuration would result in a single queue: `some-queue` which is configu ``` #### First-Class Queue Priority + When a `queue` and an existing `exchange.binding` collide via the described matching mechanism, a single queue results: `some-queue` as described in the below case. ```xml - - - +    +        +                           arg-value @@ -81,9 +88,10 @@ When a `queue` and an existing `exchange.binding` collide via the described matc             -``` +``` #### Queue-Connection Mismatch + If a `queue` is defined with a connection that is different from the `exchange` that would bind to it via the old queue-creation mechanism, two queues are created, one in each connection. We should likely warn in this scenario, as likely this is an unintentional behavior. A suggested message (appearing during `setup:upgrade`) is: > `some-queue` is defined in two connections: `amqp` and `db`. If this intentional, this message can be ignored. @@ -91,12 +99,12 @@ If a `queue` is defined with a connection that is different from the `exchange` ```xml - - - +    +        +                           arg-value @@ -104,29 +112,31 @@ If a `queue` is defined with a connection that is different from the `exchange`             -``` +``` ## Backwards Compatability + Backward compatibility is well-defined and should be as follows: ### No Queue Defined + If no `queue` is defined, the behavior is as before, a single queue is created in the defined connection. ```xml - - - +    +        +    ``` ## Open Questions 1. What about `autoDelete` differences? -    I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant.   +     I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. 2. What about `durable` differences? -    I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. +     I suspect that connection/name is a sufficient matching pair, so this case may be irrelevant. From d7b9b29c0c2b63437ed765084be9aa3be11eddb2 Mon Sep 17 00:00:00 2001 From: Damien Retzinger Date: Sun, 6 Sep 2020 13:18:33 -0400 Subject: [PATCH 311/479] style(message-queue): fix links --- .../message-queue/first-class-queue-configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/message-queue/first-class-queue-configuration.md b/design-documents/message-queue/first-class-queue-configuration.md index 691a161b6..7d7ae1408 100644 --- a/design-documents/message-queue/first-class-queue-configuration.md +++ b/design-documents/message-queue/first-class-queue-configuration.md @@ -48,9 +48,9 @@ A queue is considered a "match" (and therefore overrules the prior `exchange.bin ### Sample Configurations -- [A Simple Configuration](./#a-simple-configuration) -- [First-Class Queue Priority](./#first-class-queue-prioritization) -- [Queue-Connection Mismatch](./#queue-connection-mismatch) +- [A Simple Configuration](#a-simple-configuration) +- [First-Class Queue Priority](#first-class-queue-priority) +- [Queue-Connection Mismatch](#queue-connection-mismatch) #### A Simple Configuration From 1e94a11dc3982956494061f45d26eb214e2f3bb9 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Tue, 8 Sep 2020 15:29:51 -0500 Subject: [PATCH 312/479] Apply suggestions from code review --- .../graph-ql/coverage/customer/login-as-customer.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/design-documents/graph-ql/coverage/customer/login-as-customer.md b/design-documents/graph-ql/coverage/customer/login-as-customer.md index 416aa491f..6e410c12c 100644 --- a/design-documents/graph-ql/coverage/customer/login-as-customer.md +++ b/design-documents/graph-ql/coverage/customer/login-as-customer.md @@ -4,6 +4,9 @@ Admin user is expected to be authenticated using admin token. While there is no GraphQL Mutation for retrieving the admin token, REST should be used (see [example](https://devdocs.magento.com/guides/v2.4/graphql/queries/index.html#staging)). +Login as Customer link should contain short-lived and valid once token +Application can exchange this token via POST request to the REST endpoint to receive admin authentication token which can be used to obtain customer authentication token using GraphQL `generateCustomerTokenAsAdmin` query. + Admin bearer token must be provided in the `Authorization` header along with the following mutation. The admin user associated with that token must have `Login as Customer` (`Magento_LoginAsCustomer::login`) permissions. As usual, the store context is determined based on `Store` header in the request. ```graphql From 8d5dbf5061c2265f89906c87973cce160433b25f Mon Sep 17 00:00:00 2001 From: Chris Snedaker Date: Tue, 8 Sep 2020 16:44:45 -0400 Subject: [PATCH 313/479] Correction - AbsractMethod to AbstractMethod --- design-documents/abstract-method-removal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/abstract-method-removal.md b/design-documents/abstract-method-removal.md index 64d3cc132..dfe991e09 100644 --- a/design-documents/abstract-method-removal.md +++ b/design-documents/abstract-method-removal.md @@ -25,7 +25,7 @@ There are still a few core integrations (Offline payments and PayPal), also some The desired state for 2.4 Magento release: - Core integrations do not use `AbstractMethod` (old integrations marked as deprecated). -- All related to `AbsractMethod` infrastructure code is marked as deprecated (which not marked yet) +- All related to `AbstractMethod` infrastructure code is marked as deprecated (which not marked yet) - All appropriate methods/functions trigger `E_DEPRECATED` warning (https://devdocs.magento.com/guides/v2.3/contributor-guide/backward-compatible-development/#deprecation). @@ -34,4 +34,4 @@ The desired state for 2.5 Magento release: ## Summary -The 3rd party developers will have a full minor-release cycle to rework their integrations. As each payment integration has a lot of specific implementation details, own flow and data set for payment operations, there is no unified approach to provide tools for code migration from `AbstractMethod` to Payment Provider Gateway. \ No newline at end of file +The 3rd party developers will have a full minor-release cycle to rework their integrations. As each payment integration has a lot of specific implementation details, own flow and data set for payment operations, there is no unified approach to provide tools for code migration from `AbstractMethod` to Payment Provider Gateway. From 73aee792b9b6eb1349d2488a72079a906936ffa3 Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Tue, 8 Sep 2020 16:10:38 -0500 Subject: [PATCH 314/479] Apply suggestions from code review --- design-documents/graph-ql/mutation-error-design.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/mutation-error-design.md b/design-documents/graph-ql/mutation-error-design.md index 572237b19..3ea6f8f28 100644 --- a/design-documents/graph-ql/mutation-error-design.md +++ b/design-documents/graph-ql/mutation-error-design.md @@ -74,7 +74,7 @@ type AddItemToCartOutput { # can still get the entire state of the cart post-mutation cart: Cart # can directly access errors that a shopper should be notified about - addItemUserErrors: [AddItemUserError!]! + add_item_user_errors: [AddItemUserError!]! } type AddItemUserError { @@ -104,7 +104,7 @@ mutation { } } - addItemUserErrors { + add_item_user_errors { message type } @@ -118,7 +118,7 @@ mutation { "cart": { // cart data here }, - "addItemUserErrors": [{ + "add_item_user_errors": [{ "message": "Item 'cool-hat' not added to cart (Out of Stock)", "type": "OUT_OF_STOCK" }] @@ -150,5 +150,5 @@ Instead, you would likely have your `addItemToCart` function return something th ## Open Questions -1. Should we be consistent with the field name that represents these first-class errors? `userErrors` vs something like `addToCartUserErrors` +1. Should we be consistent with the field name that represents these first-class errors? `user_errors` vs something like `add_to_cart_user_errors` 2. Should there be a basic interface that mutations should implement to enforce the pattern of returning a list of user errors? From 094b51964683f93a629f6fdca3156f554636d198 Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Tue, 8 Sep 2020 16:28:05 -0500 Subject: [PATCH 315/479] Apply suggestions from code review --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index cf07890e5..f20a29d97 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -36,7 +36,7 @@ type Mutation { id: ID! items: [ID!] ): CompareList - assignCompareListToActiveCustomer(listId: ID!): Boolean + assignCompareListToCustomer(listId: ID!): Boolean } schema { From 72f13dd3488af508a80bc85f58994ddd76e31c6b Mon Sep 17 00:00:00 2001 From: Dmitriy Gallyamov Date: Wed, 9 Sep 2020 14:40:08 +0300 Subject: [PATCH 316/479] magento/partners-magento2b2b#308 - Company credit query schema update --- design-documents/graph-ql/coverage/b2b/company-credit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md index c94094aff..89b52c06a 100644 --- a/design-documents/graph-ql/coverage/b2b/company-credit.md +++ b/design-documents/graph-ql/coverage/b2b/company-credit.md @@ -22,7 +22,7 @@ type CompanyCreditOperation { type: CompanyCreditOperationType! @doc(description: "The type of the company credit operation") amount: Money @doc(description: "The amount fo the company credit operation") balance: CompanyCredit! @doc(description: "Credit balance after the company credit operation") - purchase_order: String @doc(description: "Purchase order number associated with the company credit operation") + custom_reference_number: String @doc(description: "Purchase order number associated with the company credit operation") updated_by: CompanyCreditOperationUser! @doc(description: "The user submitting the company credit operation") } @@ -53,7 +53,7 @@ enum CompanyCreditOperationUserType { input CompanyCreditHistoryFilterInput { operation_type: CompanyCreditOperationType @doc(description: "Enum filter by the type of the company credit operation") - purchase_order: String @doc(description: "Free text filter by the purchase order number associated with the company credit operation") + custom_reference_number: String @doc(description: "Free text filter by the purchase order number associated with the company credit operation") updated_by: String @doc(description: "Free text filter by the name of the person submitting the company credit operation") } ###### End: Defining new types ###### From 9ec797d103e3cf0702ad8338a9fdbf79c549b8c8 Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Wed, 9 Sep 2020 13:07:32 -0500 Subject: [PATCH 317/479] Update company.md --- .../graph-ql/coverage/b2b/company.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company.md b/design-documents/graph-ql/coverage/b2b/company.md index 64d7287ac..c1e377ccb 100644 --- a/design-documents/graph-ql/coverage/b2b/company.md +++ b/design-documents/graph-ql/coverage/b2b/company.md @@ -20,7 +20,7 @@ type Query { } type Company @doc(description: "Company entity output data schema.") { - id: ID! @doc(description: "Company id.") + uid: ID! @doc(description: "Company id.") name: String @doc(description: "Company name.") email: String @doc(description: "Company email address.") legal_name: String @doc(description: "Company legal name.") @@ -35,18 +35,18 @@ type Company @doc(description: "Company entity output data schema.") { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): CompanyUsers @doc(description: "Information about the company users.") - user(id: ID!): Customer @doc(description: "Returns company user by id.") + user(uid: ID!): Customer @doc(description: "Returns company user by id.") roles( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Optional. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): CompanyRoles! @doc(description: "Returns the list of defined roles at Company.") - role(id: ID!): CompanyRole @doc(description: "Returns company role by id.") + role(uid: ID!): CompanyRole @doc(description: "Returns company role by id.") acl_resources: [CompanyAclResource] @doc(description: "Returns the list of all permission resources.") structure( root_id: ID = 0 @doc(description: "Tree depth to begin query") depth: Int = 10 @doc(description: "Specifies how deeply results are fetched") ): CompanyStructure @doc(description: "Company structure of teams and customers in depth-first order") - team(id: ID!): CompanyTeam @doc(description: "Returns company team data by id.") + team(uid: ID!): CompanyTeam @doc(description: "Returns company team data by id.") } type CompanyLegalAddress @doc(description: "Company legal address output data schema.") { @@ -59,7 +59,7 @@ type CompanyLegalAddress @doc(description: "Company legal address output data sc } type CompanyAdmin @doc(description: "Company Administrator (Customer with corresponding privileges) output data schema.") { - id: ID! @doc(description: "Company Administrator's id.") + uid: ID! @doc(description: "Company Administrator's id.") email: String @doc(description: "Company Administrator email address.") firstname: String @doc(description: "Company Administrator first name.") lastname: String @doc(description: "Company Administrator last name.") @@ -86,14 +86,14 @@ type CompanyRoles @doc(description: "Output data schema for an object returned b } type CompanyRole @doc(description: "Company role output data schema returned in response to a query by Role id.") { - id: ID! @doc(description: "Role id.") + uid: ID! @doc(description: "Role id.") name: String @doc(description: "Role name.") users_count: Int @doc(description: "Total number of Users with such Role within Company Structure.") permissions: [CompanyAclResource] @doc(description: "A list of permission resources defined for a Role.") } type CompanyAclResource @doc(description: "Output data schema for an object with Role permission resource information.") { - id: ID! @doc(description: "ACL resource id.") + uid: ID! @doc(description: "ACL resource id.") text: String @doc(description: "ACL resource label.") sortOrder: Int @doc(description: "ACL resource sort order.") children: [CompanyAclResource!] @doc(description: "An array of sub-resources.") @@ -118,7 +118,7 @@ type CompanyEmailCheckResponse @doc(description: "Response object schema for a C union CompanyStructureEntity = CompanyTeam | Customer type CompanyStructureItem @doc(description: "Company Team and Customer structure") { - id: ID! @doc(description: "ID of the item in the hierarchy") + uid: ID! @doc(description: "ID of the item in the hierarchy") parent_id: ID @doc(description: "ID of the parent item in the hierarchy") entity: CompanyStructureEntity } @@ -128,7 +128,7 @@ type CompanyStructure { } type CompanyTeam @doc(description: "Company Team entity output data schema.") { - id: ID! @doc(description: "Team id.") + uid: ID! @doc(description: "Team id.") name: String @doc(description: "Team name.") description: String @doc(description: "Team description.") } @@ -151,14 +151,14 @@ type Mutation { updateCompany(input: CompanyUpdateInput!): UpdateCompanyOutput @doc(description:"Update Company information.") createCompanyUser(input: CompanyUserCreateInput!): CreateCompanyUserOutput @doc(description:"Create new Company User (Customer assigned to Company).") updateCompanyUser(input: CompanyUserUpdateInput!): UpdateCompanyUserOutput @doc(description:"Update Company User information.") - deleteCompanyUser(id: ID!): DeleteCompanyUserOutput @doc(description:"Delete Company User by ID.") + deleteCompanyUser(uid: ID!): DeleteCompanyUserOutput @doc(description:"Delete Company User by ID.") createCompanyRole(input: CompanyRoleCreateInput!): CreateCompanyRoleOutput @doc(description:"Create new Company role.") updateCompanyRole(input: CompanyRoleUpdateInput!): UpdateCompanyRoleOutput @doc(description:"Update Company role data.") - deleteCompanyRole(id: ID!): DeleteCompanyRoleOutput @doc(description:"Delete Company Role by ID.") + deleteCompanyRole(uid: ID!): DeleteCompanyRoleOutput @doc(description:"Delete Company Role by ID.") updateCompanyStructure(input: CompanyStructureUpdateInput!): UpdateCompanyStructureOutput @doc(description:"Update Company Structure element's parent node assignment.") createCompanyTeam(input: CompanyTeamCreateInput!): CreateCompanyTeamOutput @doc(description:"Create Company Team.") updateCompanyTeam(input: CompanyTeamUpdateInput!): UpdateCompanyTeamOutput @doc(description:"Update Company Team data.") - deleteCompanyTeam(id: ID!): DeleteCompanyTeamOutput @doc(description:"Delete Company Team entity by ID.") + deleteCompanyTeam(uid: ID!): DeleteCompanyTeamOutput @doc(description:"Delete Company Team entity by ID.") } type CreateCompanyTeamOutput @doc(description: "Create company team output data schema.") { @@ -267,7 +267,7 @@ input CompanyUserCreateInput @doc(description: "Defines the input data schema fo } input CompanyUserUpdateInput @doc(description: "Defines the input data schema for updating an existing Customer - Company user.") { - id: ID! @doc(description: "Company user's ID (Customer ID). Required.") + uid: ID! @doc(description: "Company user's ID (Customer ID). Required.") role_id: ID @doc(description: "Company user's role ID.") status: Int @doc(description: "Company user's status ID.") job_title: String @doc(description: "Company user's job title.") @@ -283,7 +283,7 @@ input CompanyRoleCreateInput @doc(description: "Defines the input data schema fo } input CompanyRoleUpdateInput @doc(description: "Defines the input data schema for updating an existing Company role.") { - id: ID! @doc(description: "Role ID. Required.") + uid: ID! @doc(description: "Role ID. Required.") name: String @doc(description: "Role name.") permissions: [String!] @doc(description: "A list of Role permission resources. Array value for a field, if provided, should consist only of string values.") } @@ -300,7 +300,7 @@ input CompanyTeamCreateInput @doc(description: "Defines the input data schema fo } input CompanyTeamUpdateInput @doc(description: "Defines the input data schema for updating an existing Company team.") { - id: ID! @doc(description: "Team ID. Required.") + uid: ID! @doc(description: "Team ID. Required.") name: String @doc(description: "Team name.") description: String @doc(description: "Team description.") } From f02d55b5a17f225bed456c3b7cbd8b39adc38074 Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Wed, 9 Sep 2020 13:13:37 -0500 Subject: [PATCH 318/479] Update negotiableQuotes.graphqls --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index af0ec3dbc..4684bbbcb 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -4,7 +4,7 @@ type Query { # in a company can see quotes belonging to their # employees. If `Customer.negotiable_quotes` is desirable for buyer's # that aren't managers, we can always add that on - negotiableQuote(id: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") + negotiableQuote(iud: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") negotiableQuotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, @@ -153,7 +153,7 @@ input AddNegotiableQuoteCommentInput { } type NegotiableQuoteComment { - id: ID! + uid: ID! created_at: String! author: NegotiableQuoteUser! creator_type: NegotiableQuoteCommentCreatorType! @@ -166,7 +166,7 @@ enum NegotiableQuoteCommentCreatorType { } type NegotiableQuote { - id: ID! + uid: ID! name: String! items: [CartItemInterface!] # Attachment Support is dependent on headless File Upload design @@ -200,7 +200,7 @@ input NegotiableQuoteFilterInput { } type NegotiableQuoteHistoryEntry { - id: ID! + uid: ID! author: NegotiableQuoteUser! change_type: NegotiableQuoteHistoryEntryChangeType! created_at: String From fe9a3ae6c267dc3c4b6f386ce9bfa378d9e9b39d Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Wed, 9 Sep 2020 13:19:19 -0500 Subject: [PATCH 319/479] Update requisitionList.graphqls --- .../coverage/b2b/requisitionList.graphqls | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 273c8e894..05b7ed3a8 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -39,7 +39,7 @@ type RequisitionLists @doc(description: "Provides Customer's Requisition Lists") } type RequisitionList @doc(description: "Requisition List Type") { - id: ID! @doc(description: "Unique Identifier of Requisition List") + uid: ID! @doc(description: "Unique Identifier of Requisition List") name: String! @doc(description: "Name of the list") description: String @doc(description: "Description of the list") items( @@ -57,14 +57,14 @@ type RequistionListItems { } interface RequisitionListItemInterface @doc(description: "Interface type for Requisition List Item") { - id: ID! @doc(description:"Unique Identifier of Requisition List Item") + uid: ID! @doc(description:"Unique Identifier of Requisition List Item") product: ProductInterface! qty: Float! @doc(description: "Quantity added") } type DefaultRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Simple and Virtual Products") { - id: ID! @doc(description: "Unique Identifier of Requisition List Item") + uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! qty: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") @@ -72,7 +72,7 @@ type DefaultRequisitionListItem implements RequisitionListItemInterface type DownloadableRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Downloadable Products") { - id: ID! @doc(description: "Unique Identifier of Requisition List Item") + uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! qty: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") @@ -82,7 +82,7 @@ type DownloadableRequisitionListItem implements RequisitionListItemInterface type BundleRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Bundle Products") { - id: ID! @doc(description: "Unique Identifier of Requisition List Item") + uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! qty: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") @@ -91,7 +91,7 @@ type BundleRequisitionListItem implements RequisitionListItemInterface type ConfigurableRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Configurable Products") { - id: ID! @doc(description: "Unique Identifier of Requisition List Item") + uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! qty: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") @@ -105,27 +105,27 @@ type Mutation { ): CreateRequisitionListOutput @doc(description: "Create Empty Requisition List") renameRequisitionList( - id: ID!, @doc(description: "unique Id of requisition list") + uid: ID!, @doc(description: "unique Id of requisition list") name: String!, @doc(description: "new name for list") description: String @doc(description: "new description For the List") ): RenameRequisitionListOutput @doc(description: "Rename a requisition list and change description") deleteRequisitionList( - id: ID! @doc(description: "unique Id of requisition list") + uid: ID! @doc(description: "unique Id of requisition list") ): DeleteRequisitionListOutput @doc(description: "Delete a requisition list with Id") addProductsToRequisitionList( - id: ID!, @doc(description: "unique Id of requisition list") + uid: ID!, @doc(description: "unique Id of requisition list") items: [RequisitionListItemsInput!]! @doc(description: "Products to be added to requisition list") ): AddProductsToRequisitionListOutput @doc(description: "Add items to requisition list") removeRequisitionListItems( - id: ID!, @doc(description: "unique Id of requisition list") + uid: ID!, @doc(description: "unique Id of requisition list") items: [ID!]! @doc(description: "unique Ids of Items to be removed from requisition list") ): RemoveRequisitionListItemsOutput @doc(description: "Remove Items in requisition list") updateRequisitionListItems( - id: ID!, @doc(description: "unique Id of requisition list") + uid: ID!, @doc(description: "unique Id of requisition list") items: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") From 97ac25c18ce8dff0ad0e1bae11b0d3c070afd4c8 Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Wed, 9 Sep 2020 13:22:56 -0500 Subject: [PATCH 320/479] Update returns.graphqls --- .../graph-ql/coverage/returns.graphqls | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 97957e6c0..ed6db8eee 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -27,7 +27,7 @@ input RequestReturnItemInput { } input EnteredCustomAttributeInput { - id: ID! + uid: ID! value: String! } @@ -81,7 +81,7 @@ type Customer { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. Defaults to 20."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), ): Returns @doc(description: "Information about the customer returns.") - return(id: ID!): Return @doc(description: "Get customer return details by its ID.") + return(uid: ID!): Return @doc(description: "Get customer return details by its ID.") } type CustomerOrder { @@ -103,7 +103,7 @@ type Returns { } type Return @doc(description: "Customer return") { - id: ID! + uid: ID! number: String! @doc(description: "Human-readable return number") order: CustomerOrder @doc(description: "The order associated with the return.") creation_date: String! @doc(description: "The date when the return was requested.") @@ -117,7 +117,7 @@ type Return @doc(description: "Customer return") { } type ReturnItem { - id: ID! + uid: ID! order_item: OrderItemInterface! @doc(description: "Order item provides access to the product being returned, including selected/entered options information.") custom_attributes: [CustomAttribute] @doc(description: "Return item custom attributes, which are marked by the admin to be visible on the storefront.") request_quantity: Float! @doc(description: "The quantity of the item requested to be returned.") @@ -127,13 +127,13 @@ type ReturnItem { # See https://github.com/magento/architecture/blob/master/design-documents/graph-ql/custom-attributes-container.md type CustomAttribute { - id: ID! + uid: ID! label: String! value: String! @doc(description: "JSON encoded value of the attribute.") } type ReturnComment { - id: ID! @doc(description: "Comment ID.") + uid: ID! @doc(description: "Comment ID.") author_firstname: String! @doc(description: "First name of the user who posted the comment.") author_lastname: String! @doc(description: "Last name of the user who posted the comment.") created_at: String! @doc(description: "The date and time when the comment was posted.") @@ -142,16 +142,16 @@ type ReturnComment { type ReturnShipping { address: ReturnShippingAddress @doc(description: "Return shipping address, which is specified by the admin.") - tracking(id: ID): [ReturnShippingTracking] @doc(description: "Tracking information for all or a single tracking record when ID is provided.") + tracking(uid: ID): [ReturnShippingTracking] @doc(description: "Tracking information for all or a single tracking record when ID is provided.") } type ReturnShippingCarrier { - id: ID! + uid: ID! label: String! } type ReturnShippingTracking { - id: ID! + uid: ID! carrier: ReturnShippingCarrier! tracking_number: String! status: ReturnShippingTrackingStatus @@ -207,9 +207,9 @@ type StoreConfig { } type Attribute { - id: ID! + uid: ID! } type AttributeOption { - id: ID! + uid: ID! } From 8b3565a388f9580ab2dd683218936aa6ec656f6c Mon Sep 17 00:00:00 2001 From: Soumya Unnikrishnan Date: Wed, 9 Sep 2020 18:14:54 -0500 Subject: [PATCH 321/479] MQE-2247: Implement additional
and entity use cases in SVC tool --- .../functional/versioning-and-backward-compatibility-policy.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md b/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md index a52710d10..526965ee2 100644 --- a/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md +++ b/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md @@ -82,6 +82,8 @@ It MAY include minor and patch level changes. Patch and minor version MUST be re |Metadata|`` added|MINOR | |`` removed|MAJOR | |`` changed|MINOR +| |`` child element added|MINOR +| |`` child element removed|MAJOR |Page|`` added|MINOR | |`` removed|MAJOR | |`` `
` added|MINOR From bde94bfdf8aafb3212915880ec4034ed659cbdef Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Wed, 9 Sep 2020 19:48:17 -0500 Subject: [PATCH 322/479] Rename all id fields to uid in cart.graphqls --- .../graph-ql/coverage/cart/Cart.graphqls | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/cart/Cart.graphqls b/design-documents/graph-ql/coverage/cart/Cart.graphqls index 28f54eff8..556c3b411 100644 --- a/design-documents/graph-ql/coverage/cart/Cart.graphqls +++ b/design-documents/graph-ql/coverage/cart/Cart.graphqls @@ -3,7 +3,7 @@ type Query { } input CartQueryInput { - cart_id: String! + cart_id: ID! } type CartQueryOutput { @@ -11,7 +11,8 @@ type CartQueryOutput { } type Cart { - id: ID! @doc(description: "The ID of the cart.") + id: ID! @doc(description: "The ID of the cart.") @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart.") items: [CartItemInterface] applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code. By default Magento supports only one coupon.") email: String @@ -36,7 +37,8 @@ type SelectedPaymentMethod { } interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { - id: String! + id: String! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart item.") quantity: Float! prices: CartItemPrices product: ProductInterface! @@ -74,8 +76,9 @@ type GiftCardCartItem implements CartItemInterface { type SelectedConfigurableOption { # Hash which includes option ID, option type and slected values. Can be used to move configurable product to wishlist or gift registry - id_v2: ID! - id: Int! + id_v2: ID! @deprecated(reason: "use uid") + id: Int! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart.") option_label: String! value_id: Int! value_label: String! @@ -83,7 +86,8 @@ type SelectedConfigurableOption { type DownloadableProductLink @doc(description: "Defines characteristics of a downloadable product") { # Hash based on option type and slected link. Can be used to move downloadable product to wishlist or gift registry - id: ID! + id: ID! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart.") title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") price: Float @doc(description: "The price of the downloadable product") @@ -97,14 +101,16 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de } type SelectedBundleOption { - id: Int! + id: Int! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart.") label: String! type: String! values: [SelectedBundleOptionValue!]! } type SelectedBundleOptionValue { - id: Int! + id: Int! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the cart.") label: String! price: Float! quantity: Float! @@ -113,8 +119,9 @@ type SelectedBundleOptionValue { type SelectedCustomizableOption { # Hash which includes option ID, option type and slected values. Can be used to move a product to wishlist or gift registry - id_v2: ID! - id: Int! + id_v2: ID! @deprecated(reason: "use uid") + id: Int! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the selected option.") label: String! is_required: Boolean! values: [SelectedCustomizableOptionValue!]! @@ -122,7 +129,8 @@ type SelectedCustomizableOption { } type SelectedCustomizableOptionValue { - id: Int! + id: Int! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the selected option.") label: String! value: String! price: CartItemSelectedOptionValuePrice! @@ -136,7 +144,8 @@ type CartItemSelectedOptionValuePrice { type SelectedGiftCardAmount { # Hash from the type of the option and value - id: ID! + id: ID! @deprecated(reason: "use uid") + uid: ID! @doc(description: "The unique ID of the selected option.") value: Money! } From 3f242fed9e087efc5d78d82f0344d6d1432dc0d6 Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Thu, 10 Sep 2020 07:30:08 -0500 Subject: [PATCH 323/479] Rename all the id fields to uid in compare-list.graphqls Rename all the id fields to uid in compare-list.graphqls --- .../graph-ql/coverage/catalog/compare-list.graphqls | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index f20a29d97..c004a236b 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -14,7 +14,7 @@ type ComparableAttribute { } type CompareList { - list_id: ID! @doc(description: " Compare list id") + uid: ID! @doc(description: " Compare list unique id") items: [ComparableItem] @doc(description: "Comparable products") attributes: [ComparableAttribute] @doc(description: "Comparable attributes, provides codes and titles for the attributes") } @@ -24,19 +24,19 @@ type Customer { } type Query { - compareList(id: ID!): CompareList @doc(description: "Compare list") + compareList(uid: ID!): CompareList @doc(description: "Compare list") } type Mutation { addItemsToCompareList( - id: ID! + uid: ID! items: [ID!] ): CompareList removeItemsFromCompareList( - id: ID! + uid: ID! items: [ID!] ): CompareList - assignCompareListToCustomer(listId: ID!): Boolean + assignCompareListToCustomer(listUid: ID!): Boolean } schema { From 5eac1147cebcf4ae7576ed5f905233c6aa37ab64 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Thu, 10 Sep 2020 23:47:46 -0500 Subject: [PATCH 324/479] added productId to variant --- .../storefront/catalog/product-options-and-variants.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 85abe6ab9..9b2b54170 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -80,7 +80,9 @@ Such a decision brings us unseen before the level of the flexibility, since ther We do not have such behaviors either Magento 1 or 2 and as a result, the whole layer of product types such as bundle and downloadable do not support B2B prices or special prices for options. -The link on a product that exists in the same domain could be done through the shared ID, which as for me makes sense since I can see a possible scenario when variation that initially had only a price will be "promoted" to a product. For example due to integration reasons, to pass additional attributes data along with variant to Google Merchant Center. +In most cases, the product variant of configurable and bundle products may represent the child product, and via versa. +But it does not mean that two different variants can not reference the same product. For instance, two different bundle products may contain the same product, refer to the same inventory but have different prices. + ### Enhanced option values @@ -122,6 +124,7 @@ message ProductOption { message ProductVariant { repeated string optionValueId = 1; string id = 2; + string productId = 3; string productIdentifierInPricing = 500; #* string productIdentifierInInventory = 600; #* } From e5481f58e11cbf23343688d6f395ea1ccf0417a3 Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Fri, 11 Sep 2020 08:07:49 -0500 Subject: [PATCH 325/479] Apply suggestions from code review Co-authored-by: Alex Paliarush --- .../coverage/deprecate-createEmptyCart-add-createGuestCart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md b/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md index 41cdd2cb1..e8175a5a8 100644 --- a/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md +++ b/design-documents/graph-ql/coverage/deprecate-createEmptyCart-add-createGuestCart.md @@ -28,7 +28,7 @@ Because the UI knows if it has a token for a customer, `Query.cart` should be us type Mutation { + createGuestCart( + input: CreateGuestCartInput -+ ): CreateCartOutput @doc(description: "Create a new shopping cart") ++ ): CreateGuestCartOutput @doc(description: "Create a new shopping cart") createEmptyCart( input: createEmptyCartInput + ): String @deprecated(reason: "Use `Mutation.createGuestCart`, or `Query.cart` for logged-in shoppers") @@ -36,7 +36,7 @@ type Mutation { } +input CreateGuestCartInput { -+ cart_id: String @doc(description: "Optional client-generated ID") ++ cart_uid: ID @doc(description: "Optional client-generated ID") +} + +type CreateGuestCartOutput { From 564a3fac10c315fb80c7945e87d61d880f5965fa Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 11 Sep 2020 12:33:25 -0500 Subject: [PATCH 326/479] added example --- .../catalog/product-options-and-variants.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 9b2b54170..859a3bda3 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -356,3 +356,118 @@ This proposal continues the idea of product options unification and aligned with * [Single mutation for adding products to cart](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md) * [Configurable options selection](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/catalog/configurable-options-selection.md) * [Gift Registry](https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/gift-registry.md) + + +## Question & Answers + +### How to express two configurable products that share the same attribute set? + +*Comment: Although this case natively supported by Magento, +it could be less common than we used to think since the two regular t-shorts +from Walmart will have different sets of values for sizes and colors depends +on the manufacturer. proof https://www.walmart.com/search/?query=tshirt* + + +#### Product #1 +```json +{ + "id": "a82deb7a-dee7-48e7-ab34-75026e576fab", + "name": "Fruit of the Loom Men's Short Sleeve Assorted Crew T-Shirt", + "options": { + "color": { + "label": "Color", + "values": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + } + } + }, + "size" : { + "label": "Size", + "values": { + "m": { + "label": "M" + }, + "l": { + "label": "L" + } + } + } + } +} +``` + +#### Product 1 variants +```json +[ + { + "variantId": "500d0366-777f-4a45-92b6-8e5197ce9992", + "optionValueIds": [ + "a82deb7a-dee7-48e7-ab34-75026e576fab:color/red", "a82deb7a-dee7-48e7-ab34-75026e576fab:size/l" + ], + "productId": "8b6be8b0-2e21-4763-806c-f383a8591d21" + }, + { + "variantId": "b843e139-aa04-44d0-a9a7-b439a17ce941", + "optionValueIds": [ + "a82deb7a-dee7-48e7-ab34-75026e576fab:color/green", "a82deb7a-dee7-48e7-ab34-75026e576fab:size/m" + ], + "productId": "96a5a8ed-7cfe-4626-be29-f60fe0bf7b33" + } +] +``` + +#### Product 2 +```json +{ + "id": "747d8c9b-e5fc-437a-8263-271dd8352976", + "name": "George Men's Assorted Crew T-Shirt", + "options": { + "color": { + "label": "Color", + "values": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + } + } + }, + "size" : { + "label": "Size", + "values": { + "m": { + "label": "M" + }, + "l": { + "label": "L" + } + } + } + } +} +``` + +#### Product 2 variants +```json +[ + { + "variantId": "edbb59fb-f303-4970-9f03-889e71374a90", + "optionValueIds": [ + "747d8c9b-e5fc-437a-8263-271dd8352976:color/green", "747d8c9b-e5fc-437a-8263-271dd8352976:size/l" + ], + "productId": "7f4db047-604f-41ce-8998-a015f578e023" + }, + { + "variantId": "2c865d16-1723-49a3-8fd1-9b46613b5c12", + "optionValueIds": [ + "747d8c9b-e5fc-437a-8263-271dd8352976:color/red", "747d8c9b-e5fc-437a-8263-271dd8352976:size/m" + ], + "productId": "8eab9d67-791a-4c34-bc30-8bd034856ee2" + } +] +``` \ No newline at end of file From 095e5e1a6ad73d6e017b68dbe0affe8b18e9fd8b Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 11 Sep 2020 14:16:19 -0500 Subject: [PATCH 327/479] added rpc --- .../catalog/product-options-and-variants.md | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 859a3bda3..6e7fd2e06 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -324,6 +324,8 @@ As were mentioned previously, options belong to a product, full options list can Variants do not expose at the storefront but help to filter options after one or several variants were selected. + + Such behavior was recently [approved for configurable product](https://github.com/magento/architecture/pull/394). And since the most complex part of designing API was done the main thing we have to do in the scope of this chapter - generalize the behavior to support not only configurable products. @@ -334,7 +336,9 @@ With the response API has to return: * List of images & videos that should be used on PDP. * List of products that were exactly matched by the selected options. ```proto -message OptionSelection +syntax = "proto3"; + +message OptionSelectionRequest { repeated string values = 2; } @@ -344,9 +348,36 @@ message OptionResponse { repeated ProductVarinat matchedVariants = 3; } -service SearchService { +service OptionSearchService { rpc GetOptions(OptionSelection) returns (OptionResponse); } + +``` + +In the perfect world, variants should not appear at the presentation level. Still, the storefront lies under the presentation, so it has to provide a way to retrieve variants depends on the scenario efficiently. +So far, we can imagine the following cases: +* match the variants which correspond, and do not contradict, the merchant selection - such API. +* match the variants which exactly matched with merchant selection. +* get all variants which contain at least one of merchant selection. +* get all variants that belong to a product. This method is code-sugar for the previous one because all the product variants could be retrieved by using the previous method in case of passing all the options values which belong to the product. + +```proto +syntax = "proto3"; + +message VariantResponse { + repeated ProductVarinat matchedVariants = 3; +} + +message ProductRequest { + string productId = 1; +} + +service VaraintSearchService { + rpc GetVariantsMatch(OptionSelection) returns (VariantResponse); + rpc GetVariantsExactlyMatch(OptionSelection) returns (VariantResponse); + rpc GetVariantsInclude(OptionSelection) returns (VariantResponse); + rpc GetProductVariants(ProductRequest) returns (VariantResponse); +} ``` ## Proposal cross references @@ -470,4 +501,11 @@ on the manufacturer. proof https://www.walmart.com/search/?query=tshirt* "productId": "8eab9d67-791a-4c34-bc30-8bd034856ee2" } ] -``` \ No newline at end of file +``` + +## How to return all variants for the product + +*Comment: A client should never request all the variants within a single call +the number of such variants is unpredictable, +and could significantly affect store performance.* + From 3af89f24e105298d1e5f4f76fc9205cfa39cd55e Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Fri, 11 Sep 2020 14:16:26 -0500 Subject: [PATCH 328/479] Patching back updates from code reviews --- .../coverage/customer/Wishlist.graphqls | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index ccd81d0d2..51f41c8d7 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -1,9 +1,12 @@ type Mutation { - createWishlist(name: String!): ID # Multiple wishlists Commerce functionality - removeWishlist(id: ID!): Boolean # Commerce fucntionality - in Opens Source we assume customer always has one wishlist + createWishlist(name: String!, visibility: WishlistVisibilityEnum!): CreateWishlistOutput # Multiple wishlists Commerce functionality + deleteWishlist(wishlistId: ID!): Boolean # Commerce fucntionality - in Opens Source we assume customer always has one wishlist addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput + copyProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemCopyInput!]!): UpdateProductsInWishlistOutput @doc(description: "Copy a product to the wish list") + moveProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemMoveInput!]!): UpdateProductsInWishlistOutput @doc(description: "Move products from one wish list to another") + updateWishlist(wishlistId: ID!, name: String, visibility: WishlistVisibilityEnum): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") } type Customer { @@ -23,8 +26,9 @@ type Wishlist { } input WishlistItemUpdateInput { - wishlist_item_id: ID + wishlist_item_id: ID! quantity: Float + description: String selected_options: [ID!] entered_options: [EnteredOptionInput!] } @@ -42,8 +46,8 @@ type UpdateProductsInWishlistOutput { } input WishlistItemInput { - sku: String - quantity: Float + sku: String! + quantity: Float! parent_sku: String, parent_quantity: Float, selected_options: [ID!] @@ -88,3 +92,35 @@ type GiftCardWishlistItem implements WishlistItemInterface { message: String } +enum WishlistVisibilityEnum @doc(description: "This enumeration defines the wish list visibility types") { + PUBLIC + PRIVATE +} + +type CreateWishlistOutput { + uid: ID! @doc(description: "The ID of the new wish list") + name: String! @doc(description: "The wish list name") + visibility: WishlistVisibilityEnum! @doc(description: "The wish list visibility") +} + +input WishlistItemCopyInput { + wishlist_item_id: ID! @doc(description: "The ID of the item to be copied") + quantity: Float @doc(description: "The quantity of this item to copy to the destination wish list. This value can't be greater than the quantity in the source wish list.") +} + +input WishlistItemMoveInput { + wishlist_item_id: ID! @doc(description: "The ID of the item to be moved") + quantity: Float @doc(description: "The quantity of this item to move to the destination wish list. This value can't be greater than the quantity in the source wish list.") +} + +type UpdateWishlistOutput { + uid: ID! @doc(description: "The ID of the updated wish list") + name: String! @doc(description: "The wish list name") + visibility: WishlistVisibilityEnum! @doc(description: "The wish list visibility") +} + +type StoreConfig { + maximum_number_of_wishlists: String @doc(description: "If multiple wish lists are enabled, the maximum number of wish lists the customer can have") + enable_multiple_wishlists: String @doc(description: "Indicates whether customers can have multiple wish lists. Possible values: 1 (Yes) and 0 (No)") +} + From 6a8ab76efd17ebc03d875290ffc8e480fb48176e Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Fri, 11 Sep 2020 14:17:18 -0500 Subject: [PATCH 329/479] added example --- .../storefront/catalog/product-options-and-variants.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 6e7fd2e06..85df9c959 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -509,3 +509,5 @@ on the manufacturer. proof https://www.walmart.com/search/?query=tshirt* the number of such variants is unpredictable, and could significantly affect store performance.* +@see `rpc:GetProductVariants` + From e9a6233d13aa5e11121ac4102267364c20813425 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Fri, 19 Jun 2020 20:33:02 +0530 Subject: [PATCH 330/479] pagination to support entities in b2b --- .../graph-ql/coverage/cart/Cart.graphqls | 10 ++++++++++ .../customer-address-pagination-change.graphqls | 12 ++++++++++++ .../graph-ql/coverage/customer/customer-orders.md | 15 +++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls diff --git a/design-documents/graph-ql/coverage/cart/Cart.graphqls b/design-documents/graph-ql/coverage/cart/Cart.graphqls index 556c3b411..7144f489a 100644 --- a/design-documents/graph-ql/coverage/cart/Cart.graphqls +++ b/design-documents/graph-ql/coverage/cart/Cart.graphqls @@ -14,6 +14,10 @@ type Cart { id: ID! @doc(description: "The ID of the cart.") @deprecated(reason: "use uid") uid: ID! @doc(description: "The unique ID of the cart.") items: [CartItemInterface] + items_v2( + currentPage: Int = 1 @doc(description: "current page of the customer cart items. default is 1") + pageSize: Int = 20 @doc(description: "page size for the customer cart items. default is 20") + ): CartItems! @doc(description: "collection of all the items purchased") applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code. By default Magento supports only one coupon.") email: String shipping_addresses: [ShippingCartAddress]! @@ -25,6 +29,12 @@ type Cart { is_virtual: Boolean! } +type CartItems { + items: [CartItemInterface] @doc(description: "Cart items list") + page_info: SearchResultPageInfo + total_count: Int +} + type AvailablePaymentMethod { code: String! @doc(description: "The payment method code") title: String! @doc(description: "The payment method title.") diff --git a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls new file mode 100644 index 000000000..767f291cd --- /dev/null +++ b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls @@ -0,0 +1,12 @@ +type Customer { + addresses_v2( + currentPage: Int = 1 @doc(description: "current page of the customer address list. default is 1") + pageSize: Int = 10 @doc(description: "page size for the customer address list. default is 10") + ): CustomerAddresses +} + +type CustomerAddresses { + items: [CustomerAddress] @doc(description: "Customer Address List") + page_info: SearchResultPageInfo + total_count: Int +} diff --git a/design-documents/graph-ql/coverage/customer/customer-orders.md b/design-documents/graph-ql/coverage/customer/customer-orders.md index c11b40e04..db76985dc 100644 --- a/design-documents/graph-ql/coverage/customer/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer/customer-orders.md @@ -68,6 +68,10 @@ type CustomerOrder { status: String! @doc("current status of the order") number: String! @doc("sequential order number") items: [OrderItemInterface] @doc("collection of all the items purchased") + items_v2( + currentPage: Int = 1 @doc("current page of the customer order item list. default is 1") + pageSize: Int = 20 @doc("page size for the customer orders item list. default is 20") + ): OrderItems! @doc("collection of all the items purchased with pagination") total: OrderTotal @doc("total amount details for the order") invoices: [Invoice] @doc("invoice list for the order") credit_memos: [CreditMemo] @doc("credit memo list for the order") @@ -86,6 +90,17 @@ The `id` will be a `base64_encode(increment_id)` which in future can be replaced > The order `status` should be filtered in the same way as for Luma via `Order Status` and `Visible On Storefront` configuration ### Order Item +The order items will be presented as separate interface which will have multiple implementations for invoice, shipment and credit memo types. +The order items will be paginated. + +```graphql +@doc("Grpahql Order Item Output Wrapper") +type OrderItems { + items: [OrderItem] @doc("collection of customer orders items that contains individual order item details.") + page_info: SearchResultPageInfo + total_count: Int +} +``` ```graphql interface OrderItemInterface @doc("Order item details") { From be489da52664ac06808ebef287282784162f67ec Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Wed, 5 Aug 2020 20:46:42 +0530 Subject: [PATCH 331/479] added deprecation notice --- .../graph-ql/coverage/cart/Cart.graphqls | 2 +- .../customer-address-pagination-change.graphqls | 2 ++ .../graph-ql/coverage/customer/Wishlist.graphqls | 14 ++++++++++++-- .../graph-ql/coverage/customer/customer-orders.md | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/cart/Cart.graphqls b/design-documents/graph-ql/coverage/cart/Cart.graphqls index 7144f489a..75fa53dd4 100644 --- a/design-documents/graph-ql/coverage/cart/Cart.graphqls +++ b/design-documents/graph-ql/coverage/cart/Cart.graphqls @@ -13,7 +13,7 @@ type CartQueryOutput { type Cart { id: ID! @doc(description: "The ID of the cart.") @deprecated(reason: "use uid") uid: ID! @doc(description: "The unique ID of the cart.") - items: [CartItemInterface] + items: [CartItemInterface] @deprecated(reason: "The `items` field is deprecated. Use `items_v2` instead.") items_v2( currentPage: Int = 1 @doc(description: "current page of the customer cart items. default is 1") pageSize: Int = 20 @doc(description: "page size for the customer cart items. default is 20") diff --git a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls index 767f291cd..1dd8f7167 100644 --- a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls +++ b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls @@ -1,4 +1,6 @@ +# type Customer { + addresses: CustomerAddresses @deprecated(reason: "The `address` field is deprecated. Use `addresses_v2` instead.") addresses_v2( currentPage: Int = 1 @doc(description: "current page of the customer address list. default is 1") pageSize: Int = 10 @doc(description: "page size for the customer address list. default is 10") diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index ccd81d0d2..8be8ec69a 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -14,14 +14,24 @@ type Customer { type Wishlist { id: ID - items: [WishlistItem] @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") - items_v2: [WishlistItemInterface] @doc(description: "An array of items in the customer's wishlist") + items: [WishlistItem] @deprecated(reason: "Use field `items_v3` from type `Wishlist` instead") + items_v2: [WishlistItemInterface] @doc(description: "An array of items in the customer's wishlist") @deprecated(reason: "Use field `items_v3` from type `Wishlist` instead") + items_v3( + currentPage: Int = 1 @doc(description: "current page of the customer wishlist items. default is 1") + pageSize: Int = 20 @doc(description: "page size for the customer wishlist items. default is 20") + ): WishlistItems @doc(description: "An array of items in the customer's wishlist") items_count: Int sharing_code: String updated_at: String name: String @doc(description: "Avaialble in Commerce edition only") } +type WishlistItems { + items: [WishlistItemInterface] + page_info: SearchResultPageInfo + total_count: Int +} + input WishlistItemUpdateInput { wishlist_item_id: ID quantity: Float diff --git a/design-documents/graph-ql/coverage/customer/customer-orders.md b/design-documents/graph-ql/coverage/customer/customer-orders.md index db76985dc..8395814c1 100644 --- a/design-documents/graph-ql/coverage/customer/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer/customer-orders.md @@ -67,7 +67,7 @@ type CustomerOrder { order_date: String! @doc("date when the order was placed") status: String! @doc("current status of the order") number: String! @doc("sequential order number") - items: [OrderItemInterface] @doc("collection of all the items purchased") + items: [OrderItemInterface] @doc("collection of all the items purchased") @deprecated("The `items` field is derecated. Use `items_v2` instead.") items_v2( currentPage: Int = 1 @doc("current page of the customer order item list. default is 1") pageSize: Int = 20 @doc("page size for the customer orders item list. default is 20") From b28e9220ab0270e9474462c16a9d739ecc405bde Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Tue, 8 Sep 2020 16:50:46 -0500 Subject: [PATCH 332/479] Apply suggestions from code review --- design-documents/graph-ql/coverage/cart/Cart.graphqls | 6 +++--- .../coverage/customer-address-pagination-change.graphqls | 4 ++-- .../graph-ql/coverage/customer/customer-orders.md | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/cart/Cart.graphqls b/design-documents/graph-ql/coverage/cart/Cart.graphqls index 75fa53dd4..e50fdf455 100644 --- a/design-documents/graph-ql/coverage/cart/Cart.graphqls +++ b/design-documents/graph-ql/coverage/cart/Cart.graphqls @@ -17,7 +17,7 @@ type Cart { items_v2( currentPage: Int = 1 @doc(description: "current page of the customer cart items. default is 1") pageSize: Int = 20 @doc(description: "page size for the customer cart items. default is 20") - ): CartItems! @doc(description: "collection of all the items purchased") + ): CartItems! @doc(description: "Cart items") applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code. By default Magento supports only one coupon.") email: String shipping_addresses: [ShippingCartAddress]! @@ -30,9 +30,9 @@ type Cart { } type CartItems { - items: [CartItemInterface] @doc(description: "Cart items list") + items: [CartItemInterface]! @doc(description: "Cart items list") page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } type AvailablePaymentMethod { diff --git a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls index 1dd8f7167..ad0e4abde 100644 --- a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls +++ b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls @@ -8,7 +8,7 @@ type Customer { } type CustomerAddresses { - items: [CustomerAddress] @doc(description: "Customer Address List") + items: [CustomerAddress]! @doc(description: "List of customer address") page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } diff --git a/design-documents/graph-ql/coverage/customer/customer-orders.md b/design-documents/graph-ql/coverage/customer/customer-orders.md index 8395814c1..6472608fe 100644 --- a/design-documents/graph-ql/coverage/customer/customer-orders.md +++ b/design-documents/graph-ql/coverage/customer/customer-orders.md @@ -71,7 +71,7 @@ type CustomerOrder { items_v2( currentPage: Int = 1 @doc("current page of the customer order item list. default is 1") pageSize: Int = 20 @doc("page size for the customer orders item list. default is 20") - ): OrderItems! @doc("collection of all the items purchased with pagination") + ): OrderItems! @doc("Collection of all of the purchased items") total: OrderTotal @doc("total amount details for the order") invoices: [Invoice] @doc("invoice list for the order") credit_memos: [CreditMemo] @doc("credit memo list for the order") @@ -96,9 +96,9 @@ The order items will be paginated. ```graphql @doc("Grpahql Order Item Output Wrapper") type OrderItems { - items: [OrderItem] @doc("collection of customer orders items that contains individual order item details.") + items: [OrderItem]! @doc("List of orders items") page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } ``` From 4224d7e9ac631c4b92d2e11009e0b0aac7ae07a7 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 12 Sep 2020 19:53:50 +0530 Subject: [PATCH 333/479] Update design-documents/graph-ql/coverage/customer/Wishlist.graphqls --- .../graph-ql/coverage/customer/Wishlist.graphqls | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 8be8ec69a..99911703f 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -27,9 +27,9 @@ type Wishlist { } type WishlistItems { - items: [WishlistItemInterface] + items: [WishlistItemInterface]! page_info: SearchResultPageInfo - total_count: Int + total_count: Int! } input WishlistItemUpdateInput { @@ -97,4 +97,3 @@ type GiftCardWishlistItem implements WishlistItemInterface { amount: SelectedGiftCardAmount message: String } - From 8e91fe84d9fd2d4550c13e8bf0ce14e84c23e1fa Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 12 Sep 2020 19:55:02 +0530 Subject: [PATCH 334/479] Update design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls --- .../coverage/customer-address-pagination-change.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls index ad0e4abde..1c5092faa 100644 --- a/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls +++ b/design-documents/graph-ql/coverage/customer-address-pagination-change.graphqls @@ -4,7 +4,7 @@ type Customer { addresses_v2( currentPage: Int = 1 @doc(description: "current page of the customer address list. default is 1") pageSize: Int = 10 @doc(description: "page size for the customer address list. default is 10") - ): CustomerAddresses + ): CustomerAddresses! } type CustomerAddresses { From e454a7da8a5cd156e92f1b09621beb2b549b3152 Mon Sep 17 00:00:00 2001 From: Christopher Daniel Date: Sat, 12 Sep 2020 19:56:10 +0530 Subject: [PATCH 335/479] Update design-documents/graph-ql/coverage/customer/Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 99911703f..dae7e020a 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -19,7 +19,7 @@ type Wishlist { items_v3( currentPage: Int = 1 @doc(description: "current page of the customer wishlist items. default is 1") pageSize: Int = 20 @doc(description: "page size for the customer wishlist items. default is 20") - ): WishlistItems @doc(description: "An array of items in the customer's wishlist") + ): WishlistItems! @doc(description: "An array of items in the customer's wishlist") items_count: Int sharing_code: String updated_at: String From 4f28fde7aa0e24b6e0b834cd35591637b40f60b6 Mon Sep 17 00:00:00 2001 From: Anton Kaplia Date: Tue, 15 Sep 2020 12:37:54 -0500 Subject: [PATCH 336/479] review fixes --- .../storefront/catalog/product-options-and-variants.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/design-documents/storefront/catalog/product-options-and-variants.md b/design-documents/storefront/catalog/product-options-and-variants.md index 85df9c959..e928d88ff 100644 --- a/design-documents/storefront/catalog/product-options-and-variants.md +++ b/design-documents/storefront/catalog/product-options-and-variants.md @@ -122,7 +122,7 @@ message ProductOption { } message ProductVariant { - repeated string optionValueId = 1; + repeated string optionValues = 1; string id = 2; string productId = 3; string productIdentifierInPricing = 500; #* @@ -340,6 +340,7 @@ syntax = "proto3"; message OptionSelectionRequest { + string storeViewId = 1; repeated string values = 2; } message OptionResponse { @@ -363,13 +364,14 @@ So far, we can imagine the following cases: ```proto syntax = "proto3"; - +productId message VariantResponse { repeated ProductVarinat matchedVariants = 3; } message ProductRequest { string productId = 1; + string storeViewId = 2; } service VaraintSearchService { From 52e8c18419a1577bc47a2a383a9bc2a9bf575708 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 15 Sep 2020 16:00:50 -0500 Subject: [PATCH 337/479] Update compare-list.graphqls --- .../graph-ql/coverage/catalog/compare-list.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index 816e1cbb6..0703889b5 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -20,7 +20,7 @@ type CompareList { } type Customer { - compareList(uid: ID): [CompareList] @doc(description: "Active customers compare list") #id is optional. Introduces the possibility of more than 1 compare list. + compare_list: CompareList @doc(description: "Active customers compare list") } type Query { @@ -37,12 +37,12 @@ type Mutation { uid: ID!, items: [ID!] ): CompareList - assignCompareListToCustomer(customerId: ID!, uid: ID!): Boolean! + assignCompareListToCustomer(uid: ID!): CompareList deleteCompareList(uid: ID!): Boolean! } input CreateCompareListInput { - items: [ID!] + products: [ID!] } schema { From 5ca38b94e8851ae2829c81861897ab17cdb739db Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 15 Sep 2020 17:51:10 -0500 Subject: [PATCH 338/479] Update compare-list.md --- .../graph-ql/coverage/catalog/compare-list.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index b59a92162..7fbb657fb 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -18,7 +18,7 @@ Use createCompareList mutation to create a new compare list. The server should c } ``` * For a guest user, new list will be created -* For a logged in user, exisiting list_id will be returned (Can be extended in future ee - to support multiple compare list per customer) +* For a logged in user, exisiting list_id will be returned ```graphql @@ -33,7 +33,7 @@ Use createCompareList mutation to create a new compare list. The server should c { query { customer { - compare_list(id) { # id filter is optional. Will return an array of compare lists (Future extensibility) + compare_list { list_id items { sku @@ -46,7 +46,7 @@ Use createCompareList mutation to create a new compare list. The server should c * If the registered customer does not have an active list then null will be returned. ``` -assignCompareListToCustomer(customerId: ID!, listId: ID!): CompareList +assignCompareListToCustomer(uid: ID!): CompareList ``` mutation can be used to assign a guest compare list to a registered customer. @@ -112,16 +112,15 @@ catalog_compare_item | catalog_compare_item_id | visitor_id | customer_id | product_id | store_id | ============================================================================== ``` -The visitor_id (non nullable) is tied to session \Magento\Customer\Model\Visitor in Luma. GraphQl layer should not be tied to Luma's session. So Compare-lists via GraphQl will have to differ/deviate from Luma. This dependency can be solved by managing the compare list state in a new table ``` catalog_compare_list ------------------------------------------------------------------------------ -| entity_id (int) (primary)| customer_id (int) (nullable)(indexed)| product_ids (json) (non nullable)| store_id (int) (non nullable)| last_modfied (date) | additional_data (json) (nullable) +| list_id (varchar) (primary)| additional_data (json) (nullable) ============================================================================== ``` -* On first client request, a row will be populated with an entity_id, server generated masked_id, customer_id (? if logged in), product_id and store_id. -* encoded entity_id will be used for client communications -* product_ids is a json field for reducing storage complexity. -* introducing entity_id for the list will enable customers to have more than one compare list (future extensibility) + +and adding list_id field to catalog_compare_item table. + +* encoded list_id will be used for client communications From 8cf881a254ee648470819428c71fafa1dd27c7bf Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 16 Sep 2020 13:47:24 -0500 Subject: [PATCH 339/479] Update compare-list.md --- design-documents/graph-ql/coverage/catalog/compare-list.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index 7fbb657fb..6e70dcd1b 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -103,13 +103,13 @@ preconfigured at the backoffice. Guest compare list business logic not implemented yet. Additional development required. -## Current limitations +## DB changes -Existing table structure for compare list +To the existing table structure for compare list, list_id will be added ``` catalog_compare_item ------------------------------------------------------------------------------ -| catalog_compare_item_id | visitor_id | customer_id | product_id | store_id | +| catalog_compare_item_id | visitor_id | customer_id | product_id | store_id | list_id ============================================================================== ``` @@ -124,3 +124,4 @@ catalog_compare_list and adding list_id field to catalog_compare_item table. * encoded list_id will be used for client communications +* For the visitor ids created via GrahQl session will be null. From b6e841a25d83d5681279d0e2440d47c0e6f43e55 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 16 Sep 2020 15:19:29 -0500 Subject: [PATCH 340/479] Update compare-list.graphqls --- .../coverage/catalog/compare-list.graphqls | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index 0703889b5..1648afb52 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -29,23 +29,30 @@ type Query { type Mutation { createCompareList(input: CreateCompareListInput): CompareList @doc(description: "Creates a new compare list. For a logged in user, the created list is assigned to the user") - addItemsToCompareList( - uid: ID!, - items: [ID!] + addProductsToCompareList( + input: AddProductsToCompareListInput ): CompareList - removeItemsFromCompareList( - uid: ID!, - items: [ID!] + removeProductsFromCompareList( + input: RemoveProductsFromCompareListInput ): CompareList - assignCompareListToCustomer(uid: ID!): CompareList - deleteCompareList(uid: ID!): Boolean! + assignCompareListToCustomer(uid: ID!): CompareList # Customer token needs to be passed + deleteCompareList(uid: ID!): DeleteCompareListOutput } input CreateCompareListInput { - products: [ID!] + products: [ID!]! } -schema { - query: Query, - mutation: Mutation +input AddProductsToCompareListInput { + uid: ID!, + products: [ID!]! +} + +input RemoveProductsFromCompareListInput { + uid: ID!, + products: [ID!]! +} + +type DeleteCompareListOutput { + result: Boolean! } From 28fd8920cf018fe7da18894b2b804159c4c16c9c Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 16 Sep 2020 15:23:41 -0500 Subject: [PATCH 341/479] Update compare-list.graphqls --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index 1648afb52..a35858b57 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -40,7 +40,7 @@ type Mutation { } input CreateCompareListInput { - products: [ID!]! + products: [ID!] } input AddProductsToCompareListInput { From b5ac23de2bc5dc2d738538a0f4939189bfd4669f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 16 Sep 2020 15:24:00 -0500 Subject: [PATCH 342/479] Update compare-list.md --- .../graph-ql/coverage/catalog/compare-list.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index 6e70dcd1b..d5f4fecfb 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -8,7 +8,9 @@ Use createCompareList mutation to create a new compare list. The server should c ```graphql { mutation { - createCompareList(items: ["100123", "234567", "874321"]) #items optional + createCompareList(input: { + products: ["123", "456"] + }) #products optional } { list_id items { @@ -117,11 +119,11 @@ This dependency can be solved by managing the compare list state in a new table ``` catalog_compare_list ------------------------------------------------------------------------------ -| list_id (varchar) (primary)| additional_data (json) (nullable) +| list_id (varchar) (primary)| visitor_id | customer_id ============================================================================== ``` and adding list_id field to catalog_compare_item table. -* encoded list_id will be used for client communications -* For the visitor ids created via GrahQl session will be null. +* encoded list_id will be used for client communications # \Magento\Framework\Math\Random::getUniqueHash can be used for hashes +* For visitorsa created via GraphQl, session will be null. From 036a5f2e5e67f081a4f601ba83eedb89b3f18092 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 16 Sep 2020 15:24:19 -0500 Subject: [PATCH 343/479] Update compare-list.md --- design-documents/graph-ql/coverage/catalog/compare-list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.md b/design-documents/graph-ql/coverage/catalog/compare-list.md index d5f4fecfb..678f478bc 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.md +++ b/design-documents/graph-ql/coverage/catalog/compare-list.md @@ -126,4 +126,4 @@ catalog_compare_list and adding list_id field to catalog_compare_item table. * encoded list_id will be used for client communications # \Magento\Framework\Math\Random::getUniqueHash can be used for hashes -* For visitorsa created via GraphQl, session will be null. +* For visitors created via GraphQl, session will be null. From a42b1469095b94e1406bab7318f8b482c60b1a86 Mon Sep 17 00:00:00 2001 From: rakesh Date: Tue, 22 Sep 2020 19:18:22 +0530 Subject: [PATCH 344/479] uid and label changes --- .../coverage/b2b/negotiableQuotes.graphqls | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 4684bbbcb..71ad69b77 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -4,12 +4,12 @@ type Query { # in a company can see quotes belonging to their # employees. If `Customer.negotiable_quotes` is desirable for buyer's # that aren't managers, we can always add that on - negotiableQuote(iud: ID!): NegotiableQuote @doc(description: "Get a buyer's NegotiableQuote by ID") + negotiableQuote(uid: ID!): NegotiableQuote @doc(description: "Get a buyer's negotiable quote by ID") negotiableQuotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") + ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of negotiable quotes viewable by the logged-in customer") } type NegotiableQuotesOutput { @@ -25,17 +25,17 @@ type Mutation { # https://docs.magento.com/user-guide/sales/quote-request.html requestNegotiableQuote( input: RequestNegotiableQuoteInput! - ): RequestNegotiableQuoteOutput @doc(description: "Request a new Negotiable Quote for a buyer") + ): RequestNegotiableQuoteOutput @doc(description: "Request a new negotiable quote for a buyer") # Covers "Add your Comment" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information addNegotiableQuoteComment( input: AddNegotiableQuoteCommentInput! - ): AddNegotiableQuoteCommentOutput @doc(description: "Append a new comment from the buyer to a Negotiable Quote") + ): AddNegotiableQuoteCommentOutput @doc(description: "Append a new comment from the buyer to a negotiable quote") # https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#change-the-quantity updateNegotiableQuoteQuantities( input: UpdateNegotiableQuoteQuantitiesInput! - ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Change the quantity of 1 or more items already in a NegotiableQuote") + ): UpdateNegotiableQuoteItemsQuantityOutput @doc(description: "Change the quantity of 1 or more items already in a negotiable quote") # Covers "Delete Line Item" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#tabbed-sections removeNegotiableQuoteItems( @@ -45,18 +45,18 @@ type Mutation { # Covers first half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request closeNegotiableQuotes( input: CloseNegotiableQuotesInput! - ): CloseNegotiableQuotesOutput @doc(description: "Mark a Negotiable Quote as closed, leaving it visible in the storefront") + ): CloseNegotiableQuotesOutput @doc(description: "Mark a negotiable quote as closed, leaving it visible in the storefront") # Covers second half of https://docs.magento.com/user-guide/customers/account-dashboard-quotes.html#cancel-a-quote-request deleteNegotiableQuotes( input: DeleteNegotiableQuotesInput! - ): DeleteNegotiableQuotesOutput @doc(description: "Delete a Negotiable Quote, removing it from the display in the storefront") + ): DeleteNegotiableQuotesOutput @doc(description: "Delete a negotiable quote, removing it from the display in the storefront") # Covers "Select Existing Address" in https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information # "New Address" flow is covered by Mutation.createCustomerAddress setNegotiableQuoteShippingAddress( input: SetNegotiableQuoteShippingAddressInput! - ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a Negotiable Quote") + ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a negotiable quote") # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( @@ -85,14 +85,14 @@ type SetNegotiableQuoteShippingAddressOutput { } input AddNegotiableQuoteItemsInput { - quote_id: ID! @doc(description: "ID from a NegotiableQuote object") + quote_id: ID! @doc(description: "ID from a negotiable quote object") # Implementation Note: This *should* be compatible with the new, single # add to cart mutation. https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/AddProductsToCart.graphqls cart_items: [CartItemInput!]! } input DeleteNegotiableQuotesInput { - quote_ids: [ID!]! @doc(description: "A List of IDs obtained from NegotiableQuote types") + quote_ids: [ID!]! @doc(description: "A List of IDs obtained from negotiable quote types") } type DeleteNegotiableQuotesOutput { @@ -107,7 +107,7 @@ type DeleteNegotiableQuotesOutput { } input CloseNegotiableQuotesInput { - quote_ids: [ID!]! @doc(description: "A List of IDs from NegotiableQuote objects") + quote_ids: [ID!]! @doc(description: "A List of IDs from negotiable quote objects") } type CloseNegotiableQuotesOutput { @@ -117,7 +117,7 @@ type CloseNegotiableQuotesOutput { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 - ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of Negotiable Quotes viewable by the logged-in customer") + ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of negotiable quotes viewable by the logged-in customer") } input RemoveNegotiableQuoteItemsInput { @@ -195,8 +195,8 @@ enum NegotiableQuoteStatus { } input NegotiableQuoteFilterInput { - ids: FilterEqualTypeInput @doc(description: "Filter by Negotiable Quote ID(s)") - name: FilterMatchTypeInput @doc(description: "Filter by Negotiable Quote name") + ids: FilterEqualTypeInput @doc(description: "Filter by negotiable quote ID(s)") + name: FilterMatchTypeInput @doc(description: "Filter by negotiable quote name") } type NegotiableQuoteHistoryEntry { @@ -238,8 +238,8 @@ type NegotiableQuoteHistoryChanges { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L40-L73 type NegotiableQuoteHistoryStatusChange { - old_status: NegotiableQuoteStatus @doc(description: "Will be null for the first history entry on a Negotiable Quote") - new_status: NegotiableQuoteStatus! @doc(description: "Negotiable Quote History New Status.") + old_status: NegotiableQuoteStatus @doc(description: "Will be null for the first history entry on a negotiable quote") + new_status: NegotiableQuoteStatus! @doc(description: "Negotiable quote history new status.") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L48 @@ -276,8 +276,8 @@ type NegotiableQuoteHistoryProductsChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L132-L146 type NegotiableQuoteHistoryProductsRemovedChange { # Open Question: Should `removed_from_catalog` ID type represent the SKU or the product id? - removed_from_catalog: [ID!] @doc(description: "List of product skus removed from Seller's catalog") - removed_from_quote: [ProductInterface!] @doc(description: "List of products removed by a Buyer or Seller") + removed_from_catalog: [ID!] @doc(description: "List of product skus removed from seller's catalog") + removed_from_quote: [ProductInterface!] @doc(description: "List of products removed by a buyer or seller") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 @@ -323,7 +323,7 @@ type UpdateNegotiableQuoteItemsQuantityOutput { # Implementation Note: We don't want to expose the `Customer` object of the Seller to the client, and we don't # have much of a permissions model in the storefront to limit access by field. We're using a limited view # instead, excluding the ID because a Seller's ID shouldn't be exposed to the client. -type NegotiableQuoteUser @doc(description: "A limited view of a Buyer or Seller in the Negotiable Quote Process") { +type NegotiableQuoteUser @doc(description: "A limited view of a Buyer or Seller in the negotiable quote process") { firstname: String! lastname: String! } From 8aa0649e54dfc5150b41484b4cff6a60eb8434be Mon Sep 17 00:00:00 2001 From: Igor Melnikov Date: Tue, 22 Sep 2020 14:58:24 -0500 Subject: [PATCH 345/479] Apply suggestions from code review --- design-documents/graph-ql/coverage/b2b/company-credit.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md index 89b52c06a..bd87e7d56 100644 --- a/design-documents/graph-ql/coverage/b2b/company-credit.md +++ b/design-documents/graph-ql/coverage/b2b/company-credit.md @@ -22,7 +22,7 @@ type CompanyCreditOperation { type: CompanyCreditOperationType! @doc(description: "The type of the company credit operation") amount: Money @doc(description: "The amount fo the company credit operation") balance: CompanyCredit! @doc(description: "Credit balance after the company credit operation") - custom_reference_number: String @doc(description: "Purchase order number associated with the company credit operation") + custom_reference_number: String @doc(description: "Custom reference number associated with the company credit operation") updated_by: CompanyCreditOperationUser! @doc(description: "The user submitting the company credit operation") } @@ -53,7 +53,7 @@ enum CompanyCreditOperationUserType { input CompanyCreditHistoryFilterInput { operation_type: CompanyCreditOperationType @doc(description: "Enum filter by the type of the company credit operation") - custom_reference_number: String @doc(description: "Free text filter by the purchase order number associated with the company credit operation") + custom_reference_number: String @doc(description: "Free text filter by the custom reference number associated with the company credit operation") updated_by: String @doc(description: "Free text filter by the name of the person submitting the company credit operation") } ###### End: Defining new types ###### @@ -61,4 +61,3 @@ input CompanyCreditHistoryFilterInput { ``` - From f0ed46489a73206b992cafb511ff444375454dfa Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 1 Oct 2020 11:20:31 -0500 Subject: [PATCH 346/479] Add missing uids to the types and validate optional/required fields --- design-documents/graph-ql/coverage/b2b/company-credit.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md index bd87e7d56..2f5148598 100644 --- a/design-documents/graph-ql/coverage/b2b/company-credit.md +++ b/design-documents/graph-ql/coverage/b2b/company-credit.md @@ -14,10 +14,11 @@ type Company { type CompanyCreditHistory { items: [CompanyCreditOperation]! @doc(description: "An array of company credit operations") page_info: SearchResultPageInfo! @doc(description: "Metadata for pagination rendering") - total_count: Int @doc(description: "The number of the company credit operations matching the specified filter") + total_count: Int! @doc(description: "The number of the company credit operations matching the specified filter") } type CompanyCreditOperation { + uid: ID! @doc(description: "Unique identifier") date: String! @doc(description: "The date of the company credit operation") type: CompanyCreditOperationType! @doc(description: "The type of the company credit operation") amount: Money @doc(description: "The amount fo the company credit operation") From 3620b9b544ea9e25338d8bff5475c009f149730f Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 1 Oct 2020 11:41:53 -0500 Subject: [PATCH 347/479] Add missing uids to the types and validate optional/required fields --- design-documents/graph-ql/coverage/b2b/company-credit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md index 2f5148598..e04c7ffde 100644 --- a/design-documents/graph-ql/coverage/b2b/company-credit.md +++ b/design-documents/graph-ql/coverage/b2b/company-credit.md @@ -13,7 +13,7 @@ type Company { ###### Begin: Defining new types ###### type CompanyCreditHistory { items: [CompanyCreditOperation]! @doc(description: "An array of company credit operations") - page_info: SearchResultPageInfo! @doc(description: "Metadata for pagination rendering") + page_info: SearchResultPageInfo @doc(description: "Metadata for pagination rendering") total_count: Int! @doc(description: "The number of the company credit operations matching the specified filter") } @@ -21,7 +21,7 @@ type CompanyCreditOperation { uid: ID! @doc(description: "Unique identifier") date: String! @doc(description: "The date of the company credit operation") type: CompanyCreditOperationType! @doc(description: "The type of the company credit operation") - amount: Money @doc(description: "The amount fo the company credit operation") + amount: Money! @doc(description: "The amount fo the company credit operation") balance: CompanyCredit! @doc(description: "Credit balance after the company credit operation") custom_reference_number: String @doc(description: "Custom reference number associated with the company credit operation") updated_by: CompanyCreditOperationUser! @doc(description: "The user submitting the company credit operation") From 9618c91f61661ef3c6e084e47263be1e0985d848 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Fri, 2 Oct 2020 12:29:30 -0500 Subject: [PATCH 348/479] Fix naming conventions and optional/required fields --- .../graph-ql/coverage/b2b/company.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company.md b/design-documents/graph-ql/coverage/b2b/company.md index c1e377ccb..8563c9d9c 100644 --- a/design-documents/graph-ql/coverage/b2b/company.md +++ b/design-documents/graph-ql/coverage/b2b/company.md @@ -24,7 +24,7 @@ type Company @doc(description: "Company entity output data schema.") { name: String @doc(description: "Company name.") email: String @doc(description: "Company email address.") legal_name: String @doc(description: "Company legal name.") - vat_id: String @doc(description: "Company VAT/TAX id.") + vat_tax_id: String @doc(description: "Company VAT/TAX id.") reseller_id: String @doc(description: "Company re-seller id.") legal_address: CompanyLegalAddress @doc(description: "Company legal address.") company_admin: Customer @doc(description: "An object containing information about Company Administrator.") @@ -74,14 +74,14 @@ type CompanySalesRepresentative @doc(description: "Company sales representative } type CompanyUsers @doc(description: "Output data schema for an object returned by a Company users search query.") { - items: [Customer] @doc(description: "An array of 'CompanyUser' objects that match the specified search criteria.") - total_count: Int @doc(description: "The number of objects returned.") + items: [Customer]! @doc(description: "An array of 'CompanyUser' objects that match the specified search criteria.") + total_count: Int! @doc(description: "The number of objects returned.") page_info: SearchResultPageInfo @doc(description: "Pagination meta data.") } type CompanyRoles @doc(description: "Output data schema for an object returned by a Company roles search query.") { - items: [CompanyRole] @doc(description: "A list of company roles that match the specified search criteria.") - total_count: Int @doc(description: "The total number of objects matching the specified filter.") + items: [CompanyRole]! @doc(description: "A list of company roles that match the specified search criteria.") + total_count: Int! @doc(description: "The total number of objects matching the specified filter.") page_info: SearchResultPageInfo @doc(description: "Pagination meta data.") } @@ -95,24 +95,24 @@ type CompanyRole @doc(description: "Company role output data schema returned in type CompanyAclResource @doc(description: "Output data schema for an object with Role permission resource information.") { uid: ID! @doc(description: "ACL resource id.") text: String @doc(description: "ACL resource label.") - sortOrder: Int @doc(description: "ACL resource sort order.") + sort_order: Int @doc(description: "ACL resource sort order.") children: [CompanyAclResource!] @doc(description: "An array of sub-resources.") } type CompanyRoleNameCheckResponse @doc(description: "Response object schema for a role name validation query.") { - isNameValid: Boolean @doc(description: "Role name validation result") + is_name_valid: Boolean @doc(description: "Role name validation result") } type CompanyUserEmailCheckResponse @doc(description: "Response object schema for a Company User email validation query.") { - isEmailValid: Boolean @doc(description: "Email validation result") + is_email_valid: Boolean @doc(description: "Email validation result") } type CompanyAdminEmailCheckResponse @doc(description: "Response object schema for a Company Admin email validation query.") { - isEmailValid: Boolean @doc(description: "Email validation result") + is_email_valid: Boolean @doc(description: "Email validation result") } type CompanyEmailCheckResponse @doc(description: "Response object schema for a Company email validation query.") { - isEmailValid: Boolean @doc(description: "Email validation result") + is_email_valid: Boolean @doc(description: "Email validation result") } union CompanyStructureEntity = CompanyTeam | Customer From 2a42ae5d8731e09d5ec0bd3fa6ef0ee8ba0f563a Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 6 Oct 2020 15:38:27 -0500 Subject: [PATCH 349/479] Enabling adding gift registrants while creating gift registry --- .../graph-ql/coverage/customer/gift-registry.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 9efa70f2d..7a1d01681 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -86,6 +86,7 @@ input CreateGiftRegistryInput { privacy_settings: GiftRegistryPrivacySettings! status: GiftRegistryStatus! shipping_address: GiftRegistryShippingAddressInput + registrants: [AddGiftRegistryRegistrantInput!]! dynamic_attributes: [GiftRegistryDynamicAttributeInput] } From fdc1665dcbe7002114fdcc511c15b3f37f42dba4 Mon Sep 17 00:00:00 2001 From: Ji Lu Date: Wed, 7 Oct 2020 11:04:25 -0500 Subject: [PATCH 350/479] MQE-2298: Move and update test BIC document to DevDocs --- ...oning-and-backward-compatibility-policy.md | 119 +----------------- 1 file changed, 2 insertions(+), 117 deletions(-) diff --git a/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md b/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md index 526965ee2..246a2f6c3 100644 --- a/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md +++ b/design-documents/testing/functional/versioning-and-backward-compatibility-policy.md @@ -1,119 +1,4 @@ # Magento MFTF test versioning and backward compatibility policy - -## Goals and requirements -1. Release MFTF tests as a separate magento package on repo.magento.com. -2. Define the versioning strategy for MFTF test packages. -3. Outline what is considered a backward incompatible change to MFTF tests. -4. List of what should be implemented. - -## Backwards compatibility definition for MFTF tests - -When a test undergoes changes, but achieves the same testing results as before and remains compatible with potential test customizations, this is defined as a 'backwards compatible' change. - -Types of changes: - -- **Test Flow change (Test/ActionGroup)** - A backwards compatible modification of a test flow would not diminish the original set of actions in the test. Some changes may change action's sequence (behavior), but they allow any extension to achieve the same test results without changing the test extension (e.g a 'merge file'). -- **Test Entity change (Data/Section/Page/Metadata)** - Compatible modifications of entities are 1) adding new entities or 2) updating a `value` of an existing entity in a way where the test will **NOT** require updates. -- **Test Annotation change** - Annotations can be changed without limitation and will always be considered a backward compatible change, but removing or changing a `` annotation will be considered a backward incompatible change. -- Changes which delete and/or rename a (Test/Action Group/Data/Metadata/Page/Section/Action)'s `id` attribute will be considered a backward incompatible change. Changing a reference to a data entity will also be considered a backward incompatible change. - -## Versioning policy - -The approach of defining what each release should include was taken from [Semantic Versioning](https://semver.org/). - -3-component version numbers ---------------------------- - - X.Y.Z - | | | - | | +-- Backward Compatible changes (bug fixes) - | +---- Backward Compatible changes (new features) - +------ Backward Incompatible changes - -### Z release - -Patch version **Z** MUST be incremented if only backward compatible changes to tests are introduced. -For instance: a fix which aims to resolve test flakiness. This can be done by updating an unreliable selector, adding a `wait` to an element, or updating a data entity value. - -### Y release - -Minor version **Y** MUST be incremented if a new, backwards compatible test or test entity is introduced. -It MUST be incremented if any test or test entity is marked as `deprecated`. -It MAY include patch level changes. Patch version MUST be reset to 0 when the minor version is incremented. - -### X release - -Major version **X** MUST be incremented if any backwards incompatible changes are introduced to a test or test entity. -It MAY include minor and patch level changes. Patch and minor version MUST be reset to 0 when the major version is incremented. - -## Implementation tasks - -1. Add Semantic Version analyzer to be able automatically define the release type of the MFTF tests package. -2. Update publication infrastructure to exclude tests from `magento2-module` package type. -3. Introduce publication functionality for publishing `magento2-test-module` package type. -4. Create a metapackage with test packages specifically for each Magento edition. - -## Version increase matrix - -|Entity Type|Change|Version Increase| -|---|---|---| -|ActionGroup|`` added|MINOR -| |`` removed|MAJOR -| |`` `` added|MINOR -| |`` `` removed|MAJOR -| |`` `` type changed|PATCH -| |`` `` attribute changed|PATCH -| |`` `` with `defaultValue`added|MINOR -| |`` `` without `defaultValue` added|MAJOR -| |`` `` removed|MAJOR -| |`` `` changed|MAJOR -|Data|`` added|MINOR -| |`` removed|MAJOR -| |`` `` added|MINOR -| |`` `` removed|MAJOR -| |`` `` `` removed|PATCH -| |`` `` added|MINOR -| |`` `` removed|MAJOR -| |`` `` added|MAJOR -| |`` `` removed|MAJOR -| |`` `` added|MAJOR -| |`` `` removed|MAJOR -|Metadata|`` added|MINOR -| |`` removed|MAJOR -| |`` changed|MINOR -| |`` child element added|MINOR -| |`` child element removed|MAJOR -|Page|`` added|MINOR -| |`` removed|MAJOR -| |`` `
` added|MINOR -| |`` `
` removed|MAJOR -|Section|`
` added|MINOR -| |`
` removed|MAJOR -| |`
` `` added|MINOR -| |`
` `` removed|MAJOR -| |`
` `` `selector` changed|PATCH -| |`
` `` `type` changed|PATCH -| |`
` `` `parameterized` changed|MAJOR -|Test|`` added|MINOR -| |`` removed|MAJOR -| |`` `` added|MINOR -| |`` `` removed|MAJOR -| |`` `` changed|PATCH -| |`` `` sequence changed|MAJOR -| |`` `` type (`click`, `fillField`, etc) changed|PATCH -| |`` `` `ref` changed|MAJOR -| |`` (before/after) `` added|MINOR -| |`` (before/after) `` removed|MAJOR -| |`` (before/after) `` changed|PATCH -| |`` (before/after) `` `ref` changed|MINOR -| |`` (before/after) `` sequence changed|MAJOR -| |`` (before/after) `` type (`click`, `fillField`, etc) changed|PATCH -| |`` (before/after) `` `ref` changed|MAJOR -| |`` `` `` added|PATCH -| |`` `` `` changed|PATCH -| |`` `` `` GROUP removed|MAJOR - ---------------------------- - - ⃰ - `` refers to any of the available [MFTF Actions](https://github.com/magento/magento2-functional-testing-framework/blob/develop/docs/test/actions.md). +Refer to: +https://devdocs.magento.com/guides/v2.4/extension-dev-guide/versioning/mftf-tests-codebase-changes.html From 328df05efa442ce1439e045211c1c75165fd1d15 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Wed, 7 Oct 2020 15:39:28 -0500 Subject: [PATCH 351/479] Fix naming and align with best practices --- .../coverage/b2b/requisitionList.graphqls | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 05b7ed3a8..b9ec6b6ec 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -25,7 +25,7 @@ # type Customer { - requisitionLists( + requisition_lists( pageSize: Int = 20, currentPage: Int = 1, filter: RequisitionListFilterInput @@ -33,9 +33,9 @@ type Customer { } type RequisitionLists @doc(description: "Provides Customer's Requisition Lists") { - items: [RequisitionList] @doc(description: "List of Requisition Lists") + items: [RequisitionList]! @doc(description: "List of Requisition Lists") page_info: SearchResultPageInfo @doc(description: "Page Information for pagination") - total_count: Int @doc(description: "Total count of Requisition Lists") + total_count: Int! @doc(description: "Total count of Requisition Lists") } type RequisitionList @doc(description: "Requisition List Type") { @@ -51,9 +51,9 @@ type RequisitionList @doc(description: "Requisition List Type") { } type RequistionListItems { - items: [RequisitionListItemInterface] @doc(description: "Requisition List items list") + items: [RequisitionListItemInterface]! @doc(description: "Requisition List items list") page_info: SearchResultPageInfo - total_pages: Int @doc(description: "total count of req list items") + total_pages: Int! @doc(description: "total count of req list items") } interface RequisitionListItemInterface @doc(description: "Interface type for Requisition List Item") { @@ -129,25 +129,25 @@ type Mutation { items: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") - addRequisitionListItemToCart( - requisitionListId: ID!, @doc(description: "unique Id of requisition list") - itemIds: [ID!]! @doc(description: "selected requisition list items that are to be added") - ): AddRequisitionListItemToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") + addRequisitionListItemsToCart( + listUid: ID!, @doc(description: "unique Id of requisition list") + itemUids: [ID!]! @doc(description: "selected requisition list items that are to be added") + ): AddRequisitionListItemsToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") - copyItemsBetweenRequisitionList( - sourceId: ID!, @doc(description: "unique Id of source requisition list") - destinationId: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - itemIds: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") - ): CopyItemsFromRequisitionListOutput @doc(description: "Copy Items from Requisition List to another requisition list") + copyItemsBetweenRequisitionLists( + sourceUid: ID!, @doc(description: "unique Id of source requisition list") + destinationUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + itemUids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") + ): CopyItemsBetweenRequisitionListsOutput @doc(description: "Copy Items from Requisition List to another requisition list") - moveItemsBetweenRequisitionList( - sourceId: ID!, @doc(description: "unique Id of source requisition list") - destinationId: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - itemIds: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") - ): MoveItemsFromRequisitionListOutput @doc(description: "Move Items from Requisition List to another requisition List") + moveItemsBetweenRequisitionLists( + sourceUid: ID!, @doc(description: "unique Id of source requisition list") + destinationUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + itemUids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") + ): MoveItemsBetweenRequisitionListsOutput @doc(description: "Move Items from Requisition List to another requisition List") clearCustomerCart( - cartId: String! @doc(description: "masked Cart Id") + cartUid: String! @doc(description: "masked Cart Id") ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } @@ -164,7 +164,7 @@ type AddProductsToRequisitionListOutput { } input RequisitionListFilterInput { - ids: FilterEqualTypeInput, @doc(description: "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs") + uids: FilterEqualTypeInput, @doc(description: "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs") name: FilterMatchTypeInput @doc(description: "Filter by display name of the Requisition list") } @@ -192,18 +192,18 @@ type RenameRequisitionListOutput { } type DeleteRequisitionListOutput { - result: Boolean + requisition_list : RequisitionList } -type AddRequisitionListItemToCartOutput { +type AddRequisitionListItemsToCartOutput { cart: Cart # since requisition list is not mutated it is not part of the output } -type CopyItemsFromRequisitionListOutput { +type CopyItemsBetweenRequisitionListsOutput { list : RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output } -type MoveItemsFromRequisitionListOutput { +type MoveItemsBetweenRequisitionListsOutput { source : RequisitionList @doc(description: "Source Requisition List") destination : RequisitionList @doc(description: "Destination Requisition List") } From bb14b030971c998fd8ba94813d8b5f564b86f016 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 8 Oct 2020 15:44:58 -0500 Subject: [PATCH 352/479] GraphQL schema for dynamic blocks --- .../graph-ql/coverage/dynamic-blocks.graphqls | 49 +++++++++++++++++++ .../graph-ql/coverage/dynamic-blocks.md | 41 ++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 design-documents/graph-ql/coverage/dynamic-blocks.graphqls create mode 100644 design-documents/graph-ql/coverage/dynamic-blocks.md diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls new file mode 100644 index 000000000..5b5d73728 --- /dev/null +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -0,0 +1,49 @@ +type DynamicBlock { + uid: ID! + name: String! + content: String! +} + +type DynamicBlocks { + items: [DynamicBlock]! + page_info: SearchResultPageInfo + total_count: Int! +} + +# We don't need is_enabled, locations, ga_creative, catalog_price_rule_ids, cart_price_rules_ids, locations on storefront + +type DynamicBlocksFilterInput { + type: DynamicBlockTypeEnum! + locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied + rotation_mode: DynamicBlockRotationModeEnum! + dynamic_block_uids: [Int!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED +} + +type DynamicBlocksOutput { + dynamic_blocks: DynamicBlocks! +} + +enum DynamicBlockTypeEnum { + SPECIFIED + CART_PRICE_RULE_RELATED + CATALOG_PRICE_RULE_RELATED +} + +enum DynamicBlockLocationEnum { + CONTENT + HEADER + FOOTER + LEFT + RIGHT +} + +enum DynamicBlockRotationModeEnum { + NO_ROTATION @doc(description: "Display all.") + RANDOM + SERIES + SHUFFLE +} + +type Query { + dynamic_blocks(input: DynamicBlocksFilterInput): DynamicBlocksOutput +} \ No newline at end of file diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.md b/design-documents/graph-ql/coverage/dynamic-blocks.md new file mode 100644 index 000000000..cb1b8f02c --- /dev/null +++ b/design-documents/graph-ql/coverage/dynamic-blocks.md @@ -0,0 +1,41 @@ +## Dynamic Blocks + +Dynamic blocks can be inserted in CMS pages and blocks using widgets. Below is the sample markup of the widget inserted on the page. + +``` +{{widget type="Magento\Banner\Block\Widget\Banner" display_mode="fixed" types="content" rotate="random" banner_ids="1" template="widget/block.phtml" unique_id="34f6520f51ca9b79c91d1f56b5b453adc4d9ff235a7c9f5b04d1b0557584c5bc"}} +``` + +In Luma dynamic blocks rendered to a JavaScript component definition that does request to a backend to load content of the dynamic block. Here is an example of a JavaScript component definition. + +``` +
+ +
+``` + +PWA will receive similar JavaScript component definition that can be parsed to extract parameters and make a [GraphQL query](./dynamic-blocks.graphqls) to load dynamic blocks. + +```graphql +{ + dynamic_blocks(input: {type: SPECIFIED, locations: [CONTENT], rotation_mode: RANDOM, dynamic_block_uids: [1, 2]}) { + items { + uid + name + content + } + page_info { + current_page + page_size + total_pages + } + total_count + } +} +``` From 3e61b27f7b69df73ae760736247f86aa29566f47 Mon Sep 17 00:00:00 2001 From: Nishant Kapoor Date: Mon, 12 Oct 2020 10:11:49 -0500 Subject: [PATCH 353/479] Rename requsition_list to list to be consistent @melnikovi @RakeshJesadiya --- .../graph-ql/coverage/b2b/requisitionList.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 05b7ed3a8..7f37bb790 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -152,15 +152,15 @@ type Mutation { } type RemoveRequisitionListItemsOutput { - requisition_list : RequisitionList + list : RequisitionList } type UpdateRequisitionListItemsOutput { - requisition_list : RequisitionList + list : RequisitionList } type AddProductsToRequisitionListOutput { - requisition_list : RequisitionList + list : RequisitionList } input RequisitionListFilterInput { From 3db244b63f8dd7c4049a9263bc100fb498100569 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 15 Oct 2020 14:46:02 -0500 Subject: [PATCH 354/479] Update configurable-options-selection.graphqls --- .../catalog/configurable-options-selection.graphqls | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index b77985033..e56f431ef 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -1,21 +1,18 @@ type ConfigurableProduct { configurable_options_selection_metadata(selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") + variants: [ConfigurableVariant] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") + configurable_options: [ConfigurableProductOptions] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") } type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { - options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") + options_available_for_selection: [ConfigurableProductOptions!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } -type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { - option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") - attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") -} - # Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart type ConfigurableProductOptionsValues { uid: ID! - is_available_for_selection: Boolean! + is_available_for_selection: Boolean! # when display OOS is enabled, if the variant selection is OOS, this will return false. } From 4c7db6c643d6ab44bd0092e6d6482eb7b76192af Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 15 Oct 2020 15:29:22 -0500 Subject: [PATCH 355/479] Update design-documents/graph-ql/coverage/catalog/compare-list.graphqls Co-authored-by: Igor Melnikov --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index a35858b57..d2cfc840b 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -10,7 +10,7 @@ type ComparableItem { type ComparableAttribute { code: String! @doc(description: "Attribute code ") - title: String! @doc(description: "Attribute display title") + label: String! @doc(description: "Attribute label") } type CompareList { From b9679f5e99f57ec55f5d47be54cb74187ea59795 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 15 Oct 2020 15:57:40 -0500 Subject: [PATCH 356/479] Update compare-list.graphqls --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index d2cfc840b..a12e4683e 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -2,7 +2,7 @@ type ComparableItem { productId: ID! @doc(description: "Product Id") name: String! @doc(description: "Product name") sku: String! @doc(description: "Product SKU") - priceRange: ProductPriceRange! @doc(description: "Product prices") + price_range: ProductPriceRange! @doc(description: "Product prices") canonical_url: String @doc(description: "Product URL") images: [ProductImage]! @doc(description: "Product Images") values: [ProductAttribute]! @doc(description: "Product comparable attributes") From bd8e5b6df1e885bdb6e81019c83b6f644d8b662f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Fri, 16 Oct 2020 09:12:55 -0500 Subject: [PATCH 357/479] Removing product_id for now, since its not in scope. Will have to decide between product_id and exposing product interface --- design-documents/graph-ql/coverage/catalog/compare-list.graphqls | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index a12e4683e..c1eca23c1 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -1,5 +1,4 @@ type ComparableItem { - productId: ID! @doc(description: "Product Id") name: String! @doc(description: "Product name") sku: String! @doc(description: "Product SKU") price_range: ProductPriceRange! @doc(description: "Product prices") From 6547dbd71d11c3f31c890ad1fbbb0c0542fdf726 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Tue, 20 Oct 2020 12:34:51 -0500 Subject: [PATCH 358/479] Minor updates --- .../graph-ql/coverage/dynamic-blocks.graphqls | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index 5b5d73728..905025b0d 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -1,26 +1,19 @@ type DynamicBlock { uid: ID! - name: String! content: String! } -type DynamicBlocks { - items: [DynamicBlock]! - page_info: SearchResultPageInfo - total_count: Int! -} - -# We don't need is_enabled, locations, ga_creative, catalog_price_rule_ids, cart_price_rules_ids, locations on storefront +# We don't need name, is_enabled, locations, ga_creative, catalog_price_rule_ids, cart_price_rules_ids, locations on storefront type DynamicBlocksFilterInput { type: DynamicBlockTypeEnum! locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied rotation_mode: DynamicBlockRotationModeEnum! - dynamic_block_uids: [Int!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED + dynamic_block_uids: [ID!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED } type DynamicBlocksOutput { - dynamic_blocks: DynamicBlocks! + items: [DynamicBlock]! } enum DynamicBlockTypeEnum { From 74b69e4e1e916351a16cd93b8150aedc1d76997c Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Oct 2020 17:20:11 -0500 Subject: [PATCH 359/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 51f41c8d7..45be81a6f 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -84,6 +84,10 @@ type BundleWishlistItem implements WishlistItemInterface { } type GiftCardWishlistItem implements WishlistItemInterface { + gift_card_options: GiftCardOptions! +} + +type GiftCardOptions { sender_name: String! sender_email: String! recipient_name: String From 9b76d27ac776da1d69895eb3ba8b1ed61acfc67e Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Oct 2020 17:21:12 -0500 Subject: [PATCH 360/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 45be81a6f..484df73db 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -92,7 +92,7 @@ type GiftCardOptions { sender_email: String! recipient_name: String recipient_email: String - amount: SelectedGiftCardAmount + amount: Amount message: String } From 76c40d7aafa082cd5056f91ead64b5cde191584b Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Oct 2020 17:58:15 -0500 Subject: [PATCH 361/479] Update configurable-options-selection.graphqls --- .../configurable-options-selection.graphqls | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index e56f431ef..d0ba57e00 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -6,13 +6,24 @@ type ConfigurableProduct { type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { - options_available_for_selection: [ConfigurableProductOptions!] @doc(description: "Configurable options available for further selection based on current selection.") + options_available_for_selection: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } -# Configurable option value type has to be extended to include ID which can be used to uniquely identify the option value across the system. This is consistent with proposal of single mutation for add-to-cart -type ConfigurableProductOptionsValues { +type ConfigurableProductOption { uid: ID! - is_available_for_selection: Boolean! # when display OOS is enabled, if the variant selection is OOS, this will return false. + attribute_code: String! + attribute_uid: ID! + label: String! + position: Int! + values: [ConfigurableProductOptionValue!] +} + +type ConfigurableProductOptionValue { + uid: ID! + is_available: Boolean! # when display OOS is enabled, if the variant selection is OOS, this will return false. + is_default: Boolean! + label: String! + swatch: SwatchDataInterface } From 161852f2bfe533bc181d67b6e4c5da7e5af4b6d4 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Oct 2020 18:04:28 -0500 Subject: [PATCH 362/479] Update configurable-options-selection.graphqls --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index d0ba57e00..00b0ba06a 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -1,5 +1,5 @@ type ConfigurableProduct { - configurable_options_selection_metadata(selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") + configurable_options_v2(selected_configurable_option_values: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") variants: [ConfigurableVariant] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") } From 77ee9b898fea3008b3a1312b87dba9d0a0796e4f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 20 Oct 2020 18:51:47 -0500 Subject: [PATCH 363/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 484df73db..733d16f4f 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -92,7 +92,7 @@ type GiftCardOptions { sender_email: String! recipient_name: String recipient_email: String - amount: Amount + amount: Money message: String } From cc87e2dcd22b09be02eaabd7affc553b4d7d13df Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Wed, 21 Oct 2020 09:41:49 -0500 Subject: [PATCH 364/479] Fix naming conventions --- .../graph-ql/coverage/b2b/company.md | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/company.md b/design-documents/graph-ql/coverage/b2b/company.md index 8563c9d9c..8d411c184 100644 --- a/design-documents/graph-ql/coverage/b2b/company.md +++ b/design-documents/graph-ql/coverage/b2b/company.md @@ -13,10 +13,10 @@ Delete mutations return a field indicating success or failure. In the case of fa ```graphql type Query { company: Company @doc(description: "Company assigned to the currently authenticated user") - checkCompanyEmail(email: String!): CompanyEmailCheckResponse @doc(description: "Check if an email is valid for company registration") - checkCompanyAdminEmail(email: String!): CompanyAdminEmailCheckResponse @doc(description: "Check if an email is valid for company admin registration") - checkCompanyUserEmail(email: String!): CompanyUserEmailCheckResponse @doc(description: "Check if an email is valid for company user registration") - checkCompanyRoleName(name: String!): CompanyRoleNameCheckResponse @doc(description: "Check if a role name is valid for company") + isCompanyEmailAvailable(email: String!): IsCompanyEmailAvailableOutput @doc(description: "Check if an email is valid for company registration") + isCompanyAdminEmailAvailable(email: String!): IsCompanyAdminEmailAvailableOutput @doc(description: "Check if an email is valid for company admin registration") + isCompanyUserEmailAvailable(email: String!): IsCompanyUserEmailAvailableOutput @doc(description: "Check if an email is valid for company user registration") + isCompanyRoleNameAvailable(name: String!): IsCompanyRoleNameAvailableOutput @doc(description: "Check if a role name is valid for company") } type Company @doc(description: "Company entity output data schema.") { @@ -99,20 +99,20 @@ type CompanyAclResource @doc(description: "Output data schema for an object with children: [CompanyAclResource!] @doc(description: "An array of sub-resources.") } -type CompanyRoleNameCheckResponse @doc(description: "Response object schema for a role name validation query.") { - is_name_valid: Boolean @doc(description: "Role name validation result") +type IsCompanyRoleNameAvailableOutput @doc(description: "Response object schema for a role name validation query.") { + is_role_name_available: Boolean! @doc(description: "Role name validation result") } -type CompanyUserEmailCheckResponse @doc(description: "Response object schema for a Company User email validation query.") { - is_email_valid: Boolean @doc(description: "Email validation result") +type IsCompanyUserEmailAvailableOutput @doc(description: "Response object schema for a Company User email validation query.") { + is_email_available: Boolean! @doc(description: "Email validation result") } -type CompanyAdminEmailCheckResponse @doc(description: "Response object schema for a Company Admin email validation query.") { - is_email_valid: Boolean @doc(description: "Email validation result") +type IsCompanyAdminEmailAvailableOutput @doc(description: "Response object schema for a Company Admin email validation query.") { + is_email_available: Boolean! @doc(description: "Email validation result") } -type CompanyEmailCheckResponse @doc(description: "Response object schema for a Company email validation query.") { - is_email_valid: Boolean @doc(description: "Email validation result") +type IsCompanyEmailAvailableOutput @doc(description: "Response object schema for a Company email validation query.") { + is_email_available: Boolean! @doc(description: "Email validation result") } union CompanyStructureEntity = CompanyTeam | Customer @@ -209,7 +209,6 @@ type UpdateCompanyStructureOutput @doc(description: "Update company structure ou company: Company! @doc(description: "Updated company instance.") } - input CompanyCreateInput @doc(description: "Defines the Company input data schema for creating a new entity."){ company_name: String! @doc(description: "Company name. Required.") company_email: String! @doc(description: "Company email address. Required.") From c9ce7e974a0918ee7682eec3ef5c8f019535014a Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Wed, 21 Oct 2020 09:47:13 -0500 Subject: [PATCH 365/479] Clarify what is CompanyCreditOperation.uid --- design-documents/graph-ql/coverage/b2b/company-credit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/company-credit.md b/design-documents/graph-ql/coverage/b2b/company-credit.md index e04c7ffde..b0b1000f4 100644 --- a/design-documents/graph-ql/coverage/b2b/company-credit.md +++ b/design-documents/graph-ql/coverage/b2b/company-credit.md @@ -18,7 +18,7 @@ type CompanyCreditHistory { } type CompanyCreditOperation { - uid: ID! @doc(description: "Unique identifier") + uid: ID! @doc(description: "Unique identifier") # id of the log entry date: String! @doc(description: "The date of the company credit operation") type: CompanyCreditOperationType! @doc(description: "The type of the company credit operation") amount: Money! @doc(description: "The amount fo the company credit operation") From 4f3ab72eb66e64b9f1f5cb80d8ffb2663647d079 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 14:55:59 -0500 Subject: [PATCH 366/479] Update Wishlist.graphqls --- .../graph-ql/coverage/customer/Wishlist.graphqls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 733d16f4f..4cc054d7a 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -88,11 +88,12 @@ type GiftCardWishlistItem implements WishlistItemInterface { } type GiftCardOptions { - sender_name: String! - sender_email: String! + sender_name: String + sender_email: String recipient_name: String recipient_email: String amount: Money + custom_giftcard_amount: Money message: String } From daea56c6eccf99c6b0d0bf1f6336b0db406341b9 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 15:39:19 -0500 Subject: [PATCH 367/479] Update design-documents/graph-ql/coverage/customer/Wishlist.graphqls Co-authored-by: Igor Melnikov --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 4cc054d7a..31e7fd720 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -125,7 +125,6 @@ type UpdateWishlistOutput { } type StoreConfig { - maximum_number_of_wishlists: String @doc(description: "If multiple wish lists are enabled, the maximum number of wish lists the customer can have") + maximum_number_of_wishlists: Int @doc(description: "If multiple wish lists are enabled, the maximum number of wish lists the customer can have") enable_multiple_wishlists: String @doc(description: "Indicates whether customers can have multiple wish lists. Possible values: 1 (Yes) and 0 (No)") } - From a0727f015733e9087c5c9d553c37990a976c2a4e Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 15:39:35 -0500 Subject: [PATCH 368/479] Update design-documents/graph-ql/coverage/customer/Wishlist.graphqls Co-authored-by: Igor Melnikov --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 31e7fd720..c11046311 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -126,5 +126,5 @@ type UpdateWishlistOutput { type StoreConfig { maximum_number_of_wishlists: Int @doc(description: "If multiple wish lists are enabled, the maximum number of wish lists the customer can have") - enable_multiple_wishlists: String @doc(description: "Indicates whether customers can have multiple wish lists. Possible values: 1 (Yes) and 0 (No)") + enable_multiple_wishlists: Boolean @doc(description: "Indicates whether customers can have multiple wish lists.") } From 32f1cbe733c29925a6de82a9d2a45ee77c683e0a Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 16:18:34 -0500 Subject: [PATCH 369/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 00b0ba06a..ef6a405a2 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -16,7 +16,7 @@ type ConfigurableProductOption { attribute_code: String! attribute_uid: ID! label: String! - position: Int! + sort_order: Int! values: [ConfigurableProductOptionValue!] } From eb9ca5bb5fa9b5da2560b146dd5bd9a80fc9140f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 16:19:10 -0500 Subject: [PATCH 370/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index ef6a405a2..011957962 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -1,5 +1,5 @@ type ConfigurableProduct { - configurable_options_v2(selected_configurable_option_values: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") + configurable_product_options_selection(configurableOptionValues: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") variants: [ConfigurableVariant] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") } From e28ca7a7b64892c3cf7065ab9033815f987542ed Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 16:19:16 -0500 Subject: [PATCH 371/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 011957962..630d2e6a2 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -6,7 +6,7 @@ type ConfigurableProduct { type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { - options_available_for_selection: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") + configurable_options: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") variant: SimpleProduct @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") } From c69b6a3d09c28c62c22261523c4637644827b26e Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 21 Oct 2020 16:20:20 -0500 Subject: [PATCH 372/479] Update configurable-options-selection.graphqls --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 630d2e6a2..615e10302 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -4,7 +4,7 @@ type ConfigurableProduct { configurable_options: [ConfigurableProductOptions] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") } -type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") +type ConfigurableProductOptionsSelection @doc(description: "Metadata corresponding to the configurable options selection.") { configurable_options: [ConfigurableProductOption!] @doc(description: "Configurable options available for further selection based on current selection.") media_gallery: [MediaGalleryInterface!] @doc(description: "Product images and videos corresponding to the specified configurable options selection.") From 56809278f56b45a07db2512aca938ad8a6bf5470 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 12:37:48 -0500 Subject: [PATCH 373/479] aligning schema structure with requisition list. --- .../coverage/customer/Wishlist.graphqls | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index c11046311..012e019f9 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -1,12 +1,12 @@ type Mutation { - createWishlist(name: String!, visibility: WishlistVisibilityEnum!): CreateWishlistOutput # Multiple wishlists Commerce functionality - deleteWishlist(wishlistId: ID!): Boolean # Commerce fucntionality - in Opens Source we assume customer always has one wishlist + createWishlist(input: CreateWishlistInput!): CreateWishlistOutput # Multiple wishlists Commerce functionality + deleteWishlist(wishlistId: ID!): DeleteWishlistOutput # Commerce fucntionality - in Opens Source we assume customer always has one wishlist addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput - copyProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemCopyInput!]!): UpdateProductsInWishlistOutput @doc(description: "Copy a product to the wish list") - moveProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemMoveInput!]!): UpdateProductsInWishlistOutput @doc(description: "Move products from one wish list to another") - updateWishlist(wishlistId: ID!, name: String, visibility: WishlistVisibilityEnum): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") + copyProductsBetweenWishlists(sourceUid: ID!, destinationUid: ID!, wishlistItems: [WishlistItemCopyInput!]!): CopyProductsBetweenWishlistsOutput @doc(description: "Copy a product to the wish list") + moveProductsBetweenWishlists(sourceUid: ID!, destinationUid: ID!, wishlistItems: [WishlistItemMoveInput!]!): MoveProductsBetweenWishlistsOutput @doc(description: "Move products from one wish list to another") + updateWishlist(input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") } type Customer { @@ -23,6 +23,18 @@ type Wishlist { sharing_code: String updated_at: String name: String @doc(description: "Avaialble in Commerce edition only") + visibility: WishlistVisibilityEnum! +} + +input CreateWishlistInput { + name: String! + visibility: WishlistVisibilityEnum! +} + +input UpdateWishlistInput { + wishlistId: ID! + name: String + visibility: WishlistVisibilityEnum } input WishlistItemUpdateInput { @@ -103,9 +115,12 @@ enum WishlistVisibilityEnum @doc(description: "This enumeration defines the wish } type CreateWishlistOutput { - uid: ID! @doc(description: "The ID of the new wish list") - name: String! @doc(description: "The wish list name") - visibility: WishlistVisibilityEnum! @doc(description: "The wish list visibility") + wishlist: Wishlist +} + +type DeleteWishlistOutput { + status: Boolean! + wishlists: [Wishlist!]! } input WishlistItemCopyInput { @@ -119,9 +134,17 @@ input WishlistItemMoveInput { } type UpdateWishlistOutput { - uid: ID! @doc(description: "The ID of the updated wish list") - name: String! @doc(description: "The wish list name") - visibility: WishlistVisibilityEnum! @doc(description: "The wish list visibility") + wishlist: Wishlist +} + +type CopyProductsBetweenWishlistsOutput { + source_wishlist: Wishlist! + destination_wishlist: Wishlist! +} + +type MoveProductsBetweenWishlistsOutput { + source_wishlist: Wishlist! + destination_wishlist: Wishlist! } type StoreConfig { From 2f7025544acaec122b60580f34b59dfc8e4f93bc Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 12:45:40 -0500 Subject: [PATCH 374/479] After internal discussion, decided its better to return ProductInterface at the ComparableItem Simplifies implementation and usage. --- .../graph-ql/coverage/catalog/compare-list.graphqls | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index c1eca23c1..ea5a4e1be 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -1,13 +1,10 @@ type ComparableItem { - name: String! @doc(description: "Product name") - sku: String! @doc(description: "Product SKU") - price_range: ProductPriceRange! @doc(description: "Product prices") - canonical_url: String @doc(description: "Product URL") - images: [ProductImage]! @doc(description: "Product Images") - values: [ProductAttribute]! @doc(description: "Product comparable attributes") + uid: ID! + product: ProductInterface! } type ComparableAttribute { + uid: ID! code: String! @doc(description: "Attribute code ") label: String! @doc(description: "Attribute label") } From faadf5030f18c172def742e593f3228e6d7c6096 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 13:07:00 -0500 Subject: [PATCH 375/479] Update compare-list.graphqls --- design-documents/graph-ql/coverage/catalog/compare-list.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index ea5a4e1be..430649ec1 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -1,6 +1,7 @@ type ComparableItem { uid: ID! product: ProductInterface! + attributes: [Attribute]! @doc(description: "Product comparable attributes") } type ComparableAttribute { From a67bd69836675782dc34a6c1ff986fed7328eda3 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 13:15:31 -0500 Subject: [PATCH 376/479] Update compare-list.graphqls --- .../graph-ql/coverage/catalog/compare-list.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls index 430649ec1..1f6d7c667 100644 --- a/design-documents/graph-ql/coverage/catalog/compare-list.graphqls +++ b/design-documents/graph-ql/coverage/catalog/compare-list.graphqls @@ -1,7 +1,7 @@ type ComparableItem { uid: ID! product: ProductInterface! - attributes: [Attribute]! @doc(description: "Product comparable attributes") + attributes: [ProductAttribute]! @doc(description: "Product comparable attributes") } type ComparableAttribute { From d6c66398e56f36f22a68108cabcd60685cae7f28 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 13:50:27 -0500 Subject: [PATCH 377/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 615e10302..45161f4b3 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -22,7 +22,7 @@ type ConfigurableProductOption { type ConfigurableProductOptionValue { uid: ID! - is_available: Boolean! # when display OOS is enabled, if the variant selection is OOS, this will return false. + is_available: Boolean! # When display out of stock is enabled, if the option value is out of stock, this will return false is_default: Boolean! label: String! swatch: SwatchDataInterface From a19c2e68ff1dfb2cdf3fce065e1b8d0918611a2f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 13:51:00 -0500 Subject: [PATCH 378/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 45161f4b3..f5bab0e3e 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -14,7 +14,6 @@ type ConfigurableProductOptionsSelection @doc(description: "Metadata correspondi type ConfigurableProductOption { uid: ID! attribute_code: String! - attribute_uid: ID! label: String! sort_order: Int! values: [ConfigurableProductOptionValue!] From 7af32d5bf423106dee8c441a580f5d4c1d48ea5b Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 13:51:07 -0500 Subject: [PATCH 379/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index f5bab0e3e..98ae12954 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -15,7 +15,6 @@ type ConfigurableProductOption { uid: ID! attribute_code: String! label: String! - sort_order: Int! values: [ConfigurableProductOptionValue!] } From 4565c793c3416da249eb82d8c295a73748ab713f Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 14:09:49 -0500 Subject: [PATCH 380/479] Update configurable-options-selection.graphqls --- .../coverage/catalog/configurable-options-selection.graphqls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 98ae12954..037a53fbe 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -12,14 +12,15 @@ type ConfigurableProductOptionsSelection @doc(description: "Metadata correspondi } type ConfigurableProductOption { - uid: ID! + uid: ID! attribute_code: String! label: String! values: [ConfigurableProductOptionValue!] } type ConfigurableProductOptionValue { - uid: ID! + uid: ID! # Encoding of this uid should respect encode_algm(format: configurable//) + # refer to https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/cart/add-items-to-cart-single-mutation.md#single-mutation-for-adding-products-to-cart is_available: Boolean! # When display out of stock is enabled, if the option value is out of stock, this will return false is_default: Boolean! label: String! From 0f8c22bc841b4a20ad552df0b10f5024ac1942a4 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 22 Oct 2020 15:01:36 -0500 Subject: [PATCH 381/479] Add pagination back and change type of the content field --- .../graph-ql/coverage/dynamic-blocks.graphqls | 10 ++++++++-- design-documents/graph-ql/coverage/dynamic-blocks.md | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index 905025b0d..b0e05987c 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -1,10 +1,16 @@ type DynamicBlock { uid: ID! - content: String! + content: ComplexTextValue! } # We don't need name, is_enabled, locations, ga_creative, catalog_price_rule_ids, cart_price_rules_ids, locations on storefront +type DynamicBlocks { + items: [DynamicBlock]! + page_info: SearchResultPageInfo + total_count: Int! +} + type DynamicBlocksFilterInput { type: DynamicBlockTypeEnum! locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied @@ -13,7 +19,7 @@ type DynamicBlocksFilterInput { } type DynamicBlocksOutput { - items: [DynamicBlock]! + dynamic_blocks: DynamicBlocks! } enum DynamicBlockTypeEnum { diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.md b/design-documents/graph-ql/coverage/dynamic-blocks.md index cb1b8f02c..b56bdca37 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.md +++ b/design-documents/graph-ql/coverage/dynamic-blocks.md @@ -28,7 +28,9 @@ PWA will receive similar JavaScript component definition that can be parsed to e items { uid name - content + content { + html + } } page_info { current_page From b3e9d8ef4449e222a3149c9d7bd7d2f5a3319bd2 Mon Sep 17 00:00:00 2001 From: Igor Melnykov Date: Thu, 22 Oct 2020 15:22:57 -0500 Subject: [PATCH 382/479] Remove name field from example --- design-documents/graph-ql/coverage/dynamic-blocks.md | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.md b/design-documents/graph-ql/coverage/dynamic-blocks.md index b56bdca37..cc2df883a 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.md +++ b/design-documents/graph-ql/coverage/dynamic-blocks.md @@ -27,7 +27,6 @@ PWA will receive similar JavaScript component definition that can be parsed to e dynamic_blocks(input: {type: SPECIFIED, locations: [CONTENT], rotation_mode: RANDOM, dynamic_block_uids: [1, 2]}) { items { uid - name content { html } From 4a4b6c0dc6f9131b544a2c7c7ca4d2b8b0328a03 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Oct 2020 17:08:28 -0500 Subject: [PATCH 383/479] Update design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls Co-authored-by: Igor Melnikov --- .../coverage/catalog/configurable-options-selection.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls index 037a53fbe..52c7a9c2d 100644 --- a/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls +++ b/design-documents/graph-ql/coverage/catalog/configurable-options-selection.graphqls @@ -1,5 +1,5 @@ type ConfigurableProduct { - configurable_product_options_selection(configurableOptionValues: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") + configurable_product_options_selection(configurableOptionValueUids: [ID!]): ConfigurableProductOptionsSelection @doc(description: "Specified configurable product options selection") variants: [ConfigurableVariant] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @deprecated(reason: "Use configurable_options_selection_metadata instead.") @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") } From 20da1739e70f3e0bb0332cf808e52e01719bdd72 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 27 Oct 2020 21:36:49 -0500 Subject: [PATCH 384/479] Update design-documents/graph-ql/coverage/customer/Wishlist.graphqls Co-authored-by: Igor Melnikov --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 012e019f9..15d025bf3 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -1,6 +1,6 @@ type Mutation { createWishlist(input: CreateWishlistInput!): CreateWishlistOutput # Multiple wishlists Commerce functionality - deleteWishlist(wishlistId: ID!): DeleteWishlistOutput # Commerce fucntionality - in Opens Source we assume customer always has one wishlist + deleteWishlist(wishlistUid: ID!): DeleteWishlistOutput # Commerce fucntionality - in Opens Source we assume customer always has one wishlist addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput From 1bb0bcd3f3c45a11f7a8ceab4ebddf0c84036524 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 27 Oct 2020 21:37:31 -0500 Subject: [PATCH 385/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 15d025bf3..1f480956b 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -115,7 +115,7 @@ enum WishlistVisibilityEnum @doc(description: "This enumeration defines the wish } type CreateWishlistOutput { - wishlist: Wishlist + wishlist: Wishlist! } type DeleteWishlistOutput { From 050d0e2ea583e2a9af42d30b4c28f6e7770af630 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 28 Oct 2020 14:33:09 -0500 Subject: [PATCH 386/479] align schema with uids and delete success --- .../coverage/b2b/requisitionList.graphqls | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 996ff10de..07cef0d01 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -17,7 +17,7 @@ # # 3. In requistion list view, `renameRequistionList` mutatation can be used to rename the list # -# 4. In requistion list view, one can edit items quantity, options etc or remove items with `removeRequisitionListItems` and +# 4. In requistion list view, one can edit items quantity, options etc or remove items with `deleteRequisitionListItems` and # `updateRequisitionListItems` mutation # # 5. In requistion list view, select requistion list items and move them , copy them to different requistion list with @@ -104,46 +104,46 @@ type Mutation { description: String @doc(description: "description For the list") ): CreateRequisitionListOutput @doc(description: "Create Empty Requisition List") - renameRequisitionList( - uid: ID!, @doc(description: "unique Id of requisition list") + updateRequisitionList( + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") name: String!, @doc(description: "new name for list") description: String @doc(description: "new description For the List") - ): RenameRequisitionListOutput @doc(description: "Rename a requisition list and change description") + ): UpdateRequisitionListOutput @doc(description: "Rename a requisition list and change description") deleteRequisitionList( - uid: ID! @doc(description: "unique Id of requisition list") + requisitionListUid: ID! @doc(description: "unique Id of requisition list") ): DeleteRequisitionListOutput @doc(description: "Delete a requisition list with Id") addProductsToRequisitionList( - uid: ID!, @doc(description: "unique Id of requisition list") + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") items: [RequisitionListItemsInput!]! @doc(description: "Products to be added to requisition list") ): AddProductsToRequisitionListOutput @doc(description: "Add items to requisition list") - removeRequisitionListItems( - uid: ID!, @doc(description: "unique Id of requisition list") - items: [ID!]! @doc(description: "unique Ids of Items to be removed from requisition list") - ): RemoveRequisitionListItemsOutput @doc(description: "Remove Items in requisition list") + deleteRequisitionListItems( + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") + requisitionListItemUids: [ID!]! @doc(description: "unique Ids of Items to be deleted from requisition list") + ): DeleteRequisitionListItemsOutput @doc(description: "Remove Items in requisition list") updateRequisitionListItems( - uid: ID!, @doc(description: "unique Id of requisition list") + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") items: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") addRequisitionListItemsToCart( - listUid: ID!, @doc(description: "unique Id of requisition list") - itemUids: [ID!]! @doc(description: "selected requisition list items that are to be added") + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") + requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be added") ): AddRequisitionListItemsToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") copyItemsBetweenRequisitionLists( - sourceUid: ID!, @doc(description: "unique Id of source requisition list") - destinationUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - itemUids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") + sourceRequisitionListUid: ID!, @doc(description: "unique Id of source requisition list") + destinationRequisitionListUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") ): CopyItemsBetweenRequisitionListsOutput @doc(description: "Copy Items from Requisition List to another requisition list") moveItemsBetweenRequisitionLists( - sourceUid: ID!, @doc(description: "unique Id of source requisition list") - destinationUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - itemUids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") + sourceRequisitionListUid: ID!, @doc(description: "unique Id of source requisition list") + destinationRequisitionListUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created + requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") ): MoveItemsBetweenRequisitionListsOutput @doc(description: "Move Items from Requisition List to another requisition List") clearCustomerCart( @@ -151,20 +151,21 @@ type Mutation { ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } -type RemoveRequisitionListItemsOutput { - list : RequisitionList +type DeleteRequisitionListItemsOutput { + status: Boolean! + list: RequisitionList } type UpdateRequisitionListItemsOutput { - list : RequisitionList + list: RequisitionList } type AddProductsToRequisitionListOutput { - list : RequisitionList + list: RequisitionList } input RequisitionListFilterInput { - uids: FilterEqualTypeInput, @doc(description: "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs") + requisitionListUid: FilterEqualTypeInput, @doc(description: "Filter Customer Requisition lists with an requisition list ID or list of requisition list IDs") name: FilterMatchTypeInput @doc(description: "Filter by display name of the Requisition list") } @@ -172,13 +173,13 @@ input RequisitionListItemsInput { sku: String! quantity: Float parent_sku: String - selected_options: [String!] @doc(description: "selected option ID") + selected_options: [ID!] @doc(description: "selected option ID") entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") } input UpdateRequisitionListItemsInput { - item_id: ID! @doc(description: "unique ID of Requisition List Item") - selected_options: [String!] @doc(description: "selected option ID") + requisitionListItemUid: ID! @doc(description: "unique ID of Requisition List Item") + selected_options: [ID!] @doc(description: "selected option ID") entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") quantity: Float } @@ -187,12 +188,13 @@ type CreateRequisitionListOutput { list: RequisitionList } -type RenameRequisitionListOutput { +type UpdateRequisitionListOutput { list: RequisitionList } type DeleteRequisitionListOutput { - requisition_list : RequisitionList + status: Boolean! + requisition_list: RequisitionList } type AddRequisitionListItemsToCartOutput { @@ -200,12 +202,12 @@ type AddRequisitionListItemsToCartOutput { } type CopyItemsBetweenRequisitionListsOutput { - list : RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output + list: RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output } type MoveItemsBetweenRequisitionListsOutput { - source : RequisitionList @doc(description: "Source Requisition List") - destination : RequisitionList @doc(description: "Destination Requisition List") + source: RequisitionList @doc(description: "Source Requisition List") + destination: RequisitionList @doc(description: "Destination Requisition List") } type ClearCustomerCartOutput { From 9ab990992ea8edbc4662c75e10a4425ff4a35cae Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 28 Oct 2020 14:38:04 -0500 Subject: [PATCH 387/479] align schema with uids and delete success --- .../graph-ql/coverage/b2b/requisitionList.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 07cef0d01..9740bba00 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -15,9 +15,9 @@ # # 2. In requistion list view, `exportRequisitionList` query will generate a CSV file with particular requistion list data # -# 3. In requistion list view, `renameRequistionList` mutatation can be used to rename the list +# 3. In requistion list view, `updateRequistionList` mutatation can be used to update or rename the list # -# 4. In requistion list view, one can edit items quantity, options etc or remove items with `deleteRequisitionListItems` and +# 4. In requistion list view, one can edit items quantity, options etc or delete items with `deleteRequisitionListItems` and # `updateRequisitionListItems` mutation # # 5. In requistion list view, select requistion list items and move them , copy them to different requistion list with @@ -122,7 +122,7 @@ type Mutation { deleteRequisitionListItems( requisitionListUid: ID!, @doc(description: "unique Id of requisition list") requisitionListItemUids: [ID!]! @doc(description: "unique Ids of Items to be deleted from requisition list") - ): DeleteRequisitionListItemsOutput @doc(description: "Remove Items in requisition list") + ): DeleteRequisitionListItemsOutput @doc(description: "Delete Items in requisition list") updateRequisitionListItems( requisitionListUid: ID!, @doc(description: "unique Id of requisition list") From 0f0a221b81e557ac1835984c9891de314655fef1 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 28 Oct 2020 15:53:22 -0500 Subject: [PATCH 388/479] Update Wishlist.graphqls --- .../graph-ql/coverage/customer/Wishlist.graphqls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 1f480956b..fd2cc92ad 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -4,8 +4,8 @@ type Mutation { addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput - copyProductsBetweenWishlists(sourceUid: ID!, destinationUid: ID!, wishlistItems: [WishlistItemCopyInput!]!): CopyProductsBetweenWishlistsOutput @doc(description: "Copy a product to the wish list") - moveProductsBetweenWishlists(sourceUid: ID!, destinationUid: ID!, wishlistItems: [WishlistItemMoveInput!]!): MoveProductsBetweenWishlistsOutput @doc(description: "Move products from one wish list to another") + copyProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemCopyInput!]!): CopyProductsBetweenWishlistsOutput @doc(description: "Copy a product to the wish list") + moveProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemMoveInput!]!): MoveProductsBetweenWishlistsOutput @doc(description: "Move products from one wish list to another") updateWishlist(input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") } @@ -50,6 +50,7 @@ type AddProductsToWishlistOutput { } type RemoveProductsFromWishlistOutput { + status: Boolean! wishlist: Wishlist! } From 55300d25e42a8af20c4b3597165d6981589773a9 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 3 Nov 2020 17:02:20 -0600 Subject: [PATCH 389/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index fd2cc92ad..46ed9c5b2 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -110,6 +110,10 @@ type GiftCardOptions { message: String } +type GroupedProductWishlistItem implements WishlistItemInterface { + grouped_products: [GroupedProductItem!]! +} + enum WishlistVisibilityEnum @doc(description: "This enumeration defines the wish list visibility types") { PUBLIC PRIVATE From d830efcda7e70b5b8ae38b85b1c4f936f468500f Mon Sep 17 00:00:00 2001 From: Eduard Chitoraga Date: Wed, 4 Nov 2020 11:08:54 +0200 Subject: [PATCH 390/479] We shouldn't give the possibility to update the registry's type. --- .../graph-ql/coverage/customer/gift-registry.graphqls | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 7a1d01681..cb1a86599 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -70,7 +70,6 @@ input UpdateGiftRegistryItemInput { input UpdateGiftRegistryInput { event_name: String - type_id: String message: String privacy_settings: GiftRegistryPrivacySettings status: GiftRegistryStatus From 99281d715af13c46b6ab6e3c16c1823dc1fb9454 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 5 Nov 2020 13:38:06 -0600 Subject: [PATCH 391/479] Update Wishlist.graphqls --- .../graph-ql/coverage/customer/Wishlist.graphqls | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index 46ed9c5b2..ed30e28f6 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -18,7 +18,10 @@ type Customer { type Wishlist { id: ID items: [WishlistItem] @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") - items_v2: [WishlistItemInterface] @doc(description: "An array of items in the customer's wishlist") + items_v2( + currentPage: Int = 1, + pageSize: Int = 20 + ): WishlistItems @doc(description: "An array of items in the customer's wishlist") items_count: Int sharing_code: String updated_at: String @@ -26,6 +29,12 @@ type Wishlist { visibility: WishlistVisibilityEnum! } +type WishlistItems { + items: [WishlistItemInterface]! @doc(description: "Wishlist items list") + page_info: SearchResultPageInfo + total_pages: Int! @doc(description: "total count of wishlist items") +} + input CreateWishlistInput { name: String! visibility: WishlistVisibilityEnum! From 3ca8559ca034ddf3f6e37b487da8b2136b8df00d Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 5 Nov 2020 16:53:18 -0600 Subject: [PATCH 392/479] Update Wishlist.graphqls --- .../coverage/customer/Wishlist.graphqls | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index ed30e28f6..fe48b067a 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -1,12 +1,16 @@ type Mutation { createWishlist(input: CreateWishlistInput!): CreateWishlistOutput # Multiple wishlists Commerce functionality deleteWishlist(wishlistUid: ID!): DeleteWishlistOutput # Commerce fucntionality - in Opens Source we assume customer always has one wishlist - addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput copyProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemCopyInput!]!): CopyProductsBetweenWishlistsOutput @doc(description: "Copy a product to the wish list") moveProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemMoveInput!]!): MoveProductsBetweenWishlistsOutput @doc(description: "Move products from one wish list to another") - updateWishlist(input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") + updateWishlist( wishlistId: ID!, input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") + addWishlistItemsToCart( + wishlistId: ID!, @doc(description: "unique Id of wishlist") + wishlistItemUids: [ID!] @doc(description: "Optional param. selected wish list items that are to be added") + ): AddWishlistItemsToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") } type Customer { @@ -41,7 +45,6 @@ input CreateWishlistInput { } input UpdateWishlistInput { - wishlistId: ID! name: String visibility: WishlistVisibilityEnum } @@ -67,6 +70,22 @@ type UpdateProductsInWishlistOutput { wishlist: Wishlist! } +type AddWishlistItemsToCartOutput { + status: Boolean! + add_wishlist_items_to_cart_user_errors: [AddWishlistItemUserError]! +} + +type AddWishlistItemUserError { + message: String! + type: AddWishlistItemUserErrorType! +} + +enum AddWishlistItemUserErrorType { + OUT_OF_STOCK + MAX_QTY_FOR_USER + NOT_AVAILABLE +} + input WishlistItemInput { sku: String! quantity: Float! From 4bac3300dd41e4607e4fedf1501998eedeec365e Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 5 Nov 2020 16:58:30 -0600 Subject: [PATCH 393/479] add changes to match wishlist schema --- .../coverage/b2b/requisitionList.graphqls | 102 ++++++++++++------ 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 9740bba00..540114239 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -59,22 +59,38 @@ type RequistionListItems { interface RequisitionListItemInterface @doc(description: "Interface type for Requisition List Item") { uid: ID! @doc(description:"Unique Identifier of Requisition List Item") product: ProductInterface! - qty: Float! @doc(description: "Quantity added") + quantity: Float! @doc(description: "Quantity added") + customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") } -type DefaultRequisitionListItem implements RequisitionListItemInterface +type SimpleRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Simple and Virtual Products") { - uid: ID! @doc(description: "Unique Identifier of Requisition List Item") - product: ProductInterface! - qty: Float! @doc(description: "Quantity added") - customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") +} + +type GiftCardRequisitionListItem implements RequisitionListItemInterface +@doc(description: "Requisition List Item Implementation that for GiftCard Products") { + gift_card_options: GiftCardOptions! +} + +type GiftCardOptions { + sender_name: String + sender_email: String + recipient_name: String + recipient_email: String + amount: Money + custom_giftcard_amount: Money + message: String +} + +type GroupedProductRequisitionListItem implements RequisitionListItemInterface { + grouped_products: [GroupedProductItem!]! } type DownloadableRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Downloadable Products") { uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - qty: Float! @doc(description: "Quantity added") + quantity: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") links: [DownloadableProductLinks] @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") samples: [DownloadableProductSamples] @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") @@ -84,7 +100,7 @@ type BundleRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Bundle Products") { uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - qty: Float! @doc(description: "Quantity added") + quantity: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") bundle_options: [SelectedBundleOption]! @doc(description: "selected bundle options") } @@ -93,21 +109,19 @@ type ConfigurableRequisitionListItem implements RequisitionListItemInterface @doc(description: "Requisition List Item Implementation that for Configurable Products") { uid: ID! @doc(description: "Unique Identifier of Requisition List Item") product: ProductInterface! - qty: Float! @doc(description: "Quantity added") + quantity: Float! @doc(description: "Quantity added") customizable_options: [SelectedCustomizableOption] @doc(description: "custom Option selected") configurable_options: [SelectedConfigurableOption] @doc(description: "Configurable options selected") } type Mutation { createRequisitionList( - name: String!, @doc(description: "name for the list") - description: String @doc(description: "description For the list") + input: CreateRequisitionListInput ): CreateRequisitionListOutput @doc(description: "Create Empty Requisition List") updateRequisitionList( requisitionListUid: ID!, @doc(description: "unique Id of requisition list") - name: String!, @doc(description: "new name for list") - description: String @doc(description: "new description For the List") + input: UpdateRequisitionListInput ): UpdateRequisitionListOutput @doc(description: "Rename a requisition list and change description") deleteRequisitionList( @@ -124,26 +138,21 @@ type Mutation { requisitionListItemUids: [ID!]! @doc(description: "unique Ids of Items to be deleted from requisition list") ): DeleteRequisitionListItemsOutput @doc(description: "Delete Items in requisition list") - updateRequisitionListItems( - requisitionListUid: ID!, @doc(description: "unique Id of requisition list") - items: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") - ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") - addRequisitionListItemsToCart( requisitionListUid: ID!, @doc(description: "unique Id of requisition list") - requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be added") + requisitionListItemUids: [ID!] @doc(description: "selected requisition list items that are to be added. Not providing this parameter will all items from req. list to the cart") ): AddRequisitionListItemsToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") copyItemsBetweenRequisitionLists( sourceRequisitionListUid: ID!, @doc(description: "unique Id of source requisition list") destinationRequisitionListUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") + requisitionListItem: CopyItemsBetweenRequisitionListsInput ): CopyItemsBetweenRequisitionListsOutput @doc(description: "Copy Items from Requisition List to another requisition list") moveItemsBetweenRequisitionLists( sourceRequisitionListUid: ID!, @doc(description: "unique Id of source requisition list") destinationRequisitionListUid: ID, @doc(description: "unique Id of destination requisition list") # If null new requisition list will be created - requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") + requisitionListItem: MoveItemsBetweenRequisitionListsInput ): MoveItemsBetweenRequisitionListsOutput @doc(description: "Move Items from Requisition List to another requisition List") clearCustomerCart( @@ -151,17 +160,35 @@ type Mutation { ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } +type CreateRequisitionListInput { + name: String! @doc(description: "name for the list") + description: String @doc(description: "description For the list") +} + +type UpdateRequisitionListInput { + name: String! @doc(description: "new name for list") + description: String @doc(description: "new description For the List") +} + +type CopyItemsBetweenRequisitionListsInput { + requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be copied from source") +} + +type MoveItemsBetweenRequisitionListsInput { + requisitionListItemUids: [ID!]! @doc(description: "selected requisition list items that are to be moved from source") +} + type DeleteRequisitionListItemsOutput { status: Boolean! - list: RequisitionList + requisiton_list: RequisitionList } type UpdateRequisitionListItemsOutput { - list: RequisitionList + requisiton_list: RequisitionList } type AddProductsToRequisitionListOutput { - list: RequisitionList + requisiton_list: RequisitionList } input RequisitionListFilterInput { @@ -171,8 +198,7 @@ input RequisitionListFilterInput { input RequisitionListItemsInput { sku: String! - quantity: Float - parent_sku: String + quantity: Float! selected_options: [ID!] @doc(description: "selected option ID") entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") } @@ -185,11 +211,11 @@ input UpdateRequisitionListItemsInput { } type CreateRequisitionListOutput { - list: RequisitionList + requisiton_list: RequisitionList } type UpdateRequisitionListOutput { - list: RequisitionList + requisiton_list: RequisitionList } type DeleteRequisitionListOutput { @@ -198,18 +224,32 @@ type DeleteRequisitionListOutput { } type AddRequisitionListItemsToCartOutput { + status: Boolean! + add_requisition_list_items_to_cart_user_errors: [AddRequisitionListItemToCartUserError]! cart: Cart # since requisition list is not mutated it is not part of the output } +type AddRequisitionListItemToCartUserError { + message: String! + type: AddRequisitionListItemToCartUserErrorType! +} + +enum AddRequisitionListItemToCartUserErrorType { + OUT_OF_STOCK + MAX_QTY_FOR_USER + NOT_AVAILABLE +} + type CopyItemsBetweenRequisitionListsOutput { - list: RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output + requisiton_list: RequisitionList @doc(description: "Destination Requisition List")# since source requisition list is not mutated it is not part of the output } type MoveItemsBetweenRequisitionListsOutput { - source: RequisitionList @doc(description: "Source Requisition List") - destination: RequisitionList @doc(description: "Destination Requisition List") + source_requisiton_list: RequisitionList @doc(description: "Source Requisition List") + destination_requisiton_list: RequisitionList @doc(description: "Destination Requisition List") } type ClearCustomerCartOutput { + status: Boolean! cart: Cart } From 677f56b08811558f0026bffe85cec04bce2ed17d Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 5 Nov 2020 17:06:29 -0600 Subject: [PATCH 394/479] Update Wishlist.graphqls --- design-documents/graph-ql/coverage/customer/Wishlist.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls index fe48b067a..c30b45654 100644 --- a/design-documents/graph-ql/coverage/customer/Wishlist.graphqls +++ b/design-documents/graph-ql/coverage/customer/Wishlist.graphqls @@ -6,9 +6,9 @@ type Mutation { updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput copyProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemCopyInput!]!): CopyProductsBetweenWishlistsOutput @doc(description: "Copy a product to the wish list") moveProductsBetweenWishlists(sourceWishlistUid: ID!, destinationWishlistUid: ID!, wishlistItems: [WishlistItemMoveInput!]!): MoveProductsBetweenWishlistsOutput @doc(description: "Move products from one wish list to another") - updateWishlist( wishlistId: ID!, input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") + updateWishlist( wishlistUid: ID!, input: UpdateWishlistInput!): UpdateWishlistOutput @doc(description: "Change the name and visibility of the specified wish list") addWishlistItemsToCart( - wishlistId: ID!, @doc(description: "unique Id of wishlist") + wishlistUid: ID!, @doc(description: "unique Id of wishlist") wishlistItemUids: [ID!] @doc(description: "Optional param. selected wish list items that are to be added") ): AddWishlistItemsToCartOutput @doc(description: "Add Requisition List Items To Customer Cart") } From 45ed2cf5c097dc5cc7144cf196686eacecf9d489 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 5 Nov 2020 17:11:39 -0600 Subject: [PATCH 395/479] add changes to match wishlist schema --- .../graph-ql/coverage/b2b/requisitionList.graphqls | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls index 540114239..af2aa9817 100644 --- a/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls +++ b/design-documents/graph-ql/coverage/b2b/requisitionList.graphqls @@ -124,13 +124,18 @@ type Mutation { input: UpdateRequisitionListInput ): UpdateRequisitionListOutput @doc(description: "Rename a requisition list and change description") + updateRequisitionListItems( + requisitionListUid: ID!, @doc(description: "unique Id of requisition list") + requisitionListItems: [UpdateRequisitionListItemsInput!]! @doc(description: "Items to be updated from requisition list") + ): UpdateRequisitionListItemsOutput @doc(description: "Update Items in requisition list") + deleteRequisitionList( requisitionListUid: ID! @doc(description: "unique Id of requisition list") ): DeleteRequisitionListOutput @doc(description: "Delete a requisition list with Id") addProductsToRequisitionList( requisitionListUid: ID!, @doc(description: "unique Id of requisition list") - items: [RequisitionListItemsInput!]! @doc(description: "Products to be added to requisition list") + requisitionListItems: [RequisitionListItemsInput!]! @doc(description: "Products to be added to requisition list") ): AddProductsToRequisitionListOutput @doc(description: "Add items to requisition list") deleteRequisitionListItems( @@ -156,7 +161,7 @@ type Mutation { ): MoveItemsBetweenRequisitionListsOutput @doc(description: "Move Items from Requisition List to another requisition List") clearCustomerCart( - cartUid: String! @doc(description: "masked Cart Id") + cartUid: String! @doc(description: "masked Cart Id") ): ClearCustomerCartOutput @doc(description: "Clears the cart items") } @@ -204,7 +209,6 @@ input RequisitionListItemsInput { } input UpdateRequisitionListItemsInput { - requisitionListItemUid: ID! @doc(description: "unique ID of Requisition List Item") selected_options: [ID!] @doc(description: "selected option ID") entered_options: [EnteredOptionInput!] @doc(description: "entered Options ID") quantity: Float From 9d839afc1ea085a722323a093351e6de55916ad3 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 11 Nov 2020 11:09:06 -0600 Subject: [PATCH 396/479] modifying customer and author according to new requirements and schema alignment --- .../graph-ql/coverage/returns.graphqls | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index ed6db8eee..2a1da5f69 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -106,9 +106,8 @@ type Return @doc(description: "Customer return") { uid: ID! number: String! @doc(description: "Human-readable return number") order: CustomerOrder @doc(description: "The order associated with the return.") - creation_date: String! @doc(description: "The date when the return was requested.") - customer_email: String! @doc(description: "Email of the customer who created the return.") - customer_name: String @doc(description: "The name of the person returning the item") + creation_at: String! @doc(description: "The date when the return was requested.") + customer: ReturnCustomer! @doc(description: "The data for the customer who created the return request") status: ReturnStatus @doc(description: "Return status.") shipping: ReturnShipping @doc(description: "Shipping information for the return.") comments: [ReturnComment] @doc(description: "A list of comments posted for the return.") @@ -116,6 +115,12 @@ type Return @doc(description: "Customer return") { available_shipping_carriers: [ReturnShippingCarrier] @doc(description: "A list of shipping carriers available for returns.") } +type ReturnCustomer @doc(description: "The Customer information for the return.") { + email: String! @doc(description: "Customer email address.") + firstname: String @doc(description: "Customer first name.") + lastname: String @doc(description: "Customer last name.") +} + type ReturnItem { uid: ID! order_item: OrderItemInterface! @doc(description: "Order item provides access to the product being returned, including selected/entered options information.") @@ -134,8 +139,7 @@ type CustomAttribute { type ReturnComment { uid: ID! @doc(description: "Comment ID.") - author_firstname: String! @doc(description: "First name of the user who posted the comment.") - author_lastname: String! @doc(description: "Last name of the user who posted the comment.") + author: String! @doc(description: "The full name of the the author who posted the comment.") created_at: String! @doc(description: "The date and time when the comment was posted.") text: String! @doc(description: "The comment text.") } From d2f44f9bbd905b2fe724f5abbf3b9238ab7ce554 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 11 Nov 2020 11:46:29 -0600 Subject: [PATCH 397/479] modifying customer and author according to new requirements and schema alignment --- design-documents/graph-ql/coverage/returns.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 2a1da5f69..3b70bfabd 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -139,7 +139,7 @@ type CustomAttribute { type ReturnComment { uid: ID! @doc(description: "Comment ID.") - author: String! @doc(description: "The full name of the the author who posted the comment.") + author_name: String! @doc(description: "The name of the author who posted the comment.") created_at: String! @doc(description: "The date and time when the comment was posted.") text: String! @doc(description: "The comment text.") } From e8be970a8ebf0d48dc8e32ece750cb00923a722b Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 11 Nov 2020 21:30:37 -0600 Subject: [PATCH 398/479] rename id to uid --- .../coverage/customer/gift-registry.graphqls | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 7a1d01681..158cad9d5 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -1,6 +1,6 @@ type Customer { gift_registry_list: [GiftRegistry] - gift_registry(id: ID!): GiftRegistry + gift_registry(uid: ID!): GiftRegistry } type Query { @@ -8,28 +8,28 @@ type Query { giftRegistrySearch( registrantFirstname: String!, registrantLastname: String!, - giftRegistryTypeId: ID, + giftRegistryTypeUid: ID, searchableDynamicAttributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") ): [GiftRegistry] @doc(description: "Gift registry search by registrant name and additional searchable attributes.") - giftRegistry(id: ID!): GiftRegistry @doc(description: "This query is intended for guests and some fields of GiftRegistry will not be availalbe") + giftRegistry(giftRegistryUid: ID!): GiftRegistry @doc(description: "This query is intended for guests and some fields of GiftRegistry will not be availalbe") } type Mutation { # All mutations below should only be accessible to the registry owner. Guest users should be getting authorization error createGiftRegistry(giftRegistry: CreateGiftRegistryInput!): CreateGiftRegistryOutput - updateGiftRegistry(id: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput - removeGiftRegistry(id: ID!): RemoveGiftRegistryOutput + updateGiftRegistry(giftRegistryUid: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput + removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput - addGiftRegistryItems(giftRegistryId: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput - removeGiftRegistryItems(giftRegistryId: ID!, itemIds: [ID!]!): RemoveGiftRegistryItemsOutput - updateGiftRegistryItems(giftRegistryId: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput + addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput + removeGiftRegistryItems(giftRegistryUid: ID!, itemIds: [ID!]!): RemoveGiftRegistryItemsOutput + updateGiftRegistryItems(giftRegistryUid: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput - addGiftRegistryRegistrants(giftRegistryId: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput - updateGiftRegistryRegistrants(giftRegistryId: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput - removeGiftRegistryRegistrants(giftRegistryId: ID!, registrantIds: [ID!]!): RemoveGiftRegistryRegistrantsOutput + addGiftRegistryRegistrants(giftRegistryUid: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput + updateGiftRegistryRegistrants(giftRegistryUid: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput + removeGiftRegistryRegistrants(giftRegistryUid: ID!, registrantIds: [ID!]!): RemoveGiftRegistryRegistrantsOutput - shareGiftRegistry(id: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput + shareGiftRegistry(giftRegistryUid: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput } input ShareGiftRegistryInviteeInput @@ -58,19 +58,19 @@ input AddGiftRegistryItemInput { # Should be defined in scope of https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/add-items-to-cart-single-mutation.md input EnteredOptionInput { - id: String! + uid: ID! value: String! } input UpdateGiftRegistryItemInput { - id: ID! + giftRegistryItemUid: ID! quantity: Float! note: String } input UpdateGiftRegistryInput { event_name: String - type_id: String + giftRegistryTypeUid: ID message: String privacy_settings: GiftRegistryPrivacySettings status: GiftRegistryStatus @@ -79,9 +79,9 @@ input UpdateGiftRegistryInput { } input CreateGiftRegistryInput { - id: ID @doc(description: "Optional id, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") + giftRegistryUid: ID @doc(description: "Optional uid, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") event_name: String! - type_id: String! + giftRegistryTypeUid: ID! message: String! privacy_settings: GiftRegistryPrivacySettings! status: GiftRegistryStatus! @@ -92,11 +92,11 @@ input CreateGiftRegistryInput { input GiftRegistryShippingAddressInput @doc(description: "Either address data or address ID should be provided. In case both are provided, validation will fail") { address_data: CustomerAddressInput - address_id: Int + address_id: ID } input UpdateGiftRegistryRegistrantInput { - id: ID! + giftRegistryRegistrantUid: ID! first_name: String last_name: String email: String @@ -156,7 +156,7 @@ type ShareGiftRegistryOutput { } type GiftRegistryType { - id: ID! + uid: ID! label: String! dynamic_attributes_metadata: [GiftRegistryDynamicAttributeMetadataInterface] } @@ -225,11 +225,11 @@ type GiftRegistrySelectAttributeOptionMetadata { } type GiftRegistry { - id: ID! + uid: ID! event_name: String! type: GiftRegistryType message: String! - created_on: String! @doc(description: "Creation date") @doc(description: "Accessible to the registry owner only") + created_at: String! @doc(description: "Creation date") privacy_settings: GiftRegistryPrivacySettings! @doc(description: "Accessible to the registry owner only") status: GiftRegistryStatus! @doc(description: "Accessible to the registry owner only") owner_name: String! @@ -240,11 +240,11 @@ type GiftRegistry { } interface GiftRegistryItemInterface { - id: String! + uid: String! quantity: Float! quantity_fulfilled: Float! note: String - added_on: String! + date_added: String! product: ProductInterface customizable_options: [SelectedCustomizableOption] } @@ -302,7 +302,7 @@ enum GiftRegistryDynamicAttributeGroup { } type GiftRegistryRegistrant { - id: ID! + uid: ID! first_name: String! last_name: String! email: String! @doc(description: "Accessible to the registry owner only") From f0c4f95434d3abc79c1e885fce0fbf46f5a43495 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 11 Nov 2020 21:46:36 -0600 Subject: [PATCH 399/479] add uid changes to giftregistry --- .../coverage/customer/gift-registry.md | 140 +++++++++--------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.md b/design-documents/graph-ql/coverage/customer/gift-registry.md index 3e85ada60..4917eee63 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.md +++ b/design-documents/graph-ql/coverage/customer/gift-registry.md @@ -12,7 +12,7 @@ First, get a list of available gift registry types and dynamic attributes metada ```graphql { giftRegistryTypes { - id + uid label dynamic_attributes_metadata { code @@ -48,10 +48,10 @@ First, get a list of available gift registry types and dynamic attributes metada Then create a new gift registry based on the user input. Registrants are added using a separate mutation, which can be sent in the same request if gift registry ID is client-side generated. ```graphql # In real query only one should be provided: existing address ID OR address data -mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistryInput!, $giftRegistryId: ID!, $registrantsData: [AddGiftRegistryRegistrantInput!]!) { +mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistryInput!, $giftRegistryUid: ID!, $registrantsData: [AddGiftRegistryRegistrantInput!]!) { createGiftRegistry(giftRegistry: $giftRegistryData) { gift_registry { - id + uid event_name shipping_address { id @@ -59,10 +59,10 @@ mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistry } } } - addGiftRegistryRegistrants(giftRegistryId: $giftRegistryId, registrants: $registrantsData) { + addGiftRegistryRegistrants(giftRegistryUid: $giftRegistryUid, registrants: $registrantsData) { gift_registry { registrants { - id + iud first_name last_name email @@ -79,11 +79,11 @@ mutation CreateGiftRegistryWithRegistrants($giftRegistryData: CreateGiftRegistry The following JSON represents query variables for the `CreateGiftRegistryWithRegistrants` mutation above. ```json { - "giftRegistryId": "optional client-generated ID", + "giftRegistryUid": "optional client-generated UID", "giftRegistryData": { - "id": "optional client-generated ID", + "uid": "optional client-generated UID", "event_name": "My Birthday", - "type_id": "2", + "giftRegistryTypeUid": "2", "message": "Pleas come to my birthday", "privacy_settings":"PUBLIC", "status": "ACTIVE", @@ -135,7 +135,7 @@ The following JSON represents query variables for the `CreateGiftRegistryWithReg { customer { gift_registry_list { - id + uid event_name created_on message @@ -151,13 +151,13 @@ Render the edit gift registry form and populate it with current gift registry pr ```graphql { customer { - gift_registry(id: "ID obtained from the list query") { + gift_registry(giftRegistryUid: "ID obtained from the list query") { event_name message privacy_settings status registrants { - id + uid first_name last_name email @@ -224,10 +224,10 @@ Render the edit gift registry form and populate it with current gift registry pr Modify gift registry data: ```graphql -mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryData: UpdateGiftRegistryInput!, $registrantsData: [UpdateGiftRegistryRegistrantInput!]!) { - updateGiftRegistry(id: $giftRegistryId, giftRegistry: $giftRegistryData) { +mutation UpdateGiftRegistryWithRegistrants($giftRegistryUid: ID!, $giftRegistryData: UpdateGiftRegistryInput!, $registrantsData: [UpdateGiftRegistryRegistrantInput!]!) { + updateGiftRegistry(giftRegistryUid: $giftRegistryUid, giftRegistry: $giftRegistryData) { gift_registry { - id + uid event_name shipping_address { id @@ -235,10 +235,10 @@ mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryDa } } } - updateGiftRegistryRegistrants(giftRegistryId: $giftRegistryId, registrants: $registrantsData) { + updateGiftRegistryRegistrants(giftRegistryUid: $giftRegistryUid, registrants: $registrantsData) { gift_registry { registrants { - id + uid first_name last_name email @@ -255,10 +255,10 @@ mutation UpdateGiftRegistryWithRegistrants($giftRegistryId: ID!, $giftRegistryDa The JSON below should be used as query variable for the gift registry update mutation above: ```json { - "giftRegistryId": "existing-gift-registry-ID", + "giftRegistryUid": "existing-gift-registry-ID", "giftRegistryData": { "event_name": "My Birthday", - "type_id": "2", + "giftRegistryTypeUid": "2", "message": "Pleas come to my birthday", "privacy_settings":"PUBLIC", "status": "ACTIVE", @@ -290,7 +290,7 @@ The JSON below should be used as query variable for the gift registry update mut }, "registrantsData": [ { - "id": "existing-registrant-id", + "uid": "existing-registrant-uid", "email": "John@example.com", "first_name": "John", "last_name": "Roller", @@ -308,8 +308,8 @@ The JSON below should be used as query variable for the gift registry update mut ### Gift registry owner removes an existing gift registry ```graphql -mutation RemoveGiftRegistry($giftRegistryId: ID!) { - removeGiftRegistry(id: $giftRegistryId) { +mutation RemoveGiftRegistry($giftRegistryUid: ID!) { + removeGiftRegistry(giftRegistryUid: $giftRegistryUid) { is_removed } } @@ -327,13 +327,13 @@ It is critical to have ability to avoid the hash generation on the client, that { customerCart { items { - id + uid quantity product { sku } customizable_options { - id_v2 + uid } ... on BundleCartItem { bundle_options { @@ -346,17 +346,17 @@ It is critical to have ability to avoid the hash generation on the client, that ... on ConfigurableCartItem { child_sku configurable_options { - id_v2 + uid } } ... on DownloadableCartItem { links_v2 { - id + uid } } ... on GiftCardCartItem { amount { - id + uid } } } @@ -375,13 +375,13 @@ Query structure is almost identical to the query for getting items customer { wishlist { items { - id + uid quantity product { sku } customizable_options { - id_v2 + uid } ... on BundleWishlistItem { bundle_options { @@ -394,16 +394,16 @@ Query structure is almost identical to the query for getting items ... on ConfigurableWishlistItem { child_sku configurable_options { - id_v2 + uid } } ... on DownloadableWishlistItem { links_v2 { - id + uid } } ... on GiftCardWishlistItem { - id + uid } } } @@ -416,12 +416,12 @@ Query structure is almost identical to the query for getting items Based on that information we can send a mutation to add the selected item to gift registry. ```graphql -mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGiftRegistryItemInput!]!) { - addGiftRegistryItems(giftRegistryId: $giftRegistryId, items: $giftRegistryItems) { +mutation AddGiftRegistryItems($giftRegistryUid: ID!, $giftRegistryItems: [AddGiftRegistryItemInput!]!) { + addGiftRegistryItems(giftRegistryUid: $giftRegistryUid, items: $giftRegistryItems) { gift_registry { event_name items { - id + uid product { name thumbnail { @@ -429,12 +429,12 @@ mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGift } } selected_customizable_options { - id + uid is_required label sort_order values { - id + uid price { type units @@ -450,11 +450,11 @@ mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGift quantity_fulfilled ... on BundleGiftRegistryItem { selected_bundle_options { - id + uid label type values { - id + uid label price quantity @@ -463,7 +463,7 @@ mutation AddGiftRegistryItems($giftRegistryId: ID!, $giftRegistryItems: [AddGift } ... on ConfigurableGiftRegistryItem { selected_configurable_options { - id + uid option_label value_id value_label @@ -503,7 +503,7 @@ Simple product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "simple-hat", @@ -514,7 +514,7 @@ Simple product with custom options: ], "entered_options": [ { - "id": "hash from custom phrase option ID", + "uid": "hash from custom phrase option UID", "value": "Custom Hat" } ] @@ -526,7 +526,7 @@ Simple product with custom options: Configurable product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "custom-hat", @@ -539,7 +539,7 @@ Configurable product with custom options: ], "entered_options": [ { - "id": "hash from custom phrase option ID", + "uid": "hash from custom phrase option UID", "value": "Custom Hat" } ] @@ -552,7 +552,7 @@ Bundle product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "fan-kit-hat", @@ -565,7 +565,7 @@ Bundle product with custom options: ], "entered_options": [ { - "id": "hash based on custom phrase option goes here. Must be identical for all bundle children.", + "uid": "hash based on custom phrase option goes here. Must be identical for all bundle children.", "value": "Custom Text" } ] @@ -581,7 +581,7 @@ Bundle product with custom options: ], "entered_options": [ { - "id": "hash based on custom phrase option goes here. Must be identical for all bundle children.", + "uid": "hash based on custom phrase option goes here. Must be identical for all bundle children.", "value": "Custom Text" } ] @@ -593,7 +593,7 @@ Bundle product with custom options: Downloadable product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "downloadable-product", @@ -605,7 +605,7 @@ Downloadable product with custom options: ], "entered_options": [ { - "id": "hash from custom text option ID", + "uid": "hash from custom text option UID", "value": "Custom Entry" } ] @@ -617,7 +617,7 @@ Downloadable product with custom options: Gift card product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "giftcard-product", @@ -628,7 +628,7 @@ Gift card product with custom options: ], "entered_options": [ { - "id": "hash from custom text option ID", + "uid": "hash from custom text option UID", "value": "Custom Entry" } ] @@ -640,7 +640,7 @@ Gift card product with custom options: Virtual card product with custom options: ```json { - "giftRegistryId": "existing-gift-registry-id", + "giftRegistryUid": "existing-gift-registry-uid", "giftRegistryItems": [ { "sku": "virtual-product", @@ -650,7 +650,7 @@ Virtual card product with custom options: ], "entered_options": [ { - "id": "hash from custom text option ID", + "uid": "hash from custom text option ID", "value": "Custom Entry" } ] @@ -664,15 +664,15 @@ Virtual card product with custom options: The following query returns enough data to add product to cart or wishlist. ```graphql { - giftRegistry(id: "existing-gift-registry-id") { + giftRegistry(uid: "existing-gift-registry-uid") { items { - id + uid quantity product { sku } customizable_options { - id_v2 + uid } ... on BundleGiftRegistryItem { bundle_options { @@ -685,16 +685,16 @@ The following query returns enough data to add product to cart or wishlist. ... on ConfigurableGiftRegistryItem { child_sku configurable_options { - id_v2 + uid } } ... on DownloadableGiftRegistryItem { links { - id + uid } } ... on GiftCardGiftRegistryItem { - id + uid } } } @@ -704,11 +704,11 @@ The following query returns enough data to add product to cart or wishlist. ### Gift registry owner removes items from an existing gift registry ```graphql -mutation RemoveGiftRegistryItem($giftRegistryId: ID!, $itemIds: [ID!]!) { - removeGiftRegistryItems(giftRegistryId: $giftRegistryId, itemIds: $itemIds) { +mutation RemoveGiftRegistryItem($giftRegistryUid: ID!, $itemIds: [ID!]!) { + removeGiftRegistryItems(giftRegistryUid: $giftRegistryUid, itemIds: $itemIds) { gift_registry { items { - id + uid } } } @@ -718,8 +718,8 @@ mutation RemoveGiftRegistryItem($giftRegistryId: ID!, $itemIds: [ID!]!) { Query variables: ```json { - "giftRegistryId": "existing-gift-registry-id", - "itemIds": ["item-one-id", "item-two-id"] + "giftRegistryUid": "existing-gift-registry-uid", + "itemIds": ["item-one-uid", "item-two-uid"] } ``` @@ -730,7 +730,7 @@ First, storefront application retrieves gift registry search form metadata: ```graphql { giftRegistryTypes { - id + uid label dynamic_attributes_metadata { code @@ -772,10 +772,10 @@ Explicitly excluded scenarios: search by ID and by email. giftRegistrySearch( registrantFirstname: "John", registrantLastname: "Roller", - giftRegistryTypeId: "2", + giftRegistryTypeUid: "2", searchableDynamicAttributes: [{code: "event_country", value: "US"}] ) { - id + uid event_name dynamic_attributes { code @@ -794,8 +794,8 @@ This link will contain gift registry hash as query parameter and should lead to The application should parse the URL, extract gift registry ID hash and query gift registry details by ID. ```graphql -mutation ShareGiftRegistry($id: ID!, $sender: ShareGiftRegistrySenderInput!, $invitees: [ShareGiftRegistryInviteeInput!]!) { - shareGiftRegistry(id: $id, sender: $sender, invitees: $invitees) { +mutation ShareGiftRegistry($giftRegistryUid: ID!, $sender: ShareGiftRegistrySenderInput!, $invitees: [ShareGiftRegistryInviteeInput!]!) { + shareGiftRegistry(giftRegistryUid: $uid, sender: $sender, invitees: $invitees) { is_shared } } @@ -805,7 +805,7 @@ The following JSON should be provided as query variables: ```json { - "id": "existing-gift-registry-id", + "uid": "existing-gift-registry-uid", "sender": { "message": "Hi, please come to my birth day", "name": "John Roller" @@ -823,7 +823,7 @@ The following JSON should be provided as query variables: ```graphql { - giftRegistry(id: "ID obtained from the invitation link in email") { + giftRegistry(uid: "ID obtained from the invitation link in email") { event_name message dynamic_attributes { From b41c87890b14c148508bf5b2899f1ca1b50afe4f Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 11 Nov 2020 21:56:52 -0600 Subject: [PATCH 400/479] add uid changes to giftregistry --- .../graph-ql/coverage/customer/gift-registry.graphqls | 4 ++-- design-documents/graph-ql/coverage/customer/gift-registry.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 158cad9d5..00da8e88b 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -22,12 +22,12 @@ type Mutation { removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput - removeGiftRegistryItems(giftRegistryUid: ID!, itemIds: [ID!]!): RemoveGiftRegistryItemsOutput + removeGiftRegistryItems(giftRegistryUid: ID!, itemUids: [ID!]!): RemoveGiftRegistryItemsOutput updateGiftRegistryItems(giftRegistryUid: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput addGiftRegistryRegistrants(giftRegistryUid: ID!, registrants: [AddGiftRegistryRegistrantInput!]!): AddGiftRegistryRegistrantsOutput updateGiftRegistryRegistrants(giftRegistryUid: ID!, registrants: [UpdateGiftRegistryRegistrantInput!]!): UpdateGiftRegistryRegistrantsOutput - removeGiftRegistryRegistrants(giftRegistryUid: ID!, registrantIds: [ID!]!): RemoveGiftRegistryRegistrantsOutput + removeGiftRegistryRegistrants(giftRegistryUid: ID!, registrantUids: [ID!]!): RemoveGiftRegistryRegistrantsOutput shareGiftRegistry(giftRegistryUid: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput } diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.md b/design-documents/graph-ql/coverage/customer/gift-registry.md index 4917eee63..38f45bfdd 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.md +++ b/design-documents/graph-ql/coverage/customer/gift-registry.md @@ -704,8 +704,8 @@ The following query returns enough data to add product to cart or wishlist. ### Gift registry owner removes items from an existing gift registry ```graphql -mutation RemoveGiftRegistryItem($giftRegistryUid: ID!, $itemIds: [ID!]!) { - removeGiftRegistryItems(giftRegistryUid: $giftRegistryUid, itemIds: $itemIds) { +mutation RemoveGiftRegistryItem($giftRegistryUid: ID!, $itemUids: [ID!]!) { + removeGiftRegistryItems(giftRegistryUid: $giftRegistryUid, itemUids: $itemUids) { gift_registry { items { uid From 54f308452feee8fb6f917be877383e306a1ffdf1 Mon Sep 17 00:00:00 2001 From: rakesh Date: Thu, 12 Nov 2020 13:21:56 +0530 Subject: [PATCH 401/479] reward points uid changes --- .../graph-ql/coverage/customer/reward-points.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/reward-points.graphqls b/design-documents/graph-ql/coverage/customer/reward-points.graphqls index 540f74077..9e79c74f5 100644 --- a/design-documents/graph-ql/coverage/customer/reward-points.graphqls +++ b/design-documents/graph-ql/coverage/customer/reward-points.graphqls @@ -7,8 +7,8 @@ type Cart { } type Mutation { - applyRewardPointsToCart(cartId: ID!): ApplyRewardPointsToCartOutput @doc(description: "Apply all available points up to the cart total. Partial redemption is not available") - removeRewardPointsFromCart(cartId: ID!): RemoveRewardPointsFromCartOutput @doc(description: "Cancel usage of the previously applied reward points to cart") + applyRewardPointsToCart(cartUid: ID!): ApplyRewardPointsToCartOutput @doc(description: "Apply all available points up to the cart total. Partial redemption is not available") + removeRewardPointsFromCart(cartUid: ID!): RemoveRewardPointsFromCartOutput @doc(description: "Cancel usage of the previously applied reward points to cart") } type ApplyRewardPointsToCartOutput { From 7df17ea52a142785d7d9c304e151754d43474caa Mon Sep 17 00:00:00 2001 From: rakesh Date: Thu, 12 Nov 2020 13:40:29 +0530 Subject: [PATCH 402/479] customer graphql uid changes --- .../coverage/customer/customer.graphqls | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/customer.graphqls b/design-documents/graph-ql/coverage/customer/customer.graphqls index 42075d0f8..8a32a655a 100644 --- a/design-documents/graph-ql/coverage/customer/customer.graphqls +++ b/design-documents/graph-ql/coverage/customer/customer.graphqls @@ -21,8 +21,8 @@ type Mutation { updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") - updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") - deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") + updateCustomerAddress(uid: ID!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") + deleteCustomerAddress(uid: ID!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") requestPasswordResetEmail(email: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RequestPasswordResetEmail") @doc(description: "Request an email with a reset password token for the registered customer identified by the specified email.") resetPassword(email: String!, resetPasswordToken: String!, newPassword: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ResetPassword") @doc(description: "Reset a customer's password using the reset password token that the customer received in an email after requesting it using requestPasswordResetEmail.") } @@ -51,7 +51,7 @@ input CustomerAddressInput { input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "The unique ID for a pre-defined region") + region_uid: ID @doc(description: "The unique ID for a pre-defined region") } input CustomerAddressAttributeInput { @@ -88,7 +88,7 @@ type RevokeCustomerTokenOutput { type Customer @doc(description: "Customer defines the customer name and address and other details") { created_at: String @doc(description: "Timestamp indicating when the account was created") - group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") + group_uid: ID @deprecated(reason: "Customer group should not be exposed in the storefront scenarios") prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") firstname: String @doc(description: "The customer's first name") middlename: String @doc(description: "The customer's middle name") @@ -100,17 +100,17 @@ type Customer @doc(description: "Customer defines the customer name and address dob: String @doc(description: "The customer's date of birth") @deprecated(reason: "Use `date_of_birth` instead") date_of_birth: String @doc(description: "The customer's date of birth") taxvat: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") - id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") + uid: ID @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") } type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ - id: Int @doc(description: "The ID assigned to the address object") - customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") + uid: ID @doc(description: "The ID assigned to the address object") + customer_iid: ID @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") - region_id: Int @doc(description: "The unique ID for a pre-defined region") @deprecated(reason: "Use `region` instead.") + region_uid: ID @doc(description: "The unique ID for a pre-defined region") @deprecated(reason: "Use `region` instead.") country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") country_code: CountryCodeEnum @doc(description: "The customer's country") @deprecated(reason: "Use `country` instead.") country: Country @doc(description: "The customer's country") @@ -135,7 +135,7 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { region_code: String @doc(description: "The address region code") region: String @doc(description: "The state or province name") - region_id: Int @doc(description: "The unique ID for a pre-defined region") + region_uid: ID @doc(description: "The unique ID for a pre-defined region") } type CustomerAddressAttribute { From 14f99f5d90b729563c413ba5f31a5dab85b0279e Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 16 Nov 2020 11:46:08 -0600 Subject: [PATCH 403/479] rename created at --- design-documents/graph-ql/coverage/returns.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/returns.graphqls b/design-documents/graph-ql/coverage/returns.graphqls index 3b70bfabd..1249c534e 100644 --- a/design-documents/graph-ql/coverage/returns.graphqls +++ b/design-documents/graph-ql/coverage/returns.graphqls @@ -106,7 +106,7 @@ type Return @doc(description: "Customer return") { uid: ID! number: String! @doc(description: "Human-readable return number") order: CustomerOrder @doc(description: "The order associated with the return.") - creation_at: String! @doc(description: "The date when the return was requested.") + created_at: String! @doc(description: "The date when the return was requested.") customer: ReturnCustomer! @doc(description: "The data for the customer who created the return request") status: ReturnStatus @doc(description: "Return status.") shipping: ReturnShipping @doc(description: "Shipping information for the return.") From 152155912ad7afe8153b640c4dd631189fa8ef9f Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Tue, 17 Nov 2020 13:55:35 +0530 Subject: [PATCH 404/479] product review uid changes --- .../graph-ql/coverage/catalog/product-reviews.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog/product-reviews.graphqls b/design-documents/graph-ql/coverage/catalog/product-reviews.graphqls index 30cfd6e81..a723a5220 100644 --- a/design-documents/graph-ql/coverage/catalog/product-reviews.graphqls +++ b/design-documents/graph-ql/coverage/catalog/product-reviews.graphqls @@ -44,13 +44,13 @@ type ProductReviewRatingsMetadata { } type ProductReviewRatingMetadata { - id: String! @doc(description: "Base 64 encoded rating id.") + uid: ID! @doc(description: "Base 64 encoded rating uid.") name: String! @doc(description: "The review rating name for example quality, price") values: [ProductReviewRatingValueMetadata!]! @doc(description: "List of product review ratings sorted based on position.") } type ProductReviewRatingValueMetadata { - value_id: String! @doc(description: "Base 64 encoded rating value id.") + value_uid: ID! @doc(description: "Base 64 encoded rating value uid.") value: String! @doc(description: "e.g Good, Perfect, 3, 4, 5") } @@ -71,6 +71,6 @@ input CreateProductReviewInput { } type ProductReviewRatingInput { - id: String! @doc(description: "Base 64 encoded rating id.") - value_id: String! @doc(description: "Base 64 encoded rating value id.") + uid: ID! @doc(description: "Base 64 encoded rating uid.") + value_uid: ID! @doc(description: "Base 64 encoded rating value uid.") } From a80df3d1ca85fd231ce03eedcd3b834fee416884 Mon Sep 17 00:00:00 2001 From: OMelnyk Date: Tue, 8 Dec 2020 21:38:22 +0200 Subject: [PATCH 405/479] proposed changes for dynamic blocks schema --- .../graph-ql/coverage/dynamic-blocks.graphqls | 12 ++++++------ design-documents/graph-ql/coverage/dynamic-blocks.md | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index b0e05987c..36328aa5e 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -11,17 +11,13 @@ type DynamicBlocks { total_count: Int! } -type DynamicBlocksFilterInput { +input DynamicBlocksFilterInput { type: DynamicBlockTypeEnum! locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied rotation_mode: DynamicBlockRotationModeEnum! dynamic_block_uids: [ID!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED } -type DynamicBlocksOutput { - dynamic_blocks: DynamicBlocks! -} - enum DynamicBlockTypeEnum { SPECIFIED CART_PRICE_RULE_RELATED @@ -44,5 +40,9 @@ enum DynamicBlockRotationModeEnum { } type Query { - dynamic_blocks(input: DynamicBlocksFilterInput): DynamicBlocksOutput + dynamic_blocks( + input: DynamicBlocksFilterInput + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default is 20") + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"), + ): DynamicBlocks! } \ No newline at end of file diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.md b/design-documents/graph-ql/coverage/dynamic-blocks.md index cc2df883a..ab339d2d4 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.md +++ b/design-documents/graph-ql/coverage/dynamic-blocks.md @@ -24,7 +24,11 @@ PWA will receive similar JavaScript component definition that can be parsed to e ```graphql { - dynamic_blocks(input: {type: SPECIFIED, locations: [CONTENT], rotation_mode: RANDOM, dynamic_block_uids: [1, 2]}) { + dynamic_blocks( + input: {type: SPECIFIED, locations: [CONTENT], dynamic_block_uids: ["MQ==", "Mg=="]} + pageSize:10 + currentPage: 1 + ) { items { uid content { From 52ec0903450bd8d42e8f1ebb28d8225f7a31a86e Mon Sep 17 00:00:00 2001 From: OMelnyk Date: Tue, 8 Dec 2020 21:41:47 +0200 Subject: [PATCH 406/479] removed rotation mode from graphql schema for dynamic blocks --- .../graph-ql/coverage/dynamic-blocks.graphqls | 8 -------- 1 file changed, 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index 36328aa5e..823e31cd5 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -14,7 +14,6 @@ type DynamicBlocks { input DynamicBlocksFilterInput { type: DynamicBlockTypeEnum! locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied - rotation_mode: DynamicBlockRotationModeEnum! dynamic_block_uids: [ID!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED } @@ -32,13 +31,6 @@ enum DynamicBlockLocationEnum { RIGHT } -enum DynamicBlockRotationModeEnum { - NO_ROTATION @doc(description: "Display all.") - RANDOM - SERIES - SHUFFLE -} - type Query { dynamic_blocks( input: DynamicBlocksFilterInput From 3d300c304ec87d52d3e4d1a1b1b821891e031a5a Mon Sep 17 00:00:00 2001 From: Rakesh Jesadiya Date: Thu, 17 Dec 2020 11:05:07 +0530 Subject: [PATCH 407/479] Update customer.graphqls --- design-documents/graph-ql/coverage/customer/customer.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/customer/customer.graphqls b/design-documents/graph-ql/coverage/customer/customer.graphqls index 8a32a655a..8568d7593 100644 --- a/design-documents/graph-ql/coverage/customer/customer.graphqls +++ b/design-documents/graph-ql/coverage/customer/customer.graphqls @@ -108,7 +108,7 @@ type Customer @doc(description: "Customer defines the customer name and address type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ uid: ID @doc(description: "The ID assigned to the address object") - customer_iid: ID @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") + customer_uid: ID @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.") region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID") region_uid: ID @doc(description: "The unique ID for a pre-defined region") @deprecated(reason: "Use `region` instead.") country_id: String @doc(description: "The customer's country") @deprecated(reason: "Use `country_code` instead.") From 86e393c4dd741721147ae58c70503071baef5ad6 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 18:54:54 -0600 Subject: [PATCH 408/479] make corrections to the md file --- design-documents/graph-ql/coverage/dynamic-blocks.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.md b/design-documents/graph-ql/coverage/dynamic-blocks.md index ab339d2d4..c8dbb18db 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.md +++ b/design-documents/graph-ql/coverage/dynamic-blocks.md @@ -19,8 +19,18 @@ In Luma dynamic blocks rendered to a JavaScript component definition that does r ``` +By parsing `
` we get the following relevant data: -PWA will receive similar JavaScript component definition that can be parsed to extract parameters and make a [GraphQL query](./dynamic-blocks.graphqls) to load dynamic blocks. +* `data-banner-id` is the id of the actual widget that contains the dynamic blocks + +* `data-ids="1,3"` are the ids of the dynamic blocks + +* `data-rotate="random"` is an obsolete behavior that we chose not to have in GraphQl + + +Ideally we want these to be rendered with graphql uid and not be exposed with real numeric ids coming from the database. + +Next the PWA will receive similar JavaScript component definition that can be parsed to extract parameters and make a [GraphQL query](./dynamic-blocks.graphqls) to load dynamic blocks. ```graphql { From cb8ca4074f1ce2595f0cf30b24925e5cf1992d7d Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 18:57:35 -0600 Subject: [PATCH 409/479] add end of line to the end of the file --- design-documents/graph-ql/coverage/dynamic-blocks.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index 823e31cd5..df1b78eca 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -37,4 +37,4 @@ type Query { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default is 20") currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"), ): DynamicBlocks! -} \ No newline at end of file +} From 7e4f2dd37c06f26e5971fe65ed3400cfff90523d Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 19:07:14 -0600 Subject: [PATCH 410/479] modifying dynamic blocks schema --- .../graph-ql/coverage/dynamic-blocks.graphqls | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls index df1b78eca..5c9115f1b 100644 --- a/design-documents/graph-ql/coverage/dynamic-blocks.graphqls +++ b/design-documents/graph-ql/coverage/dynamic-blocks.graphqls @@ -1,20 +1,20 @@ type DynamicBlock { - uid: ID! - content: ComplexTextValue! + uid: ID! @doc(description: "The unique ID of the dynamic block") + content: ComplexTextValue! @doc(description: "Contains the renderable HTML code of the dynamic block") } # We don't need name, is_enabled, locations, ga_creative, catalog_price_rule_ids, cart_price_rules_ids, locations on storefront type DynamicBlocks { - items: [DynamicBlock]! - page_info: SearchResultPageInfo - total_count: Int! + items: [DynamicBlock]! @doc(description: "An array containing individual dynamic blocks") + page_info: SearchResultPageInfo @doc(description: "Metadata for pagination rendering") + total_count: Int! @doc(description: "The number of returned dynamic blocks") } input DynamicBlocksFilterInput { - type: DynamicBlockTypeEnum! - locations: [DynamicBlockLocationEnum!] # Blocks for all locations will be displayed if not supplied - dynamic_block_uids: [ID!] # Only makes sense when DynamicBlockTypeEnum is set to SPECIFIED + type: DynamicBlockTypeEnum! @doc(description: "A value indicating the type of dynamic block to filter on") + locations: [DynamicBlockLocationEnum!] @doc(description: "An array indicating the locations the dynamic block can be placed") # Blocks for all locations will be displayed if not supplied + dynamic_block_uids: [ID!] # This value appplies when DynamicBlockTypeEnum is set to SPECIFIED } enum DynamicBlockTypeEnum { From 73407a19195655dfc39aea46699123a407c3e665 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 19:26:20 -0600 Subject: [PATCH 411/479] modify schema to fit new type for search results which is different from owner gift registy type --- .../coverage/customer/gift-registry.graphqls | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 6ebacf478..ffa289015 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -1,16 +1,21 @@ type Customer { - gift_registry_list: [GiftRegistry] + gift_registries: [GiftRegistry] gift_registry(uid: ID!): GiftRegistry } type Query { giftRegistryTypes: [GiftRegistryType] @doc(description: "Get a list of available gift registry types") - giftRegistrySearch( - registrantFirstname: String!, - registrantLastname: String!, - giftRegistryTypeUid: ID, - searchableDynamicAttributes: [GiftRegistryDynamicAttributeInput] @doc(description: "For select attributes ID should be provided expected. For range search, '_from' and '_to' suffixes can be added to the attribute code. For text attributes provide value for exact matching.") - ): [GiftRegistry] @doc(description: "Gift registry search by registrant name and additional searchable attributes.") + giftRegistryEmailSearch( + email: String! @doc(description: "The registrant's email") + ): [GiftRegistrySearchResult] @doc(description: "Search for gift registries by specifying a registrant email address") + giftRegistryIdSearch( + giftRegistryUid: String! @doc(description: "The ID of the gift registry") + ): [GiftRegistrySearchResult] @doc(description: "Search for gift registries by specifying a registry URL key") + giftRegistryTypeSearch( + firstName: String! @doc(description: "The first name of the registrant") + lastName: String! @doc(description: "The last name of the registrant") + typeUid: String @doc(description: "The type UID of the registry") + ): [GiftRegistrySearchResult] @doc(description: "Search for gift registries by specifying the registrant name and registry type ID") giftRegistry(giftRegistryUid: ID!): GiftRegistry @doc(description: "This query is intended for guests and some fields of GiftRegistry will not be availalbe") } @@ -32,6 +37,15 @@ type Mutation { shareGiftRegistry(giftRegistryUid: ID!, sender: ShareGiftRegistrySenderInput!, invitees: [ShareGiftRegistryInviteeInput!]!): ShareGiftRegistryOutput } +type GiftRegistrySearchResult { + gift_registry_uid: ID! @doc(description: "The URL key of the gift registry") + name: String! + event_title: String! @doc(description: "The title given to the event") + type: String @doc(description: "The type of event being held") + location: String @doc(description: "The location of the event") + event_date: String @doc(description: "The date of the event") +} + input ShareGiftRegistryInviteeInput { name: String! @@ -302,8 +316,8 @@ enum GiftRegistryDynamicAttributeGroup { type GiftRegistryRegistrant { uid: ID! - first_name: String! - last_name: String! + firstname: String! + lastname: String! email: String! @doc(description: "Accessible to the registry owner only") dynamic_attributes: [GiftRegistryRegistrantDynamicAttribute] } From 562bbd4c001c4aac22af7fb441923142f3a5e9c0 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 19:42:33 -0600 Subject: [PATCH 412/479] - making small corrections --- .../graph-ql/coverage/customer/gift-registry.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index ffa289015..85fc8527d 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -253,16 +253,16 @@ type GiftRegistry { } interface GiftRegistryItemInterface { - uid: String! + uid: ID! quantity: Float! quantity_fulfilled: Float! note: String date_added: String! product: ProductInterface - customizable_options: [SelectedCustomizableOption] + customizable_options: [SelectedCustomizableOption] # added support for customizable options } -type SimpleGiftRegistryItem implements GiftRegistryItemInterface { +type GiftRegistryItem implements GiftRegistryItemInterface { } type BundleGiftRegistryItem implements GiftRegistryItemInterface { From 74d002b3b14f30cd12858502c687b5bacf790096 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 13 Jan 2021 19:45:28 -0600 Subject: [PATCH 413/479] - add more corrections --- .../graph-ql/coverage/customer/gift-registry.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.md b/design-documents/graph-ql/coverage/customer/gift-registry.md index 38f45bfdd..14d497dc5 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.md +++ b/design-documents/graph-ql/coverage/customer/gift-registry.md @@ -134,10 +134,10 @@ The following JSON represents query variables for the `CreateGiftRegistryWithReg ```graphql { customer { - gift_registry_list { + gift_registries { uid event_name - created_on + created_at message } } @@ -444,7 +444,7 @@ mutation AddGiftRegistryItems($giftRegistryUid: ID!, $giftRegistryItems: [AddGif label } } - added_on + added_at note quantity quantity_fulfilled From 7c7bf9efa01d39f639678ed809eb23c8dd959b3c Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Tue, 2 Mar 2021 17:04:31 -0600 Subject: [PATCH 414/479] PWA-1486: Define the schema for FPT cart item prices --- design-documents/graph-ql/coverage/quote.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 296f2ee88..4d38f67bb 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -367,7 +367,8 @@ type Discount @doc(description:"Defines an individual discount. A discount can b } type CartItemPrices { - price: Money! + price: Money! @doc(description:"Item price that might include tax depending on display settings for cart") + fixed_product_taxes: [FixedProductTax] @doc(description:"Applied FPT to the cart item") row_total: Money! row_total_including_tax: Money! discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") From 367a21819de969c85e4e4c4f8ebb164cf6077c3a Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 17 Mar 2021 20:35:50 -0500 Subject: [PATCH 415/479] - add uid where possible and leave id where uids are not still introduced - modify other fields to inline --- .../coverage/b2b/negotiableQuotes.graphqls | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 71ad69b77..2e5ba3f98 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -74,8 +74,8 @@ type AddNegotiableQuoteItemsOutput { } input SetNegotiableQuoteShippingAddressInput { - quote_id: ID! @doc(description: "ID obtained from NegotiableQuote type") - customer_address_id: ID! @doc( + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") + customer_address_id: Int! @doc( description: "ID obtained from the CustomerAddress type. A new address can be added using Mutation.createCustomerAddress" ) } @@ -85,14 +85,14 @@ type SetNegotiableQuoteShippingAddressOutput { } input AddNegotiableQuoteItemsInput { - quote_id: ID! @doc(description: "ID from a negotiable quote object") + quote_uid: ID! @doc(description: "UID from a negotiable quote object") # Implementation Note: This *should* be compatible with the new, single # add to cart mutation. https://github.com/magento/architecture/blob/master/design-documents/graph-ql/coverage/AddProductsToCart.graphqls cart_items: [CartItemInput!]! } input DeleteNegotiableQuotesInput { - quote_ids: [ID!]! @doc(description: "A List of IDs obtained from negotiable quote types") + quote_uids: [ID!]! @doc(description: "A List of UIDs obtained from negotiable quote types") } type DeleteNegotiableQuotesOutput { @@ -107,7 +107,7 @@ type DeleteNegotiableQuotesOutput { } input CloseNegotiableQuotesInput { - quote_ids: [ID!]! @doc(description: "A List of IDs from negotiable quote objects") + quote_uids: [ID!]! @doc(description: "A List of IDs from negotiable quote objects") } type CloseNegotiableQuotesOutput { @@ -121,22 +121,22 @@ type CloseNegotiableQuotesOutput { } input RemoveNegotiableQuoteItemsInput { - quote_id: ID! - quote_item_ids: [ID!]! + quote_uid: ID! + quote_item_uids: [ID!]! } input UpdateNegotiableQuoteQuantitiesInput { - quote_id: ID! + quote_uid: ID! items: [NegotiableQuoteItemQuantityInput!]! } input NegotiableQuoteItemQuantityInput { - quote_item_id: ID! + quote_item_uid: ID! quantity: Float! } input RequestNegotiableQuoteInput { - cart_id: ID! + cart_id: Int quote_name: String! comment: NegotiableQuoteCommentInput! # files (attachments) to be added at a later date when file upload design has been finalized @@ -148,7 +148,7 @@ input NegotiableQuoteCommentInput { } input AddNegotiableQuoteCommentInput { - quote_id: ID! + quote_uid: ID! comment: NegotiableQuoteCommentInput! } @@ -195,7 +195,7 @@ enum NegotiableQuoteStatus { } input NegotiableQuoteFilterInput { - ids: FilterEqualTypeInput @doc(description: "Filter by negotiable quote ID(s)") + negotiable_quote_uid: FilterEqualTypeInput @doc(description: "Filter by negotiable quote ID(s)") name: FilterMatchTypeInput @doc(description: "Filter by negotiable quote name") } @@ -276,8 +276,8 @@ type NegotiableQuoteHistoryProductsChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L132-L146 type NegotiableQuoteHistoryProductsRemovedChange { # Open Question: Should `removed_from_catalog` ID type represent the SKU or the product id? - removed_from_catalog: [ID!] @doc(description: "List of product skus removed from seller's catalog") - removed_from_quote: [ProductInterface!] @doc(description: "List of products removed by a buyer or seller") + removed_products_uids_from_catalog: [ID!] @doc(description: "List of product skus removed from seller's catalog") + removed_products_from_quotes: [ProductInterface!] @doc(description: "List of products removed by a buyer or seller") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 From deb7ca581d30194ee4df450abe6f5da4c37f573c Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 17 Mar 2021 21:38:52 -0500 Subject: [PATCH 416/479] - cart id is actually ID --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 2e5ba3f98..5048ca5c7 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -136,7 +136,7 @@ input NegotiableQuoteItemQuantityInput { } input RequestNegotiableQuoteInput { - cart_id: Int + cart_uid: ID quote_name: String! comment: NegotiableQuoteCommentInput! # files (attachments) to be added at a later date when file upload design has been finalized From 5f7c91045211b9975be6727bdea59c6d5df4e306 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 18 Mar 2021 14:51:19 -0500 Subject: [PATCH 417/479] - modify uid and history fields --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 5048ca5c7..1f767e5b3 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -136,7 +136,7 @@ input NegotiableQuoteItemQuantityInput { } input RequestNegotiableQuoteInput { - cart_uid: ID + quote_uid: ID! @doc(description: "Cart uid") quote_name: String! comment: NegotiableQuoteCommentInput! # files (attachments) to be added at a later date when file upload design has been finalized @@ -195,7 +195,7 @@ enum NegotiableQuoteStatus { } input NegotiableQuoteFilterInput { - negotiable_quote_uid: FilterEqualTypeInput @doc(description: "Filter by negotiable quote ID(s)") + quote_uid: FilterEqualTypeInput @doc(description: "Filter by quote UID(s)") name: FilterMatchTypeInput @doc(description: "Filter by negotiable quote name") } @@ -276,8 +276,8 @@ type NegotiableQuoteHistoryProductsChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L132-L146 type NegotiableQuoteHistoryProductsRemovedChange { # Open Question: Should `removed_from_catalog` ID type represent the SKU or the product id? - removed_products_uids_from_catalog: [ID!] @doc(description: "List of product skus removed from seller's catalog") - removed_products_from_quotes: [ProductInterface!] @doc(description: "List of products removed by a buyer or seller") + removed_from_catalog_product_uids: [ID!] @doc(description: "List of product UIDs removed from seller's catalog") + removed_from_quote_products: [ProductInterface!] @doc(description: "List of products removed by a buyer or seller") } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 From 182380fee3ab8d301b220fc13c7e97ba8bd3843a Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 18 Mar 2021 15:03:49 -0500 Subject: [PATCH 418/479] - add description to cart item id as quote id --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 1f767e5b3..d736c90b4 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -122,7 +122,7 @@ type CloseNegotiableQuotesOutput { input RemoveNegotiableQuoteItemsInput { quote_uid: ID! - quote_item_uids: [ID!]! + quote_item_uids: [ID!]! @doc(description:"Cart Item uids") } input UpdateNegotiableQuoteQuantitiesInput { @@ -131,7 +131,7 @@ input UpdateNegotiableQuoteQuantitiesInput { } input NegotiableQuoteItemQuantityInput { - quote_item_uid: ID! + quote_item_uid: ID! @doc(description:"Cart Item uid") quantity: Float! } From 6e9ad1a3cb0573abab7bc89a45cc3920090f5adf Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 22 Mar 2021 16:38:32 -0500 Subject: [PATCH 419/479] - add cart management to gift registry - add user errors - correct camel case --- .../coverage/customer/gift-registry.graphqls | 42 +++++++++++++++++-- design-documents/graph-ql/directives.graphqls | 39 +++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 design-documents/graph-ql/directives.graphqls diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 85fc8527d..c4a6ceed5 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -9,7 +9,7 @@ type Query { email: String! @doc(description: "The registrant's email") ): [GiftRegistrySearchResult] @doc(description: "Search for gift registries by specifying a registrant email address") giftRegistryIdSearch( - giftRegistryUid: String! @doc(description: "The ID of the gift registry") + giftRegistryUid: ID! @doc(description: "The ID of the gift registry") ): [GiftRegistrySearchResult] @doc(description: "Search for gift registries by specifying a registry URL key") giftRegistryTypeSearch( firstName: String! @doc(description: "The first name of the registrant") @@ -26,7 +26,10 @@ type Mutation { updateGiftRegistry(giftRegistryUid: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput - addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput + addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @doc(description: "Adds individual items to the gift registry") + addGiftRegistryItemsFromCart(giftRegistryUid: ID!, cartUid: ID!, emptyCart: Boolean = true): AddGiftRegistryItemsFromCartOutput @doc(description: "Adds the whole cart with all items to the gift registry with option to empty the cart") + moveCartItemsToGiftRegistry(cartUid: ID!, giftRegistryUid: ID!): MoveCartItemsToGiftRegistryOutput @doc(description: "Moves all items items from cart to the gift registry") + removeGiftRegistryItems(giftRegistryUid: ID!, itemUids: [ID!]!): RemoveGiftRegistryItemsOutput updateGiftRegistryItems(giftRegistryUid: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput @@ -92,7 +95,8 @@ input UpdateGiftRegistryInput { } input CreateGiftRegistryInput { - giftRegistryUid: ID @doc(description: "Optional uid, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") + giftRegistryUid: ID @deprecated(reason: "Use `gift_registry_uid` instead") @doc(description: "Optional uid, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") + gift_registry_uid: ID event_name: String! giftRegistryTypeUid: ID! message: String! @@ -109,7 +113,8 @@ input GiftRegistryShippingAddressInput @doc(description: "Either address data or } input UpdateGiftRegistryRegistrantInput { - giftRegistryRegistrantUid: ID! + giftRegistryRegistrantUid: ID! @deprecated(reason: "Use `gift_registry_registrant_uid` instead") + gift_registry_registrant_uid: ID first_name: String last_name: String email: String @@ -129,26 +134,55 @@ input GiftRegistryDynamicAttributeInput { } type CreateGiftRegistryOutput { + status: Boolean! + create_gift_registry_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } type UpdateGiftRegistryOutput { + status: Boolean! + update_gift_registry_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } type RemoveGiftRegistryOutput { + status: Boolean! + remove_gift_registry_user_errors: [GiftRegistryItemsUserError]! is_removed: Boolean! } type AddGiftRegistryItemsOutput { + status: Boolean! + add_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } +type MoveCartItemsToGiftRegistryOutput { + status: Boolean! + move_cart_items_to_gift_registry_user_errors: [GiftRegistryItemsUserError]! + gift_registry: GiftRegistry +} + +type AddGiftRegistryItemsFromCartOutput { + status: Boolean! + add_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! + gift_registry: GiftRegistry +} + +enum GiftRegistryItemsUserError { + OUT_OF_STOCK + NOT_AVAILABLE +} + type RemoveGiftRegistryItemsOutput { + status: Boolean! + remove_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } type UpdateGiftRegistryItemsOutput { + status: Boolean! + update_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } diff --git a/design-documents/graph-ql/directives.graphqls b/design-documents/graph-ql/directives.graphqls new file mode 100644 index 000000000..1f25bedd9 --- /dev/null +++ b/design-documents/graph-ql/directives.graphqls @@ -0,0 +1,39 @@ +directive @doc(description: String="") on QUERY + | MUTATION + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | SCHEMA + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + +directive @resolver(class: String="") on QUERY + | MUTATION + | FIELD + | FRAGMENT_DEFINITION + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + | SCHEMA + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + +directive @typeResolver(class: String="") on UNION + | INTERFACE + | OBJECT + +directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY From ebb56b45a2c10fb7861cc078314a2357b96dce07 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Tue, 23 Mar 2021 12:50:11 -0500 Subject: [PATCH 420/479] - keep one mutation and deprecate the other --- .../coverage/customer/gift-registry.graphqls | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index c4a6ceed5..31721af65 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -26,8 +26,7 @@ type Mutation { updateGiftRegistry(giftRegistryUid: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput - addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @doc(description: "Adds individual items to the gift registry") - addGiftRegistryItemsFromCart(giftRegistryUid: ID!, cartUid: ID!, emptyCart: Boolean = true): AddGiftRegistryItemsFromCartOutput @doc(description: "Adds the whole cart with all items to the gift registry with option to empty the cart") + addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @deprecated(reason: "Use `moveCartItemsToGiftRegistry` instead. Not supported for gift registry") @doc(description: "Adds individual items to the gift registry") moveCartItemsToGiftRegistry(cartUid: ID!, giftRegistryUid: ID!): MoveCartItemsToGiftRegistryOutput @doc(description: "Moves all items items from cart to the gift registry") removeGiftRegistryItems(giftRegistryUid: ID!, itemUids: [ID!]!): RemoveGiftRegistryItemsOutput @@ -133,39 +132,28 @@ input GiftRegistryDynamicAttributeInput { value: String! } -type CreateGiftRegistryOutput { +interface GiftRegistryUserErrors { status: Boolean! - create_gift_registry_user_errors: [GiftRegistryItemsUserError]! + gift_registry_user_errors: [GiftRegistryItemsUserError]! +} + +type CreateGiftRegistryOutput { gift_registry: GiftRegistry } type UpdateGiftRegistryOutput { - status: Boolean! - update_gift_registry_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } type RemoveGiftRegistryOutput { - status: Boolean! - remove_gift_registry_user_errors: [GiftRegistryItemsUserError]! is_removed: Boolean! } type AddGiftRegistryItemsOutput { - status: Boolean! - add_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } -type MoveCartItemsToGiftRegistryOutput { - status: Boolean! - move_cart_items_to_gift_registry_user_errors: [GiftRegistryItemsUserError]! - gift_registry: GiftRegistry -} - -type AddGiftRegistryItemsFromCartOutput { - status: Boolean! - add_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! +type MoveCartItemsToGiftRegistryOutput implements GiftRegistryUserErrors { gift_registry: GiftRegistry } @@ -175,14 +163,10 @@ enum GiftRegistryItemsUserError { } type RemoveGiftRegistryItemsOutput { - status: Boolean! - remove_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! gift_registry: GiftRegistry } -type UpdateGiftRegistryItemsOutput { - status: Boolean! - update_gift_registry_items_user_errors: [GiftRegistryItemsUserError]! +type UpdateGiftRegistryItemsOutput implements GiftRegistryUserErrors { gift_registry: GiftRegistry } From 25ca54d5eab2b4c719b137fdb1c43e02aade3b09 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 24 Mar 2021 19:52:02 -0500 Subject: [PATCH 421/479] - add proposal for impoved cache key --- design-documents/graph-ql/improved-caching.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 design-documents/graph-ql/improved-caching.md diff --git a/design-documents/graph-ql/improved-caching.md b/design-documents/graph-ql/improved-caching.md new file mode 100644 index 000000000..43d090cc6 --- /dev/null +++ b/design-documents/graph-ql/improved-caching.md @@ -0,0 +1,66 @@ +## Current caching +Like described in [dev docs](https://devdocs.magento.com/guides/v2.4/graphql/caching.html) we create a full query cache which we identify through a unique key + +The unique cache key is usually a hash based on several factors which define the type of request, scope etc. + +Currently that formula is: + +```` +hash([FULL URL + GET PARAMS] + [Store Header Value] + [Content-Currency Header Value]) +```` + +All three types of cache (FPC, Varnish and Fastly) now know about those headers and they compute different caches + +## The problem +This strategy only supposes that all components of cache have public values, and knowing their values doesn't pose any security concern. +There is however what we can call private data that should not really be exposed or at the very least guessed easily if exposed. + +Take customer-group as an example. At no point in Luma would we expose customer group. Instead we compute the same hash but including customer group and salt it: + +```` +hash([FULL URL + GET PARAMS] + [Store] + [Currency] + [CustomerGroup] + [Salt]) +```` + +This works In Luma we put that value into a cookie called X-Magento-Vary which luma will send with each request. +In this case is the server who computes a cache key on which varnish or fastly can add. +Luma also stores in cookies the Store and Currency but never the value of customer group. + +PWA and Graphql uses a cookieless approach. PWA has to know now about Store and Currency but not about the private components of cache. +Also each time we change something on private components like in this case with customer-group we have to add something to the VCL in fastly and varnish. + +We also missed the fact that the server, since it has these components as variations of cache, should respond with: +```` +Vary: Store, Content-currency +```` + +There is a bug in GraphQL, as it doesn't construct properly the Vary header: +```` +Vary: Accept-Encoding, Cookie +```` + +All components/headers that compose cache key should be included in the Vary. + +## Solution +Graphql would compute the cache key and return a header with the same function as the Luma cookie (`X-Magento-Vary`), except we won't name it that way. The `Vary` header is not meant to have a hash. +```` +X-Magento-Request-Id: 85a0b196524c60eaeb7c87d1aa4708a3fb20c6a1 +```` + +PWA would send `X-Magento-Request-Id` back to GraphQL. So it will act like the cookie in Luma. +This way we solve the public-private without adding more haders than we have. Just this one time. + +This will require a change in VCL but only once. With all the other changes if we change the way cache key is computed, no VCL changes would be required nor in PWA. + +VCL/Fastly would process this and turn it into a real cache key processed internally but the edge cache. +Currently, Fastly has this: + +```` +X-Request-Id: chn45aznc4feogj23ssw3y7k +```` + +The `X-Magento-Request-Id` will take into account `X-Magento-Request-Id` and respond with a propper Vary header: +```` +Vary: X-Magento-Request-Id +```` + +This is all we need. we don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. From c124e6b188a5145b1a829e220952389a30c6e5f8 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 24 Mar 2021 19:52:14 -0500 Subject: [PATCH 422/479] - add proposal for impoved cache key --- design-documents/graph-ql/improved-caching.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/improved-caching.md b/design-documents/graph-ql/improved-caching.md index 43d090cc6..66bc13508 100644 --- a/design-documents/graph-ql/improved-caching.md +++ b/design-documents/graph-ql/improved-caching.md @@ -40,7 +40,7 @@ Vary: Accept-Encoding, Cookie All components/headers that compose cache key should be included in the Vary. -## Solution +## The Solution Graphql would compute the cache key and return a header with the same function as the Luma cookie (`X-Magento-Vary`), except we won't name it that way. The `Vary` header is not meant to have a hash. ```` X-Magento-Request-Id: 85a0b196524c60eaeb7c87d1aa4708a3fb20c6a1 @@ -63,4 +63,5 @@ The `X-Magento-Request-Id` will take into account `X-Magento-Request-Id` and res Vary: X-Magento-Request-Id ```` -This is all we need. we don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. +This should suffice all explained. We don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. +For FPC built in cache there would be no changes besides outputing X-Magento-Request-Id and the proper Vary. We already have the code that computes the cookie hash value. From b88067ba4bcf7bd3390e6b2124dc24a495ed02db Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 29 Mar 2021 16:38:07 -0500 Subject: [PATCH 423/479] - adjust neg. quote query to contain sort and status --- .../coverage/b2b/negotiableQuotes.graphqls | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index d736c90b4..5c4441385 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -9,6 +9,7 @@ type Query { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 + sort: NegotiableQuoteSortInput ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of negotiable quotes viewable by the logged-in customer") } @@ -96,6 +97,7 @@ input DeleteNegotiableQuotesInput { } type DeleteNegotiableQuotesOutput { + status: Boolean! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin @@ -103,6 +105,7 @@ type DeleteNegotiableQuotesOutput { filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 + sort: NegotiableQuoteSortInput ): NegotiableQuotesOutput @doc(description: "List of negotiable quotes available to customer") } @@ -111,12 +114,14 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed") + status: Boolean! + closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed, returns null if none was closed") #optionally display all negotiable quotes negotiable_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, currentPage: Int = 1 + sort: NegotiableQuoteSortInput ): NegotiableQuotesOutput @doc(description: "A (optionally filtered) list of negotiable quotes viewable by the logged-in customer") } @@ -199,6 +204,17 @@ input NegotiableQuoteFilterInput { name: FilterMatchTypeInput @doc(description: "Filter by negotiable quote name") } +input NegotiableQuoteSortInput { + name: SortEnum @doc(description: "Sort by negotiable quote name") + status: SortEnum + created_at: SortEnum + updated_at: SortEnum + # Some other nice to have fields are, which will not be part of MVP + is_virtual: SortEnum + grand_total: SortEnum + total_quantity: SortEnum +} + type NegotiableQuoteHistoryEntry { uid: ID! author: NegotiableQuoteUser! From cabf7fde8aa8daed2860024b77a26e9b969a7127 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 29 Mar 2021 17:10:32 -0500 Subject: [PATCH 424/479] - adding pagination for closed_quotes --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 5c4441385..2bc626bd5 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -14,7 +14,7 @@ type Query { } type NegotiableQuotesOutput { - items: [NegotiableQuote]! + items: [NegotiableQuote] page_info: SearchResultPageInfo! total_count: Int! } @@ -115,7 +115,12 @@ input CloseNegotiableQuotesInput { type CloseNegotiableQuotesOutput { status: Boolean! - closed_quotes: [NegotiableQuote!] @doc(description: "Quotes that were just closed, returns null if none was closed") + closed_quotes( + filter: NegotiableQuoteFilterInput, + pageSize: Int = 20, + currentPage: Int = 1 + sort: NegotiableQuoteSortInput + ): NegotiableQuotesOutput @doc(description: "Quotes that were just closed") #optionally display all negotiable quotes negotiable_quotes( filter: NegotiableQuoteFilterInput, From f228dcdf56de15720e896808e146e2c429bf1210 Mon Sep 17 00:00:00 2001 From: Andrew Molina Date: Fri, 9 Apr 2021 13:54:56 -0500 Subject: [PATCH 425/479] extending StoreConfig type to include Negotiable Quote config --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index d736c90b4..96d3baf3c 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -327,3 +327,7 @@ type NegotiableQuoteUser @doc(description: "A limited view of a Buyer or Seller firstname: String! lastname: String! } + +type StoreConfig { + is_negotiable_quote_active: Boolean @doc(description: "Indicates if negotiable quote functionality is enabled.") +} From 3d779d86148ac4622bd5b14f31de5a19c35ac936 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 12 Apr 2021 09:59:03 -0500 Subject: [PATCH 426/479] - remove status - add items correct behavior of non nullable --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 2bc626bd5..bf238b0d0 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -14,7 +14,7 @@ type Query { } type NegotiableQuotesOutput { - items: [NegotiableQuote] + items: [NegotiableQuote!]! page_info: SearchResultPageInfo! total_count: Int! } @@ -97,7 +97,6 @@ input DeleteNegotiableQuotesInput { } type DeleteNegotiableQuotesOutput { - status: Boolean! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin @@ -114,7 +113,6 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - status: Boolean! closed_quotes( filter: NegotiableQuoteFilterInput, pageSize: Int = 20, From e28045b569843b0a43e82bc6a1d58cec48368aec Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 12 Apr 2021 10:30:51 -0500 Subject: [PATCH 427/479] adding sort fields --- design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index bf238b0d0..21ebecb86 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -17,6 +17,7 @@ type NegotiableQuotesOutput { items: [NegotiableQuote!]! page_info: SearchResultPageInfo! total_count: Int! + sort_fields: SortFields } # Coverage missing: From 3e7c044aba8b68faa6b9163af1f963146ede7fa7 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Mon, 12 Apr 2021 18:18:13 -0400 Subject: [PATCH 428/479] Add original_total_price to prices field on negotiable quotes --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 96d3baf3c..3aada7d3f 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -207,6 +207,17 @@ type NegotiableQuoteHistoryEntry { changes: NegotiableQuoteHistoryChanges } +type CartPrices { + grand_total: Money + subtotal_including_tax: Money + subtotal_excluding_tax: Money + discount: CartDiscount @deprecated(reason: "Use discounts instead ") + subtotal_with_discount_excluding_tax: Money + applied_taxes: [CartTaxItem] + discounts: [Discount] @doc(description:"An array of applied discounts") + original_total_price: Money +} + # Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API # support for the Negotiable Quote history log feature. If we find these types aren't ideal during implementation, # let's iterate rather than stay glued to them. From 469b628fcd8d3ca478fc597b0b5b211bebc239cb Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Mon, 12 Apr 2021 18:44:37 -0400 Subject: [PATCH 429/479] Changing negotiable quote cart price type's name to be unique --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 3aada7d3f..2f2176909 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -173,7 +173,7 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] - prices: CartPrices + prices: NegotiableQuoteCartPrices buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") @@ -207,7 +207,7 @@ type NegotiableQuoteHistoryEntry { changes: NegotiableQuoteHistoryChanges } -type CartPrices { +type NegotiableQuoteCartPrices { grand_total: Money subtotal_including_tax: Money subtotal_excluding_tax: Money From edb2eb51f586f2c1bcfa347710d85adf3d6f2679 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Mon, 12 Apr 2021 20:24:12 -0400 Subject: [PATCH 430/479] Adding fields for original price on CartItemPrices --- .../coverage/b2b/negotiableQuotes.graphqls | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 2f2176909..bf4ae818a 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -173,7 +173,7 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] - prices: NegotiableQuoteCartPrices + prices: CartPrices buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") @@ -207,17 +207,15 @@ type NegotiableQuoteHistoryEntry { changes: NegotiableQuoteHistoryChanges } -type NegotiableQuoteCartPrices { - grand_total: Money - subtotal_including_tax: Money - subtotal_excluding_tax: Money - discount: CartDiscount @deprecated(reason: "Use discounts instead ") - subtotal_with_discount_excluding_tax: Money - applied_taxes: [CartTaxItem] - discounts: [Discount] @doc(description:"An array of applied discounts") +type CartPrices { original_total_price: Money } +type CartItemPrices { + original_price: Money + original_row_total: Money +} + # Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API # support for the Negotiable Quote history log feature. If we find these types aren't ideal during implementation, # let's iterate rather than stay glued to them. From c84708c07c8fa40ee908e75bee30a85235b8f565 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 13 Apr 2021 11:24:23 -0400 Subject: [PATCH 431/479] Adding original_row_total_including_tax --- design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index bf4ae818a..118ff0709 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -214,6 +214,7 @@ type CartPrices { type CartItemPrices { original_price: Money original_row_total: Money + original_row_total_including_tax: Money } # Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API From 6207a3f33720f1624a027c1c312232527ef4d3fc Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 13 Apr 2021 12:37:11 -0400 Subject: [PATCH 432/479] Changing original to initial --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 118ff0709..f737fd3c9 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -208,13 +208,13 @@ type NegotiableQuoteHistoryEntry { } type CartPrices { - original_total_price: Money + initial_total_price: Money } type CartItemPrices { - original_price: Money - original_row_total: Money - original_row_total_including_tax: Money + initial_price: Money + initial_row_total: Money + initial_row_total_including_tax: Money } # Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API From b45c93ca189d12bb4ea40331d8b95b979d135b15 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Wed, 14 Apr 2021 09:45:13 -0500 Subject: [PATCH 433/479] PWA-1612: [GraphQl] [B2B Negotiable Quote] Add mutation to submit a negotiable quote for review --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 6157415b8..c7d365491 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -59,6 +59,10 @@ type Mutation { setNegotiableQuoteShippingAddress( input: SetNegotiableQuoteShippingAddressInput! ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a negotiable quote") + + sendNegotiableQuoteForReview( + input: SendNegotiableQuoteForReviewInput! + ) : SendNegotiableQuoteForReviewOutput @doc(description: "Send the negotiable quote for review to the seller") # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( @@ -82,6 +86,15 @@ input SetNegotiableQuoteShippingAddressInput { ) } +input SendNegotiableQuoteForReviewInput { + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") + comment: NegotiableQuoteCommentInput! +} + +type SendNegotiableQuoteForReviewOutput { + quote: NegotiableQuote +} + type SetNegotiableQuoteShippingAddressOutput { quote: NegotiableQuote } From bc86edb15e7d1dd6d277e0b4d005332f6f3afac0 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 15 Apr 2021 12:33:02 -0500 Subject: [PATCH 434/479] Update negotiableQuotes.graphqls --- .../coverage/b2b/negotiableQuotes.graphqls | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index c7d365491..c409d3715 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -29,11 +29,6 @@ type Mutation { input: RequestNegotiableQuoteInput! ): RequestNegotiableQuoteOutput @doc(description: "Request a new negotiable quote for a buyer") - # Covers "Add your Comment" section of https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information - addNegotiableQuoteComment( - input: AddNegotiableQuoteCommentInput! - ): AddNegotiableQuoteCommentOutput @doc(description: "Append a new comment from the buyer to a negotiable quote") - # https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#change-the-quantity updateNegotiableQuoteQuantities( input: UpdateNegotiableQuoteQuantitiesInput! @@ -169,11 +164,6 @@ input NegotiableQuoteCommentInput { # files (attachments) to be added at a later date when file upload design has been finalized } -input AddNegotiableQuoteCommentInput { - quote_uid: ID! - comment: NegotiableQuoteCommentInput! -} - type NegotiableQuoteComment { uid: ID! created_at: String! @@ -354,11 +344,6 @@ type RemoveNegotiableQuoteItemsOutput { quote: NegotiableQuote } -type AddNegotiableQuoteCommentOutput { - comment: NegotiableQuoteComment @doc(description: "The newly added comment") - quote: NegotiableQuote -} - type UpdateNegotiableQuoteItemsQuantityOutput { quote: NegotiableQuote } From b25729750dc566aee00e89d32f218b40d5aa3471 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 19 Apr 2021 14:08:48 -0500 Subject: [PATCH 435/479] Update negotiableQuotes.graphqls --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index c409d3715..c0cf10070 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -83,7 +83,7 @@ input SetNegotiableQuoteShippingAddressInput { input SendNegotiableQuoteForReviewInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") - comment: NegotiableQuoteCommentInput! + comment: NegotiableQuoteCommentInput } type SendNegotiableQuoteForReviewOutput { From 9a1ff039621c886323b80381bffff89e2b391e04 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 22 Apr 2021 12:02:41 -0500 Subject: [PATCH 436/479] - adding per item link and correcting type --- .../coverage/customer/gift-registry.graphqls | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 31721af65..a0b0abd58 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -26,7 +26,7 @@ type Mutation { updateGiftRegistry(giftRegistryUid: ID!, giftRegistry: UpdateGiftRegistryInput!): UpdateGiftRegistryOutput removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput - addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @deprecated(reason: "Use `moveCartItemsToGiftRegistry` instead. Not supported for gift registry") @doc(description: "Adds individual items to the gift registry") + addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @doc(description: "Adds individual items to the gift registry") moveCartItemsToGiftRegistry(cartUid: ID!, giftRegistryUid: ID!): MoveCartItemsToGiftRegistryOutput @doc(description: "Moves all items items from cart to the gift registry") removeGiftRegistryItems(giftRegistryUid: ID!, itemUids: [ID!]!): RemoveGiftRegistryItemsOutput @@ -79,7 +79,7 @@ input EnteredOptionInput { } input UpdateGiftRegistryItemInput { - giftRegistryItemUid: ID! + gift_registry_item_uid: ID! quantity: Float! note: String } @@ -94,10 +94,9 @@ input UpdateGiftRegistryInput { } input CreateGiftRegistryInput { - giftRegistryUid: ID @deprecated(reason: "Use `gift_registry_uid` instead") @doc(description: "Optional uid, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") - gift_registry_uid: ID + gift_registry_uid: ID @doc(description: "Optional uid, can be generated on the client and used for sending multiple gift-registry related mutations in a single request. For example, create registry and immediatly add items or registrants.") event_name: String! - giftRegistryTypeUid: ID! + gift_registry_type_uid: ID! message: String! privacy_settings: GiftRegistryPrivacySettings! status: GiftRegistryStatus! @@ -112,7 +111,6 @@ input GiftRegistryShippingAddressInput @doc(description: "Either address data or } input UpdateGiftRegistryRegistrantInput { - giftRegistryRegistrantUid: ID! @deprecated(reason: "Use `gift_registry_registrant_uid` instead") gift_registry_registrant_uid: ID first_name: String last_name: String @@ -137,49 +135,63 @@ interface GiftRegistryUserErrors { gift_registry_user_errors: [GiftRegistryItemsUserError]! } -type CreateGiftRegistryOutput { - gift_registry: GiftRegistry + +interface GiftRegistryUserErrors { + status: Boolean! + gift_registry_user_errors: [GiftRegistryItemsUserError]! } -type UpdateGiftRegistryOutput { +interface GiftRegistryItemUserError { + status: Boolean! + gift_registry_user_errors: GiftRegistryItemsUserError +} + +interface GiftRegistryOutputInterface { gift_registry: GiftRegistry } +type CreateGiftRegistryOutput implements GiftRegistryOutputInterface { +} + +type UpdateGiftRegistryOutput implements GiftRegistryOutputInterface { +} + type RemoveGiftRegistryOutput { is_removed: Boolean! } -type AddGiftRegistryItemsOutput { - gift_registry: GiftRegistry +type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { } -type MoveCartItemsToGiftRegistryOutput implements GiftRegistryUserErrors { - gift_registry: GiftRegistry +type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryUserErrors { +} + +type GiftRegistryItemsUserError { + message: String! + product_uid: ID + gift_registry_item_uid: ID + code: GiftRegistryItemsUserErrorType! } -enum GiftRegistryItemsUserError { +enum GiftRegistryItemsUserErrorType { OUT_OF_STOCK NOT_AVAILABLE + UNDEFINED @doc(description: "Used for other exceptions like db connection failures or others") } -type RemoveGiftRegistryItemsOutput { - gift_registry: GiftRegistry +type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { } -type UpdateGiftRegistryItemsOutput implements GiftRegistryUserErrors { - gift_registry: GiftRegistry +type UpdateGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { } -type AddGiftRegistryRegistrantsOutput { - gift_registry: GiftRegistry +type AddGiftRegistryRegistrantsOutput implements GiftRegistryOutputInterface { } -type UpdateGiftRegistryRegistrantsOutput { - gift_registry: GiftRegistry +type UpdateGiftRegistryRegistrantsOutput implements GiftRegistryOutputInterface { } -type RemoveGiftRegistryRegistrantsOutput { - gift_registry: GiftRegistry +type RemoveGiftRegistryRegistrantsOutput implements GiftRegistryOutputInterface { } type ShareGiftRegistryOutput { From 0b400aa7ad3ae4eb5557466ca6aeee60c3c77cdc Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 22 Apr 2021 12:09:21 -0500 Subject: [PATCH 437/479] - correcting duplicates --- .../graph-ql/coverage/customer/gift-registry.graphqls | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index a0b0abd58..0971b7467 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -132,18 +132,12 @@ input GiftRegistryDynamicAttributeInput { interface GiftRegistryUserErrors { status: Boolean! - gift_registry_user_errors: [GiftRegistryItemsUserError]! -} - - -interface GiftRegistryUserErrors { - status: Boolean! - gift_registry_user_errors: [GiftRegistryItemsUserError]! + user_errors: [GiftRegistryItemsUserError]! } interface GiftRegistryItemUserError { status: Boolean! - gift_registry_user_errors: GiftRegistryItemsUserError + user_errors: GiftRegistryItemsUserError } interface GiftRegistryOutputInterface { From 56451442207d94d77155f001a7626f275157b275 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 22 Apr 2021 12:12:49 -0500 Subject: [PATCH 438/479] - correcting naming convention --- .../coverage/customer/gift-registry.graphqls | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 0971b7467..2fa2bee17 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -130,12 +130,12 @@ input GiftRegistryDynamicAttributeInput { value: String! } -interface GiftRegistryUserErrors { +interface GiftRegistryUserErrorsInterface { status: Boolean! user_errors: [GiftRegistryItemsUserError]! } -interface GiftRegistryItemUserError { +interface GiftRegistryItemUserErrorInterface { status: Boolean! user_errors: GiftRegistryItemsUserError } @@ -154,10 +154,10 @@ type RemoveGiftRegistryOutput { is_removed: Boolean! } -type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { +type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { } -type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryUserErrors { +type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryUserErrorsInterface { } type GiftRegistryItemsUserError { @@ -173,10 +173,10 @@ enum GiftRegistryItemsUserErrorType { UNDEFINED @doc(description: "Used for other exceptions like db connection failures or others") } -type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { +type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { } -type UpdateGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserError { +type UpdateGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { } type AddGiftRegistryRegistrantsOutput implements GiftRegistryOutputInterface { From 79d6b2ccc44ab768f89b309bb6276b682cc20793 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 22 Apr 2021 12:13:45 -0500 Subject: [PATCH 439/479] - adding reference to parent gift registry --- .../graph-ql/coverage/customer/gift-registry.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 2fa2bee17..ef1c6e895 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -164,6 +164,7 @@ type GiftRegistryItemsUserError { message: String! product_uid: ID gift_registry_item_uid: ID + gift_registry_uid: ID code: GiftRegistryItemsUserErrorType! } From 2d99bdcdf237a1824d0d942f7d9edbf8a7846ab2 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Thu, 22 Apr 2021 12:36:40 -0500 Subject: [PATCH 440/479] - refactor exceptions --- .../graph-ql/coverage/customer/gift-registry.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index ef1c6e895..67e78a9ca 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -170,8 +170,8 @@ type GiftRegistryItemsUserError { enum GiftRegistryItemsUserErrorType { OUT_OF_STOCK - NOT_AVAILABLE - UNDEFINED @doc(description: "Used for other exceptions like db connection failures or others") + NOT_FOUND @doc(description: "Used for exceptions like EntityNotFound") + UNDEFINED @doc(description: "Used for other exceptions like db connection failures or other exceptions") } type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { From 387f428855afd72ff955847064b6963e794f7c52 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Apr 2021 13:01:12 -0500 Subject: [PATCH 441/479] Handle Sorting in Negotiable Quote --- .../coverage/b2b/negotiableQuotes.graphqls | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index c0cf10070..09829ef51 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -17,7 +17,6 @@ type NegotiableQuotesOutput { items: [NegotiableQuote!]! page_info: SearchResultPageInfo! total_count: Int! - sort_fields: SortFields } # Coverage missing: @@ -212,14 +211,15 @@ input NegotiableQuoteFilterInput { } input NegotiableQuoteSortInput { - name: SortEnum @doc(description: "Sort by negotiable quote name") - status: SortEnum - created_at: SortEnum - updated_at: SortEnum - # Some other nice to have fields are, which will not be part of MVP - is_virtual: SortEnum - grand_total: SortEnum - total_quantity: SortEnum + sort_field: NegotiableQuoteSortableField! + sort_direction: SortEnum! +} + +enum NegotiableQuoteSortableField { + QUOTE_NAME + STATUS + CREATED_AT + UPDATED_AT } type NegotiableQuoteHistoryEntry { From 9be897cc2187c2f871c7edc45b1f324d855e8af9 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Thu, 22 Apr 2021 13:35:12 -0500 Subject: [PATCH 442/479] Update negotiableQuotes.graphqls --- design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls | 1 + 1 file changed, 1 insertion(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 09829ef51..3b2e4c545 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -17,6 +17,7 @@ type NegotiableQuotesOutput { items: [NegotiableQuote!]! page_info: SearchResultPageInfo! total_count: Int! + sort_fields: SortFields } # Coverage missing: From f9c0e0c06dee3c2b67a0b0d310d7de21749a2868 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 26 Apr 2021 16:46:31 -0500 Subject: [PATCH 443/479] modify schema to fit negotiable quote needs --- .../coverage/b2b/negotiableQuotes.graphqls | 13 ++++++----- .../graph-ql/coverage/quote.graphqls | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index c0cf10070..d05449027 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -230,14 +230,15 @@ type NegotiableQuoteHistoryEntry { changes: NegotiableQuoteHistoryChanges } -type CartPrices { - initial_total_price: Money +type NegotiableQuotePrices implements QuotePricesInterface { + initial_grand_total: Money! } -type CartItemPrices { - initial_price: Money - initial_row_total: Money - initial_row_total_including_tax: Money +type NegotiableQuoteItemPrices implements QuoteItemPricesInterface { + initial_price: Money! + initial_row_total: Money! + initial_row_total_excluding_tax: Money! + initial_row_total_including_tax: Money! } # Note: Most of these HistoryChange types were built based on looking through UI code in Luma, because we don't have existing API diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 4d38f67bb..fc4737009 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -180,7 +180,7 @@ input SetGuestEmailOnCartInput { email: String! } -type CartPrices { +interface QuotePricesInterface { grand_total: Money subtotal_including_tax: Money subtotal_excluding_tax: Money @@ -190,6 +190,9 @@ type CartPrices { discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts") } +type CartPrices implements QuotePricesInterface { +} + type CartTaxItem { amount: Money! label: String! @@ -366,13 +369,20 @@ type Discount @doc(description:"Defines an individual discount. A discount can b label: String! @doc(description:"A description of the discount") } -type CartItemPrices { - price: Money! @doc(description:"Item price that might include tax depending on display settings for cart") - fixed_product_taxes: [FixedProductTax] @doc(description:"Applied FPT to the cart item") - row_total: Money! - row_total_including_tax: Money! +type QuoteItemPricesInterface { discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") total_item_discount: Money @doc(description:"The total of all discounts applied to the item") + price: Money! @doc(description:"Item display price according to settings that might include tax depending on display settings for cart") + price_including_tax: Money! + price_excluding_tax: Money! + row_total: Money! @doc(description:"Row total display total according to settings that might include tax depending on display settings for cart for prices") + row_total_excluding_tax: Money! + row_total_including_tax: Money! + fixed_product_taxes: [FixedProductTax] @doc(description:"The list of all FPT taxes applied to the cart item") +} + +type CartItemPrices implements QuoteItemPricesInterface { + custom_price: Money! } type SelectedCustomizableOption { From 873fcfc284c736fdaed20b2e10102b6e9729706b Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Tue, 27 Apr 2021 09:08:56 -0500 Subject: [PATCH 444/479] Update negotiableQuotes.graphqls --- design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls | 1 - 1 file changed, 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 3b2e4c545..e038e72de 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -218,7 +218,6 @@ input NegotiableQuoteSortInput { enum NegotiableQuoteSortableField { QUOTE_NAME - STATUS CREATED_AT UPDATED_AT } From 2106998bed66f307d78c3bc7d375bc6b44f2f09c Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Wed, 28 Apr 2021 09:58:54 -0500 Subject: [PATCH 445/479] - modify header name --- design-documents/graph-ql/improved-caching.md | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/improved-caching.md b/design-documents/graph-ql/improved-caching.md index 66bc13508..3e8622c48 100644 --- a/design-documents/graph-ql/improved-caching.md +++ b/design-documents/graph-ql/improved-caching.md @@ -43,10 +43,10 @@ All components/headers that compose cache key should be included in the Vary. ## The Solution Graphql would compute the cache key and return a header with the same function as the Luma cookie (`X-Magento-Vary`), except we won't name it that way. The `Vary` header is not meant to have a hash. ```` -X-Magento-Request-Id: 85a0b196524c60eaeb7c87d1aa4708a3fb20c6a1 +X-Magento-Cache-Id: 85a0b196524c60eaeb7c87d1aa4708a3fb20c6a1 ```` -PWA would send `X-Magento-Request-Id` back to GraphQL. So it will act like the cookie in Luma. +PWA would send `X-Magento-Cache-Id` back to GraphQL. So it will act like the cookie in Luma. This way we solve the public-private without adding more haders than we have. Just this one time. This will require a change in VCL but only once. With all the other changes if we change the way cache key is computed, no VCL changes would be required nor in PWA. @@ -58,10 +58,19 @@ Currently, Fastly has this: X-Request-Id: chn45aznc4feogj23ssw3y7k ```` -The `X-Magento-Request-Id` will take into account `X-Magento-Request-Id` and respond with a propper Vary header: +The `X-Magento-Cache-Id` will take into account `X-Magento-Cache-Id` and respond with a propper Vary header: ```` -Vary: X-Magento-Request-Id +Vary: X-Magento-Cache-Id ```` -This should suffice all explained. We don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. -For FPC built in cache there would be no changes besides outputing X-Magento-Request-Id and the proper Vary. We already have the code that computes the cookie hash value. +We don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. +For FPC built in cache there would be no changes besides outputing X-Magento-Cache-Id and the proper Vary. We already have the code that computes the cookie hash value. +This should suffice all explained above except for a major possible security flaw explained below. + +### The Solution's major flaw +Since we're caching the whole query and the caching server is hit first, and we don't have yet an auth session service nor a differentiator between auth token and session token +we wouldn't check if the barer token is valid or not. +This works in Luma because they have blocks and in graphql we don't have the equivlent of that. In luma we don't cache blocks marked as private. We use separate ajax to pupulate those after we retrieve the page from cache. +In graphql we could say that we have as blocks any node Example: product {block { subblock }}. However our use cases here are catalog, prices (which we could populate and not cache). +In category permissions and shared catalogs, suddenly the whole reponse becomes uncacheable because whole products and categories might be different. +We couldn't find a viable solution here unless there's some high availability service that checks session token once this will be available part of a full oauth2 protocol in 2.5 From 3fc478cf33cd6bcd6e1cb2f2c6c05988bdfb19b8 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 10:37:59 -0500 Subject: [PATCH 446/479] - change prices --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index d05449027..c0a04c932 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -185,7 +185,7 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] - prices: CartPrices + prices: NegotiableQuotePrices buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") From e2e96b247e4d35999f92feb269f314401da569f9 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 10:56:56 -0500 Subject: [PATCH 447/479] - change prices --- design-documents/graph-ql/coverage/quote.graphqls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index fc4737009..caaa2859e 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -360,7 +360,8 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use CartItemInterface.uid instead") uid: ID! @doc(description: "Unique identifier for a Cart Item") quantity: Float! - prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") + prices: CartItemPrices @deprecated(reason: "Use `itemPrices` instead") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") + itemPrices: QuoteItemPricesInterface @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") product: ProductInterface! } From 3f020effe08586a1282e4f07527f30d15e30b3ad Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 16:00:40 -0500 Subject: [PATCH 448/479] - revert excluding tax - modify interfaces --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 1 - design-documents/graph-ql/coverage/quote.graphqls | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index c0a04c932..6838d2013 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -237,7 +237,6 @@ type NegotiableQuotePrices implements QuotePricesInterface { type NegotiableQuoteItemPrices implements QuoteItemPricesInterface { initial_price: Money! initial_row_total: Money! - initial_row_total_excluding_tax: Money! initial_row_total_including_tax: Money! } diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index caaa2859e..68c4857c8 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -360,8 +360,7 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\ id: String! @deprecated(reason: "Use CartItemInterface.uid instead") uid: ID! @doc(description: "Unique identifier for a Cart Item") quantity: Float! - prices: CartItemPrices @deprecated(reason: "Use `itemPrices` instead") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") - itemPrices: QuoteItemPricesInterface @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") + prices: QuoteItemPricesInterface @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") product: ProductInterface! } @@ -374,16 +373,12 @@ type QuoteItemPricesInterface { discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") total_item_discount: Money @doc(description:"The total of all discounts applied to the item") price: Money! @doc(description:"Item display price according to settings that might include tax depending on display settings for cart") - price_including_tax: Money! - price_excluding_tax: Money! row_total: Money! @doc(description:"Row total display total according to settings that might include tax depending on display settings for cart for prices") - row_total_excluding_tax: Money! row_total_including_tax: Money! fixed_product_taxes: [FixedProductTax] @doc(description:"The list of all FPT taxes applied to the cart item") } type CartItemPrices implements QuoteItemPricesInterface { - custom_price: Money! } type SelectedCustomizableOption { From eb0f7fe93f028e077f58ce9f53f6c6026036df2f Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 16:08:31 -0500 Subject: [PATCH 449/479] - restore order of fields --- design-documents/graph-ql/coverage/quote.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 68c4857c8..84058798a 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -370,12 +370,12 @@ type Discount @doc(description:"Defines an individual discount. A discount can b } type QuoteItemPricesInterface { + price: Money! @doc(description:"Item price that might include tax depending on display settings for cart") + fixed_product_taxes: [FixedProductTax] @doc(description:"Applied FPT to the cart item") + row_total: Money! + row_total_including_tax: Money! discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item") total_item_discount: Money @doc(description:"The total of all discounts applied to the item") - price: Money! @doc(description:"Item display price according to settings that might include tax depending on display settings for cart") - row_total: Money! @doc(description:"Row total display total according to settings that might include tax depending on display settings for cart for prices") - row_total_including_tax: Money! - fixed_product_taxes: [FixedProductTax] @doc(description:"The list of all FPT taxes applied to the cart item") } type CartItemPrices implements QuoteItemPricesInterface { From 6da52e3463701353ad204b3e8c6b5ed1f1b55273 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 21:46:32 -0500 Subject: [PATCH 450/479] - remove resolvers --- .../graph-ql/coverage/quote.graphqls | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/design-documents/graph-ql/coverage/quote.graphqls b/design-documents/graph-ql/coverage/quote.graphqls index 84058798a..e2b1b1110 100644 --- a/design-documents/graph-ql/coverage/quote.graphqls +++ b/design-documents/graph-ql/coverage/quote.graphqls @@ -2,27 +2,27 @@ # See COPYING.txt for license details. type Query { - cart(cart_id: String!): Cart @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Cart") @doc(description:"Returns information about shopping cart") @cache(cacheable: false) - customerCart: Cart! @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CustomerCart") @doc(description:"Returns information about the customer shopping cart") @cache(cacheable: false) + cart(cart_id: String!): Cart @doc(description:"Returns information about shopping cart") @cache(cacheable: false) + customerCart: Cart! @doc(description:"Returns information about the customer shopping cart") @cache(cacheable: false) } type Mutation { - createEmptyCart(input: createEmptyCartInput): String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") - addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") - addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") - applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") - removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart") - updateCartItems(input: UpdateCartItemsInput): UpdateCartItemsOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\UpdateCartItems") - removeItemFromCart(input: RemoveItemFromCartInput): RemoveItemFromCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveItemFromCart") - setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart") - setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetBillingAddressOnCart") - setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") - setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") - setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") - setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") - mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts") - placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") - addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput @doc(description:"Add any type of product to the cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddProductsToCart") + createEmptyCart(input: createEmptyCartInput): String @doc(description:"Creates an empty shopping cart for a guest or logged in user") + addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput + addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput + applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput + removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput + updateCartItems(input: UpdateCartItemsInput): UpdateCartItemsOutput + removeItemFromCart(input: RemoveItemFromCartInput): RemoveItemFromCartOutput + setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput + setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput + setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput + setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput + setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput + setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") + mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") + placeOrder(input: PlaceOrderInput): PlaceOrderOutput + addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput @doc(description:"Add any type of product to the cart") } input createEmptyCartInput { @@ -187,7 +187,7 @@ interface QuotePricesInterface { discount: CartDiscount @deprecated(reason: "Use discounts instead ") subtotal_with_discount_excluding_tax: Money applied_taxes: [CartTaxItem] - discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts") + discounts: [Discount] @doc(description:"An array of applied discounts") } type CartPrices implements QuotePricesInterface { @@ -228,21 +228,21 @@ type PlaceOrderOutput { } type Cart { - id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The ID of the cart.") - items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") - applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") - applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") - email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") - shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") - billing_address: BillingCartAddress @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") - available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") - selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") - prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") - total_quantity: Float! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartTotalQuantity") - is_virtual: Boolean! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartIsVirtual") -} - -interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { + id: ID! @doc(description: "The ID of the cart.") + items: [CartItemInterface] + applied_coupon: AppliedCoupon @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") + email: String + shipping_addresses: [ShippingCartAddress]! + billing_address: BillingCartAddress + available_payment_methods: [AvailablePaymentMethod] @doc(description: "Available payment methods") + selected_payment_method: SelectedPaymentMethod + prices: CartPrices + total_quantity: Float! + is_virtual: Boolean! +} + +interface CartAddressInterface { firstname: String! lastname: String! company: String @@ -255,8 +255,8 @@ interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Mo } type ShippingCartAddress implements CartAddressInterface { - available_shipping_methods: [AvailableShippingMethod] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\AvailableShippingMethods") - selected_shipping_method: SelectedShippingMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddress\\SelectedShippingMethod") + available_shipping_methods: [AvailableShippingMethod] + selected_shipping_method: SelectedShippingMethod customer_notes: String items_weight: Float @deprecated(reason: "This information shoud not be exposed on frontend") cart_items: [CartItemQuantity] @deprecated(reason: "`cart_items_v2` should be used instead") @@ -349,18 +349,18 @@ type SetGuestEmailOnCartOutput { } type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") { - customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") + customizable_options: [SelectedCustomizableOption] } type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Cart Item") { - customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") + customizable_options: [SelectedCustomizableOption] } -interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { +interface CartItemInterface { id: String! @deprecated(reason: "Use CartItemInterface.uid instead") uid: ID! @doc(description: "Unique identifier for a Cart Item") quantity: Float! - prices: QuoteItemPricesInterface @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") + prices: QuoteItemPricesInterface product: ProductInterface! } From 2dce55c7b0d3d4c607422b97822fc7dfa76816fc Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 22:20:54 -0500 Subject: [PATCH 451/479] - modify namings --- .../coverage/customer/gift-registry.graphqls | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 67e78a9ca..a128e90c6 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -27,7 +27,7 @@ type Mutation { removeGiftRegistry(giftRegistryUid: ID!): RemoveGiftRegistryOutput addGiftRegistryItems(giftRegistryUid: ID!, items: [AddGiftRegistryItemInput!]!): AddGiftRegistryItemsOutput @doc(description: "Adds individual items to the gift registry") - moveCartItemsToGiftRegistry(cartUid: ID!, giftRegistryUid: ID!): MoveCartItemsToGiftRegistryOutput @doc(description: "Moves all items items from cart to the gift registry") + moveCartItemsToGiftRegistry(cartUid: ID!, giftRegistryUid: ID!): MoveCartItemsToGiftRegistryOutput @doc(description: "Moves all items from cart to the gift registry") removeGiftRegistryItems(giftRegistryUid: ID!, itemUids: [ID!]!): RemoveGiftRegistryItemsOutput updateGiftRegistryItems(giftRegistryUid: ID!, items: [UpdateGiftRegistryItemInput!]!): UpdateGiftRegistryItemsOutput @@ -130,14 +130,12 @@ input GiftRegistryDynamicAttributeInput { value: String! } -interface GiftRegistryUserErrorsInterface { - status: Boolean! - user_errors: [GiftRegistryItemsUserError]! +interface GiftRegistryErrorsInterface { + errors: [GiftRegistryItemUserError]! } -interface GiftRegistryItemUserErrorInterface { - status: Boolean! - user_errors: GiftRegistryItemsUserError +interface GiftRegistryItemErrorInterface { + error: GiftRegistryItemUserError } interface GiftRegistryOutputInterface { @@ -154,13 +152,13 @@ type RemoveGiftRegistryOutput { is_removed: Boolean! } -type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { +type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemErrorInterface { } -type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryUserErrorsInterface { +type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryErrorsInterface { } -type GiftRegistryItemsUserError { +type GiftRegistryItemUserError { message: String! product_uid: ID gift_registry_item_uid: ID @@ -174,10 +172,10 @@ enum GiftRegistryItemsUserErrorType { UNDEFINED @doc(description: "Used for other exceptions like db connection failures or other exceptions") } -type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { +type RemoveGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemErrorInterface { } -type UpdateGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemUserErrorInterface { +type UpdateGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegistryItemErrorInterface { } type AddGiftRegistryRegistrantsOutput implements GiftRegistryOutputInterface { From b38c74e64f4487c63584de43628078c881016b1e Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 22:25:11 -0500 Subject: [PATCH 452/479] - modify namings --- .../graph-ql/coverage/customer/gift-registry.graphqls | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index a128e90c6..1f4fc40ce 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -131,11 +131,11 @@ input GiftRegistryDynamicAttributeInput { } interface GiftRegistryErrorsInterface { - errors: [GiftRegistryItemUserError]! + errors: [GiftRegistryItemError]! } interface GiftRegistryItemErrorInterface { - error: GiftRegistryItemUserError + error: GiftRegistryItemError } interface GiftRegistryOutputInterface { @@ -158,15 +158,15 @@ type AddGiftRegistryItemsOutput implements GiftRegistryOutputInterface, GiftRegi type MoveCartItemsToGiftRegistryOutput implements GiftRegistryOutputInterface, GiftRegistryErrorsInterface { } -type GiftRegistryItemUserError { +type GiftRegistryItemError { message: String! product_uid: ID gift_registry_item_uid: ID gift_registry_uid: ID - code: GiftRegistryItemsUserErrorType! + code: GiftRegistryItemsErrorType! } -enum GiftRegistryItemsUserErrorType { +enum GiftRegistryItemsErrorType { OUT_OF_STOCK NOT_FOUND @doc(description: "Used for exceptions like EntityNotFound") UNDEFINED @doc(description: "Used for other exceptions like db connection failures or other exceptions") From ac9bd5a61c942b0949db001f74c8d54e8205e011 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 30 Apr 2021 22:27:56 -0500 Subject: [PATCH 453/479] - modify namings --- .../graph-ql/coverage/customer/gift-registry.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls index 1f4fc40ce..e90acb10e 100644 --- a/design-documents/graph-ql/coverage/customer/gift-registry.graphqls +++ b/design-documents/graph-ql/coverage/customer/gift-registry.graphqls @@ -163,10 +163,10 @@ type GiftRegistryItemError { product_uid: ID gift_registry_item_uid: ID gift_registry_uid: ID - code: GiftRegistryItemsErrorType! + code: GiftRegistryItemErrorType! } -enum GiftRegistryItemsErrorType { +enum GiftRegistryItemErrorType { OUT_OF_STOCK NOT_FOUND @doc(description: "Used for exceptions like EntityNotFound") UNDEFINED @doc(description: "Used for other exceptions like db connection failures or other exceptions") From 1c4e78be7c2caf21e660f6a49dc91ebd1027dbd9 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Fri, 16 Jul 2021 17:54:42 -0400 Subject: [PATCH 454/479] Updating graphql cache proposal after lots of investigation --- design-documents/graph-ql/improved-caching.md | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/design-documents/graph-ql/improved-caching.md b/design-documents/graph-ql/improved-caching.md index 3e8622c48..eea09b565 100644 --- a/design-documents/graph-ql/improved-caching.md +++ b/design-documents/graph-ql/improved-caching.md @@ -1,76 +1,82 @@ ## Current caching -Like described in [dev docs](https://devdocs.magento.com/guides/v2.4/graphql/caching.html) we create a full query cache which we identify through a unique key +For GraphQL, we create a full query cache, which has results stored under unique keys as described in the [dev docs](https://devdocs.magento.com/guides/v2.4/graphql/caching.html). -The unique cache key is usually a hash based on several factors which define the type of request, scope etc. - -Currently that formula is: +These unique keys are a combination of several factors which define the scope of a request. Currently, this is the formula used for GraphQL: ```` -hash([FULL URL + GET PARAMS] + [Store Header Value] + [Content-Currency Header Value]) +[Store Header Value] + [Content-Currency Header Value] ```` -All three types of cache (FPC, Varnish and Fastly) now know about those headers and they compute different caches +All three types of cache (FPC, Varnish, and Fastly) know about these headers and use them to compute their cache keys. ## The problem -This strategy only supposes that all components of cache have public values, and knowing their values doesn't pose any security concern. -There is however what we can call private data that should not really be exposed or at the very least guessed easily if exposed. +This strategy only allows for all the components of the cache key to have public values, and for a user knowing those values to not pose any security concerns. +There is, however, what we will call "private data" which should not be exposed, or at the very least it should not be able to be reproduced/guessed easily if it is. -Take customer-group as an example. At no point in Luma would we expose customer group. Instead we compute the same hash but including customer group and salt it: +Take Customer Group as an example: At no point in Luma do we expose Customer Group. Instead, we compute a hash using Store and Currency but also including Customer Group and a salt: ```` -hash([FULL URL + GET PARAMS] + [Store] + [Currency] + [CustomerGroup] + [Salt]) +hash([Store] + [Currency] + [CustomerGroup] + [Salt]) ```` -This works In Luma we put that value into a cookie called X-Magento-Vary which luma will send with each request. -In this case is the server who computes a cache key on which varnish or fastly can add. -Luma also stores in cookies the Store and Currency but never the value of customer group. +Using a salted hash hides the Customer Group from the user but still allows it to be considered as a cache key. +This works in Luma because we put that value into a cookie called `X-Magento-Vary`, which Luma sends with each request for the VCL code to use to do cache lookups. +In this case it is the server that computes the cache key for Varnish/Fastly. +Luma also stores Store and Currency in cookies, but never the value of Customer Group. -PWA and Graphql uses a cookieless approach. PWA has to know now about Store and Currency but not about the private components of cache. -Also each time we change something on private components like in this case with customer-group we have to add something to the VCL in fastly and varnish. +PWA and GraphQL, however, use a cookieless approach. +PWA currently knows about Store and Currency, but not about the private components of the cache key. Because of this, we explicitly bypass the cache for logged-in customers in order to hit Magento and retrieve correct results. +Also, each time we change what is used for the cache key (such as adding Customer Group), we have to change the VCL in Fastly and Varnish. -We also missed the fact that the server, since it has these components as variations of cache, should respond with: +Additionally, we missed the fact that the cache node, since it uses these values as components of its own cache key, should respond with this `Vary` header for the browser cache: ```` -Vary: Store, Content-currency +Vary: Store, Content-Currency ```` -There is a bug in GraphQL, as it doesn't construct properly the Vary header: +The Fastly VCL code for GraphQL currently has a bug where it returns the same `Vary` header as it does for non-GraphQL requests (which use a cookie to store `X-Magento-Vary`): ```` Vary: Accept-Encoding, Cookie ```` -All components/headers that compose cache key should be included in the Vary. +All components/headers that compose the cache key should be included in the `Vary` header read by the browser cache. ## The Solution -Graphql would compute the cache key and return a header with the same function as the Luma cookie (`X-Magento-Vary`), except we won't name it that way. The `Vary` header is not meant to have a hash. +On every request, our GraphQL framework will compute a salted and hashed cache key using the same factors as the Luma `X-Magento-Vary` cookie and return it in a header on the response. ```` X-Magento-Cache-Id: 85a0b196524c60eaeb7c87d1aa4708a3fb20c6a1 ```` -PWA would send `X-Magento-Cache-Id` back to GraphQL. So it will act like the cookie in Luma. -This way we solve the public-private without adding more haders than we have. Just this one time. +PWA will capture this header and send it back on following GraphQL requests, which the cache node will then use as the key. +This requires a VCL change, but only once; the VCL code will not care about the method used to generate the header, so even if that changes no further updates would be required. -This will require a change in VCL but only once. With all the other changes if we change the way cache key is computed, no VCL changes would be required nor in PWA. +PWA will continue to capture the header on every response, and when it makes a new request it will send the most recent value it has received. +This way, if something happens that changes a customer's cache key such as updating their shipping address, PWA will immediately pick it up and use the correct key on the next request. -VCL/Fastly would process this and turn it into a real cache key processed internally but the edge cache. -Currently, Fastly has this: +A different value for `X-Magento-Cache-Id` can be sent with than Magento calculates for the request, such as when a user changes their currency (which does not use a mutation). +When this happens, we cannot cache the response under the `X-Magento-Cache-Id` that was on the request, otherwise incorrect values will be returned for other requests also using the initial value. +To avoid this issue, the VCL code will compare the `X-Magento-Cache-Id` values on the request and the response and not store the result in the cache if they do not match. +The cache node will respond with a proper `Vary` header for the browser cache to use: ```` -X-Request-Id: chn45aznc4feogj23ssw3y7k +Vary: Store, Content-Currency, Authorization, X-Magento-Cache-Id ```` +There is no mutation for changing Store or Currency, so they need to remain in `Vary` for the browser cache to know to use them as keys. +Likewise, a customer logging out does not use a mutation; instead, PWA just stops sending the `Authorization` header. The browser cache needs to know that this could cause a change in result values, so `Authorization` also needs to be in the `Vary` header. Of note: The VCL code will not consider the full bearer token when doing a cache lookup, just whether it exists on the request at all. -The `X-Magento-Cache-Id` will take into account `X-Magento-Cache-Id` and respond with a propper Vary header: -```` -Vary: X-Magento-Cache-Id -```` +Like `X-Magento-Vary`, the hash calculation for the `X-Magento-Cache-Id` header will use an unpredictably random salt. To ensure the salt is consistent between requests, we will store it in the environment configuration. +However, as this will happen as part of the first GraphQL request without a pre-existing salt, multiple attempts to update the config could occur simultaneously. +To avoid issues with concurrent write attempts, we will use a lock when writing to the `app/etc/env.php` file while adding the salt. +Generating this value automatically instead of through an admin interaction will avoid adding a step to the upgrade process and allow us to ensure the salt is sufficiently random. -We don't need to add Store and Content-Currency to Vary anymore, though if we want to be transparent for the public headers we still can. -For FPC built in cache there would be no changes besides outputing X-Magento-Cache-Id and the proper Vary. We already have the code that computes the cookie hash value. -This should suffice all explained above except for a major possible security flaw explained below. +For built-in FPC, there will be no changes other than outputting `X-Magento-Cache-Id` and the proper `Vary`. + +This should account for all issues listed above except for a major possible security flaw explained below. ### The Solution's major flaw -Since we're caching the whole query and the caching server is hit first, and we don't have yet an auth session service nor a differentiator between auth token and session token -we wouldn't check if the barer token is valid or not. -This works in Luma because they have blocks and in graphql we don't have the equivlent of that. In luma we don't cache blocks marked as private. We use separate ajax to pupulate those after we retrieve the page from cache. -In graphql we could say that we have as blocks any node Example: product {block { subblock }}. However our use cases here are catalog, prices (which we could populate and not cache). -In category permissions and shared catalogs, suddenly the whole reponse becomes uncacheable because whole products and categories might be different. -We couldn't find a viable solution here unless there's some high availability service that checks session token once this will be available part of a full oauth2 protocol in 2.5 +We're caching the whole query, requests hit the caching server first, and we don't (yet) have an auth session service, nor is there a differentiator between auth token and session token. This means we can't currently validate the bearer token before the cache node checks for a hit. + +This is not a problem in Luma because it has blocks, and blocks marked as private aren't cached. We use separate ajax to populate those after we retrieve the page from the cache. +Unfortunately, GraphQL doesn't have any equivalent to Luma's blocks. We could say that a node has a set of blocks (Example: `product {block { subblock }}`), but our current use cases are catalog and prices, which we could populate and not cache. +For category permissions and shared catalogs, suddenly the whole response stops being cacheable because the products and categories might be different. + +We couldn't find a viable solution for this without having some high availability service that checks session token, which will only be possible after having full oauth2 protocol in 2.5. From 91a7e87bc96f8db8a8de32cc470bcc111daf1768 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Fri, 16 Jul 2021 18:07:32 -0400 Subject: [PATCH 455/479] Fixing minor wording issues for readability --- design-documents/graph-ql/improved-caching.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/design-documents/graph-ql/improved-caching.md b/design-documents/graph-ql/improved-caching.md index eea09b565..791f0b22a 100644 --- a/design-documents/graph-ql/improved-caching.md +++ b/design-documents/graph-ql/improved-caching.md @@ -52,20 +52,20 @@ This requires a VCL change, but only once; the VCL code will not care about the PWA will continue to capture the header on every response, and when it makes a new request it will send the most recent value it has received. This way, if something happens that changes a customer's cache key such as updating their shipping address, PWA will immediately pick it up and use the correct key on the next request. -A different value for `X-Magento-Cache-Id` can be sent with than Magento calculates for the request, such as when a user changes their currency (which does not use a mutation). -When this happens, we cannot cache the response under the `X-Magento-Cache-Id` that was on the request, otherwise incorrect values will be returned for other requests also using the initial value. +A different value for `X-Magento-Cache-Id` can be sent by PWA than Magento calculates for the response, such as when a user changes their currency (which does not use a mutation). +When this happens, we cannot cache the response under the `X-Magento-Cache-Id` that was on the request, or else incorrect results will be returned for other requests also using that initial value. To avoid this issue, the VCL code will compare the `X-Magento-Cache-Id` values on the request and the response and not store the result in the cache if they do not match. -The cache node will respond with a proper `Vary` header for the browser cache to use: +The cache node will also respond with a proper `Vary` header for the browser cache to use: ```` Vary: Store, Content-Currency, Authorization, X-Magento-Cache-Id ```` There is no mutation for changing Store or Currency, so they need to remain in `Vary` for the browser cache to know to use them as keys. -Likewise, a customer logging out does not use a mutation; instead, PWA just stops sending the `Authorization` header. The browser cache needs to know that this could cause a change in result values, so `Authorization` also needs to be in the `Vary` header. Of note: The VCL code will not consider the full bearer token when doing a cache lookup, just whether it exists on the request at all. +Likewise, a customer logging out does not use a mutation; instead, PWA just stops sending the `Authorization` header. The browser cache needs to know that this could cause a change in result values, so `Authorization` also needs to be in `Vary`. Of note: The VCL code will not consider the full bearer token when doing a cache lookup, just whether it exists on the request at all. -Like `X-Magento-Vary`, the hash calculation for the `X-Magento-Cache-Id` header will use an unpredictably random salt. To ensure the salt is consistent between requests, we will store it in the environment configuration. -However, as this will happen as part of the first GraphQL request without a pre-existing salt, multiple attempts to update the config could occur simultaneously. -To avoid issues with concurrent write attempts, we will use a lock when writing to the `app/etc/env.php` file while adding the salt. +Like `X-Magento-Vary`, the hash calculation for the `X-Magento-Cache-Id` header will use an unpredictably random salt. To ensure this salt is consistent between requests, we will store it in the environment configuration. +However, as this will happen as part of the first GraphQL request without a pre-existing salt, multiple attempts to update the config could occur simultaneously before the first finishes. +To avoid issues with concurrent writes to the same file, we will use a lock when writing to `app/etc/env.php` while adding the salt. Generating this value automatically instead of through an admin interaction will avoid adding a step to the upgrade process and allow us to ensure the salt is sufficiently random. For built-in FPC, there will be no changes other than outputting `X-Magento-Cache-Id` and the proper `Vary`. From d73f6bff6da337e2724e88904775984e5b019892 Mon Sep 17 00:00:00 2001 From: Viktor Rad Date: Mon, 16 Aug 2021 15:04:07 -0500 Subject: [PATCH 456/479] MC-42795: GraphQl products query layered navigation filters return incorrect child categories list --- design-documents/graph-ql/coverage/catalog.graphqls | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index 562444582..cb0647315 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -316,10 +316,14 @@ type Products @doc(description: "The Products object is the top-level object ret page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") total_count: Int @doc(description: "The number of products that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") - aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") + aggregations (filter: AggregationsFilterInput): [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } +input AggregationsFilterInput { + includeSubcategoriesOnly: Boolean = false @doc(description: "Flag to include only subcategories of requested category.") +} + type CategoryProducts @doc(description: "The category products object returned in the Category query.") { items: [ProductInterface] @doc(description: "An array of products that are assigned to the category.") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") From d17b6d538ff2bdb9b21c24b4217097e134e47874 Mon Sep 17 00:00:00 2001 From: Viktor Rad Date: Mon, 16 Aug 2021 16:03:33 -0500 Subject: [PATCH 457/479] MC-42795: GraphQl products query layered navigation filters return incorrect child categories list --- design-documents/graph-ql/coverage/catalog.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index cb0647315..bec859b3b 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -316,7 +316,7 @@ type Products @doc(description: "The Products object is the top-level object ret page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.") total_count: Int @doc(description: "The number of products that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead") - aggregations (filter: AggregationsFilterInput): [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") + aggregations (filter: AggregationsFilterInput): [Aggregation] @doc(description: "Layered navigation aggregations with filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } From f741d2e40998815c0e962deef9ca339b63eaf54a Mon Sep 17 00:00:00 2001 From: Viktor Rad Date: Tue, 7 Sep 2021 15:55:21 -0500 Subject: [PATCH 458/479] MC-42795: GraphQl products query layered navigation filters return incorrect child categories list --- design-documents/graph-ql/coverage/catalog.graphqls | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index bec859b3b..fc1d84a41 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -320,8 +320,12 @@ type Products @doc(description: "The Products object is the top-level object ret sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } -input AggregationsFilterInput { - includeSubcategoriesOnly: Boolean = false @doc(description: "Flag to include only subcategories of requested category.") +input AggregationsFilterInput @doc(description: "Filter aggregations in layered navigation.") { + category: AggregationsCategoryFilterInput @doc(description: "Filter category aggregations in layered navigation.") +} + +input AggregationsCategoryFilterInput @doc(description: "Filter category aggregations in layered navigation."){ + includeDirectChildrenOnly: Boolean = false @doc(description: "Flag to include only direct subcategories of requested category.") } type CategoryProducts @doc(description: "The category products object returned in the Category query.") { From 040941bfa7b3c342a399c2f98da50f4485e49758 Mon Sep 17 00:00:00 2001 From: Viktor Rad Date: Tue, 7 Sep 2021 16:27:31 -0500 Subject: [PATCH 459/479] MC-42795: GraphQl products query layered navigation filters return incorrect child categories list --- design-documents/graph-ql/coverage/catalog.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/catalog.graphqls b/design-documents/graph-ql/coverage/catalog.graphqls index fc1d84a41..6579abc1f 100644 --- a/design-documents/graph-ql/coverage/catalog.graphqls +++ b/design-documents/graph-ql/coverage/catalog.graphqls @@ -320,12 +320,12 @@ type Products @doc(description: "The Products object is the top-level object ret sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } -input AggregationsFilterInput @doc(description: "Filter aggregations in layered navigation.") { +input AggregationsFilterInput @doc(description: "An input object that specifies the filters used in product aggregations.") { category: AggregationsCategoryFilterInput @doc(description: "Filter category aggregations in layered navigation.") } input AggregationsCategoryFilterInput @doc(description: "Filter category aggregations in layered navigation."){ - includeDirectChildrenOnly: Boolean = false @doc(description: "Flag to include only direct subcategories of requested category.") + includeDirectChildrenOnly: Boolean = false @doc(description: "Indicates whether to include only direct subcategories or all children categories at all levels.") } type CategoryProducts @doc(description: "The category products object returned in the Category query.") { From 2ab45aaecd9a28f50bfd897c8de16bc2a44a6c8d Mon Sep 17 00:00:00 2001 From: Cari Spruiell Date: Fri, 17 Sep 2021 12:15:33 -0500 Subject: [PATCH 460/479] PWA-2114: [GraphQL] Create mutation to set payment method on negotiable quote - add mutation setNegotiableQuotePaymentMethod --- .../coverage/b2b/negotiableQuotes.graphqls | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 3a2b5185d..10f061c8f 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -54,7 +54,7 @@ type Mutation { setNegotiableQuoteShippingAddress( input: SetNegotiableQuoteShippingAddressInput! ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a negotiable quote") - + sendNegotiableQuoteForReview( input: SendNegotiableQuoteForReviewInput! ) : SendNegotiableQuoteForReviewOutput @doc(description: "Send the negotiable quote for review to the seller") @@ -63,6 +63,10 @@ type Mutation { # addNegotiableQuoteFiles( # input: AddNegotiableQuoteFilesInput! # ): AddNegotiableQuoteFilesInput + + setNegotiableQuotePaymentMethod( + input: SetNegotiableQuotePaymentMethodInput! + ): SetNegotiableQuotePaymentMethodOutput @doc(description: "Set the payment method on the negotiable quote") } type AddNegotiableQuoteItemsOutput { @@ -315,7 +319,7 @@ type NegotiableQuoteHistoryProductsRemovedChange { # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L148-L169 type NegotiableQuoteHistoryProductsAddedChange { - # TODO: List of products added and their respective options. + # TODO: List of products added and their respective options. } # Usage in Luma: https://github.com/magento/magento2b2b/blob/0e791b5f7cd604ee6ee40b7225807c01b7f70cf2/app/code/Magento/NegotiableQuote/view/base/templates/quote/history.phtml#L172-L197 @@ -359,3 +363,12 @@ type NegotiableQuoteUser @doc(description: "A limited view of a Buyer or Seller type StoreConfig { is_negotiable_quote_active: Boolean @doc(description: "Indicates if negotiable quote functionality is enabled.") } + +input SetNegotiableQuotePaymentMethodInput { + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote object") + code: String! @doc(description:"Payment method code") +} + +type SetNegotiableQuotePaymentMethodOutput { + quote: NegotiableQuote +} From 89bbfd1210a16c476cf1e10e2af0cda019875312 Mon Sep 17 00:00:00 2001 From: Cari Spruiell Date: Mon, 20 Sep 2021 12:17:38 -0500 Subject: [PATCH 461/479] PWA-2114: [GraphQL] Create mutation to set payment method on negotiable quote - add mutation setNegotiableQuotePaymentMethod --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 10f061c8f..886add3d3 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -189,6 +189,8 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] + available_payment_methods: [AvailablePaymentMethod] + selected_payment_method: SelectedPaymentMethod prices: NegotiableQuotePrices buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") From 035a7fc839be76ff0d23cf87cf039f89c202927c Mon Sep 17 00:00:00 2001 From: Cari Spruiell Date: Tue, 21 Sep 2021 15:17:38 -0500 Subject: [PATCH 462/479] PWA-2114: [GraphQL] Create mutation to set payment method on negotiable quote - update schema --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 886add3d3..a69467e22 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -368,7 +368,12 @@ type StoreConfig { input SetNegotiableQuotePaymentMethodInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote object") + payment_method: NegotiableQuotePaymentMethodInput! +} + +input NegotiableQuotePaymentMethodInput { code: String! @doc(description:"Payment method code") + purchase_order_number: String @doc(description:"Purchase order number") } type SetNegotiableQuotePaymentMethodOutput { From fbdf470cf4620717ba8acea95143672450409ad2 Mon Sep 17 00:00:00 2001 From: Cari Spruiell Date: Wed, 22 Sep 2021 09:31:04 -0500 Subject: [PATCH 463/479] PWA-2114: [GraphQL] Create mutation to set payment method on negotiable quote - update schema --- .../coverage/b2b/negotiableQuotes.graphqls | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index a69467e22..62db7aecc 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -189,8 +189,8 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] - available_payment_methods: [AvailablePaymentMethod] - selected_payment_method: SelectedPaymentMethod + available_payment_methods: [NegotiableQuoteAvailablePaymentMethod] + selected_payment_method: NegotiableQuoteSelectedPaymentMethod prices: NegotiableQuotePrices buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") @@ -379,3 +379,14 @@ input NegotiableQuotePaymentMethodInput { type SetNegotiableQuotePaymentMethodOutput { quote: NegotiableQuote } + +type NegotiableQuoteAvailablePaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") +} + +type NegotiableQuoteSelectedPaymentMethod { + code: String! @doc(description: "The payment method code") + title: String! @doc(description: "The payment method title.") + purchase_order_number: String @doc(description: "The purchase order number.") +} From 22de863cea586418ab468fb6970914d20dc38676 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Wed, 22 Sep 2021 12:31:05 -0400 Subject: [PATCH 464/479] Fixing bad merge of pulls 495 and 496 --- .../coverage/b2b/negotiableQuotes.graphqls | 102 +++++++++++++++++- .../cart/CartAddressOperations.graphqls | 3 +- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 62db7aecc..cea29633b 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -59,6 +59,10 @@ type Mutation { input: SendNegotiableQuoteForReviewInput! ) : SendNegotiableQuoteForReviewOutput @doc(description: "Send the negotiable quote for review to the seller") + setNegotiableQuoteBillingAddress( + input: SetNegotiableQuoteBillingAddressInput! + ): SetNegotiableQuoteBillingAddressOutput @doc(description: "Assign a billing address to a negotiable quote") + # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( # input: AddNegotiableQuoteFilesInput! @@ -80,9 +84,66 @@ type AddNegotiableQuoteItemsOutput { input SetNegotiableQuoteShippingAddressInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") - customer_address_id: Int! @doc( - description: "ID obtained from the CustomerAddress type. A new address can be added using Mutation.createCustomerAddress" - ) + customer_address_id: Int @deprecated(reason: "Use NegotiableQuoteShippingAddressInput.customer_address_id instead") + shipping_addresses: [NegotiableQuoteShippingAddressInput!]! +} + +input NegotiableQuoteShippingAddressInput { + customer_address_id: Int + address: NegotiableQuoteAddressInput + customer_notes: String +} + +input SetNegotiableQuoteBillingAddressInput { + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") + billing_address: NegotiableQuoteBillingAddressInput! +} + +input NegotiableQuoteBillingAddressInput { + customer_address_id: Int + address: NegotiableQuoteAddressInput + use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") + same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the negotiable quote") +} + +input NegotiableQuoteAddressInput { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: String + region_id: Int + postcode: String + country_code: String! + telephone: String + save_in_address_book: Boolean +} + +input SetNegotiableQuoteBillingAddressInput { + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") + billing_address: NegotiableQuoteBillingAddressInput! +} + +input NegotiableQuoteBillingAddressInput { + customer_address_id: Int + address: NegotiableQuoteAddressInput + use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") + same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the negotiable quote") +} + +input NegotiableQuoteAddressInput { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: String + region_id: Int + postcode: String + country_code: String! + telephone: String + save_in_address_book: Boolean } input SendNegotiableQuoteForReviewInput { @@ -189,6 +250,8 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] + shipping_addresses: [NegotiableQuoteShippingAddress]! + billing_address: NegotiableQuoteBillingAddress available_payment_methods: [NegotiableQuoteAvailablePaymentMethod] selected_payment_method: NegotiableQuoteSelectedPaymentMethod prices: NegotiableQuotePrices @@ -198,6 +261,35 @@ type NegotiableQuote { status: NegotiableQuoteStatus! } +type NegotiableQuoteShippingAddress implements NegotiableQuoteAddressInterface { +} + +type NegotiableQuoteBillingAddress implements NegotiableQuoteAddressInterface { +} + +interface NegotiableQuoteAddressInterface { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: NegotiableQuoteAddressRegion + postcode: String + country: NegotiableQuoteAddressCountry! + telephone: String +} + +type NegotiableQuoteAddressRegion { + code: String + label: String + region_id: Int +} + +type NegotiableQuoteAddressCountry { + code: String! + label: String! +} + # Implementation Note: Using the values from the "Buyer Status" column of the state mapping table in devdocs. # These states are identical between storefront/admin, but we name them differently for buyers. # https://devdocs.magento.com/guides/v2.4/b2b/negotiable-quote.html#quote-statuses @@ -366,6 +458,10 @@ type StoreConfig { is_negotiable_quote_active: Boolean @doc(description: "Indicates if negotiable quote functionality is enabled.") } +type SetNegotiableQuoteBillingAddressOutput { + quote: NegotiableQuote +} + input SetNegotiableQuotePaymentMethodInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote object") payment_method: NegotiableQuotePaymentMethodInput! diff --git a/design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls b/design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls index 42630f71d..9b2622938 100644 --- a/design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls +++ b/design-documents/graph-ql/coverage/cart/CartAddressOperations.graphqls @@ -22,7 +22,8 @@ input SetBillingAddressOnCartInput { input BillingAddressInput { customer_address_id: Int address: CartAddressInput - use_for_shipping: Boolean + use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") + same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the cart") } input SetShippingAddressesOnCartInput { From 1166b8878dff12d71efbfbe74765cfd7ebe5c963 Mon Sep 17 00:00:00 2001 From: Andrew Molina Date: Thu, 21 Oct 2021 16:38:06 -0500 Subject: [PATCH 465/479] Added mutation to set shipping methods on negotiable quote --- .../coverage/b2b/negotiableQuotes.graphqls | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index cea29633b..52084a7ed 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -49,28 +49,32 @@ type Mutation { input: DeleteNegotiableQuotesInput! ): DeleteNegotiableQuotesOutput @doc(description: "Delete a negotiable quote, removing it from the display in the storefront") + sendNegotiableQuoteForReview( + input: SendNegotiableQuoteForReviewInput! + ) : SendNegotiableQuoteForReviewOutput @doc(description: "Send the negotiable quote for review to the seller") + # Covers "Select Existing Address" in https://docs.magento.com/user-guide/customers/account-dashboard-quotes-negotiate.html#shipping-information # "New Address" flow is covered by Mutation.createCustomerAddress setNegotiableQuoteShippingAddress( input: SetNegotiableQuoteShippingAddressInput! ): SetNegotiableQuoteShippingAddressOutput @doc(description: "Assign one of buyers' existing addresses to a negotiable quote") - sendNegotiableQuoteForReview( - input: SendNegotiableQuoteForReviewInput! - ) : SendNegotiableQuoteForReviewOutput @doc(description: "Send the negotiable quote for review to the seller") - setNegotiableQuoteBillingAddress( input: SetNegotiableQuoteBillingAddressInput! ): SetNegotiableQuoteBillingAddressOutput @doc(description: "Assign a billing address to a negotiable quote") - # Pending decision on design of file upload (design doc still pending decisions) - # addNegotiableQuoteFiles( - # input: AddNegotiableQuoteFilesInput! - # ): AddNegotiableQuoteFilesInput + setNegotiableQuoteShippingMethods( + input: SetNegotiableQuoteShippingMethodsInput! + ): SetNegotiableQuoteShippingMethodsOutput @doc(description: "Assign the shipping methods on the negotiable quote") setNegotiableQuotePaymentMethod( input: SetNegotiableQuotePaymentMethodInput! ): SetNegotiableQuotePaymentMethodOutput @doc(description: "Set the payment method on the negotiable quote") + + # Pending decision on design of file upload (design doc still pending decisions) + # addNegotiableQuoteFiles( + # input: AddNegotiableQuoteFilesInput! + # ): AddNegotiableQuoteFilesInput } type AddNegotiableQuoteItemsOutput { @@ -146,6 +150,15 @@ input NegotiableQuoteAddressInput { save_in_address_book: Boolean } +input SetNegotiableQuoteShippingMethodsInput { + quote_uid: ID! + shipping_methods: [ShippingMethodInput!]! +} + +type SetNegotiableQuoteShippingMethodsOutput { + quote: NegotiableQuote +} + input SendNegotiableQuoteForReviewInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") comment: NegotiableQuoteCommentInput @@ -262,6 +275,8 @@ type NegotiableQuote { } type NegotiableQuoteShippingAddress implements NegotiableQuoteAddressInterface { + available_shipping_methods: [AvailableShippingMethod] + selected_shipping_method: SelectedShippingMethod } type NegotiableQuoteBillingAddress implements NegotiableQuoteAddressInterface { From 6774c72ea732bfad10906a62c4f129e754c0075b Mon Sep 17 00:00:00 2001 From: Andrew Molina Date: Fri, 22 Oct 2021 14:26:41 -0500 Subject: [PATCH 466/479] Updated shipping_addresses field so that it is not required Updated customer_address_id type from Int to ID --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index cea29633b..41ab15a00 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -84,12 +84,12 @@ type AddNegotiableQuoteItemsOutput { input SetNegotiableQuoteShippingAddressInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") - customer_address_id: Int @deprecated(reason: "Use NegotiableQuoteShippingAddressInput.customer_address_id instead") - shipping_addresses: [NegotiableQuoteShippingAddressInput!]! + customer_address_id: ID @deprecated(reason: "Use NegotiableQuoteShippingAddressInput.customer_address_id instead") + shipping_addresses: [NegotiableQuoteShippingAddressInput!] } input NegotiableQuoteShippingAddressInput { - customer_address_id: Int + customer_address_id: ID address: NegotiableQuoteAddressInput customer_notes: String } @@ -126,7 +126,7 @@ input SetNegotiableQuoteBillingAddressInput { } input NegotiableQuoteBillingAddressInput { - customer_address_id: Int + customer_address_id: ID address: NegotiableQuoteAddressInput use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the negotiable quote") From d2fb07794ea916f05814a37e22eaa2d8a5c459b0 Mon Sep 17 00:00:00 2001 From: Andrew Molina Date: Fri, 22 Oct 2021 16:51:16 -0500 Subject: [PATCH 467/479] Renamed customer_address_id to customer_address_uid --- .../coverage/b2b/negotiableQuotes.graphqls | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 41ab15a00..29a8951a6 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -84,12 +84,12 @@ type AddNegotiableQuoteItemsOutput { input SetNegotiableQuoteShippingAddressInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") - customer_address_id: ID @deprecated(reason: "Use NegotiableQuoteShippingAddressInput.customer_address_id instead") + customer_address_id: ID @deprecated(reason: "Use `NegotiableQuoteShippingAddressInput.customer_address_uid` instead") shipping_addresses: [NegotiableQuoteShippingAddressInput!] } input NegotiableQuoteShippingAddressInput { - customer_address_id: ID + customer_address_uid: ID address: NegotiableQuoteAddressInput customer_notes: String } @@ -100,33 +100,7 @@ input SetNegotiableQuoteBillingAddressInput { } input NegotiableQuoteBillingAddressInput { - customer_address_id: Int - address: NegotiableQuoteAddressInput - use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") - same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the negotiable quote") -} - -input NegotiableQuoteAddressInput { - firstname: String! - lastname: String! - company: String - street: [String!]! - city: String! - region: String - region_id: Int - postcode: String - country_code: String! - telephone: String - save_in_address_book: Boolean -} - -input SetNegotiableQuoteBillingAddressInput { - quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") - billing_address: NegotiableQuoteBillingAddressInput! -} - -input NegotiableQuoteBillingAddressInput { - customer_address_id: ID + customer_address_uid: ID address: NegotiableQuoteAddressInput use_for_shipping: Boolean @doc(description: "Indicates whether to additionally set the shipping address based on the provided billing address") same_as_shipping: Boolean @doc(description: "Indicates whether to set the billing address based on the existing shipping address on the negotiable quote") From 05341009b184a0634cabc8652010232ea7e29e92 Mon Sep 17 00:00:00 2001 From: Andrew Molina Date: Wed, 27 Oct 2021 17:00:35 -0500 Subject: [PATCH 468/479] Updated placeOrder mutation for negotiable quote usage --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 8fb25ba42..2b479470c 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -71,6 +71,10 @@ type Mutation { input: SetNegotiableQuotePaymentMethodInput! ): SetNegotiableQuotePaymentMethodOutput @doc(description: "Set the payment method on the negotiable quote") + placeOrder( + input: PlaceOrderInput + ): PlaceOrderOutput @doc(description: "Place an order using the negotiable quote") + # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( # input: AddNegotiableQuoteFilesInput! From 6167bc541eed1e369b164db96475d54145cebc5c Mon Sep 17 00:00:00 2001 From: Tommy Wiebell Date: Tue, 2 Nov 2021 12:42:52 -0500 Subject: [PATCH 469/479] Add initial requirements for recaptcha storeConfig --- .../graph-ql/coverage/re-captcha.graphqls | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 design-documents/graph-ql/coverage/re-captcha.graphqls diff --git a/design-documents/graph-ql/coverage/re-captcha.graphqls b/design-documents/graph-ql/coverage/re-captcha.graphqls new file mode 100644 index 000000000..8c284acb6 --- /dev/null +++ b/design-documents/graph-ql/coverage/re-captcha.graphqls @@ -0,0 +1,31 @@ +enum ReCaptchaVersion { + V3 +} + +enum ReCaptchaForms { + APPLY_GIFT_CARD + APPLY_COUPON + CHANGE_PASSWORD + CHECKOUT + LOGIN + CREATE_CUSTOMER + EDIT_CUSTOMER + NEWSLETTER +} + +type ReCaptchaConfiguration { + type: ReCaptchaVersion! + website_key: String! + minimum_score: Float! + badge_position: String! + language_code: String + failure_message: String! + forms: [ReCaptchaForms!]! +} + +# Google reCAPTCHA config - will return null if v3 invisible is not configured +# for at least one Storefront form. +type StoreConfig { + recaptcha: ReCaptchaConfiguration + @doc(description: "Google reCAPTCHA Configuration") +} From beeb1ccc68b8daadea1f8a3ce0fc53dde1444999 Mon Sep 17 00:00:00 2001 From: Nicolas Marquez Date: Wed, 3 Nov 2021 13:42:38 -0400 Subject: [PATCH 470/479] Add NegotiableQuote specific place order mutation and input --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 2b479470c..965983e92 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -71,8 +71,8 @@ type Mutation { input: SetNegotiableQuotePaymentMethodInput! ): SetNegotiableQuotePaymentMethodOutput @doc(description: "Set the payment method on the negotiable quote") - placeOrder( - input: PlaceOrderInput + placeNegotiableQuoteOrder( + input: PlaceNegotiableQuoteOrderInput ): PlaceOrderOutput @doc(description: "Place an order using the negotiable quote") # Pending decision on design of file upload (design doc still pending decisions) @@ -142,6 +142,10 @@ input SendNegotiableQuoteForReviewInput { comment: NegotiableQuoteCommentInput } +input PlaceNegotiableQuoteOrderInput { + quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") +} + type SendNegotiableQuoteForReviewOutput { quote: NegotiableQuote } From edd71e38e15d899287840635093f78867fd4e48b Mon Sep 17 00:00:00 2001 From: Nicolas Marquez Date: Wed, 3 Nov 2021 14:23:38 -0400 Subject: [PATCH 471/479] Add Negotiable Quote specifi place order mutation output --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 965983e92..f204fb800 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -73,7 +73,7 @@ type Mutation { placeNegotiableQuoteOrder( input: PlaceNegotiableQuoteOrderInput - ): PlaceOrderOutput @doc(description: "Place an order using the negotiable quote") + ): PlaceNegotiableQuoteOrderOutput @doc(description: "Place an order using the negotiable quote") # Pending decision on design of file upload (design doc still pending decisions) # addNegotiableQuoteFiles( @@ -146,6 +146,10 @@ input PlaceNegotiableQuoteOrderInput { quote_uid: ID! @doc(description: "ID obtained from NegotiableQuote type") } +type PlaceNegotiableQuoteOrderOutput { + order: Order! +} + type SendNegotiableQuoteForReviewOutput { quote: NegotiableQuote } From 69e56aa2321c0c5d807dcc38948346a72dc3cc83 Mon Sep 17 00:00:00 2001 From: Cari Spruiell Date: Thu, 4 Nov 2021 16:25:01 -0500 Subject: [PATCH 472/479] PWA-2150: [Spike] Identify discrepancies between GraphQL types Cart and Negotiable Quote --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 8fb25ba42..578d831ee 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -237,11 +237,15 @@ type NegotiableQuote { # attachments: [AttachmentContent] comments: [NegotiableQuoteComment!] history: [NegotiableQuoteHistoryEntry!] + applied_coupons: [AppliedCoupon] @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") + email: String shipping_addresses: [NegotiableQuoteShippingAddress]! billing_address: NegotiableQuoteBillingAddress available_payment_methods: [NegotiableQuoteAvailablePaymentMethod] selected_payment_method: NegotiableQuoteSelectedPaymentMethod prices: NegotiableQuotePrices + total_quantity: Float! + is_virtual: Boolean! buyer: NegotiableQuoteUser! created_at: String @doc(description: "Timestamp indicating when the negotiable quote was created.") updated_at: String @doc(description: "Timestamp indicating when the negotiable quote was updated.") From fda525be673f91937756f7d9e8f66ba8400be683 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 9 Nov 2021 16:20:01 -0500 Subject: [PATCH 473/479] PWA-1611: Adding the ability to report partial successes on batch operations in Negotiable Quote --- .../coverage/b2b/negotiableQuotes.graphqls | 50 ++++++++++++++++--- design-documents/graph-ql/directives.graphqls | 6 +++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 578d831ee..836ce9e19 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -157,7 +157,29 @@ input DeleteNegotiableQuotesInput { quote_uids: [ID!]! @doc(description: "A List of UIDs obtained from negotiable quote types") } +type NegotiableQuoteOperationSuccess { + quote_uid: ID! +} + +interface NegotiableQuoteOperationErrorInterface { + quote_uid: ID! + message: String +} + +type NegotiableQuoteInvalidStateError implements NegotiableQuoteOperationErrorInterface { +} + +type NegotiableQuoteNotFoundError implements NegotiableQuoteOperationErrorInterface { +} + +type NegotiableQuoteUndefinedError implements NegotiableQuoteOperationErrorInterface { +} + +union NegotiableQuoteOperationResult = NegotiableQuoteOperationSuccess | NegotiableQuoteOperationErrorInterface + type DeleteNegotiableQuotesOutput { + result_status: TriStateMutationResultStatus + operation_results: [NegotiableQuoteOperationResult!]! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin @@ -174,12 +196,9 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - closed_quotes( - filter: NegotiableQuoteFilterInput, - pageSize: Int = 20, - currentPage: Int = 1 - sort: NegotiableQuoteSortInput - ): NegotiableQuotesOutput @doc(description: "Quotes that were just closed") + result_status: TriStateMutationResultStatus + operation_results: [NegotiableQuoteOperationResult!]! + closed_quotes: [NegotiableQuote!] @doc(description: "An array containing the negotiable quotes that were just closed") @deprecated(reason: "Replaced with operation_results") #optionally display all negotiable quotes negotiable_quotes( filter: NegotiableQuoteFilterInput, @@ -431,7 +450,26 @@ type RequestNegotiableQuoteOutput { quote: NegotiableQuote } +type NegotiableQuoteItemOperationSuccess { + quote_item_uid: ID! +} + +interface NegotiableQuoteItemOperationErrorInterface { + quote_item_uid: ID! + message: String +} + +type NegotiableQuoteItemNotFoundError implements NegotiableQuoteItemOperationErrorInterface { +} + +type NegotiableQuoteUndefinedError implements NegotiableQuoteItemOperationErrorInterface { +} + +union NegotiableQuoteItemOperationResult = NegotiableQuoteItemOperationSuccess | NegotiableQuoteItemOperationErrorInterface + type RemoveNegotiableQuoteItemsOutput { + result_status: TriStateMutationResultStatus + operation_results: [NegotiableQuoteItemOperationResult!]! quote: NegotiableQuote } diff --git a/design-documents/graph-ql/directives.graphqls b/design-documents/graph-ql/directives.graphqls index 1f25bedd9..5af1c5e1f 100644 --- a/design-documents/graph-ql/directives.graphqls +++ b/design-documents/graph-ql/directives.graphqls @@ -37,3 +37,9 @@ directive @typeResolver(class: String="") on UNION | OBJECT directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY + +enum TriStateMutationResultStatus { + SUCCESS + FAILURE + PARTIAL_SUCCESS +} From a73c7e405935b58324f940df94ea87ca90609993 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 9 Nov 2021 16:28:37 -0500 Subject: [PATCH 474/479] Fixing NQ Item Undefined type --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 836ce9e19..59a5b7143 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -462,7 +462,7 @@ interface NegotiableQuoteItemOperationErrorInterface { type NegotiableQuoteItemNotFoundError implements NegotiableQuoteItemOperationErrorInterface { } -type NegotiableQuoteUndefinedError implements NegotiableQuoteItemOperationErrorInterface { +type NegotiableQuoteItemUndefinedError implements NegotiableQuoteItemOperationErrorInterface { } union NegotiableQuoteItemOperationResult = NegotiableQuoteItemOperationSuccess | NegotiableQuoteItemOperationErrorInterface From f2099c8c0cc59838ee345c7094390bb2759187a1 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 9 Nov 2021 16:49:34 -0500 Subject: [PATCH 475/479] Making result status a required output field --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 59a5b7143..51e05b377 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -178,7 +178,7 @@ type NegotiableQuoteUndefinedError implements NegotiableQuoteOperationErrorInter union NegotiableQuoteOperationResult = NegotiableQuoteOperationSuccess | NegotiableQuoteOperationErrorInterface type DeleteNegotiableQuotesOutput { - result_status: TriStateMutationResultStatus + result_status: TriStateMutationResultStatus! operation_results: [NegotiableQuoteOperationResult!]! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will @@ -196,7 +196,7 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - result_status: TriStateMutationResultStatus + result_status: TriStateMutationResultStatus! operation_results: [NegotiableQuoteOperationResult!]! closed_quotes: [NegotiableQuote!] @doc(description: "An array containing the negotiable quotes that were just closed") @deprecated(reason: "Replaced with operation_results") #optionally display all negotiable quotes @@ -468,7 +468,7 @@ type NegotiableQuoteItemUndefinedError implements NegotiableQuoteItemOperationEr union NegotiableQuoteItemOperationResult = NegotiableQuoteItemOperationSuccess | NegotiableQuoteItemOperationErrorInterface type RemoveNegotiableQuoteItemsOutput { - result_status: TriStateMutationResultStatus + result_status: TriStateMutationResultStatus! operation_results: [NegotiableQuoteItemOperationResult!]! quote: NegotiableQuote } From 5b01542090d82349754184934547915042c5c52c Mon Sep 17 00:00:00 2001 From: Tommy Wiebell Date: Wed, 10 Nov 2021 15:43:10 -0600 Subject: [PATCH 476/479] Move to dedicated endpoint to accomodate model based recaptcha config --- .../graph-ql/coverage/re-captcha.graphqls | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/design-documents/graph-ql/coverage/re-captcha.graphqls b/design-documents/graph-ql/coverage/re-captcha.graphqls index 8c284acb6..404304bc5 100644 --- a/design-documents/graph-ql/coverage/re-captcha.graphqls +++ b/design-documents/graph-ql/coverage/re-captcha.graphqls @@ -1,31 +1,44 @@ -enum ReCaptchaVersion { - V3 -} - -enum ReCaptchaForms { - APPLY_GIFT_CARD - APPLY_COUPON - CHANGE_PASSWORD - CHECKOUT - LOGIN - CREATE_CUSTOMER - EDIT_CUSTOMER +enum ReCaptchaFormEnum { + PLACE_ORDER + CONTACT + CUSTOMER_LOGIN + CUSTOMER_FORGOT_PASSWORD + CUSTOMER_CREATE + CUSTOMER_EDIT NEWSLETTER + PRODUCT_REVIEW + SENDFRIEND + BRAINTREE } -type ReCaptchaConfiguration { - type: ReCaptchaVersion! +type ReCaptchaConfigurationV3 { website_key: String! + @doc( + description: "The website key that is created when you register your Google reCAPTCHA account" + ) minimum_score: Float! + @doc( + description: "The minimum score that identifies a user interaction as a potential risk" + ) badge_position: String! + @doc( + description: "The position of the invisible reCAPTCHA badge on each page" + ) language_code: String + @doc( + description: "A two-character code that specifies the language that is used for Google reCAPTCHA text and messaging." + ) failure_message: String! - forms: [ReCaptchaForms!]! + @doc( + description: "The message that appears to the user if validation fails" + ) + forms: [ReCaptchaFormEnum!]! + @doc(description: "A list of forms that have reCAPTCHA V3 enabled") } # Google reCAPTCHA config - will return null if v3 invisible is not configured # for at least one Storefront form. -type StoreConfig { - recaptcha: ReCaptchaConfiguration - @doc(description: "Google reCAPTCHA Configuration") +type Query { + recaptchaV3Config: ReCaptchaConfigurationV3 + @doc(description: "Google reCAPTCHA V3-Invisible Configuration") } From 08cf9d98bd995f13443cb08cc94f9cfd2546e498 Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Thu, 11 Nov 2021 11:32:49 -0500 Subject: [PATCH 477/479] Renaming shared mutation result status enum --- .../graph-ql/coverage/b2b/negotiableQuotes.graphqls | 6 +++--- design-documents/graph-ql/directives.graphqls | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index 51e05b377..f01e76bdf 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -178,7 +178,7 @@ type NegotiableQuoteUndefinedError implements NegotiableQuoteOperationErrorInter union NegotiableQuoteOperationResult = NegotiableQuoteOperationSuccess | NegotiableQuoteOperationErrorInterface type DeleteNegotiableQuotesOutput { - result_status: TriStateMutationResultStatus! + result_status: MutationResultStatus! operation_results: [NegotiableQuoteOperationResult!]! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will @@ -196,7 +196,7 @@ input CloseNegotiableQuotesInput { } type CloseNegotiableQuotesOutput { - result_status: TriStateMutationResultStatus! + result_status: MutationResultStatus! operation_results: [NegotiableQuoteOperationResult!]! closed_quotes: [NegotiableQuote!] @doc(description: "An array containing the negotiable quotes that were just closed") @deprecated(reason: "Replaced with operation_results") #optionally display all negotiable quotes @@ -468,7 +468,7 @@ type NegotiableQuoteItemUndefinedError implements NegotiableQuoteItemOperationEr union NegotiableQuoteItemOperationResult = NegotiableQuoteItemOperationSuccess | NegotiableQuoteItemOperationErrorInterface type RemoveNegotiableQuoteItemsOutput { - result_status: TriStateMutationResultStatus! + result_status: MutationResultStatus! operation_results: [NegotiableQuoteItemOperationResult!]! quote: NegotiableQuote } diff --git a/design-documents/graph-ql/directives.graphqls b/design-documents/graph-ql/directives.graphqls index 5af1c5e1f..58e415d57 100644 --- a/design-documents/graph-ql/directives.graphqls +++ b/design-documents/graph-ql/directives.graphqls @@ -38,7 +38,7 @@ directive @typeResolver(class: String="") on UNION directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY -enum TriStateMutationResultStatus { +enum MutationResultStatus { SUCCESS FAILURE PARTIAL_SUCCESS From 3360a7c608343d8424902a3c66bab1e6990554de Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 23 Nov 2021 15:40:44 -0500 Subject: [PATCH 478/479] PWA-1611: Updating proposal for union type result reporting --- .../coverage/b2b/negotiableQuotes.graphqls | 65 ++++++++----------- design-documents/graph-ql/directives.graphqls | 6 -- .../graph-ql/result-status.graphqls | 16 +++++ 3 files changed, 43 insertions(+), 44 deletions(-) create mode 100644 design-documents/graph-ql/result-status.graphqls diff --git a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls index f01e76bdf..4cdfd8c8c 100644 --- a/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls +++ b/design-documents/graph-ql/coverage/b2b/negotiableQuotes.graphqls @@ -77,6 +77,16 @@ type Mutation { # ): AddNegotiableQuoteFilesInput } +interface NegotiableQuoteUidNonFatalResultInterface { + quote_uid: ID! +} + +type NegotiableQuoteUidOperationSuccess implements NegotiableQuoteUidNonFatalResultInterface { +} + +type NegotiableQuoteInvalidStateError implements ErrorInterface { +} + type AddNegotiableQuoteItemsOutput { quote: NegotiableQuote # TODO: We'll probably want to add the same @@ -157,29 +167,18 @@ input DeleteNegotiableQuotesInput { quote_uids: [ID!]! @doc(description: "A List of UIDs obtained from negotiable quote types") } -type NegotiableQuoteOperationSuccess { - quote_uid: ID! -} +union DeleteNegotiableQuoteError = NegotiableQuoteInvalidStateError | NoSuchEntityUidError | InternalError -interface NegotiableQuoteOperationErrorInterface { +type DeleteNegotiableQuoteOperationFailure { quote_uid: ID! - message: String -} - -type NegotiableQuoteInvalidStateError implements NegotiableQuoteOperationErrorInterface { -} - -type NegotiableQuoteNotFoundError implements NegotiableQuoteOperationErrorInterface { -} - -type NegotiableQuoteUndefinedError implements NegotiableQuoteOperationErrorInterface { + errors: [DeleteNegotiableQuoteError!]! } -union NegotiableQuoteOperationResult = NegotiableQuoteOperationSuccess | NegotiableQuoteOperationErrorInterface +union DeleteNegotiableQuoteOperationResult = NegotiableQuoteUidOperationSuccess | DeleteNegotiableQuoteOperationFailure type DeleteNegotiableQuotesOutput { - result_status: MutationResultStatus! - operation_results: [NegotiableQuoteOperationResult!]! + result_status: BatchMutationStatus! + operation_results: [DeleteNegotiableQuoteOperationResult!]! # Implementation Note: We don't make the deleted quotes accessible # because "deleted" means they're hidden from the storefront UI. They will # still be visible (and labeled as deleted) for a Seller in the admin @@ -195,9 +194,18 @@ input CloseNegotiableQuotesInput { quote_uids: [ID!]! @doc(description: "A List of IDs from negotiable quote objects") } +union CloseNegotiableQuoteError = NegotiableQuoteInvalidStateError | NoSuchEntityUidError | InternalError + +type CloseNegotiableQuoteOperationFailure { + quote_uid: ID! + errors: [CloseNegotiableQuoteError!]! +} + +union CloseNegotiableQuoteOperationResult = NegotiableQuoteUidOperationSuccess | CloseNegotiableQuoteOperationFailure + type CloseNegotiableQuotesOutput { - result_status: MutationResultStatus! - operation_results: [NegotiableQuoteOperationResult!]! + result_status: BatchMutationStatus! + operation_results: [CloseNegotiableQuoteOperationResult!]! closed_quotes: [NegotiableQuote!] @doc(description: "An array containing the negotiable quotes that were just closed") @deprecated(reason: "Replaced with operation_results") #optionally display all negotiable quotes negotiable_quotes( @@ -450,26 +458,7 @@ type RequestNegotiableQuoteOutput { quote: NegotiableQuote } -type NegotiableQuoteItemOperationSuccess { - quote_item_uid: ID! -} - -interface NegotiableQuoteItemOperationErrorInterface { - quote_item_uid: ID! - message: String -} - -type NegotiableQuoteItemNotFoundError implements NegotiableQuoteItemOperationErrorInterface { -} - -type NegotiableQuoteItemUndefinedError implements NegotiableQuoteItemOperationErrorInterface { -} - -union NegotiableQuoteItemOperationResult = NegotiableQuoteItemOperationSuccess | NegotiableQuoteItemOperationErrorInterface - type RemoveNegotiableQuoteItemsOutput { - result_status: MutationResultStatus! - operation_results: [NegotiableQuoteItemOperationResult!]! quote: NegotiableQuote } diff --git a/design-documents/graph-ql/directives.graphqls b/design-documents/graph-ql/directives.graphqls index 58e415d57..1f25bedd9 100644 --- a/design-documents/graph-ql/directives.graphqls +++ b/design-documents/graph-ql/directives.graphqls @@ -37,9 +37,3 @@ directive @typeResolver(class: String="") on UNION | OBJECT directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY - -enum MutationResultStatus { - SUCCESS - FAILURE - PARTIAL_SUCCESS -} diff --git a/design-documents/graph-ql/result-status.graphqls b/design-documents/graph-ql/result-status.graphqls new file mode 100644 index 000000000..036ce1a66 --- /dev/null +++ b/design-documents/graph-ql/result-status.graphqls @@ -0,0 +1,16 @@ +enum BatchMutationStatus { + SUCCESS + FAILURE + MIXED_RESULTS +} + +interface ErrorInterface { + message: String! +} + +type NoSuchEntityUidError implements ErrorInterface { + uid: ID! +} + +type InternalError implements ErrorInterface { +} From f4abd7b0b9b52ad610075566e5b522c0bd72a085 Mon Sep 17 00:00:00 2001 From: Nathan Smith Date: Tue, 21 Jan 2025 13:17:32 -0600 Subject: [PATCH 479/479] Update jwt-support.md --- design-documents/jwt-support.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/design-documents/jwt-support.md b/design-documents/jwt-support.md index b8f86eb3c..a9d9e7571 100644 --- a/design-documents/jwt-support.md +++ b/design-documents/jwt-support.md @@ -237,5 +237,3 @@ The `\Magento\Framework\Jwt\KeyGeneratorInterface` provides a possibility to cre ## Summary The proposed functionality can be added in a patch release. The introduced interfaces can be marked as @api in the next minor release. - -The [POC](https://github.com/joni-jones/magento2/tree/jwt-auth) provides a possibility to use JWT wrappers instead of own implementation for Cardinal Commerce integration.