From ed556357a18a7ef23d9a9db49984ece2f274ef9f Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 13 Jan 2024 18:05:11 +0100 Subject: [PATCH] Removed leftover exercises --- .../introduction_to_event_sourcing/README.md | 10 +- .../06_business_logic_eventstoredb/README.md | 23 -- .../assets/events.jpg | Bin 67506 -> 0 bytes .../immutable/businessLogic.exercise.test.ts | 304 -------------- .../oop/businessLogic.exercise.test.ts | 316 -------------- .../oop/aggregate_returning_events/api.ts | 192 +++++---- .../applicationLogic.solved.test.ts | 16 +- .../applicationService.ts | 48 +++ .../core/repository.ts | 31 ++ .../core/service.ts | 18 + .../06_business_logic_eventstoredb/README.md | 34 -- .../assets/events.jpg | Bin 67506 -> 0 bytes .../solution1/businessLogic.solved.test.ts | 384 ------------------ .../immutable/solution1/businessLogic.ts | 210 ---------- .../solution2/businessLogic.solved.test.ts | 362 ----------------- .../immutable/solution2/businessLogic.ts | 227 ----------- .../solution1/businessLogic.solved.test.ts | 249 ------------ .../oop/solution1/businessLogic.ts | 360 ---------------- .../solution2/businessLogic.solved.test.ts | 252 ------------ .../oop/solution2/businessLogic.ts | 362 ----------------- 20 files changed, 225 insertions(+), 3173 deletions(-) delete mode 100644 workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/README.md delete mode 100644 workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/assets/events.jpg delete mode 100644 workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/immutable/businessLogic.exercise.test.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/oop/businessLogic.exercise.test.ts create mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationService.ts create mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/repository.ts create mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/service.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/README.md delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/assets/events.jpg delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.solved.test.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.solved.test.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.solved.test.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.solved.test.ts delete mode 100644 workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.ts diff --git a/workshops/introduction_to_event_sourcing/README.md b/workshops/introduction_to_event_sourcing/README.md index aca1f87b..dca1274c 100644 --- a/workshops/introduction_to_event_sourcing/README.md +++ b/workshops/introduction_to_event_sourcing/README.md @@ -24,12 +24,12 @@ Follow the instructions in exercises folders. - [EventStoreDB](./src/03_appending_events_eventstoredb/) 4. Getting State from events - [EventStoreDB](./src/04_getting_state_from_events_eventstoredb/) -5. Business logic: - - [General](./src/05_business_logic/) - - [EventStoreDB](./src/06_business_logic_eventstoredb/) -6. Optimistic Concurrency: +5. [Business logic](./src/05_business_logic/) +6. Application logic: + - [EventStoreDB](./src/06_application_logic_eventstoredb/) +7. Optimistic Concurrency: - [EventStoreDB](./src/07_optimistic_concurrency_eventstoredb/) -7. Projections: +8. Projections: - [General](./src/08_projections_single_stream/) - [Idempotency](./src/09_projections_single_stream_idempotency/) - [Eventual Consistency](./src/10_projections_single_stream_eventual_consistency/) diff --git a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/README.md b/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/README.md deleted file mode 100644 index c0f01776..00000000 --- a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Exercise 06 - Business Logic with EventStoreDB - -Having the following shopping cart process: - -1. The customer may add a product to the shopping cart only after opening it. -2. When selecting and adding a product to the basket customer needs to provide the quantity chosen. The product price is calculated by the system based on the current price list. -3. The customer may remove a product with a given price from the cart. -4. The customer can confirm the shopping cart and start the order fulfilment process. -5. The customer may also cancel the shopping cart and reject all selected products. -6. After shopping cart confirmation or cancellation, the product can no longer be added or removed from the cart. - -Write the code that fulfils this logic. Remember that in Event Sourcing each business operation has to result with a new business fact (so event). Use events and entities defined in previous exercises. - -![events](./assets/events.jpg) - -There are two variations: - -- using mutable entities: [./oop/businessLogic.exercise.test.ts](./oop/businessLogic.exercise.test.ts), -- using fully immutable structures: [./immutable/businessLogic.exercise.test.ts](./immutable/businessLogic.exercise.test.ts), - -Select your preferred approach (or all) to solve this use case using EventStoreDB. - -_**Note**: If needed update entities, events or test setup structure_ diff --git a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/assets/events.jpg b/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/assets/events.jpg deleted file mode 100644 index 35522f5804b2be096c7dbbc10ca8bedf69faa212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67506 zcmeFYWmsFy*DsuUp@kN2vED$j0HwGUXp!KdNRR-b#oeWN4Y%SB#R3TgiUgM;#VG|6 zBv^3>1SswtdfW4Vo^!suAI|&f4A-@1HZx21?6rPt&Fsm=*u_u4ePxgm2yo>J0C45< z4{$L7kOf@3`n&wTUb`&UZ(jdhZr{9l<0jEbpK|NU)oV9yUcXIr*==?oaOLWat5-x3J(O0iszj2f7 z7C95&ZHgBfuPL1?2C1l-`Gwuz#l&Z`XzIk}l$8(BJb28?CLkjVf|!_^x#YHpXqmgZ zdA$GdF}9LjP)J_8wT(km&O%{!>(`|e`b+V@mH0=4%lc0#w{G6P?3E`2T)n)#N__M7 zjq6vh{W-N)uaR9RXS%_6lk$az)@x^%m@*1xp?9&Ditr0+zRxbd#VVuS!Xn$c_1Hu} zE@$@F1rBicQunK5*T?|UfHSTCko5nW|8EBV-wga;o`I_=;4R|!Dd1hs+Qy$WSEu?n zxduMnc>aG`>i^FE%FW$ypK*;GE*R?Mu7gVSlwb(CjdcUuq#y;aa>f$3=S~uk5`Cn~ zRPk{Ojjk%1$neK_k`h1Om!#W~o*a7WkT4h zg}rZq?|CcKawWh|aP8Y(@JDOBDq%kz8%r=+{el+&{gNBXjgUI^Z}D}MnTMs5J|sb26o@7`EKU0g!Hj{4zPW`!nkBCu{sWUl88>ektv7P^lOPcVyakft<>5#5P| zsRJ#YV>@+y`1N0X3QD}x3Y2ZT&Rd{WUX>OxBOyn4`(A>|^fp)*Vz3$`Y4M4o(^>Ly zgFd}F$zW#--GHM=s!JXI0-zeY227g5XSMb;4>NoB-)E0DsnhpWUs51zz2s)h;+sTR zPcOm!?(9t~6zTOcLK`PqubnMR-Foz`^uYX=bPByJ@dW@E-P)v}m>TU@oiM6-WQt6J z!;QOp@L5MDW=mI~I6Z7#WfS`jr;o(VgTdcJwNvv8NfzC{`_}88gbzdZNE%8)vl1eV z&k7zBCysQYu*<(xmNfTaH#T$c1^Py{2)Sw)^ejbgw|s0qQ5n{AXA(0kJu?4Xdfpas zL&c6jRZk`zWz3&cF6=rZ^XRU>f?aAqg7ft8KqmOs5)E4=UMAo+ZS$6s^wZfE-C-Z~ zldvrMCUa?-^%=tKmcoK3C(1ke)ce~7;Nyp*{BBfgMAi|8_3|$8sNgoyv8PRdvvim- zztqR^tckYS=&ns^5JCQ2nTlv`Tk>wEu9T-K!eTmZk+hd)Of zvox!@U>;up`dD?(sWNjGM%7-(_k33>ecw|V{Z3k)puWNWi(l&9pMw=}g|%|9?fg%= zPm>!7>g_PUn`nh=-cJFK-~MaHa=1-rT@DpFds>tb>!_DSQ=R(xD;*%~;_`nV4D2p76i!w#nMXynjVDm73j-7ObCyI=b8oC+%y zyQ%MD!@8T*T9-6~S$^@&Kc>U2{4lF;y;&46 zt8>9U6nXqHtJONXvF&td0gIAVpA~d$XgwW{xqil5>Fsr@c@(n?9{6kQYEf!PR;ym~ zo4}*|0C|G^Nq)fK&-2%v@BLG7#*^yvNqy1T`=0w-hb-F?k4G33)Mr_m!msTR!^?h~ z$>_=_>8JBoq}A-DH-Nq_mwbi$&!-<<_gAn=?L`osGRt28KD;{$3;@|HoP1W#d4Ad! zcKuAG@&Ztc_|?0l@S|SjS!(JATaDFZ!a(PXaAnll6@^<&lit zz@@QvE_ikq<35}^69vq^_O~`3%z7CR`T30c(Na3gqiHjsf#;<$I$zFn4`bVS^QE;K zv(%hD+`D!-a&PeUaq!AOzy*Nz#j${-0x5A*#$Tq&x69af`G~|v44o?j{uh7;y=M6M30mWg;qZfZFp4)gv&%1eMRxp^Vu9FWHGMR>J!y!eC7MgeU>a}i{x{?`v zT%V6yaK-ZodN=*ZH+KycRoW4{=lInxKfGhkrj0^VOPRB%>PVfY%_!|-%j;G3dusBv zg6pU*k3Y~Q16Sws=@wdvMcA}| zfZkiz*8Tt;y0D`vJ@V#-CQ8F0?iZ(lDC&2LX60-N?5HPi)%&&*fdCcAs)5Cy3Y z?8Vsk`Ju(tYKP^)K{*F4Z&>V-+(*(&Y^sM%E8O*zdvDN6_K>#Wu$ch;M#^= zLORdikRGK?#e;}4DEf_qNfl4k{T}#oEx03)$3fG&asxdbyyru^*eQ@BS1NDsP4Wu> zL3B&Is_BP8(ZF0%U;cVV=E0BPwns7J@G=`&ufLJLb$7&1Nqv^lcl#2?=U!+|7$`Zk zYacFNMin}X#=}2-uGJQt=l4S>;O9^O*V-eE3g?nR&0C)W1ZUsr{^NA+`B1D2mAos-inC8#-IjOVQ$$pzy@pQ;bnYh8NiWf=V7BXH@>T$fH)LK~EIlwNlM zXl=X1t6CfKfTQiA3jlGe^z}38-#+>q1#eVHR|mI-g2zXE=JK6~5EXGQCGSKeu3pzx z^2jM-Afv6(UTjhSJn)HoZ^C1cVM-&>m;m+jcPqJOS|*C8%4Ew;Q-f}t28+!$-6DBk zs`UDEPO4{lH3xuk<=5Y~eNDqtwNDA^j_6h_31qw>Zo$nFX~@Mm2G-X8B`@n>%^Uqr z_trO8|IiL$zM5IQ(4lcm%txg;&Vs|Fl$~4EYx5g*Wta9*MmRn^DJlMKG(SEpH%W)D z{*L7{HBmIVm+={NGO_pTz)0`dOkHpP%-3&JM=FJ4#OnF{Vbj01a*6g$|J5hiZ2ZU_ z8x5D+OT908&YPdqer_5v>n(a2I~-%rn*_H0lv=*&SlvA3ZhV=NSW2b$hP*j7R^bgj z?fji>9O`z>`1m!RuOa_TOBiPy1M2X9W+k^d|A*Y>{WG7lQONnFPLj>A3xGQ|;J0AF z--69mt_7~-Uh4Pb($ME;?SG3iogi!;80cO;kbXA!x%1z0@3-!1T*_S`KW!}A`qLrG z2;2K7sG_sq^HRW^8$EaYI~@{@qRII^Ee8{egul4x?H~F0jbMMPVDRm{o5Dqm{dLWr=z4^$hr)1R% zylJY7=TCmD%dfdMLzESIcX{}!XP9OG@Q^BiIPCd9WLJoGBaE?>l;?5-^FeAKS%!E5 zAV1=cYNI>adYuNw(9OIX3he@OqcZY<#jeBhUqkj=~pdhB_Z z6Lv#sGN||}lG$7a$u2;bQLFTaSP$|gb?w;Cx3?td?;m@rJW3N!=)3@QnE2^Q8A~M( zB>~4VeNX(qhVB3}PY!)A06WOiGwGgZ?Dvw+sn{<7)rXcI;ZbaI^X={F5fKN4?zssx z|Eh2q^gbS1a{h9YR=R)u;Akz6w~bReJY|)cQ94+vE6JO^q+d)`4llP|Kj-1mExX){F#9v`0%APUDunW zWAW0p1Z#<(FN|BAS4Ei#RfQhSxB-dQZ+_7RW&R7`Kd#lMi~d9VpEVO>%<8`P)e8W# z<*zoT2wrC)wd_+s<5M}lKvCVhl4#@Y^RA5M_fj3XN6l{GPaE5ajRPNO^<{qQ zxSOmI2>9z-acA;r*nCrgLS+ z+)f3^OrnbN^#2H{{ zg*a_@?6F>aW4#V-EIaGkqbJ?}_N3c?L=JE*n4uWp+i5nIaHij|M{ZG2>>a5P2&!dK z>`@-#8hCf*Q&nyZS(Jk5!ww3fs5a>vAA;C~fZCaQEo@V1(}b>9^L%k_%9N{Zf^Zx( zzt%YZ*y%nib3q|avPfs~gA1&})3404>KW(5un6HJyZAog4ctoPSs9gtZgT~_`S==s z2(GhakBfA;TfCbgX%j~hG0OKT-_w4LflSMU%}2!YtBHr7GJkZg*F0;A=!`p|wx~$R z(mA+u81>LnpOq$|z<$5!WPC~@*Oyee9cz0{OXqVUPiqfSb9&4b9DJ3Fwo^j1EpsHd zB5)3?EqGw5=tonA|DJ7FefqKC5<&46H zmxIQ2jd0wnF{$a2ab-ir)jCQl+qn)O*T`;SI>S@T>IXb+Bv6#P>}?S#vowFY-imgO z0`+0AwL%8kk)c!mWr>Eh0J%#7v>`s-I|7yirf+X!YBFIHD5n+EfUSG5X&*e2urWHa z#4`-$3DpCC#uq7-uZ@#!(bonozDGv-K*;(Go!eq4(~7Dy(Do23j51cGyTE}cSa~?x zo$t};H_(R8B1+qlbQH*x5Hh~UA(MsN=~hD9>*$OSP7=n$gK?B=;DX}vZ{OJc7>0Zf z96dnm4=gQNSGS#)mzmw2ShK|ib!T;LX~U2;V6GOLn7OJFWuBQAv9+l287?^H^E>O`oE z$jC&TJcWLSAT2e$8m`E z-qE$=QH*8;4Hb80q{O`+8=Rod5a2;0YkIh?>=c|gy2q2<3n6X&!zh}Eu0obS`|5-c zb4;t8N=mK;?#BR`)Ei!X`Eg!`G#PDT0=z1j`s>Vp|b$dMY`H$MTcC$+Vi;r*;Kp&R+PActflN z+mK^`$l0WAc9l~tr*d(;kxFHninH`J4cDov{uISfSlwD|eC=&L1%;H52pi3#K}`O;D9$)2jB65w6Lw1$o=2 z7)WmdMf*JKv8t6S)}5z|Hcr)TEenf=lxbeeNN0ZR?x@`m5D!tg7FJ5Xi|A4D8*)?| z9dzI}Sm_Uk?2|4xo0%63c8z`wB;{_)Ms6$Ysd)_S zfAP?U?lCUrtBmsC^UyL`!c$uotaB`5yIb1F!>Po{hVryFC}n&3jmsBpx=8s;8;Eg3 z^o#MV3gQTc!L{EzuP^CcdDV0;zcMW{N!4>lIqPFV--d^nt5nk9E~Jdp0Rrh|_Euor zISe2WXLO$Ayzyr&JtcO$OejyxJW&flIwu%Db{3$EYzcjO;XlB7WoJgw>?~RF)NtT3 z%-%mb$zujiZA;S#49PtUAoYuRvgj6Vl=Io~5RV{?Yt zil{`-RP*9#`V>OZM&)#5klvsip8I-xeY(98#Vv()Y9IgvQITw!w=+$u|k-k0|`u{ehMW9VM~9o2qe zizscb#~~rO;tX+(5?cm75r!|XbJGh)a%y`*6CiSO1V{^Ccw8@)hg@DxDSP(n?;r@r z%4yGqcZ2S~R}ub<3p{-B5l~@6(!}~_!!cxH?lNv-0G((Mmfv0`&G)&a+p{XPx~Ch4 z^;A%*Q%WS?Cp9mvqQ`{xocxg)gUX@X1z@=SNzDZyF8Km*U-vlK6U(~=xTPfaYAgbG2&ZcCByw36W)gts@bM?A4T~}B` zqJSZW01o1}{}|CYZ?J=3Nh+ zs9{>Skxo140^ED*{Xdeb|H1QwIH)#9YqUv+W`Su#X#aTWgBt8eRQglU*JFcwD2{zz zmSx&wNRsUi;OeT?zqI!)6|+t?BhRC0wm+qJ@#e{#H)sqW3##|X_Bf4>VI@pYV(@S1 z2w{G)XT>33Z{3r4DWx;))2YoYmsX;iyv)&x$ELwk$_X0!1}JDJZ&P)(Xa7Bn}wB)FR>{=^M{G2 z4~lbsiKQ}3gI}9ZtyQ${soDz@u_T~>b z%?u?>+)K0=h|?;GU)Qs7!e^q^lBQyw*6E{G2423q(O}sD2kqwR)|SH7fl&y%=6W$($rAKg%AE63Y;+*Huj0cUq z-+GgHg(T{*%%En)jBHG{du-hHFVZ-)(m?k3;nlkCb!>&1Y z>e+;Rv&Dod$!-zkqDq`*m0e|H%#indh5ZT<52*CzAEV6OEoxt0_2)r?mUFc}Kvt&) z7I;=J08E^2#;Gv~suT}}V&S<+P(mn`h+IO#+mJ|8*G)37t)*mfKkngrWR{z*kgO(K z9yHw*Mk&ol)GfAC-!>p~i;J0{_Z~x7HO?l7J4Gs}lsi?y?h1Ch1X>~a zSON0kWKi8>j%axkboV!nH_Lr?K-~f(Y&LPY#7CBtdQQ;^{=RkevctfWGc{ih?Ht5t zI-|T;FHqN6vq?9>XtfWmUu|Wxhr{G3+p@(?mAgFstZlJ&t|UNCM^?m8*x#=^KLd1@ zgUknNi_k8fQ|`K+y^3B@Q|mNegMEhWQ|&nau*Re)lbDwo%Ozx)^)7zqqA5-eXYgQ| ziZ|8xH0Tso)J|}(S1++`)tRZW^9ltS>SU;)#W=JabXtS|ONc8vjIpNUcQPy=2Xu3@A`^S54Us2X>0psjv|s6|Y+s zBO^}{L?Rm~*1=3bhBghfH>HX$g3O561B!#eS-jd@(@~R|ob}R7bwz! zE*U`c{JdR|g$@`-fBPv?u&OPXBxIxvA#&$-NIDsbU;U%J#AHoY?l-i~N=NsT!08rUr)PA@V;>kcSBSLkN(+(;EV&Sn5+ z(amhU9}<93Z#NTV5UklVDOri7jfT67%$xrqRGjev8Yn#8h4=`uV`F=bep6ZpfweUW zz6s-R!?-OBuiL166ONoB9xPP@bCzI+x2*ZeR}fpUjSGOGO;gN0V+H98z^8yw&I`a9 zye;=E$yJ|{E=qYBene?~l9>H6$ZdGSVOTE5;^dbE!dz!=4SKSE>s1~sgN2KV)0(rN zDraJ-n~_#^0BrbaDGjrswnj=%y)jiS;R$4Oj$^+UmV+2XDf2m`+ihy`WLEDP=*O4( z@%oG>W4F*o${3p-CG7+(VySLux*YsnxT*?W5@njVcT;8qwJLwJ?3!=B%pyot#>LcC zyi%7>tMh?Y+3Fqp9~N%R>oK8<@!Q_+%z|Eu);9{r$e09N>DxPNwY9;ry2ZVTEA^Fg z2pRa1D*9?u1)65>$2g`1j}>|SUEY(iBd7G;5B%Oul$*6X*5PHoFFaQ>udi3RWsUZG zIZJZk$c9U<7O(-ULE`re8rC8Y?%}B}*)+l*RaV!GsS_{!igl`wRkCiz7}V~asR_Ts zxe+r)8@R*@cDpGZn>`He6sLm>^D*<$AIKT{|3t(WlrTpHPYAAnBum z#&v}<#QeY2tY#|Wu!8~`F|ay%aGg<~iO~6MZ#6KyWz6_$SaLF^AYwqGR!OMj@{2dV zcK3`JF*_wT%iqz%XT4gFg-Y26XsWtGkb29e-@PIHuR@7j;{`)^(%=ya!%(q~mi*F> za_892zTn|Hfll>Rm2;n=hJyycek6GQZ19=%J;^Nfre?dS^*$5+45x*!|0W@C#bwyn zt6l_j_wSYwoxb&!DOg8lHty~h>_9s?hQ7p2`()UhwQ1uwp0Tv%DCKxr7S#>t_oTTvB5&Z4up(aMA;c{H7i+bpRmIm zeQx$Z0&QF2J=)BQ<-n%95xWXNv10k}{rz*O9Z!1y_bUxtLw(7momdjtytZ(Cgsi)8 zjFG-RxA5!LR$4v?l~*Yv(C?PXDmELw+f`Yw93R`C5>mEgDi^X+wq#N&@-1XfS902Y zcKidD;Aus)mWC>rw*LR=Zx;GA26PYC8O`pz_17q$tWBAq^|pKut0$NKC03qp+aSaDRg zfXG!sQUapwgXl`rUfh%TiF+>F@-y-CI1Sl5!5zoIaelU?bS)1rlYF=Z&e0XStyj{- zMfQF--gU|P1wyc-Lgh)7l3+E8hNrK(sdy&hONsR0J$Ymz8t9P5$9prcC$I6vrzvt; zbT}jZqjDK}h#aIXhU(3H%39Ulz1=4(Ef!Pf?LO?7VNT6y^+g7-_hRrp4?@Xey=W)B zX|O=U$Q#TGt(vD?kvOgL$vwf*z05sV5)Fz)hzw;zcI_FleR7JN84)r4nGY_Ov z*Zf;X31=OrmTl}5a9jba-|8BgIG?%aaUMrSWK?J09_nNFn^M!vQrt9g#CN_n(K&bL zWpma-XCU8vS?rH|9f2I=NxX>waQ)!bkqd6SF6qU@dse1LWP^?IVQCsJD=_GRMDQNJ z?Ux6%Z;-rFk(dIy9Zrj8U1nj?b$eAI^Ek)~UU_mu%zm_|`-zs}9_hlzp2ZKJAxLaB z738%x9JpQ1XWFJd!=%#Qn~q%ykMChLa=}C@R@+hw@sCvGiY0hgHZ8hC>C5}&L1Jrc zHS#*ugvMc~&p33G7rU zmtS6}xehNemY=(S)k3DKz~G@?x1PJOI?L?r)3bG34N)YVZ%m<)yZLMTW{2CrahF_& zY{q}xf!KOdGcmaw-{dW$!uFvh9y8(m`+0`K=2ksw4Y63Oq+Rk!|ww{$$dzVdh!|peCAoFIw2ey2WhWi-l$!V32W3M7U-?R5V@QTo}VdotvK- zhi_(QAvDb^_PaOXUAkRxxH~W;&ZZM&u7(w}qTu^nu=y!(y3uq{EBs9k()8V^y3IEd zoQh;o;sO;?QF)jd^ZKjnagyX~G~~6I6Cp}o-e$`X=#eg&W@^)KF_ep0k$k-57nvIK zfn5I!%G))0%%;6f!{b-0)6tzLJfcS2E4{wBjgt1FS`0pY4Q~8ojJPChRIIxUd3j6$ zrU!l&j&_=mg1lB1bF~bs^kbcLv`iCMK56Q&b>kUoH=PGV%+sELHhC5DQ0_@w4io3t zBO%F8LUV^TRzpdkEGu)9pV=L6V{+8xkSg7lB)vm-IkL`rb@rqF-1-4NTtrL=V(J!L!` zUqbH!;e0U~fS0e%bF<|g-@80XQ^be0PApKtb2n;2O#;PaP(R=eQ3#bZS7UM*bG0jk zamHmI>8!vmP{l+LfSfU+Ap&iCDChH8rH>=_w1O+6XjU_fQEi31gimR9`8d`R8i<#u zTqqq#R5RWMTmyU;P%IfA_wQ@wg^o@&XXkFCC&X z($;hVIDHOSwG{a~VAC2*{4@`kt1XqAce8eMe3x(m2wQ5Th%j`Vvhg&I<99OlDwu{@ zcK>m0X@2GNpG4ajq#IuFqjx^cAEpe}4<{FFZ1zaz>p~S%ue* z1<7|TtEdX!o6&isdkBKurDms0%LZ$z_{Dh;$|8*)2DMfeVImBsJQ7ZoUeOB|;3E>V ztn-%gwemiX=9|y9hTo)rSQ*qEB4th@?z-S+vLU zAy4l;O-igaFQlpwKWcbvMAgwKhH=X|GmATr7~gRbcnT@z4u^%)6u8c53G3wNF^?q}yTl{(01)!JR=D+`4Y!;aa)*eLt*3Y<%vfE*e#w3+qTQhQk9s%87LZxyCScele51C1c;%e zzE~YQI-;s&Q_`ckS8GH2cN|S~93#nK3%yWGE3ib=g`kxWsMRT6Q^OQ)OrsK4wE`ai z-LSEPEXiep=aB{FXeDLtzI6)-GET^9O>ke#fL5Sd5``O?Eo_K!X#$!zuG5*mT;<6# z+JBe?9n8dMzb-_=J^;J>yX51+ zQTy?MFr5mg$%DWd@~lTaX0Ij_3ci0$2pwCJZ54t@P<74MG}ukJoPAa`5K$AWD5ntB zkjWhgT{*!UGCXnkE@H+ojRdyL&r zlUE!>G`jV{N^DIJK_SbH$+plmGFV|~l%D*6K*9A<+=F6O7i^lX!lq$k-6o~e1G{Gx zn(x&;YqveDYeF6kCJrV=Elnh$j_ik~59Hh#qn+fEVjwPyU7xm)7|6QqCjzge0J^ju zdL!Q`P-vmtA&^}28BwroN@6(C)2>f*?3CV~C9V^3MOS?kXf!p_@xt>+-|FRNY3uvr zV^HUK7B#5BD7$%-Lq~aRCtMzWVpp?q3lu(5-0u?5lPfZr>w)$_(@sBZWfy`hFWs4*pd-=D{aGhT1Y;@B5vTe;lSC=6Sw>gOhkG11MLIYcuftNA%r^g=(6i~s88@!PEiPEfH@;1!`h<@jY z@+hA9!P!Fs2L-QRk@rkudc0mq#IZebGdWbvaIv?HL}Nn3C$%tROoB5~uzG$Kvy}4q zn|x*Mxag3EskpUCKbOg}&v94CdLyLga>d=VE&dgTsC0HOKUR)h^fW4RUGvLN^^v}# zAd>rDSH~|p4&lK_e1leJDtmZ^T!Xm@mcc`+Ly?2zn(Ppw4O^6xgS0xgG8KJUhk`O+ z&b{QQpom$V1FH^j!81ptizCTQ#P~Zrcm@;#sCxsq0vv+R%uX%GL%Z)qd_Knwtg+8a^@Ej`#67o` zJ=nJcx|W*Tc@6^xE&$}8uffyD{)WK+h@p{#|Lxm``{lU{)$SI*r7SP6tjBNfrA6nw z{Y?}ouB*kqT&%2$?^4BZ#MiCo5xkX+kjo06OiE7CqD2}IDy_Cf2_H~x9xmKLHnB3- z02`((11gT^c$q$?)_Jpu7_PtM`f?p>kP(JR02{5yiZ^F_JMzgRjpGp(I1wr!lzW#L z%X+T3FJ}rF4~fvW9fUkZA~K)#f2Sc+pa5l(R>adfawj6Us}5JrR;MQ;hUctw+D0;; zglVdA4|+@T*5Vucp{IpzUlJ1IR`#5fdR_$bUcD=3c%gPvP|{4Kwlsbi2XkA^eKkjFciNFk1*nom5>Vn zdrDB#D%ljuFem5mwBQedHr8IY2x7J?HC{~}2TFB|hfZ}eimUW1ST>tIaH8+GZGV(rn{ZM^&)(3eZq|Re^jE(^P zzBr5pjbKsQLr9paZ!*{%Oeu4JI)>EPjk3w#$f%{-C{CzF)oZ)gDHJ9Y z?T?n|tdW<$G#%y=B6efzClAcTY)Ipb%c& zdS$QFF-3f)Eiroi-Z`@Gh#3z{+k}hKOXTDYo!4=0muCoX9XK8jLU)nIgG7-IAzE0J zoG!)&xVUKy49N<;-`}p;HcQQx15b;X%k>#IQa&~|>R@CnVWHf@Oo8IF*}0$_--Q>3 zz0kvcn)P@Ql!6LcYN}vpL;s<$_8$sx1ve$JRdXZcq0iZb)5Zgslt~g}-VDU=dDgHI zXRZeG_d#hF(4 zx!j}rgrD+=B(WDmUPw*^=Kbkv+3<_$rr1Sc#uO-bC(J*tC0i;KB56b zq_kXaag9EdthmpvikW?a?b*UPNH{ZXYDafn@+IcDkg4vHpbF@HSU=t?#* zS8^Eb$&F-Y_8@mI#wA!`(plu?p;A>4v&CKqKOU^lgsgc5r%nM;T?u$o_4_6->k+!S zZWIF`(XY5Xp~t!FZ9J|fFR_Ym#yt%ulFh}P_ZAGFre6S5?#^r(<8~!xW>cR~*p}N~ zt~vYw0EyYw=f5M&|0q{kR!Jv^Ilga1Q%T>rEp08n``*E7H(?{i+n?wbK{T3#<#Z_* zA5BoO`svmjuVv(N*4GxuVLtIw1L~#12w!@}qz0FFd?#;B&9<1P9pZ4N*Mz58*hsM= z6saPxoKKlLkr?F8yv5YqxL6(UX^;osPmIB;jphKI! zwa1{o$I@7I01KP)#2i0+Bw!1Q#?i7^^x1SIpfukZ=BlamfqZ~*WGRv)CPJmb56fT+ zE%73+-t5T)hnr+P^L&hdlL@0uLKs$4et2urNY1v+_ zbpxcGZ$P5aEuYWT`Q)tMW}_5dsn+z=ym!Qs<%gF8w6nGiC4|?Wh z6JOVe3K>I%Et`+Q?!=!ehpeYQaa*Yy$&ja^N>b@{iEBz-b<2|Yl87zN)hy3{qvfLM zg|76hM3tlyT)E<4*~7ebsHSlvi5hbbw07Ft1J(ZLip6@j*V`?qO6E|DZHr**p)&eN zIq#fWim)70piCp1Oim4x4_zM@_PquS^GAm-+?MtNIJNt3R>ZKu_ylTU6 zRu(ReZc?slp%QJFRaisVd_Dc*pNb3_LbzSQ+sjWk%8%JT_0#P9+$F!F=1xE&Xrg9 z&SRubxNb8g>^RUngGrhw29}n^M{P9tptHVF*93TwX#O@|Tq=n~ODHdAlfJHJ-&WT> zcg7L}ixMve9&^h-s}7H?=+Cov+tu+#jT)H^-Amuyl&wtC+AT@)HqP+t)-CEfGCVV} zK`@xeL@e|U)_4ok^1ZQdWwxISuFqb~a?lh!muykXe*R93Pg3MDF5J^GD?ad{CZ&AO zd_^iV^E~T1sK0G6SH~H_4s_A5Wmeue$+VqN==Q_8LfZ(mdqtWb5&5(+F6t5vJrg4a z#*kizCa*()wc6L;*!lkI#Dm{3CZ0JgWS>?j{hYLX0e7DoY*bX5&1%LRE zVCA9G6;>6GGw1v$nyFP`_^lE?ER5kmF}E(!bMP1W65IcgmJTYJDRW!uOs{!to~@=m zeW>ZytrvnE5A7-e$(ZB-fpA@}K=AE`FzXv9k2H$)x!&jU5bGd2zr`VyTjfE$-(|eZ zxyYy>8vH4$$Sv);JZFSRM!wGIgv)T915$`N+M*ZbVs9hxBR`GNYqbhS>*&Ig5DD=v zuy|Pwc2tpyD?MO9*-p86H!|-g**ie2cedaLH@#`?qe;B0fkK8Dg&dDrb@^SJW9fBi zB}b*yf%WNeuWIk553B5*=eS+ZU5~mGohApr^K7Yfc+vOoT9w~dvI_b;5>k@#`r1Fx z-jz2~bX2q{T&fkl+|kMYYHPUXy^f(%kmuM#^r$AdP<~Uc5Mm{J-RYM1Xj*S~ZfY$a zA_&bG+#dcS_&dQ-)ce|0j3~b5pWIlX*Uw{Fsfug0{e8860n6W*B)$uI1^)Zsm&JiLyuKJ{p z70=skUs%A>>Tr97hLnU-bB)jfYrnf7Yaw1p#Kfn_tGm$!8RVQa5k(JLDD# zAscmEs+3i%uG%#WwiQ4Xkh&;9*X3RMkyW z59U9#-q%kKiFt6CxyF(1l&rnZ|I0_ngwI98KaWYR`oUCvE0=u!gg1VaXFU8Pj`v<* zcg(Kytbdh* zlQ{>T$*yDPQ3a)Jh{XX<27BLTi<2E|+lj<|OmEQWZv2_zG~6yxU1dT9SG8U@J-UGY z9-T*UKtCT&QNmNfJkt2_1o1n$%BoU#M(A>x#gI$LCs;6XdVFHaw@Ao_>>}(FR8nzR z;z#46u2~klIqS;TIJgcR|@Zqc3`vF5?jAasp7Rz(1S$x zGY?$}?}Ul!2g{veEO#t5j&-Iyg?eqY%mfi}iIwe~)}tWv7hk4VBBrrHg?kWH*B++b zcSKgzht?zNk!As_y)}2`7^y1~I7Dv4)1Xt|xs2Qg%cmvo35Dk6>R5x-1T#}X>X(Z% za!3$C#+`^p>!D*+#i%r=>{>$_a<+PZ)4e{^hcqJ6`-QPk5rw*m>IAQyct6Yo7Q;l< z9Un+7vA>s0(1AlJns2^0$O%f5j}MkGNUO2ja1Y*B3EV@#zq|LOn~2;SOMjDNZyf(E ziZ)W&*Or1iOf4^$ZUepBSDY})>?^z9;9L7p}WBOX> zSi*B@2X&g+1)xG3-he9+W|u&GtOC*%z9G>vIDDd;C7h+Ol3VdwncYpV`z$AO{w?W* zreEhW#W$y4#uf@q4SAl$2OY9>Vkx8@F8BY$@!yvh8BF@A-IM`^r zpPK;v$r*Kn!cJ%l>;haQ6|QAeHA-Fn+(Y&RMA=&jpSc!dE&(>Z-}+EEUgz)*g)8GA zS_PXg9P`?>S5kyDidKPf&j^I=8;onV}?u-X_u6t?+?>9NTDQU!b6p|t=xdmDRK4OA#mw?3=l_Ar&z z{drlV4a3W$Vhe^Irfl<9f(o8-uPMhBVseVpbkvk) zO_Xta<71=+AYY-N@$Yc*JpDwp290e-_t< zWwgYVD4o5sEV7jd1t2hoPawkiL1fB<(bNbG6MeNyQlC$9XrH#O;96_J6 zEM`$2iPrGws}PL!mNW*UyT6i^B-&85xhPNW_s`taq#Cg7=Z;_32O*!Q$Oku3dI*i{ zY&cTcY#U#DJA@xlWU&0QSX7=G4z3)(Y9d%+F(DTy$@kJtG)g{XsaBtqBEThN1X+SL z{KbQc)39{^U!1*XTvOTmFUrhVQBVMY z%Rw0wlhjun6nm}YOY~cHOiDb>XajyU2?mL@xszvDlBxa?Z9f)|R_ze-=m-)LJDO^| z$=1^z*&b0Lz^`sgG&Y7kETVyVY18~TRvIO}S6l%)d$7J+RlS;78h~Zgc7YTxQUvov zT&hUv-dQzsqyWUv{Q4U{B=1ddzBTKXfW8#RE!%Wg2_^q%tg5O=UVBD>ulGY6)$V>E zAw6UJ8}h32xMSW1_3&9aP^(42Q^#_Hi9xDP`p-#GQo5b51p61niX-<7(Q=N`nizB+ zB0U`w;Mh~*f;gQ`@u?mNz_MP57%EOC>#YpnE#}7N(}Pp`Tn-X$2uR8|O6iQR>=eM0 zwEIMyM_se$t7Ax!q-ZM z!{c|hQy{DsEk2IUO`aPfa>l=Lp~kpuxyYw2uhi=q>~?f7ZS-b1CLH6EXKES@Q{o4j z^L0OlpvWZT*o$Ma**8Jm=BEAcewyE8Cu+SlPA!zUFV3&=60z*6ARcF2NEy20^c1iB z#$y1s>vG+gM{x7FfT|Ux3ShALoS7I-^?G#93b$+O&5OIwu?&YhLUA`A_w*_Zum%n{ z=vhfN6J2yo-&w|mN%^zJgwSZaf7f0fMHJB!&)G@q>_=UWOG#@#?65CvJd|Hhk>!- z-{AK9x^=bOS$EKfL5b(7ci-ilpZxf8uJEAodFq|apGv>zwl58TaF2fv{Y5wa^B3JW zDLNkc)ldI6q@(*oFCg%C=}$OtJ;N zHn=WOSZ|F%dnIPzKC|JZnKC)-dJQ0J46VfsQi$m+Ol~*4PJQ=Z2F_>3W(Ribz z2rXRdLz4jo2H^gscs23brOywuu2>bCCjEZ0@l2~`*ZfLaLbOwV3s+KX*hxQswE!v|%m(s#N>I-+UpEnFu9M>?Y*!5YHbKHsQB3ooLi`=Rrn2;W8 znk$O1C9D__rP}LjuQ$4clvrb3XgI*AF`_6DdJwh`70%Q1HLsrip^dmY zZg8-1HJaY$@8e;3*HJzlh<$3s?Hhh=kTe3}8w|EcAP|WCljOp&Jj4eLeLe_V^AqNX$O2OguKVZN zyalBv=T+N6{u=0};=l@}KCaK4(^s>s@R(DG7wscYpYfBbC5}gSsg?^gcn%ZB$c%-= z&#bHgRK2P-!@?;W^rtFOX1Pix?6*8^mo?t54!CCcdQ5LQN#CI_hPz=fg9X}QO2{t| zUq#U{E~m4KS{vsP5BKN_e2R%r{RLxh3)ndhD9lzQK{>4`D{{+MpjXW@q|~h+V{6=Y zCcy#aF|9l`+vzI#4ny7)Z(CU9sLzV6Z5*GDy+g8BVlmL5oVzTk99e!}sxO1I`)s`} ztkFvwv^N!L>K3JJ>X;LP9()?bW~vgWw^axXx& z&unkD>52F7o9M~QRB3JY9;-Sngq8tmPrISKbol+i9==*Q(n`Uu3jKITc(n-bfLk>A z8@i;W1*7Fp zX|z+s1Pew2+i7o(Q{*}uIS22hz;l z{7pwEcp000Ypq%GB`!?sO7`&SLdaf6=~3Ch={42Tw|kswOy|NaUT0V6d`Cn64L|?i z;|hG_x4Ms`JhKYRKd=5d;xkUUh=HWk`~ISfZfEy!VW#_|Ex@F0ri|>F`g( z4TUUlS5ckYJ^Vb=4&k9@WQloIL`iT0r<2_b^U2PBQtxeZ(Y~9N%<><0Q&ROE9^BlY z+8IFp=+I)aaoouMetvH_Pp4oQ8Ne|WG zpSa*G3c88h8}r39xDc0h*<~h4XU8IbN}ZUgKB}&{_(txUYp6f6hVwU1Kiq}f?XzjS z@sbOLT#BR;G_Ayf84Ii92kxY^sZj8xRX(jEF*3lA^BAaX9O4~88#IPUdus?-61HFF6cR9WX66!i^22Uvz9) zOHOJWvu~zz6kx&Q8bvWeWJM37YP(R^?L1?-#ik#^S5a$OVQW45 zGKD3cIjW3|j|dBz`VOqR!)*``6IAcPKm!&bF1|&Y+tGY1}>9y zOVq3f77TZpuPZy@zATeDP4htc^}$%6a_JqAJf+yD*y(xL+EbTgZmp&VyOt4&t+|;= zsVXWY?nTUU)8=$#|EZChOH`BEwVNX&4VPxfMdoY1ZbgYaitha z;HI5+N(ikqDf`UO>xA5$9#=>Ruj~3lxClMKg_K-FDa5E3j^Vcw?%_5eL~!iy$aINV zZ4EEDrk(q?FyoV!8%-psHx_S4(rVn>^M(|X*eJ;H2o)^b#5P?a+11F*C~_ z9_BH|a8iodrtl1W;w4*BSO*01PKwPS%*^r#My%G( zQd=z3J62~fHz==~%vo)}h#1V%Z?*4Gf zIKuzT)ctE6nCEStE=GlXiW?`4XL9n;yREoZRgr|s(>VoiI^QT9?+j4wZTHy~jjRP< zTSG&apal@749>A9n~nyv;-Xu)5*kt%S1|d0k!*8Y4q~YLvLFfPL`+Vb>m7n)P3d@z zj{p2O@CA7~OJS7xPe*Z0%m#G@m2ibnhL=4Ra}oHjXL{F9(LvPZz>TQ$4)Eb2-5-Ko z|BaPb7)u-7BChPdf1dBB@?NXyTm3SmK8vQ#J+K6x=r`D{{!1J)RgFIoxc!1k_&F2Hg&AP8z7G5?=qWSAqjkV`}m`{VvQ5 zgEZkjF+@Wxto1cuhhBWR7bh`Na1!Jqj@;0-uuyDz6tR6aG$Q7W;mmfSHZ;Y%Nu60q z4(PmISJ3awInv6T9Pwc9NlPM%_4q!S%ZG-I%c*gth5NY<#-CWRz0xNG@0ir7H!M3$ zitxTadrWa2&TT_0K1}A~9w$%qNVWHE$*^QG9hbq7RZ6~(Lj(O*)3bf zxL6}TT2hih`Gov_qC9O}!@sVZNPZZe3RBB}yT`TqOnB(`$dOKI4ltSBDDQUpn*{1? zF6+(EJs6Jd@6>rspLX9^?O z}8?DtNpSK~%LCx6&{kz8tA7 zl0T0*YA6~kfR!pYf{4r3(FbK7)hvoj3Dw`A+dbm{*e!mO?HEf!iCf3H={l#>IosQ~ ze`__4W^b5II;bHApBB=wcm733cQ?06b-LJ0_oR|BVZZWXZLr`=J=4XQUiwL89Ap#y zi_UN7#kC!>wa1UV&ixf@hf9a$9{vYFX^FQ}NFTe{{|2ew**31tQxoC7tufgaH}k{l zpB7B6yZnrZ?Ye!)Ed!sRzNDE9;l4&yPvCyExbS^NLT-47^JJ~$-LR@@$FoE+V`phR zl%=aT$`yfdm6vwj&yUzIquX!hDhuK7&uja$C=bLvGe0BAY)3#a%Jg91<{H0-&b03? znA>wAh=+~k(f<7Jj}_YAG7`qiHxfPJuXA$`pAG2cn-qF|mroOtHOUr`m@RX_{awu> zidhRQFAR@ZefH#TOi^fCZ;~)2BTu{l(`zk9BYd%EXrQUO9a_ef@VW8s@FV|a;y7mN zBo>*#CDn&u-l$}R&lJxPKagI@&mDQfWSce?pG_It9NEWxR307!sa}in`;yz9R9IbZ z6qwe(&{rW34Ad&$p~=D04NFu@ja1W4dbYXWR$Le+_`OV!>-EoABW`&#xV_jJyes+H zesbJaMv4fIHs5Sot6;i^SNKswc6St)w?e zXVkM#Nqxf!4wf2gkxxo)tUugnLt$=d0-*j!R`)>`E0#_AKM`J|t9;7hHUzRFF@p+q z`9+6qYBapANz+)Ai;4W$XgLcHAsXhHpqLEsBPeXV35Et;^?i1I&-*vo6+t+q z;vy|s)5wbB3}n=dCa2sb)A7p-o8(_~`8F%rhLwb6bJT??qE6Zs`oM}v5wsOV zp`1|+=65(d+{{V?l3wtiFnc@Bd9V_|3&8z9&UN272_rocV=1VTioI#x#P0C+ zTc)H!Z!>gn^!XD`!iwdtJ>fjU(HGAr0ACT}Y+{RZhb)e`P*Rl>-vtVqqiPwV>4onD z>S)@N#qdU>AxTL|5$QW7CN$acJ+t==bN0CXR7xIL5_YSvT<8i%MjGe zUw_;Ax#v_Dp4xTi@uLK-bOa|Oz3F1g@JpL9iT={Kp)NeQ*_M+g%;awgNKfo7t+UMG zhwpHiY`yLY$eqNQ@;$RGPdG4I<)rRw=YBmhafYI=7T z!yW8=`pe&AJqz=9UaU5%_NAv{Jsa3-C1bg{M~vP?Hf32D&i(i?FD-M!nGJ7t0?q<$ z%$|CiV0tfzOp?l26Qa@`*zVM+xUF0wUvW~PV1=j>XHn4}5X`sBlC{Jp&Pw-<0#LeT;5 zggjb<{FI9Fhh_lQKG_rT=D(35*Ck=5ZSY(NL?oebWqIz9~gh_b~y7Hx@IzPo@H`Ras&~p3d-*`+^J@fl=E_;8+^^)*_ zqh?+t4_1U-omJ_Avcpj~O{I}izk8A4FnS~O+Au(%XL<9 zQBfV*Hw|?0(Qm|SI<(3c?I7}W?FIcekv!csIs2yNp?oyRoz^q-v1FzLUe zH%*(o{q_mG$N;h~i(VhCER9f<{qCv>SKXw*RXrvmR@Li|+z5yjLQl{nFCn1#AHk&gA2x3USbbK}k5u8} zK$v5vwT?%zzv!9+-uX{k-vlWomJ$#yw58O%3R~w*-n8(Z~x{t;G)1!2{vk%}Wc1qv840Uh6 z(^bqnZ3DDK<@)P5p1x5VZ##{Ca8otGx<2R{oyxb?|1|l(PQS~IC$CPtM{b;)DZWQk zWgbsHu^qAA*g5jJA;GxsP`D*36xky-U;5a%}Uzr|VCH#0`ZKkImninS@nu zH%O*)M({2*xX4oKy()s@ANwxIO;EsY*b^WlEZ|-Q%}XIPlP5w|f3!)b$b?l<2x&SU zkWX#F)D3FeT_lL#tt@|3xR!Tq{E3LekF$4(^Jfg9$%snbL+r_PdU0AAj z(c6EB;{YA9u{?$xpJ_@J7~~m(t66rezVY>_Ppz1hgmnab&$W3-8HC@Q954ubeA7MA z1oNEUBLSt_@#cnQ10r%wiji52puw7z$ms`wrsW|hQTXGuc>lIkJ;(Py#?-N%n!$(; z+D8I*Ya);*&;tFAo76m$S^B;*ywj*Pe@j>Ey#D+?Kx%;=BhYsp>2+>I*sIPL7TJJ( z*EfsFHx-;0Q6wu7fHOR+XhJ+dGZ1JgalzwU+gwr4o@VyJDNjgB1l}PTv3MhAX)Li+j7snZaNQt2rqfdFsOkv z*XTCJOgQS9)Ed8e^(FH!5;{0TcaHUkIl!+cqNw(ceL8Qq&V=ziL#=jQUj|UFE8FAG zuMH7xL$?r6v7o~0Y_#OURvRk_ShO2&;3NCVDrA>k{qu{nk-jA-Blyo$@%1#fXrY+8 ziuEUKeIh3YicTYsc-*Vbi9HXLm%Q33druzH$-evhzeDdS=eN$C1n{P(s)V}lpV3}R zzI`IzYMd`o|7)!b+0!_-WH`y)tx#xvBYbT%#zA?f-weD!z8yIdIU<|j$vg64UlwEX z!9>EK{{?uVD!Mf5y^m^=bx_{5Z0kDWzFC~W$j+uqf!!zc0er!jbm%tpD%;SzmqGzi z^c+niJdMR4;>J)1K!S#@S;S3NMyo7|)n$FR-V^)&07aG}e_w+Y@*Z_)a=fKPdQ`q&2cf^~i=1ED?g-oL z$3JzDrfc_TS$034CMtNIIjL!v7&(#l@vj_nxtc4=WZig3oKP-C_lA^azwdccA7t5( z*PM{aVVbP-d`IutfmN(C(X-FjE?8T?Sd=7d6Y%Oe0J|!QV{N`>DdOJBXzOM(1)+DG zf;hcm6JGt(-ZXLQue!gb{-w2kjDP!u^V-gsYi|%qoj>@B+}clo+WPOMWDd$NI>3`* zb=BD4tg7pGyAQY(;WX1~CL`L?!u)Q0N@n5G|J$=~&;F_re``Q@x{W@zq)1NK%BDr7ls>f+gmagF*2xE({4xgqx^mvk2dGhETre|oc_3vnA*ObKo zYCc7=T6TqS>768g=FqYoAl%x=J=8MsWc!_|4)})^;jGSjv`H$dY+77BE+Hd=t+}T? z{Ic%;Yl-V(Qmn-D#KR)$1YqR;WLI$HlbrdoOYlYWCqLiB;~#D_$CpcvicU3;zR3|> zCx`I_um@as>THroQn}3;pv8(-b`Qx+hgYY7hma%ctt?Za=O7z{py<#(pTy~}mb9gh zglVE_>c9Qu$yA0Pm7;Lz^n&f~u;ipae`P@H#!d;^e@$aEOn=pkpv_r*KpX->FCk@i zD~#qY21z|}eK2K1%YB|Oe#@e$^(I-3e94^aETwo2Ee!rNT>e+4O0zSyDEmd1eJO18 z+vwo`cDItRsrl6Vi>|{H8PT~uGB^JRjZeoq;4i{nJ6})PZ4tKr+I(o(*frM3jAA;)nX7zTj*MC;Vf2ayd_o|8wK$mOIdc`-_~ zausalCcwh`2vy=T*5d+U`vU3a!w`8##y~Ov(9bB4M6Z5Y(k*L#8B2UQ>K-ztKpZTsaAx-#=ELG#&7iU$eqX!#>12hw@Dvzc$~7Ph%^^73DkJ6qE(Mr*E)~#2$TFM@#)}<`2JjS{jezREODKxjyI~(8UcA#@+k+WU-NfZ z&)Sr5CTS}TBB@KUvizLHykB(nE+QA^MVA)fLw!udxiomE4^+g7lvv?U{~fHdGP9=c zInj6b>T=_SZuj#-O`z`2@-}Mx;m@$=l z_;-6jcmc$DEMv{#iB3X5ll@4GaSG(FN!C!_4fS2#=&G7ClA>FM@wJ!%%;wbX2gBEd z7>{wD$+E-KtQCbb=ZVh|gr7@kMUC=gzId==Q=RFO$4#fmBvNqDS2`op{|r*Wx!7NH zxAB)!5gUAQal&3NzdHf{S5`B(?=tm0_&`2G@B|jTar7tMUf1>i4($Kq$BlU_$ceac z)9Cim^FLcX$E{zFyxjQrcU$cKz)N6?kBH@0-->a+e|}ru_D@(NW~E3HE&wYS@t9k} z>uf?CNsiqeOZ4*FPW-?<^u7?<1AO8ry%~rcB_{@7L8|qw6`l#q!K?@IBIV8zGQUOo_BU9EmXEk9lAFc6SgIYa6!EasY$ne5 z{#z$0$MgJaMKIS)mADdfRt}aZi=y698(2nktTf<`ToT(8)soIfPGNo-CAf_ysmU_2 z6wRHHTSj&ST@6pm-a{dw)!cesE{6Aimec5_?%{(W%GQ1jS=xm^f-cP1?r@G~aoxR^R1wTxkiT4m z3#JC0HR)$&Yu#&K6ijb^-aa)Z6OdlhJgcY9xnbb}!(8xWC+F(dBd`sMlG^q@gxoe) z-#lbtVNvLGotnvCW!v?vuTY~HJ%5_*X2!c~mABFS?ZK*}GfgGf6&Q5cU*GIFYD-tl zL}7+C8jkOH1B(xdNQz?nWB8o8!CJN-C(TkWsT=^!qvEC?PZ+pfs0nJg_UC)) ztH#nqf)5L8Eo}nmMQ$28v<>e^9aQr0sjg%2iYMzf`I47dtw zz@J5)Na#4nx+Kg3VRfC5#}CnG`m{V5A2PsDQSdj+Exp2)C2s|XoFEE`#cFFtbKMH# z2R7A09_ZU?s^eTFj_$pOlA(3t#;YOpEShw{+mJ2CuF(CQw#{` zzm0LaA4PndL^7}{ZLbV2LR>Dn$UHwvtrN1DmA}ycIBjhAZ0oB<(KCAri>7`xH>#ka zc(xB)UYI)gh=(n5(`zgrQJDb0s>~|e^fWVCuei4jX6C-i=jrL$<&r?3`eQtG^oz6$ zqS8qZX_C|X*vl5ojQ!24f%dFWK78lJSmSEV zCaliG;?R@Ho0C;xS@)62uB&F-!Y4CyVVCfIC7O0dI9h%d0f0Bs7&77)!`f;si zK(@v#RE3TEYOr~tmcUdhdPzJc@b)ZD@4EJ=4YdijImPAzb6jiIE%6zvF@0E#v2=K@ z8SZx%EAkI5Q0PEr^GQA<0QpRv?9({}9cCF6K80Rw zP#!174bC$5g$$`(uSc$qNs3U!5#tn#!EAq{-Hi|N_e3T^Zw=FuQnhx`;(TJ|{Ya}z zSC8X$y_Y-I=HuK!WuJ}&@;QTtj(jiB)*AVG!+jj>)JKJngfCzLXOG7RTu6SIb;Hmg zLH7&{B_S;{rFiGLZt|qBsuJ}_Y76y3rLk2nu#`h&%@Byf+jdT=zwfe zj^035 z1MM}y=@1?lAG{Ot!>kwyXb6;y0b6#rL0XO4zIbDj(FezJid*s{$=|C;E*4f+yE3dY z-dp3)(zH9n#|OAge0t1=@Ls^Y0ozjbm?Si{ToPnIOEe6Hl~|6tqmGnbL>AEtIu|7d zeCx8a=y-AJq11hHs;lCd0`!CeW#4*BJh~Gt_q;x==MkEgVod(-5Ch-s^dU|a65pj= zyo`Hgk597l;Nb+)50(7&MwM5JyX>RdedxqcE5AEbzl1q5er4Eq+DlIjyPjPPQb>z0 zjqC}Dq}oGGY0gar&2TA3M!6*D!#r07-l!Y{J(Z;CNX+DgvpZ##aNp@U(S*InNKjN@?4c6^csSftD&x;X8+Ws6q~**L61tVpHNu zxZknQynk<=`!c&^OE|pcOO8nY=ts4v$+HE3uibKN*0(d;%~Ra!C%S%_|0XEE{oxY1 z`t5b*$h}{5lFJ8w)lYXG*!t?K8@Bo_*Sc)i%vmoV2Qr*wue|&k`^qYh9(#)iz4DtS zKwq@%>ch~dY=S>>XBz+)Jl_1LDO66^qa@+IVv-~=Sy3juiO3p0)A)$bl}sMftf@QtWcaPQj9w5NqnC!XMS@B}gul@BSK1$W zEOes9q`14ttnP)4tStW&LesUzH{g7F!z;@PmuZvyH5wnl(^-O55Y?>L4giTx;)8u!+u$U@e@>*pa4)0KC^4N~D{Q-$R`9gL)2e!@3PK61TF+D1+#OM2Qq z1%Wxl3{n4ttbJ8}OP?tt9G6KheO;DOc%*sm`issVvr}a_8uX&%(K+<}QRu96%V%iH zSr_PN-KjGlZCm!;TwO{wgt^|Jp)0NlRwpqWvLN;X{r$rr$T(xF5`~h2)yL zjw;9f$}-CzrInM6JD!Roz{hPgM) zYbCL&QCYX%)`USK|9*E=UpXRRA-@=n?Ll8v7z`^@CTAcd06z$crb@=&GP+ztuSOPy zxFCq1;cx$#{|Jrv0gi>UqWuvP4Q`_3$o4AHh)~x?d;c~|9|z_qfZcUpUt{sJ2kQB~ zhEEU9lwIZ6-ho{v>hfr zcF=z;K2S`VMk#VNbfpcTd~uBo$;oJ*{l4AOys5&fACVUd#z`EU8Sj9h^m)Q-n7Oj? zxh7%=6#PH|o}JZl{JN$D?v`j?nYhkWQGtaRp_xZ^BJQLx^q=!ra~YSsJFOd-iuIwi z{zyLK(K7wH;iQD9UV4^}pHi4EN*Hxd@dBnr-^Z$Ji(G|jST5BWGL-rZi}E~to+gR( zzg>)=3TNLj9=42j)hU~ZqUM$|^v1w6RvX~lk;%ODI8bCf_6TH-$xT`!)ya=dldE^= zk^F8fUpE_@le&P%9#vH>QuWrsDf30^q?t2nPLi^wh0z|!`x~t>a-mS7Nuw%BCiwk1zBG~M?}Ty%zX3H zSp&XZ6JuK+3FG@hGQ9~x#%~BFfRrB>2{mtN_6%B*Oka2-qX9MS%axewbbn$^(3pQS z((Z|l0jRNRwZP`w=*@~7$uv_yhL8LqL;G19d#NYPCRf!MHqg4D5_@&%>4vH%Zg8%m zzVf-gR(wj}^bV!QtVdCR(Q+M=3NLoBS4DC%RjX3WZuk1kG3tL)7d7%!joM!V1F z>6flwKU0tdyxh;0I#8Wa-V3Nyo&N4ryDBW%9PiyABlW||EiNC57YwXREONn`ACw&o zY}_J6)${K!)CC0GPE!Qu!TbeHmZdmd5uwVG*X(*7a)KvW!%JZI@Vi3@J*0go0JYBtNmb@HI9a4bWw7)x+ zumTH!8LZx~J^ZlwzBHo9Bm>(~<6vK0$C{iT`|MRn;X)2Lzf>@3cDiB3*MCm}5F!oT zQ%^T)D^J1B7D7b4`hvg#b6c*Xi z`{Ael;6Bl6pB=O+8vZX@ z>Fki@qOZ_&T$i{hoV^iSBRafJsqSw*L0?trNeka8>4!ptVX%i8WqSU8zMHtNCqU_! z&TCa0W=6*Q!a-$yQ028X`hoXpk#pSGMh2nmXCytZxR$L1`G^VVWG_&PrD|zqZh|pu z;C!k(GTpK$v6p{eXaK*3vIPL?(HRy!cM%6Ilh)@Bo99o83MMIHB4RQ5@k38chij%@ zo~>@3Tjw0$Ls9x!M02t;XA}^qg3+}E>ad(Wu9`tP8)EzGzVUaNfn9P{Jd-#hYc;?i zVnRjBitUVl66z>8pDN-!Hp3}_z!_A?Nrmz{BABXK0B+^?1I!4`Tep(HqXEOop@@$Y ztN`U5exJ;T=P^Y*W%!l(tGi*}H%b1$?nu(VI~Ew>ixA@uTt<=?4i(OZC0#h(E*uPH zlnabo70gxQX}+_fMVwe&qve-M9OQD7qAJ4EKFxIDW)J zpXbKbqj}iih-JpO>7xPd5w7P?cK`z>!($XtxufgVK3Mo9AT zGwuo1l=BKfBn`udyDW}Tks2MyYT_NYYswQ37yAOpZpOwu<%1fQ?y#Qt(|YNkz>k`v z7Iq-1b4L)7QT`vJ&dQ227Dan2ww#O<7br zljpE$44$y(lDuR$(2RHN6GcO(L`|dzFV&Qo(&4ib2tJBYRx}Z-Qe~{#0M^^js`(=f zEI1Kvw?1}+?d)1A#b`j0s)f2QAdo5>_9ZdGODA~}W~@P>K@FSClu&e1(Rn%_Aiyrw z%7ABm*OyGglcc8d=xMXjZ6rPL77aF7i5#Imf0bxMtAopk2i`5)*s51IpWQcWdHuGV z%joPNeuq-0=-bz3{rOq`%R5I|eBRoZ+A5cSy4+UTO8IGMMz^sh|DR~_ALGTI|1n-f zDtP-n=}Dj!x1|(!sVb%2|22wQ2zZrb{?*tlaNaS9*^^y*l7?6O<}6-?XLH>nG;>X)HYJX=ekW)qBp0ej@$xF|+F6$9zsRw)0pJ%)k}q>3zz%mi7c0 z>@a_d5RZlGr{Cj0($O&?{|S^&{a!DUAEp(O^6>$KquRf9%=AHq0E@eGS_vV+y`{5G zUq1fPlEM9-=lowMflRFa)JQXP|IfGYeJCQ5?%yRP7ak9F`(5XL8|LyOZ#R$Rt}+?b zNW&kdkaq!gl$m|1=u!QOnyN4voSYq>>efAS_%NM~Me9_lL{rmHiM3-rc6dti*`&xu zMw%9EmR#v68G4!@{_clD!oAON&+dn};tQdP&-ed@9*(uL$k9xQs zkZ`M#6cCr8Hhv|OtP)1K*RhD|Gz!%flH5>qnjX~Oul0|Aw+5E%@~jxG{IHtub@>H9 z=~94|_X>S3QKkEYUuRf`(xQ^yWYr{B_hPr#hl|Vql@8e%%SQbj(u1n%@d5RI-r~M2 zVP*%Q@<7ib5uQaDbj zZcS;-(Ri(L5zGtq24{atjN7?$iW=MlJ3^^5K)B2+>jYZ@89 ziUOWxkfL$^zW6fRr{yHhAb}DMSF%@vx(#C+Ys^(^#t%luVt390H;knS}X?BdHdcp-` z4e{M3_J2cASa%^Ip%3pretN~gafgdJ0YUj1@%1f^k#;)vU8erVsXx0JZ)@!bcndb4y&&J{KHVT4@1yS%s6=Qe<7PWeA(MQPI0e_fAJNv)8m+6Tx}i`yZJRb>sm2fT&fx&o zRb@6BGH4}%iy=!R=nAI8c*w&s7SxOV;r&G!Kk@*QKI=-5o0r@0EpUB`&~@;}z8J$x z^E-mt9q%S+8%^@7_S>F{8$ZE6Q3?%8 z(L6+Fi)u|iQH3Uz?tCsz`PQd8r4iwA$Ws56O=8C&tkXTU)rO&YRB9w$j~QDeqSR2+ zRB^QJZeantD_Yq9juB5)o6X}58qWH~{BU52+S%cYLO2BNip(Py%8B5}0dlqp!Qdw+Z~JN0G4 zn7iDRHBCGG=FL_2hgcOJ%a3A(4<&B7KeP%3#M0fsFRhb5`dWb`k>mmrfaL`vE1O7ydfOra3va@%l^!R76FGn~ zwP_TpSd!pU|Fp}e&Vn~>#@V<6u$bkB;7@}qv41jBqm#P|FRlSBbu; z1`xw?!co-MNi|j?ZhdJgJ@4K+!1>y2fzh}DAPH^5$aJeLJmh|Ey#Nl8_i;>P<?sA< zHebsUt}lWCG&PC9pdB@2o0ani9O*8keEhN^RtcK~$-m=FJmk28 z#&S1s@v4~bA-;#FXGBay5???3J~@0;ncxO7Z8~mtZkT_`_vjsqxW8zJ5WC9Ml!dry z^)4i~T%JqkXyoG=Dal?JvWASD2Y4=OM^cc^;~kz!;Y|b3XE}8eMD$LqZfY%{8OHSX zac|W)T5!LkjDh{eEPwr7r0(PIeemXTc$f^VPYA&uTWVbOXUDW1}>{rAkoYp zI~-w(GTnvCy*7~7r2=D+8wk&IU4^4rYF~i@j(S6Q%Hs6+0R7Z|#n30&c|bPQ=2=$W zbOj8cgh*eu(+czhS>#@14ulidBR;>VyDu8q2vUILWN>;JT%0+N&*f`mY*5}KuwC@U}c4cA1^uy;uNd#L4Vmk{qyCRvdmv4ExH<|Gh zKNzDXmJBLE!!flDO2~PaQiHR-85fto1vl65xzu=&p3ZbSk!Ud8-I7aHxZT*a0jahZ zwl7Xg@)+JJlu26!{_Uy}!NkndQ8sh5&=;q)BP+mI7#`g!azz7B^|^DASGJJ|beS&2 zR+lDi5Lf(DYac&h$>9Xd&F$=-(Ks=6jJCOnU9S?ZKEw;XBYvO-TD(E?A85wJj7}i< z!2OHnMGS@c3|)HS$|spW{c-*~gXR+)O}dTR{LVrmFHA}c^zHY|+xJzg+2aST0Uj<& zfw(T`N9+HMSo}f$`k(T?t36q?kzmwn_(eBcbnEBx^Y+o9ec~i*(|jG*<*jo>Yzu8| zJC)HZuJ@{=*hhUJ5A}lWzRXuVOW&&y{`Q&n+v!I5cDDNxS9=FnxLC~6lV8^YLU;K; z(cSn@wHHrwHj=txW?6~)pOtVl!Wd!S%8=ViE33&vhiQeXRgL+jI^9P#v zkovzUV7;&pU7>0tlR?{N*^)uDl7EKbJt9=gQC^ADK2nO!(T#uM z-H;_9mM@4Ypee2b4hb&+AYqc$M zSoPNxutU{?70MNgDcq!3@E+3~cX36pKx6Ex^~A1BL+ekLWSus%wv`qoRi6oH%@N4#Uw-Mng3nwB%+5 zs;xXkHZR#m<#?17N*SFYafId2&i0-`+N|LZdR#T+ue<6yM@38o2twkd6sZf&9V8?^ z9{UX-H_PI0ioNBOG$~ii_@4xj+|8~+P_L;4}u45w7GC7nH|IuV3Bc}9>nc94UR1zURuvg67vCAF*% zFSoZvz2xdi&o$X|{)lF?GL*l8bfs2nd|V<1j>jdi3drg99;$hsmAP!Xdj5pj!d16- zIFWi`HLr)ZPLeDd<36_u6>F=w^U@5Ew-DLx3Ja<5#>lLJiFR`eq;5Z=O^<;`tZcr!ze>imN%ln9svT3$c(lh{_oW$ppYKw*4GL+=$B7x%)4{Sv z=YJ+feb6AI{aBK*XEeeQd|K()(<2cwgbBu5zf|+NQT{N{EHXpN5={3>W@mFQx(Wi* zhBt+A@lgo27jzp(Ms>ANmF9;@!|ydJdD4@m6bCj7D6qffnne8(9&Fc>9Y@*5G3LGFoY#2++1P8fu^1Wi`k) z8?CFOP$Vyr9OKWRn(Xv2Ooo#tYsXQoy>19fz4T+ksX0si12;X2k4*fDk$ez4nKF=r zpr2y25?Sg&fPsG|DnULeFVTo63zK0Pi9a66o=(+@KZ#E%s+DcFd2CG+h*V+IWp`9Q zln1KlZ8Ugg7!QrP%dUG>>*=`^=f|FF^td;9P?fP*NVq%7G=FX52u`a_O{A53aRJOt zB@5%VeLP&)MTo(3G5lqAnPVV)u%!fQT{qefB`9fsL(mt;$7@opJ(n%Vp_%-N~8E@ohn@P}ya4OhzZq z;I^6U-dAK-p{$ZC_;wV(>5IMUZn7rM3K|rLkX6iE*B~4q*mm`Kwxc|}8qjt5sGy>Y zPX%VU^K=AfM7^vyy+DiBCO0uaHH4tE;Iw6nz_6Q*=&LY(bxw1SE@h_C0j)J|_Gl$g z;SO9S2BmI98@UQ1E*&IuFrhA>DH*v4opx#dN|vUNfUCrPo7BzSjxU{ab~)DnN{MUT zN2H!^k&I}MEv8^AnvF=BU^QZ*n{%Inxqsln7eB3A-WYNqcp2T^9<@XRj!A7-a!d}H z%@33Y(~a30UY_NGiD5eN1w$qZBjYiIebd7}T7CArqqP^z{`G-eh7*$m1J?`v@u^EA)!ZucgF1xqoKLLO80(pxHml_$zc6!X$L1HVX*NToJo!Hc z5wxey1T27)vl$#*@dBC_I6!X(dNycqf@%v<%W;ERHC0t^X**~m1?^C2X40?lz<47W zrXfq6RRv+|srt0znzG8X+E?PAp8(xIW1Ku%pRai%4m^#zzDnoDWGY#yZ^v59 zjm(h11K8l4A@Ldi*8crJ_QJm|!kug93mYyy#Hf}jl`f|nN*Qr)L|-ine~D_NT1crH z_8N9hf8fF4nq~rk(rCmZ;UtBmJh0<+lq6xs&*=A|K=0XKjkr+-JXMPAVERMAs9BsXf8j z5WAEwpu~>Zw-q0$V0?z<4cl-}><5@+FCPfM=#u4w#c6^Eew>f%GP;n4%rZY}c&M4MKHx6xmJ=C@&7!LNd z%;WIEc)l7%cUMM1JXGMa-z zuX#x#vOc9sxM`FrczB%EPSDj)?CFwSD^nNeXsf7urs;;73t&md1>FybSN- z#V)Smui=UUWY@Nmm~!g^tZxxpRzQ1%yPlQq!Fxd?R1m0Wn7Rs=+FYYx^L2G0sFl>k zp$!|Mo)cJ*6L11*C{XAfI7B;WqQq0;X+~Xd6C0QtgeVpisR*J4OD&V5r8k z$7eR%crqYa-dh}wWno!9cx10fupdwR5EVG=@e<_$#YB!mfj}1^kcnJY@i->T7ECin zTXCDFp~Z0=|09Tx81P5zkc?^5fR;JPw z?P=l9QDDx%jn;|_h;!OBNq3@Qy%&ji1%fqr4SC$mnm@4=Q^Ju?tv8$98MXAupxIzy z62E5Tap85v*vx?#XgKJAA9IDVhd;(G{nX|Fi19^ZSiJ4GPZ&5*k$fdV{BJp$DK|`{ z`%k}?C?*pkw#}6q`l=6A)PBtH)(O!q_i;7dxM*IPt_FcpLqezy)sMT%Tb(SO$^>h zjK2{lR92^M27#bFA-nUopv6Maeq+_d$;J?zk%>{M(vrv7#+@M|=oQ zoP(ZCdhW7^jSs(^gv${br&nhS<4_|BG z8vA!wo+fH)SLzq)p_ax4g(fzavKu6EH#)X1mTU$jc`W9@Z^ia@1@?(&XQ6WpQ8NcSdZ(n?KUYQw9&U4@N! zu-i;7uuy8b*o+>nld(emQZyXNNS1;cE|9F?CyaI$&>1J}pZdznEhp(QLf*kAzzJ{2 zHsDpKkM5jRo6p&Dl^pJoM)DDGk#T>JcDHdvFXRUXrUmLCFepgVo5Quf9cIZYW8vnm z78QEOBS)kgOIC}|m^8{#`do&rhU3BJPA6XOY*4jIc5>azoS(&$#2bQ@qCh03QjR2) zCMHEcbrM6lq^3a4YRv{~C4*XevEiWl8-&JQo)WE=q-+p(Yfc(o=u{1Lj($iyO-uw= zaCYa9iW;rqk5$L_;^EVyH6RFQzlJq!ia*8k@mdXEksfGvvF#I5nJ(`LBG8HNS`ZTs zoiasdnf;PLez89;(khO81`Q4u*il|+w%^x4@-YxU?_ebwfmv*yS)pon+GQx4m6~GJ z)uFL^BPinCxY1jy6aCmiJn&GxdTfY*u>sWC^i<&5@F zh=n^!>6jZdhF*Wh!3TmHx8wv6KWcM7l8uMY232T`hvY>o1!s z@AL)ZKx~Zz^PxOSS}ZVOmFDfp>2Hb-^^9gPrZ?88ckrfw`wg-+OoxXgg!3!*rm2y%dK|U-NqlE6HNsa(6Acm z95Y6NT=Df)N5|a{a&=SGyr?_smkYSMWrRB7U&<2b2leQ-qagJ}^=N(|s8kwt+=fOH z%Q0$jtdDm4$%$6xF9#6pY9R+mW1pjVW;drcrDH3pCPnLp#;{lLO zQ-w~!8#W3LuI@MtYD?bdu0d-=mpC`|4`0ocl7CcC&qG)W4GXdWovyXRv?K$=q_lD- zgNI!N_L=Z&#Ye)R<-2LTGivauXvYpMRMY2E1%W8tumG0vB1zeznFM3B^iuevtn&R^ ze-QEo_u{PCU@+t~q|UI0?)xQ5VDWo@@kSa@Js2WqZi(jJabuRVM0%id#*(2@GSmVB z?1bdQLW@^5&M%T#VLbfL+z@WO!^|h^C4_=Q^CI@`_F2Vf?V`0^G()cvIZ=tPQ<@IV zpbQw{j88O_-rA?YoPcv-2)DbpE5kmvXkvXX1*}X~YT}dIEOsgyV=eZuUE@6UTb=0RCi*)YX6_37w zzB!gIi90>8^E)jgQbQTy?oFrPpHoG-gC+*6df=m3EN=WH-P6-@=Ag}0;;2cKne?Ft z!IVKIG|B3dJr`Zs{>9=HgpDvhoYRW+xMpB>M(a&Z1@fC5ZEEZi#bB@-3YA_kwk6o{ zJ$h1ni?{mI0NXdvBVFcrgDre@L0Y*u6LjQMJpJ`j3N$bkq*@fYbrVyd9$RBpsn(08 zXWB_gjSWyt`Es9p05M8KEoIRn30ep!^5C?qjDN}RF7=6*oNQSByV#`jKCVt~nh zpO5Z6k5(DrZi^FsMHHvq1nx24V&z^ralR2!4D2H*je;??PT1ani|W#=GK!5SY?%Dn z?q@Jnp-rlPTo#NQjIOYHEN8m&X0W_c*H!o%eI4E~j~}Biazc52{b2;Z8hSK|Y726ey#lT@ zI1QZMz!eWSeTQ=`2uTg_Nyiu3tkP?<1yJ*W$j}v!M<7)j@nMG%Q^RyBI(G<@qkC;} zarokPi6%@ukD}TV&DXbOSCvlQ-e$Oc_4RnrJ*?b%=(-F>F^I%}pxf?IC2g9vo4$-W z{sAYAjc$}X#(Und>Ed7H5097aFYHQoOZlNBJBx@SZ0+shV7w&!%{MPCbui@)Txva| zeEKB@WY)5x$K2Xy9rRyT}aS}OQ0OqmHCj4~1gX3gv{qJYJrh(NyuS`^a$B}s$A0Cfr)*Y7*MPFwT2V{ZG z<3%#HA{fJIr!ghhCdFaS{hfX#O5QF3#6b8&?QT&3uFE(IdX*n;rQyrYL&y3Zn>XgM zv##^U5jn#+)R?7!Rk6@=mG37q5YuSxRHfeB95DvIRlj<8RIW?vi|lXA1SuwEIwjN7 z@bA(K8lq61e%w2?mK&N1@~64Eh0Si8U$VOjKRT%On@T~eXCxd`T8dUu&!ShGLZ196 zeJLfYmiByd(DG!Kud+_ZUEAFeN!I77VW8zwe@y^TWxPDfR>v>jAqS@TOsNl0RwS?* z#9nmBvOx>pg^Xrbu1_xzlk#6gE0h3rW3&=dQ-&2x;@*Kp5Dauo!sQOS(`+jt1O(5K zsiqLu$N)GNfpa=Nq01M z!i)#xXP`#P))Tm+BPf;cFjneKB-4@R`T!{h`F9}mD?D(pX~fxNJY~_edxr%iY(@#* zsh}~z$4-(&$35PY*XYDExz^$`5=OA6&(^v?fLLRwPsip>_@0P#6^`m13W-abgiLN( zO~uj9v7XdtWHU`j2$#|4<~W-TeNQ$VTeg99*s#6@|B@=QFPJ#OJF@*!Ee8Zb%hd3N zH(?#5?-EEm&pV`4PwsaKKy2~8P`hX5SVr|F?^LJ(9qi$J;L3O^Fr9>LyLcz4t#CI7VJs>Zv zDxBGafk6lr#U4#WRMuPN^Azat1&9FuLk@lB;9eMgwt{$)DP)*Q+o`57`6{t&U_P&9 zy4OZ(Dz@*8iG={PGk{boX7;6noK>$p)V_iq8r;!+kPhHRUZ1dcA40S5Q;`$-qhB z{$j4qv5<1kw!U2ZaZ5&8O8Bspdk*EP#Mj>KY{ZbZE}q@E^Q!)C9lKCC9EW^CJi-Vi zYKOEM!vASMy7rRmOPA9zw89oQ8M>Dkt;7!epsp(KwTVc?5(~^ zsVztp4gH=h*SaCJwOO?XXWn07H6h`wqaL;?DpfCsOuBb4Tea7@J?vFZa#9o2(-}rq zYZ!e}k;qhOp1!oMOMyM#Rn%&fl$!C^uZ_5iEP59}l&4g&9%>a&k-~clgx4F^zEFt* zuL({$=G>-FR!h+z<^0TtnIzy_kgqQF6n+}oq9BX#?3N8I4qTp0@fxHyw@;k6%X69x z;6Xs$5rnpGz(MZz*hG8XQ#Zjzk2B`DT36dX+hvuz^i=~{0&vorlz<7fk=12gIz*Vl zhtW_nOcgK(g&l#9Wry{8{p*{;mPc~eUB3h9WD~MAwbh(>Lq;U6P8F3)r@;JB#pX`- zAPyjP9x*=?vD&4;B=5|0wGztNkX!ainyw#&*5AB;!(j=y0@~*-QUfR3HWvRJ$;L#T zX}Z|tr4^&)^ya))y{U<{sMFjb8}#W_vx|F|g!zCUT)S^DxiYFOY#NxfcYEp|)tKy+ zrH3qX5q5!-uybp{AJjF0$^``Ks zj%Cbj___g2*expmC(1zV(vQF81Dm3;KuT~o162XL^=4Sk0yfM>8Dh?27q}~LBag3% z-Ze#-X>A*!$#)-P&SI?&&A36N*jUX)E{y`wsF$lvv zn=SL(Y^1o|LF6<;SjC6f-zs2(ohicgX7Fo-SZrj}jG-r&ulaq$S0wB7u_k-HF&IP_ zX&z5t(NGVat;GPP4w-ZnD99+&L-4Z2eMJXRkfvoGgxhJc#GzMf(`>2ik2c=qs3#wg5u z_!g`!sA>K}L#s2{AT00ZOt1a6aj0Mfg&_0)q-kMh(mqoTby!|#^q3(I^T(n>Q)gEa z5QF%Q4Sr%y>n*>QS?2bLYe=#&%d_5r2@twfAd?Nv)iVfb;syxs$+?eHim!#-G?lmYjYk`PbQ%vSdt!v_uu%k%^-)|wk5 zRlBZWJm_wbcmO*kcLnwrW+qlVkPbUmaKTP~K>;QeRWtB-b`Ima4?w!oCrrl%>4vbm zcFMrV=oY0SlT5^|Fxk_NCK4W?-ZQyaO?{w!#|fqCV#S$Keb#6(PD8n&!A@C8rEPIX zO)-5JS#PmWTEpy`3wd8uoW#dUvZ4H;*jRO~!44Fw6>aq9=5QOTJbOi+dq^!L#`bas zj@omX@;Q0g69ubYUO9W9SsA^MGtBICA=5F4z*IzgLG^m*xfEiUi271HCX zDU!4$k__t{@(k!WJrPleb3I8(!0;Qyv|b7iMhA=oa;GbnY7M!&RHCeme3FPyo%Ai6 zl5IZpFnGE0c+8&7)V430IhAV_Bg=y9x58SiMKf#^FRcB^rhlWLTk2t`3!^v~gCaf1 zQn^k}h*2Ua&C2gn!}&z&N_lGQL$T}o>1VmFlA+65NZqhWl%fL&%I+Kelmk}5EDzR_ z?`5JhiKcBf@33`9^G&fk}Z^N-Y(^RyTToz|Zwo)x+@ z`>ueQlTnjl`um#DVqcQY*|gEU$+!psndB(nE{mlv+9`4g^#$iFUr&+7J62`aXnr=L z3_?ll%Fu!W>4Mp~1QV@8=1+pksR$^a>y*buq;i2won4q}?qEe&#kY~EM_MizC{>?Q z)lmzwDsm9jO_V7O=AaMpHM`ye>nfV` zL9mfz5!sD&h6vIfF6P=bnD;_}Rq>1R7v(7JZT&J@ZTmApVCAQ5fpe*C-r`rF=RUSP z;&L#0qC)gU24wKbs(nZoJ{o8%o-w${JuIMG-v}EVWc5%xf~q?X(5H4EXqt{t<-`fE zyW8z>4nG+n(bMA2s}}#Fd&M=y2Vt9YD|3uIag zHL0+K5nH5R9sLdnm!R0bgnpHiSJ51Tx+>XL-;Bk6fme8dil%yd8upM)0fa7-1sC#z z8f^8oB009*io z1YiuH{*gCKF8|wm4M=R8JLw%3T;41x%b166{IpZb|A5u1l{c2{4C?Nd$ACNB<*fdZ zT@jkQ`euizR48;x-kOFk&b?!^&6!^)Up#|KocDqi-`Qnw-=O>x?IDh|~{^uFj zXip)88Y&5hI6-llp*6}Ngtwe`trhJZj_;S}S%2!9;dta-zP1!?KPlEd=ImCE+-Jq) zvwE#9ht@TQ6-}+OOGyb^X^A*@e#VsQw7;);#}+rLMQxVub4thyK4K}357^M$>5vy# zCj97^O6|b=s)ShTpYLR5sY(UMwo1!q`{Fz%zIgjO?>_El`jv0b*%}qG9M_dt&T$ov zq>hV)?nVsGeXi&``yRLU-qkB=`;wylcG!Lgb#{4k;Z_3RrT>o!U3ndm&f~f;eRlCp zGz{K9AVB>}7_#=W<9ER3yJIEc5BOdwl$VE5+x!GOFI-=xD)R%NdK8+NrNFZ1gtqa7zhf&Eni9P7tSyfDTS}D4zHZ2L4p6Dlo(YY!MjHtLgb-un_yT^s0K9)+ z{02+$F)0AoUiI61BIuxYKPIxQm3+E{bdW&#k%suWR3Y45;dj6xWr(lslwri`O&O+G z6OQu)4+e8pvi3xRRJ zN{RGQ+^Ldn%WBx#{h2Bh<=Aq&QmGKdQPN{6-R+lB%{!!=fZ$*pxnJF-_@Xs|;jDM& z>nRq&4xwfR%i3L$rqeLB=IIbp-@rw*xGiSmm6>^u_|@wd_q3lx=3l(Se)9%|<7=zu z|D5glofY^HiSFt!#%Y!L_V}acz?TgxCLa?gyJJSN$bg!8hS`EMaMB{bLlIem+XeYN z)=)U)uW9^CPUim|P}cWf0cB+X(N99>n6KQM{`EbjAJ}iPapv#5eg5Kzc>W<;k+D(# zF&4Dn$dAeIwSEQZdm9o`Rtz$ef&X)L7CGAy3|drOW(kWH+(9Yto!_rRK)6HBqpFMF zex}yhb@10wO|GBeuIjZuw(HQaDv7H~f7S4cOQ)LR)W&`|Q`~ z|NX$Fb`(D%zxZr_**Y`I-RJj-`CO*&ip&cexsgI?*F;NvI{@!%V_2xUHK$xW`k* z2h8Zl?-86V=ACybSAgO4i!Cb(Y8TA0q_2zQv*UebOTFKHY^QGdkte_W)C(Jyfo0jS z=f&LHDDcuH?iw5Jw&V^@3;9%uzgK#bCV_r68UCuWKFbJmimwTxrLwUtS4PoUG7{@6$|j?uzlJegZ77{gMkn$S*|( zkopV&;H-Fb-m%5`wdK6Ch}mblKJVOB&uuS_VZ`Nod>#X3F~qS|`=%d4`Z%`pj%+EE zO+KFZ9_gK7bu<9MxNdS=fP{OzE?$XpkrR_hIB^DvrTCvuyW`^u*|=kn@F#noJUW>G zVPYwX9*YhhwtEiO?L46Pn|uIAL(&VYa#uY}Ak^{PO*rkC#|n*-qssVp@R@S;K;6K8 z%mk=btdmcl;uJJe7_5tNo&O76GTjBt5sBTn|MKI9$9&OtnY5cwGk zGqGQfJM+X+>Yr15mmM@6-eWfYU9m4X%;k60ZCT8)0}q!SFuUrXbex}~Sq!4l;!Ws# z+TTc)i{6%MYzSe5f!pVp(I`5gCty%ZrV&T%@+EtRNFSEy3ApxFh2W=8ÐTmn>|e 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 diff --git a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/immutable/businessLogic.exercise.test.ts b/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/immutable/businessLogic.exercise.test.ts deleted file mode 100644 index aad3f510..00000000 --- a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/immutable/businessLogic.exercise.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type ShoppingCartEvent = - | { - type: 'ShoppingCartOpened'; - data: { - shoppingCartId: string; - clientId: string; - openedAt: string; - }; - } - | { - type: 'ProductItemAddedToShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'ProductItemRemovedFromShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'ShoppingCartConfirmed'; - data: { - shoppingCartId: string; - confirmedAt: string; - }; - } - | { - type: 'ShoppingCartCanceled'; - data: { - shoppingCartId: string; - canceledAt: string; - }; - }; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export const merge = ( - array: T[], - item: T, - where: (current: T) => boolean, - onExisting: (current: T) => T, - onNotFound: () => T | undefined = () => undefined, -) => { - let wasFound = false; - - const result = array - // merge the existing item if matches condition - .map((p: T) => { - if (!where(p)) return p; - - wasFound = true; - return onExisting(p); - }) - // filter out item if undefined was returned - // for cases of removal - .filter((p) => p !== undefined) - // make TypeScript happy - .map((p) => { - if (!p) throw Error('That should not happen'); - - return p; - }); - - // if item was not found and onNotFound action is defined - // try to generate new item - if (!wasFound) { - const result = onNotFound(); - - if (result !== undefined) return [...array, item]; - } - - return result; -}; - -export type ShoppingCart = Readonly<{ - id: string; - clientId: string; - status: ShoppingCartStatus; - productItems: PricedProductItem[]; - openedAt: Date; - confirmedAt?: Date; - canceledAt?: Date; -}>; - -export const evolve = ( - state: ShoppingCart, - { type, data: event }: ShoppingCartEvent, -): ShoppingCart => { - switch (type) { - case 'ShoppingCartOpened': - return { - id: event.shoppingCartId, - clientId: event.clientId, - openedAt: new Date(event.openedAt), - productItems: [], - status: ShoppingCartStatus.Pending, - }; - case 'ProductItemAddedToShoppingCart': { - const { productItems } = state; - const { productItem } = event; - - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity + productItem.quantity, - }; - }, - () => productItem, - ), - }; - } - case 'ProductItemRemovedFromShoppingCart': { - const { productItems } = state; - const { productItem } = event; - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity - productItem.quantity, - }; - }, - ), - }; - } - case 'ShoppingCartConfirmed': - return { - ...state, - status: ShoppingCartStatus.Confirmed, - confirmedAt: new Date(event.confirmedAt), - }; - case 'ShoppingCartCanceled': - return { - ...state, - status: ShoppingCartStatus.Canceled, - canceledAt: new Date(event.canceledAt), - }; - } -}; - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - // 1. Add logic here - return events.reduce(evolve, {} as ShoppingCart); -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - // const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - // TODO: Fill the events store results of your business logic - // to be the same as events below - - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toStrictEqual({ - id: shoppingCartId, - clientId, - status: ShoppingCartStatus.Confirmed, - productItems: [pairOfShoes, tShirt], - openedAt, - confirmedAt, - }); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/oop/businessLogic.exercise.test.ts b/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/oop/businessLogic.exercise.test.ts deleted file mode 100644 index 8ce4f4b8..00000000 --- a/workshops/introduction_to_event_sourcing/src/06_business_logic_eventstoredb/oop/businessLogic.exercise.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export type ShoppingCartOpened = Event< - 'ShoppingCartOpened', - { - shoppingCartId: string; - clientId: string; - openedAt: string; - } ->; - -export type ProductItemAddedToShoppingCart = Event< - 'ProductItemAddedToShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ProductItemRemovedFromShoppingCart = Event< - 'ProductItemRemovedFromShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ShoppingCartConfirmed = Event< - 'ShoppingCartConfirmed', - { - shoppingCartId: string; - confirmedAt: string; - } ->; - -export type ShoppingCartCanceled = Event< - 'ShoppingCartCanceled', - { - shoppingCartId: string; - canceledAt: string; - } ->; - -export type ShoppingCartEvent = - | ShoppingCartOpened - | ProductItemAddedToShoppingCart - | ProductItemRemovedFromShoppingCart - | ShoppingCartConfirmed - | ShoppingCartCanceled; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export class ShoppingCart { - constructor( - private _id: string, - private _clientId: string, - private _status: ShoppingCartStatus, - private _openedAt: Date, - private _productItems: PricedProductItem[] = [], - private _confirmedAt?: Date, - private _canceledAt?: Date, - ) {} - - get id() { - return this._id; - } - - get clientId() { - return this._clientId; - } - - get status() { - return this._status; - } - - get openedAt() { - return this._openedAt; - } - - get productItems() { - return this._productItems; - } - - get confirmedAt() { - return this._confirmedAt; - } - - get canceledAt() { - return this._canceledAt; - } - - public evolve = ({ type, data: event }: ShoppingCartEvent): void => { - switch (type) { - case 'ShoppingCartOpened': { - this._id = event.shoppingCartId; - this._clientId = event.clientId; - this._status = ShoppingCartStatus.Pending; - this._openedAt = new Date(event.openedAt); - this._productItems = []; - return; - } - case 'ProductItemAddedToShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = this._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (currentProductItem) { - currentProductItem.quantity += quantity; - } else { - this._productItems.push(event.productItem); - } - return; - } - case 'ProductItemRemovedFromShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = this._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (!currentProductItem) { - return; - } - - currentProductItem.quantity -= quantity; - - if (currentProductItem.quantity <= 0) { - this._productItems.splice( - this._productItems.indexOf(currentProductItem), - 1, - ); - } - return; - } - case 'ShoppingCartConfirmed': { - this._status = ShoppingCartStatus.Confirmed; - this._confirmedAt = new Date(event.confirmedAt); - return; - } - case 'ShoppingCartCanceled': { - this._status = ShoppingCartStatus.Canceled; - this._canceledAt = new Date(event.canceledAt); - return; - } - } - }; -} - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - return events.reduce( - (state, event) => { - state.evolve(event); - return state; - }, - new ShoppingCart( - undefined!, - undefined!, - undefined!, - undefined!, - undefined, - undefined, - undefined, - ), - ); -}; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - // const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - // TODO: Fill the events store results of your business logic - // to be the same as events below - - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toBeInstanceOf(ShoppingCart); - expect(JSON.stringify(shoppingCart)).toBe( - JSON.stringify( - new ShoppingCart( - shoppingCartId, - clientId, - ShoppingCartStatus.Confirmed, - openedAt, - [pairOfShoes, tShirt], - confirmedAt, - ), - ), - ); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/api.ts b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/api.ts index 3f566ee9..04977d50 100644 --- a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/api.ts +++ b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/api.ts @@ -6,6 +6,7 @@ import { import { sendCreated } from '../../tools/api'; import { v4 as uuid } from 'uuid'; import { PricedProductItem, ProductItem } from './shoppingCart'; +import { ShoppingCartService } from './applicationService'; export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; @@ -13,89 +14,114 @@ const dummyPriceProvider = (_productId: string) => { return 100; }; -export const shoppingCartApi = (router: Router) => { - // Open Shopping cart - router.post( - '/clients/:clientId/shopping-carts/', - (request: Request, response: Response) => { - const shoppingCartId = uuid(); - const _clientId = assertNotEmptyString(request.params.clientId); - - // Fill the gap here - throw new Error('Not Implemented!'); - - sendCreated(response, shoppingCartId); - }, - ); - - router.post( - '/clients/:clientId/shopping-carts/:shoppingCartId/product-items', - (request: AddProductItemRequest, response: Response) => { - const _shoppingCartId = assertNotEmptyString( - request.params.shoppingCartId, - ); - const _productItem: ProductItem = { - productId: assertNotEmptyString(request.body.productId), - quantity: assertPositiveNumber(request.body.quantity), - }; - - // Fill the gap here - throw new Error('Not Implemented!'); - - response.sendStatus(204); - }, - ); - - // Remove Product Item - router.delete( - '/clients/:clientId/shopping-carts/:shoppingCartId/product-items', - (request: Request, response: Response) => { - const _shoppingCartId = assertNotEmptyString( - request.params.shoppingCartId, - ); - const _productItem: PricedProductItem = { - productId: assertNotEmptyString(request.query.productId), - quantity: assertPositiveNumber(Number(request.query.quantity)), - unitPrice: assertPositiveNumber(Number(request.query.unitPrice)), - }; - - // Fill the gap here - throw new Error('Not Implemented!'); - - response.sendStatus(204); - }, - ); - - // Confirm Shopping Cart - router.post( - '/clients/:clientId/shopping-carts/:shoppingCartId/confirm', - (request: Request, response: Response) => { - const _shoppingCartId = assertNotEmptyString( - request.params.shoppingCartId, - ); - - // Fill the gap here - throw new Error('Not Implemented!'); - - response.sendStatus(204); - }, - ); - - // Cancel Shopping Cart - router.delete( - '/clients/:clientId/shopping-carts/:shoppingCartId', - (request: Request, response: Response) => { - const _shoppingCartId = assertNotEmptyString( - request.params.shoppingCartId, - ); - - // Fill the gap here - throw new Error('Not Implemented!'); - - response.sendStatus(204); - }, - ); -}; +export const shoppingCartApi = + (shoppingCartService: ShoppingCartService) => (router: Router) => { + // Open Shopping cart + router.post( + '/clients/:clientId/shopping-carts/', + async (request: Request, response: Response) => { + const shoppingCartId = uuid(); + const clientId = assertNotEmptyString(request.params.clientId); + + await shoppingCartService.open({ + type: 'OpenShoppingCart', + data: { + shoppingCartId, + clientId, + now: new Date(), + }, + }); + + sendCreated(response, shoppingCartId); + }, + ); + + router.post( + '/clients/:clientId/shopping-carts/:shoppingCartId/product-items', + async (request: AddProductItemRequest, response: Response) => { + const shoppingCartId = assertNotEmptyString( + request.params.shoppingCartId, + ); + const productItem: ProductItem = { + productId: assertNotEmptyString(request.body.productId), + quantity: assertPositiveNumber(request.body.quantity), + }; + const unitPrice = dummyPriceProvider(productItem.productId); + + await shoppingCartService.addProductItem({ + type: 'AddProductItemToShoppingCart', + data: { + shoppingCartId, + productItem: { + ...productItem, + unitPrice, + }, + }, + }); + + response.sendStatus(204); + }, + ); + + // Remove Product Item + router.delete( + '/clients/:clientId/shopping-carts/:shoppingCartId/product-items', + async (request: Request, response: Response) => { + const shoppingCartId = assertNotEmptyString( + request.params.shoppingCartId, + ); + const productItem: PricedProductItem = { + productId: assertNotEmptyString(request.query.productId), + quantity: assertPositiveNumber(Number(request.query.quantity)), + unitPrice: assertPositiveNumber(Number(request.query.unitPrice)), + }; + + await shoppingCartService.removeProductItem({ + type: 'RemoveProductItemFromShoppingCart', + data: { + shoppingCartId, + productItem, + }, + }); + + response.sendStatus(204); + }, + ); + + // Confirm Shopping Cart + router.post( + '/clients/:clientId/shopping-carts/:shoppingCartId/confirm', + async (request: Request, response: Response) => { + const shoppingCartId = assertNotEmptyString( + request.params.shoppingCartId, + ); + + await shoppingCartService.confirm({ + type: 'ConfirmShoppingCart', + data: { shoppingCartId, now: new Date() }, + }); + + response.sendStatus(204); + }, + ); + + // Cancel Shopping Cart + router.delete( + '/clients/:clientId/shopping-carts/:shoppingCartId', + async (request: Request, response: Response) => { + const shoppingCartId = assertNotEmptyString( + request.params.shoppingCartId, + ); + + await shoppingCartService.cancel({ + type: 'CancelShoppingCart', + data: { shoppingCartId, now: new Date() }, + }); + + response.sendStatus(204); + }, + ); + }; // Add Product Item type AddProductItemRequest = Request< diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationLogic.solved.test.ts b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationLogic.solved.test.ts index d10a96d9..9db213be 100644 --- a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationLogic.solved.test.ts +++ b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationLogic.solved.test.ts @@ -6,9 +6,11 @@ import { getEventStore } from '../../tools/eventStore'; import { TestResponse } from '../../tools/testing'; import { getApplication } from '../../tools/api'; import { mapShoppingCartStreamId, shoppingCartApi } from './api'; -import { ShoppingCartEvent } from './shoppingCart'; +import { ShoppingCart, ShoppingCartEvent } from './shoppingCart'; import { Application } from 'express'; import { ShoppingCartErrors } from './businessLogic'; +import { ShoppingCartService } from './applicationService'; +import { EventStoreRepository } from './core/repository'; describe('Application logic', () => { let app: Application; @@ -16,7 +18,17 @@ describe('Application logic', () => { beforeAll(async () => { eventStoreDB = await getEventStoreDBTestClient(); - app = getApplication(shoppingCartApi); + const repository = new EventStoreRepository< + ShoppingCart, + ShoppingCartEvent + >( + getEventStore(eventStoreDB), + ShoppingCart.default, + ShoppingCart.evolve, + mapShoppingCartStreamId, + ); + const service = new ShoppingCartService(repository); + app = getApplication(shoppingCartApi(service)); }); afterAll(() => eventStoreDB.dispose()); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationService.ts b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationService.ts new file mode 100644 index 00000000..859dc062 --- /dev/null +++ b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/applicationService.ts @@ -0,0 +1,48 @@ +import { + AddProductItemToShoppingCart, + CancelShoppingCart, + ConfirmShoppingCart, + OpenShoppingCart, + RemoveProductItemFromShoppingCart, +} from './businessLogic'; +import { Repository } from './core/repository'; +import { ApplicationService } from './core/service'; +import { ShoppingCart, ShoppingCartEvent } from './shoppingCart'; + +export class ShoppingCartService extends ApplicationService< + ShoppingCart, + ShoppingCartEvent +> { + constructor( + protected repository: Repository, + ) { + super(repository); + } + + public open = ({ + data: { shoppingCartId, clientId, now }, + }: OpenShoppingCart) => + this.on(shoppingCartId, () => + ShoppingCart.open(shoppingCartId, clientId, now), + ); + + public addProductItem = ({ + data: { shoppingCartId, productItem }, + }: AddProductItemToShoppingCart) => + this.on(shoppingCartId, (shoppingCart) => + shoppingCart.addProductItem(productItem), + ); + + public removeProductItem = ({ + data: { shoppingCartId, productItem }, + }: RemoveProductItemFromShoppingCart) => + this.on(shoppingCartId, (shoppingCart) => + shoppingCart.removeProductItem(productItem), + ); + + public confirm = ({ data: { shoppingCartId, now } }: ConfirmShoppingCart) => + this.on(shoppingCartId, (shoppingCart) => shoppingCart.confirm(now)); + + public cancel = ({ data: { shoppingCartId, now } }: CancelShoppingCart) => + this.on(shoppingCartId, (shoppingCart) => shoppingCart.cancel(now)); +} diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/repository.ts b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/repository.ts new file mode 100644 index 00000000..d8289f15 --- /dev/null +++ b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/repository.ts @@ -0,0 +1,31 @@ +import { EventStore } from '../../../tools/eventStore'; +import { Event } from '../../../tools/events'; + +export interface Repository { + find(id: string): Promise; + store(id: string, ...events: StreamEvent[]): Promise; +} + +export class EventStoreRepository + implements Repository +{ + constructor( + private eventStore: EventStore, + private getInitialState: () => Entity, + private evolve: (state: Entity, event: StreamEvent) => Entity, + private mapToStreamId: (id: string) => string, + ) {} + + find = async (id: string): Promise => + (await this.eventStore.aggregateStream( + this.mapToStreamId(id), + { + evolve: this.evolve, + getInitialState: this.getInitialState, + }, + )) ?? this.getInitialState(); + + store = async (id: string, ...events: StreamEvent[]): Promise => { + await this.eventStore.appendToStream(this.mapToStreamId(id), ...events); + }; +} diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/service.ts b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/service.ts new file mode 100644 index 00000000..f7cd6eed --- /dev/null +++ b/workshops/introduction_to_event_sourcing/src/solved/06_application_logic_eventstoredb/oop/aggregate_returning_events/core/service.ts @@ -0,0 +1,18 @@ +import { Event } from '../../../tools/events'; +import { Repository } from './repository'; + +export abstract class ApplicationService { + constructor(protected repository: Repository) {} + + protected on = async ( + id: string, + handle: (state: Entity) => StreamEvent | StreamEvent[], + ) => { + const aggregate = await this.repository.find(id); + + const result = handle(aggregate); + + if (Array.isArray(result)) return this.repository.store(id, ...result); + else return this.repository.store(id, result); + }; +} diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/README.md b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/README.md deleted file mode 100644 index 06a1e6c3..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Exercise 06 - Business Logic with EventStoreDB - -Having the following shopping cart process: - -1. The customer may add a product to the shopping cart only after opening it. -2. When selecting and adding a product to the basket customer needs to provide the quantity chosen. The product price is calculated by the system based on the current price list. -3. The customer may remove a product with a given price from the cart. -4. The customer can confirm the shopping cart and start the order fulfilment process. -5. The customer may also cancel the shopping cart and reject all selected products. -6. After shopping cart confirmation or cancellation, the product can no longer be added or removed from the cart. - -Write the code that fulfils this logic. Remember that in Event Sourcing each business operation has to result with a new business fact (so event). Use events and entities defined in previous exercises. - -![events](./assets/events.jpg) - -There are two variations: - -- using mutable entities: [./oop/businessLogic.exercise.test.ts](./oop/businessLogic.exercise.test.ts), -- using fully immutable structures: [./immutable/businessLogic.exercise.test.ts](./immutable/businessLogic.exercise.test.ts), - -Select your preferred approach (or both) to solve this use case. - -## Solution - -1. Classical, mutable aggregates (rich domain model): [oop/solution1/businessLogic.solved.test.ts](./oop/solution1/businessLogic.solved.test.ts). -2. Mixed approach, mutable aggregates (rich domain model), returning events from methods: [oop/solution2/businessLogic.solved.test.ts](./oop/solution2/businessLogic.solved.test.ts). -3. Immutable, with functional command handlers composition and entities as anemic data model: [./immutable/solution1/businessLogic.solved.test.ts](./immutable/solution1/businessLogic.solved.test.ts). -4. Immutable with composition using [the Decider](https://thinkbeforecoding.com/post/2021/12/17/functional-event-sourcing-decider) pattern and entities as anemic data model: [./immutable/solution2/businessLogic.solved.test.ts](./immutable/solution2/businessLogic.solved.test.ts). - -Read also my articles: - -- [Straightforward Event Sourcing with TypeScript and NodeJS](https://event-driven.io/en/type_script_node_Js_event_sourcing/?utm_source=eventsourcing_nodejs?utm_campaign=workshop) -- [How to effectively compose your business logic](https://event-driven.io/en/how_to_effectively_compose_your_business_logic//?utm_source=eventsourcing_nodejs?utm_campaign=workshop) -- [Slim your aggregates with Event Sourcing!](https://event-driven.io/en/slim_your_entities_with_event_sourcing/?utm_source=eventsourcing_nodejs?utm_campaign=workshop) diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/assets/events.jpg b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/assets/events.jpg deleted file mode 100644 index 35522f5804b2be096c7dbbc10ca8bedf69faa212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67506 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ÐTmn>|e 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 diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.solved.test.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.solved.test.ts deleted file mode 100644 index c341f45c..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.solved.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient, StreamNotFoundError } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; -import { - ShoppingCartErrors, - openShoppingCart, - addProductItemToShoppingCart, - removeProductItemFromShoppingCart, - confirmShoppingCart, - cancelShoppingCart, - handleCommand, -} from './businessLogic'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type ShoppingCartOpened = Event< - 'ShoppingCartOpened', - { - shoppingCartId: string; - clientId: string; - openedAt: string; - } ->; - -export type ProductItemAddedToShoppingCart = Event< - 'ProductItemAddedToShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ProductItemRemovedFromShoppingCart = Event< - 'ProductItemRemovedFromShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ShoppingCartConfirmed = Event< - 'ShoppingCartConfirmed', - { - shoppingCartId: string; - confirmedAt: string; - } ->; - -export type ShoppingCartCanceled = Event< - 'ShoppingCartCanceled', - { - shoppingCartId: string; - canceledAt: string; - } ->; - -export type ShoppingCartEvent = - | ShoppingCartOpened - | ProductItemAddedToShoppingCart - | ProductItemRemovedFromShoppingCart - | ShoppingCartConfirmed - | ShoppingCartCanceled; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export const merge = ( - array: T[], - item: T, - where: (current: T) => boolean, - onExisting: (current: T) => T, - onNotFound: () => T | undefined = () => undefined, -) => { - let wasFound = false; - - const result = array - // merge the existing item if matches condition - .map((p: T) => { - if (!where(p)) return p; - - wasFound = true; - return onExisting(p); - }) - // filter out item if undefined was returned - // for cases of removal - .filter((p) => p !== undefined) - // make TypeScript happy - .map((p) => { - if (!p) throw Error('That should not happen'); - - return p; - }); - - // if item was not found and onNotFound action is defined - // try to generate new item - if (!wasFound) { - const result = onNotFound(); - - if (result !== undefined) return [...array, item]; - } - - return result; -}; - -export type ShoppingCart = Readonly<{ - id: string; - clientId: string; - status: ShoppingCartStatus; - productItems: PricedProductItem[]; - openedAt: Date; - confirmedAt?: Date; - canceledAt?: Date; -}>; - -export const evolve = ( - state: ShoppingCart, - { type, data: event }: ShoppingCartEvent, -): ShoppingCart => { - switch (type) { - case 'ShoppingCartOpened': - return { - id: event.shoppingCartId, - clientId: event.clientId, - openedAt: new Date(event.openedAt), - productItems: [], - status: ShoppingCartStatus.Pending, - }; - case 'ProductItemAddedToShoppingCart': { - const { productItems } = state; - const { productItem } = event; - - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity + productItem.quantity, - }; - }, - () => productItem, - ), - }; - } - case 'ProductItemRemovedFromShoppingCart': { - const { productItems } = state; - const { productItem } = event; - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity - productItem.quantity, - }; - }, - ), - }; - } - case 'ShoppingCartConfirmed': - return { - ...state, - status: ShoppingCartStatus.Confirmed, - confirmedAt: new Date(event.confirmedAt), - }; - case 'ShoppingCartCanceled': - return { - ...state, - status: ShoppingCartStatus.Canceled, - canceledAt: new Date(event.canceledAt), - }; - default: { - const _: never = type; - throw new Error(ShoppingCartErrors.UNKNOWN_EVENT_TYPE); - } - } -}; - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - // 1. Add logic here - return events.reduce(evolve, {} as ShoppingCart); -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -const handle = handleCommand( - evolve, - () => ({}) as ShoppingCart, - mapShoppingCartStreamId, -); - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - try { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; - } catch (error) { - if (error instanceof StreamNotFoundError) { - return []; - } - - throw error; - } -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - await handle(eventStore, shoppingCartId, (_) => - openShoppingCart({ clientId, shoppingCartId, now: openedAt }), - ); - - await handle(eventStore, shoppingCartId, (state) => - addProductItemToShoppingCart( - { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - state, - ), - ); - - await handle(eventStore, shoppingCartId, (state) => - addProductItemToShoppingCart( - { - shoppingCartId, - productItem: tShirt, - }, - state, - ), - ); - - await handle(eventStore, shoppingCartId, (state) => - removeProductItemFromShoppingCart( - { - shoppingCartId, - productItem: pairOfShoes, - }, - state, - ), - ); - - await handle(eventStore, shoppingCartId, (state) => - confirmShoppingCart({ shoppingCartId, now: confirmedAt }, state), - ); - - const cancel = () => - handle(eventStore, shoppingCartId, (state) => - cancelShoppingCart({ shoppingCartId, now: canceledAt }, state), - ); - - await expect(cancel).rejects.toThrow( - ShoppingCartErrors.CART_IS_ALREADY_CLOSED, - ); - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toStrictEqual({ - id: shoppingCartId, - clientId, - status: ShoppingCartStatus.Confirmed, - productItems: [pairOfShoes, tShirt], - openedAt, - confirmedAt, - }); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.ts deleted file mode 100644 index 2bd52d9d..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution1/businessLogic.ts +++ /dev/null @@ -1,210 +0,0 @@ -////////////////////////////////////// -/// Commands -////////////////////////////////////// - -import { - EventStoreDBClient, - jsonEvent, - StreamNotFoundError, -} from '@eventstore/db-client'; -import { - PricedProductItem, - ProductItemAddedToShoppingCart, - ProductItemRemovedFromShoppingCart, - ShoppingCart, - ShoppingCartCanceled, - ShoppingCartConfirmed, - ShoppingCartOpened, - ShoppingCartStatus, -} from './businessLogic.solved.test'; - -export type OpenShoppingCart = { - shoppingCartId: string; - clientId: string; - now: Date; -}; - -export type AddProductItemToShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type RemoveProductItemFromShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type ConfirmShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type CancelShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type ShoppingCartCommand = - | OpenShoppingCart - | AddProductItemToShoppingCart - | RemoveProductItemFromShoppingCart - | ConfirmShoppingCart - | CancelShoppingCart; - -////////////////////////////////////// -/// Decide -////////////////////////////////////// - -export const enum ShoppingCartErrors { - CART_ALREADY_EXISTS = 'CART_ALREADY_EXISTS', - CART_IS_ALREADY_CLOSED = 'CART_IS_ALREADY_CLOSED', - PRODUCT_ITEM_NOT_FOUND = 'PRODUCT_ITEM_NOT_FOUND', - CART_IS_EMPTY = 'CART_IS_EMPTY', - UNKNOWN_EVENT_TYPE = 'UNKNOWN_EVENT_TYPE', - UNKNOWN_COMMAND_TYPE = 'UNKNOWN_COMMAND_TYPE', -} - -export const assertProductItemExists = ( - productItems: PricedProductItem[], - { productId, quantity, unitPrice }: PricedProductItem, -): void => { - const currentQuantity = - productItems.find( - (p) => p.productId === productId && p.unitPrice == unitPrice, - )?.quantity ?? 0; - - if (currentQuantity < quantity) { - throw new Error(ShoppingCartErrors.PRODUCT_ITEM_NOT_FOUND); - } -}; - -export const openShoppingCart = ( - command: OpenShoppingCart, -): ShoppingCartOpened => { - return { - type: 'ShoppingCartOpened', - data: { - shoppingCartId: command.shoppingCartId, - clientId: command.clientId, - openedAt: command.now.toISOString(), - }, - }; -}; - -export const addProductItemToShoppingCart = ( - command: AddProductItemToShoppingCart, - shoppingCart: ShoppingCart, -): ProductItemAddedToShoppingCart => { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - return { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId: command.shoppingCartId, - productItem: command.productItem, - }, - }; -}; - -export const removeProductItemFromShoppingCart = ( - command: RemoveProductItemFromShoppingCart, - shoppingCart: ShoppingCart, -): ProductItemRemovedFromShoppingCart => { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - assertProductItemExists(shoppingCart.productItems, command.productItem); - - return { - type: 'ProductItemRemovedFromShoppingCart', - data: { - shoppingCartId: command.shoppingCartId, - productItem: command.productItem, - }, - }; -}; - -export const confirmShoppingCart = ( - command: ConfirmShoppingCart, - shoppingCart: ShoppingCart, -): ShoppingCartConfirmed => { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - if (shoppingCart.productItems.length === 0) { - throw new Error(ShoppingCartErrors.CART_IS_EMPTY); - } - - return { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId: command.shoppingCartId, - confirmedAt: command.now.toISOString(), - }, - }; -}; - -export const cancelShoppingCart = ( - command: CancelShoppingCart, - shoppingCart: ShoppingCart, -): ShoppingCartCanceled => { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - return { - type: 'ShoppingCartCanceled', - data: { - shoppingCartId: command.shoppingCartId, - canceledAt: command.now.toISOString(), - }, - }; -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export const handleCommand = - ( - evolve: (state: State, event: StreamEvent) => State, - getInitialState: () => State, - mapToStreamId: (id: string) => string, - ) => - async ( - eventStore: EventStoreDBClient, - id: string, - handle: (state: State) => StreamEvent | StreamEvent[], - ) => { - const streamId = mapToStreamId(id); - let state = getInitialState(); - try { - const readResult = eventStore.readStream(streamId); - - for await (const { event } of readResult) { - if (!event) continue; - - state = evolve(state, { - type: event.type, - data: event.data, - }); - } - } catch (error) { - if (!(error instanceof StreamNotFoundError)) { - throw error; - } - } - - const result = handle(state); - - const eventsToAppend = Array.isArray(result) ? result : [result]; - - return eventStore.appendToStream(streamId, eventsToAppend.map(jsonEvent)); - }; diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.solved.test.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.solved.test.ts deleted file mode 100644 index 08e3827b..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.solved.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient, StreamNotFoundError } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; -import { - CommandHandler, - decide, - Decider, - ShoppingCartCommand, - ShoppingCartErrors, -} from './businessLogic'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type ShoppingCartEvent = - | { - type: 'ShoppingCartOpened'; - data: { - shoppingCartId: string; - clientId: string; - openedAt: string; - }; - } - | { - type: 'ProductItemAddedToShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'ProductItemRemovedFromShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'ShoppingCartConfirmed'; - data: { - shoppingCartId: string; - confirmedAt: string; - }; - } - | { - type: 'ShoppingCartCanceled'; - data: { - shoppingCartId: string; - canceledAt: string; - }; - }; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export const merge = ( - array: T[], - item: T, - where: (current: T) => boolean, - onExisting: (current: T) => T, - onNotFound: () => T | undefined = () => undefined, -) => { - let wasFound = false; - - const result = array - // merge the existing item if matches condition - .map((p: T) => { - if (!where(p)) return p; - - wasFound = true; - return onExisting(p); - }) - // filter out item if undefined was returned - // for cases of removal - .filter((p) => p !== undefined) - // make TypeScript happy - .map((p) => { - if (!p) throw Error('That should not happen'); - - return p; - }); - - // if item was not found and onNotFound action is defined - // try to generate new item - if (!wasFound) { - const result = onNotFound(); - - if (result !== undefined) return [...array, item]; - } - - return result; -}; - -export type ShoppingCart = Readonly<{ - id: string; - clientId: string; - status: ShoppingCartStatus; - productItems: PricedProductItem[]; - openedAt: Date; - confirmedAt?: Date; - canceledAt?: Date; -}>; - -export const evolve = ( - state: ShoppingCart, - { type, data: event }: ShoppingCartEvent, -): ShoppingCart => { - switch (type) { - case 'ShoppingCartOpened': - return { - id: event.shoppingCartId, - clientId: event.clientId, - openedAt: new Date(event.openedAt), - productItems: [], - status: ShoppingCartStatus.Pending, - }; - case 'ProductItemAddedToShoppingCart': { - const { productItems } = state; - const { productItem } = event; - - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity + productItem.quantity, - }; - }, - () => productItem, - ), - }; - } - case 'ProductItemRemovedFromShoppingCart': { - const { productItems } = state; - const { productItem } = event; - return { - ...state, - productItems: merge( - productItems, - productItem, - (p) => - p.productId === productItem.productId && - p.unitPrice === productItem.unitPrice, - (p) => { - return { - ...p, - quantity: p.quantity - productItem.quantity, - }; - }, - ), - }; - } - case 'ShoppingCartConfirmed': - return { - ...state, - status: ShoppingCartStatus.Confirmed, - confirmedAt: new Date(event.confirmedAt), - }; - case 'ShoppingCartCanceled': - return { - ...state, - status: ShoppingCartStatus.Canceled, - canceledAt: new Date(event.canceledAt), - }; - default: { - const _: never = type; - throw new Error(ShoppingCartErrors.UNKNOWN_EVENT_TYPE); - } - } -}; - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - // 1. Add logic here - return events.reduce(evolve, {} as ShoppingCart); -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -const decider: Decider = { - decide, - evolve, - getInitialState: () => ({}) as ShoppingCart, -}; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -export const handle = CommandHandler(decider, mapShoppingCartStreamId); - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - try { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; - } catch (error) { - if (error instanceof StreamNotFoundError) { - return []; - } - - throw error; - } -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - await handle(eventStore, shoppingCartId, { - type: 'OpenShoppingCart', - data: { clientId, shoppingCartId, now: openedAt }, - }); - - await handle(eventStore, shoppingCartId, { - type: 'AddProductItemToShoppingCart', - data: { shoppingCartId, productItem: twoPairsOfShoes }, - }); - - await handle(eventStore, shoppingCartId, { - type: 'AddProductItemToShoppingCart', - data: { shoppingCartId, productItem: tShirt }, - }); - - await handle(eventStore, shoppingCartId, { - type: 'RemoveProductItemFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }); - - await handle(eventStore, shoppingCartId, { - type: 'ConfirmShoppingCart', - data: { shoppingCartId, now: confirmedAt }, - }); - - const cancel = () => - handle(eventStore, shoppingCartId, { - type: 'CancelShoppingCart', - data: { shoppingCartId, now: canceledAt }, - }); - - await expect(cancel).rejects.toThrow( - ShoppingCartErrors.CART_IS_ALREADY_CLOSED, - ); - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toStrictEqual({ - id: shoppingCartId, - clientId, - status: ShoppingCartStatus.Confirmed, - productItems: [pairOfShoes, tShirt], - openedAt, - confirmedAt, - }); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.ts deleted file mode 100644 index 92501085..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/immutable/solution2/businessLogic.ts +++ /dev/null @@ -1,227 +0,0 @@ -////////////////////////////////////// -/// Commands -////////////////////////////////////// - -import { - EventStoreDBClient, - jsonEvent, - StreamNotFoundError, -} from '@eventstore/db-client'; -import { - PricedProductItem, - ShoppingCart, - ShoppingCartEvent, - ShoppingCartStatus, -} from './businessLogic.solved.test'; - -export type ShoppingCartCommand = - | { - type: 'OpenShoppingCart'; - data: { - shoppingCartId: string; - clientId: string; - now: Date; - }; - } - | { - type: 'AddProductItemToShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'RemoveProductItemFromShoppingCart'; - data: { - shoppingCartId: string; - productItem: PricedProductItem; - }; - } - | { - type: 'ConfirmShoppingCart'; - data: { - shoppingCartId: string; - now: Date; - }; - } - | { - type: 'CancelShoppingCart'; - data: { - shoppingCartId: string; - now: Date; - }; - }; - -////////////////////////////////////// -/// Decide -////////////////////////////////////// - -export const enum ShoppingCartErrors { - CART_ALREADY_EXISTS = 'CART_ALREADY_EXISTS', - CART_IS_ALREADY_CLOSED = 'CART_IS_ALREADY_CLOSED', - PRODUCT_ITEM_NOT_FOUND = 'PRODUCT_ITEM_NOT_FOUND', - CART_IS_EMPTY = 'CART_IS_EMPTY', - UNKNOWN_EVENT_TYPE = 'UNKNOWN_EVENT_TYPE', - UNKNOWN_COMMAND_TYPE = 'UNKNOWN_COMMAND_TYPE', -} - -export const assertProductItemExists = ( - productItems: PricedProductItem[], - { productId, quantity, unitPrice }: PricedProductItem, -): void => { - const currentQuantity = - productItems.find( - (p) => p.productId === productId && p.unitPrice == unitPrice, - )?.quantity ?? 0; - - if (currentQuantity < quantity) { - throw new Error(ShoppingCartErrors.PRODUCT_ITEM_NOT_FOUND); - } -}; - -export const decide = ( - { type, data: command }: ShoppingCartCommand, - shoppingCart: ShoppingCart, -): ShoppingCartEvent | ShoppingCartEvent[] => { - switch (type) { - case 'OpenShoppingCart': { - return { - type: 'ShoppingCartOpened', - data: { - shoppingCartId: command.shoppingCartId, - clientId: command.clientId, - openedAt: command.now.toISOString(), - }, - }; - } - - case 'AddProductItemToShoppingCart': { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - return { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId: command.shoppingCartId, - productItem: command.productItem, - }, - }; - } - - case 'RemoveProductItemFromShoppingCart': { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - assertProductItemExists(shoppingCart.productItems, command.productItem); - - return { - type: 'ProductItemRemovedFromShoppingCart', - data: { - shoppingCartId: command.shoppingCartId, - productItem: command.productItem, - }, - }; - } - - case 'ConfirmShoppingCart': { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - if (shoppingCart.productItems.length === 0) { - throw new Error(ShoppingCartErrors.CART_IS_EMPTY); - } - - return { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId: command.shoppingCartId, - confirmedAt: command.now.toISOString(), - }, - }; - } - - case 'CancelShoppingCart': { - if (shoppingCart.status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - - return { - type: 'ShoppingCartCanceled', - data: { - shoppingCartId: command.shoppingCartId, - canceledAt: command.now.toISOString(), - }, - }; - } - default: { - const _: never = command; - throw new Error(ShoppingCartErrors.UNKNOWN_COMMAND_TYPE); - } - } -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export type Command< - CommandType extends string = string, - CommandData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export type Decider< - State, - CommandType extends Command, - StreamEvent extends Event, -> = { - decide: (command: CommandType, state: State) => StreamEvent | StreamEvent[]; - evolve: (currentState: State, event: StreamEvent) => State; - getInitialState: () => State; -}; - -export const CommandHandler = - ( - { - decide, - evolve, - getInitialState, - }: Decider, - mapToStreamId: (id: string) => string, - ) => - async (eventStore: EventStoreDBClient, id: string, command: CommandType) => { - const streamId = mapToStreamId(id); - - let state = getInitialState(); - - try { - const readResult = eventStore.readStream(streamId); - - for await (const { event } of readResult) { - if (!event) continue; - - state = evolve(state, { - type: event.type, - data: event.data, - }); - } - } catch (error) { - if (!(error instanceof StreamNotFoundError)) { - throw error; - } - } - - const result = decide(command, state); - - const eventsToAppend = Array.isArray(result) ? result : [result]; - - return eventStore.appendToStream(streamId, eventsToAppend.map(jsonEvent)); - }; diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.solved.test.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.solved.test.ts deleted file mode 100644 index 45324a56..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.solved.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient, StreamNotFoundError } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; -import { - EventStoreRepository, - ShoppingCart, - ShoppingCartErrors, - ShoppingCartService, -} from './businessLogic'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export type ShoppingCartOpened = Event< - 'ShoppingCartOpened', - { - shoppingCartId: string; - clientId: string; - openedAt: string; - } ->; - -export type ProductItemAddedToShoppingCart = Event< - 'ProductItemAddedToShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ProductItemRemovedFromShoppingCart = Event< - 'ProductItemRemovedFromShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ShoppingCartConfirmed = Event< - 'ShoppingCartConfirmed', - { - shoppingCartId: string; - confirmedAt: string; - } ->; - -export type ShoppingCartCanceled = Event< - 'ShoppingCartCanceled', - { - shoppingCartId: string; - canceledAt: string; - } ->; - -export type ShoppingCartEvent = - | ShoppingCartOpened - | ProductItemAddedToShoppingCart - | ProductItemRemovedFromShoppingCart - | ShoppingCartConfirmed - | ShoppingCartCanceled; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - return events.reduce((state, event) => { - state.evolve(event); - return state; - }, ShoppingCart.default()); -}; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - try { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; - } catch (error) { - if (error instanceof StreamNotFoundError) { - return []; - } - - throw error; - } -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - const repository = new EventStoreRepository< - ShoppingCart, - ShoppingCartEvent - >(eventStore, ShoppingCart.default, mapShoppingCartStreamId); - - const shoppingCartService = new ShoppingCartService(repository); - - await shoppingCartService.open({ shoppingCartId, clientId, now: openedAt }); - await shoppingCartService.addProductItem({ - shoppingCartId, - productItem: twoPairsOfShoes, - }); - await shoppingCartService.addProductItem({ - shoppingCartId, - productItem: tShirt, - }); - await shoppingCartService.removeProductItem({ - shoppingCartId, - productItem: pairOfShoes, - }); - await shoppingCartService.confirm({ shoppingCartId, now: confirmedAt }); - - const cancel = () => - shoppingCartService.cancel({ shoppingCartId, now: canceledAt }); - - await expect(cancel).rejects.toThrow( - ShoppingCartErrors.CART_IS_ALREADY_CLOSED, - ); - - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toBeInstanceOf(ShoppingCart); - - const actual = { - shoppingCartId: shoppingCart.id, - clientId: shoppingCart.clientId, - status: shoppingCart.status, - openedAt: shoppingCart.openedAt, - productItems: shoppingCart.productItems, - confirmedAt: shoppingCart.confirmedAt, - }; - - expect(actual).toEqual({ - shoppingCartId, - clientId, - status: ShoppingCartStatus.Confirmed, - openedAt, - productItems: [pairOfShoes, tShirt], - confirmedAt, - }); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.ts deleted file mode 100644 index 761d2e9e..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution1/businessLogic.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { - EventStoreDBClient, - jsonEvent, - StreamNotFoundError, -} from '@eventstore/db-client'; -import { - PricedProductItem, - ShoppingCartEvent, - ShoppingCartStatus, -} from './businessLogic.solved.test'; -import { Event } from './businessLogic.solved.test'; - -export abstract class Aggregate { - #uncommitedEvents: E[] = []; - - abstract evolve(event: E): void; - - protected enqueue = (event: E) => { - this.#uncommitedEvents = [...this.#uncommitedEvents, event]; - this.evolve(event); - }; - - dequeueUncommitedEvents = (): Event[] => { - const events = this.#uncommitedEvents; - - this.#uncommitedEvents = []; - - return events; - }; -} - -export interface Repository { - find(id: string): Promise; - store(id: string, entity: Entity): Promise; -} - -export class EventStoreRepository< - Entity extends Aggregate, - StreamEvent extends Event, -> implements Repository -{ - constructor( - private eventStore: EventStoreDBClient, - private getInitialState: () => Entity, - private mapToStreamId: (id: string) => string, - ) {} - - find = async (id: string): Promise => { - const state = this.getInitialState(); - - try { - const readResult = this.eventStore.readStream( - this.mapToStreamId(id), - ); - - for await (const { event } of readResult) { - if (!event) continue; - - state.evolve({ - type: event.type, - data: event.data, - }); - } - } catch (error) { - if (!(error instanceof StreamNotFoundError)) { - throw error; - } - } - - return state; - }; - - store = async (id: string, entity: Entity): Promise => { - const events = entity.dequeueUncommitedEvents(); - - if (events.length === 0) return; - - await this.eventStore.appendToStream( - this.mapToStreamId(id), - events.map(jsonEvent), - ); - }; -} - -export abstract class ApplicationService { - constructor(protected repository: Repository) {} - - protected on = async (id: string, handle: (state: Entity) => void) => { - const aggregate = await this.repository.find(id); - - handle(aggregate); - - await this.repository.store(id, aggregate); - }; -} - -export class ShoppingCart extends Aggregate { - private constructor( - private _id: string, - private _clientId: string, - private _status: ShoppingCartStatus, - private _openedAt: Date, - private _productItems: PricedProductItem[] = [], - private _confirmedAt?: Date, - private _canceledAt?: Date, - ) { - super(); - } - - get id() { - return this._id; - } - - get clientId() { - return this._clientId; - } - - get status() { - return this._status; - } - - get openedAt() { - return this._openedAt; - } - - get productItems() { - return this._productItems; - } - - get confirmedAt() { - return this._confirmedAt; - } - - get canceledAt() { - return this._canceledAt; - } - - public static default = () => - new ShoppingCart( - undefined!, - undefined!, - undefined!, - undefined!, - undefined, - undefined, - undefined, - ); - - public open = (shoppingCartId: string, clientId: string, now: Date) => { - this.enqueue({ - type: 'ShoppingCartOpened', - data: { shoppingCartId, clientId, openedAt: now.toISOString() }, - }); - }; - - public addProductItem = (productItem: PricedProductItem): void => { - this.assertIsPending(); - - this.enqueue({ - type: 'ProductItemAddedToShoppingCart', - data: { productItem, shoppingCartId: this._id }, - }); - }; - - public removeProductItem = (productItem: PricedProductItem): void => { - this.assertIsPending(); - this.assertProductItemExists(productItem); - - this.enqueue({ - type: 'ProductItemRemovedFromShoppingCart', - data: { productItem, shoppingCartId: this._id }, - }); - }; - - public confirm = (now: Date): void => { - this.assertIsPending(); - this.assertIsNotEmpty(); - - this.enqueue({ - type: 'ShoppingCartConfirmed', - data: { shoppingCartId: this._id, confirmedAt: now.toISOString() }, - }); - }; - - public cancel = (now: Date): void => { - this.assertIsPending(); - - this.enqueue({ - type: 'ShoppingCartCanceled', - data: { shoppingCartId: this._id, canceledAt: now.toISOString() }, - }); - }; - - public evolve = ({ type, data: event }: ShoppingCartEvent): void => { - switch (type) { - case 'ShoppingCartOpened': { - this._id = event.shoppingCartId; - this._clientId = event.clientId; - this._status = ShoppingCartStatus.Pending; - this._openedAt = new Date(event.openedAt); - this._productItems = []; - return; - } - case 'ProductItemAddedToShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = this._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (currentProductItem) { - currentProductItem.quantity += quantity; - } else { - this._productItems.push({ ...event.productItem }); - } - return; - } - case 'ProductItemRemovedFromShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = this._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (!currentProductItem) { - return; - } - - currentProductItem.quantity -= quantity; - - if (currentProductItem.quantity <= 0) { - this._productItems.splice( - this._productItems.indexOf(currentProductItem), - 1, - ); - } - return; - } - case 'ShoppingCartConfirmed': { - this._status = ShoppingCartStatus.Confirmed; - this._confirmedAt = new Date(event.confirmedAt); - return; - } - case 'ShoppingCartCanceled': { - this._status = ShoppingCartStatus.Canceled; - this._canceledAt = new Date(event.canceledAt); - return; - } - default: { - const _: never = type; - throw new Error(ShoppingCartErrors.UNKNOWN_EVENT_TYPE); - } - } - }; - - private assertIsPending = (): void => { - if (this._status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - }; - - private assertProductItemExists = ({ - productId, - quantity, - unitPrice, - }: PricedProductItem): void => { - const currentQuantity = - this.productItems.find( - (p) => p.productId === productId && p.unitPrice == unitPrice, - )?.quantity ?? 0; - - if (currentQuantity < quantity) { - throw new Error(ShoppingCartErrors.PRODUCT_ITEM_NOT_FOUND); - } - }; - - private assertIsNotEmpty = (): void => { - if (this._productItems.length === 0) { - throw new Error(ShoppingCartErrors.CART_IS_EMPTY); - } - }; -} - -export const enum ShoppingCartErrors { - CART_IS_ALREADY_CLOSED = 'CART_IS_ALREADY_CLOSED', - PRODUCT_ITEM_NOT_FOUND = 'PRODUCT_ITEM_NOT_FOUND', - CART_IS_EMPTY = 'CART_IS_EMPTY', - UNKNOWN_EVENT_TYPE = 'UNKNOWN_EVENT_TYPE', - UNKNOWN_COMMAND_TYPE = 'UNKNOWN_COMMAND_TYPE', -} - -export type OpenShoppingCart = { - shoppingCartId: string; - clientId: string; - now: Date; -}; - -export type AddProductItemToShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type RemoveProductItemFromShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type ConfirmShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type CancelShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type ShoppingCartCommand = - | OpenShoppingCart - | AddProductItemToShoppingCart - | RemoveProductItemFromShoppingCart - | ConfirmShoppingCart - | CancelShoppingCart; - -export class ShoppingCartService extends ApplicationService { - constructor(protected repository: Repository) { - super(repository); - } - - public open = ({ shoppingCartId, clientId, now }: OpenShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.open(shoppingCartId, clientId, now), - ); - - public addProductItem = ({ - shoppingCartId, - productItem, - }: AddProductItemToShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.addProductItem(productItem), - ); - - public removeProductItem = ({ - shoppingCartId, - productItem, - }: RemoveProductItemFromShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.removeProductItem(productItem), - ); - - public confirm = ({ shoppingCartId, now }: ConfirmShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => shoppingCart.confirm(now)); - - public cancel = ({ shoppingCartId, now }: CancelShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => shoppingCart.cancel(now)); -} diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.solved.test.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.solved.test.ts deleted file mode 100644 index ae41d4cc..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.solved.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { getEventStoreDBTestClient } from '#core/testing/eventStoreDB'; -import { EventStoreDBClient, StreamNotFoundError } from '@eventstore/db-client'; -import { v4 as uuid } from 'uuid'; -import { - EventStoreRepository, - ShoppingCart, - ShoppingCartErrors, - ShoppingCartService, -} from './businessLogic'; - -export interface ProductItem { - productId: string; - quantity: number; -} - -export type PricedProductItem = ProductItem & { - unitPrice: number; -}; - -export type Event< - EventType extends string = string, - EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; - -export type ShoppingCartOpened = Event< - 'ShoppingCartOpened', - { - shoppingCartId: string; - clientId: string; - openedAt: string; - } ->; - -export type ProductItemAddedToShoppingCart = Event< - 'ProductItemAddedToShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ProductItemRemovedFromShoppingCart = Event< - 'ProductItemRemovedFromShoppingCart', - { - shoppingCartId: string; - productItem: PricedProductItem; - } ->; - -export type ShoppingCartConfirmed = Event< - 'ShoppingCartConfirmed', - { - shoppingCartId: string; - confirmedAt: string; - } ->; - -export type ShoppingCartCanceled = Event< - 'ShoppingCartCanceled', - { - shoppingCartId: string; - canceledAt: string; - } ->; - -export type ShoppingCartEvent = - | ShoppingCartOpened - | ProductItemAddedToShoppingCart - | ProductItemRemovedFromShoppingCart - | ShoppingCartConfirmed - | ShoppingCartCanceled; - -export enum ShoppingCartStatus { - Pending = 'Pending', - Confirmed = 'Confirmed', - Canceled = 'Canceled', -} - -export const getShoppingCart = (events: ShoppingCartEvent[]): ShoppingCart => { - return events.reduce( - ShoppingCart.evolve, - ShoppingCart.default(), - ); -}; - -export const mapShoppingCartStreamId = (id: string) => `shopping_cart-${id}`; - -export const readStream = async ( - eventStore: EventStoreDBClient, - shoppingCartId: string, -) => { - try { - const readResult = eventStore.readStream( - mapShoppingCartStreamId(shoppingCartId), - ); - - const events: ShoppingCartEvent[] = []; - - for await (const { event } of readResult) { - if (!event) continue; - events.push({ type: event.type, data: event.data }); - } - - return events; - } catch (error) { - if (error instanceof StreamNotFoundError) { - return []; - } - - throw error; - } -}; - -describe('Getting state from events', () => { - let eventStore: EventStoreDBClient; - - beforeAll(async () => { - eventStore = await getEventStoreDBTestClient(); - }); - - it('Should return the state from the sequence of events', async () => { - const shoppingCartId = uuid(); - - const clientId = uuid(); - const openedAt = new Date(); - const confirmedAt = new Date(); - const canceledAt = new Date(); - - const shoesId = uuid(); - - const twoPairsOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 2, - unitPrice: 100, - }; - const pairOfShoes: PricedProductItem = { - productId: shoesId, - quantity: 1, - unitPrice: 100, - }; - - const tShirtId = uuid(); - const tShirt: PricedProductItem = { - productId: tShirtId, - quantity: 1, - unitPrice: 5, - }; - - const repository = new EventStoreRepository< - ShoppingCart, - ShoppingCartEvent - >( - eventStore, - ShoppingCart.default, - ShoppingCart.evolve, - mapShoppingCartStreamId, - ); - - const shoppingCartService = new ShoppingCartService(repository); - - await shoppingCartService.open({ shoppingCartId, clientId, now: openedAt }); - await shoppingCartService.addProductItem({ - shoppingCartId, - productItem: twoPairsOfShoes, - }); - await shoppingCartService.addProductItem({ - shoppingCartId, - productItem: tShirt, - }); - await shoppingCartService.removeProductItem({ - shoppingCartId, - productItem: pairOfShoes, - }); - await shoppingCartService.confirm({ shoppingCartId, now: confirmedAt }); - - const cancel = () => - shoppingCartService.cancel({ shoppingCartId, now: canceledAt }); - - await expect(cancel).rejects.toThrow( - ShoppingCartErrors.CART_IS_ALREADY_CLOSED, - ); - const events = await readStream(eventStore, shoppingCartId); - - expect(events).toEqual([ - { - type: 'ShoppingCartOpened', - data: { - shoppingCartId, - clientId, - openedAt: openedAt.toISOString(), - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: twoPairsOfShoes, - }, - }, - { - type: 'ProductItemAddedToShoppingCart', - data: { - shoppingCartId, - productItem: tShirt, - }, - }, - { - type: 'ProductItemRemovedFromShoppingCart', - data: { shoppingCartId, productItem: pairOfShoes }, - }, - { - type: 'ShoppingCartConfirmed', - data: { - shoppingCartId, - confirmedAt: confirmedAt.toISOString(), - }, - }, - // This should fail - // { - // type: 'ShoppingCartCanceled', - // data: { - // shoppingCartId, - // canceledAt: canceledAt.toISOString(), - // }, - // }, - ]); - - const shoppingCart = getShoppingCart(events); - - expect(shoppingCart).toBeInstanceOf(ShoppingCart); - const actual = { - shoppingCartId: shoppingCart.id, - clientId: shoppingCart.clientId, - status: shoppingCart.status, - openedAt: shoppingCart.openedAt, - productItems: shoppingCart.productItems, - confirmedAt: shoppingCart.confirmedAt, - }; - - expect(actual).toEqual({ - shoppingCartId, - clientId, - status: ShoppingCartStatus.Confirmed, - openedAt, - productItems: [pairOfShoes, tShirt], - confirmedAt, - }); - }); -}); diff --git a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.ts b/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.ts deleted file mode 100644 index b1695ac5..00000000 --- a/workshops/introduction_to_event_sourcing/src/solved/06_business_logic_eventstoredb/oop/solution2/businessLogic.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { - EventStoreDBClient, - jsonEvent, - StreamNotFoundError, -} from '@eventstore/db-client'; -import { - Event, - PricedProductItem, - ProductItemAddedToShoppingCart, - ProductItemRemovedFromShoppingCart, - ShoppingCartCanceled, - ShoppingCartConfirmed, - ShoppingCartEvent, - ShoppingCartOpened, - ShoppingCartStatus, -} from './businessLogic.solved.test'; - -export interface Repository { - find(id: string): Promise; - store(id: string, ...events: StreamEvent[]): Promise; -} - -export class EventStoreRepository - implements Repository -{ - constructor( - private eventStore: EventStoreDBClient, - private getInitialState: () => Entity, - private evolve: (state: Entity, event: StreamEvent) => Entity, - private mapToStreamId: (id: string) => string, - ) {} - - find = async (id: string): Promise => { - const state = this.getInitialState(); - try { - const readResult = this.eventStore.readStream( - this.mapToStreamId(id), - ); - - for await (const { event } of readResult) { - if (!event) continue; - - this.evolve(state, { - type: event.type, - data: event.data, - }); - } - } catch (error) { - if (!(error instanceof StreamNotFoundError)) { - throw error; - } - } - - return state; - }; - - store = async (id: string, ...events: StreamEvent[]): Promise => { - if (events.length === 0) return; - - await this.eventStore.appendToStream( - this.mapToStreamId(id), - events.map(jsonEvent), - ); - }; -} - -export abstract class ApplicationService { - constructor(protected repository: Repository) {} - - protected on = async ( - id: string, - handle: (state: Entity) => StreamEvent | StreamEvent[], - ) => { - const aggregate = await this.repository.find(id); - - const result = handle(aggregate); - - return this.repository.store( - id, - ...(Array.isArray(result) ? result : [result]), - ); - }; -} - -export class ShoppingCart { - private constructor( - private _id: string, - private _clientId: string, - private _status: ShoppingCartStatus, - private _openedAt: Date, - private _productItems: PricedProductItem[] = [], - private _confirmedAt?: Date, - private _canceledAt?: Date, - ) {} - - get id() { - return this._id; - } - - get clientId() { - return this._clientId; - } - - get status() { - return this._status; - } - - get openedAt() { - return this._openedAt; - } - - get productItems() { - return this._productItems; - } - - get confirmedAt() { - return this._confirmedAt; - } - - get canceledAt() { - return this._canceledAt; - } - - public static default = () => - new ShoppingCart( - undefined!, - undefined!, - undefined!, - undefined!, - undefined, - undefined, - undefined, - ); - - public open = ( - shoppingCartId: string, - clientId: string, - now: Date, - ): ShoppingCartOpened => { - return { - type: 'ShoppingCartOpened', - data: { shoppingCartId, clientId, openedAt: now.toISOString() }, - }; - }; - - public addProductItem = ( - productItem: PricedProductItem, - ): ProductItemAddedToShoppingCart => { - this.assertIsPending(); - - return { - type: 'ProductItemAddedToShoppingCart', - data: { productItem, shoppingCartId: this._id }, - }; - }; - - public removeProductItem = ( - productItem: PricedProductItem, - ): ProductItemRemovedFromShoppingCart => { - this.assertIsPending(); - this.assertProductItemExists(productItem); - - return { - type: 'ProductItemRemovedFromShoppingCart', - data: { productItem, shoppingCartId: this._id }, - }; - }; - - public confirm = (now: Date): ShoppingCartConfirmed => { - this.assertIsPending(); - this.assertIsNotEmpty(); - - return { - type: 'ShoppingCartConfirmed', - data: { shoppingCartId: this._id, confirmedAt: now.toISOString() }, - }; - }; - - public cancel = (now: Date): ShoppingCartCanceled => { - this.assertIsPending(); - - return { - type: 'ShoppingCartCanceled', - data: { shoppingCartId: this._id, canceledAt: now.toISOString() }, - }; - }; - - public static evolve = ( - state: ShoppingCart, - { type, data: event }: ShoppingCartEvent, - ): ShoppingCart => { - switch (type) { - case 'ShoppingCartOpened': { - state._id = event.shoppingCartId; - state._clientId = event.clientId; - state._status = ShoppingCartStatus.Pending; - state._openedAt = new Date(event.openedAt); - state._productItems = []; - return state; - } - case 'ProductItemAddedToShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = state._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (currentProductItem) { - currentProductItem.quantity += quantity; - } else { - state._productItems.push({ ...event.productItem }); - } - return state; - } - case 'ProductItemRemovedFromShoppingCart': { - const { - productItem: { productId, quantity, unitPrice }, - } = event; - - const currentProductItem = state._productItems.find( - (pi) => pi.productId === productId && pi.unitPrice === unitPrice, - ); - - if (!currentProductItem) { - return state; - } - - currentProductItem.quantity -= quantity; - - if (currentProductItem.quantity <= 0) { - state._productItems.splice( - state._productItems.indexOf(currentProductItem), - 1, - ); - } - return state; - } - case 'ShoppingCartConfirmed': { - state._status = ShoppingCartStatus.Confirmed; - state._confirmedAt = new Date(event.confirmedAt); - return state; - } - case 'ShoppingCartCanceled': { - state._status = ShoppingCartStatus.Canceled; - state._canceledAt = new Date(event.canceledAt); - return state; - } - default: { - const _: never = type; - throw new Error(ShoppingCartErrors.UNKNOWN_EVENT_TYPE); - } - } - }; - - private assertIsPending = (): void => { - if (this._status !== ShoppingCartStatus.Pending) { - throw new Error(ShoppingCartErrors.CART_IS_ALREADY_CLOSED); - } - }; - - private assertProductItemExists = ({ - productId, - quantity, - unitPrice, - }: PricedProductItem): void => { - const currentQuantity = - this.productItems.find( - (p) => p.productId === productId && p.unitPrice == unitPrice, - )?.quantity ?? 0; - - if (currentQuantity < quantity) { - throw new Error(ShoppingCartErrors.PRODUCT_ITEM_NOT_FOUND); - } - }; - - private assertIsNotEmpty = (): void => { - if (this._productItems.length === 0) { - throw new Error(ShoppingCartErrors.CART_IS_EMPTY); - } - }; -} - -export const enum ShoppingCartErrors { - CART_IS_ALREADY_CLOSED = 'CART_IS_ALREADY_CLOSED', - PRODUCT_ITEM_NOT_FOUND = 'PRODUCT_ITEM_NOT_FOUND', - CART_IS_EMPTY = 'CART_IS_EMPTY', - UNKNOWN_EVENT_TYPE = 'UNKNOWN_EVENT_TYPE', - UNKNOWN_COMMAND_TYPE = 'UNKNOWN_COMMAND_TYPE', -} - -export type OpenShoppingCart = { - shoppingCartId: string; - clientId: string; - now: Date; -}; - -export type AddProductItemToShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type RemoveProductItemFromShoppingCart = { - shoppingCartId: string; - productItem: PricedProductItem; -}; - -export type ConfirmShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type CancelShoppingCart = { - shoppingCartId: string; - now: Date; -}; - -export type ShoppingCartCommand = - | OpenShoppingCart - | AddProductItemToShoppingCart - | RemoveProductItemFromShoppingCart - | ConfirmShoppingCart - | CancelShoppingCart; - -export class ShoppingCartService extends ApplicationService< - ShoppingCart, - ShoppingCartEvent -> { - constructor( - protected repository: Repository, - ) { - super(repository); - } - - public open = ({ shoppingCartId, clientId, now }: OpenShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.open(shoppingCartId, clientId, now), - ); - - public addProductItem = ({ - shoppingCartId, - productItem, - }: AddProductItemToShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.addProductItem(productItem), - ); - - public removeProductItem = ({ - shoppingCartId, - productItem, - }: RemoveProductItemFromShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => - shoppingCart.removeProductItem(productItem), - ); - - public confirm = ({ shoppingCartId, now }: ConfirmShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => shoppingCart.confirm(now)); - - public cancel = ({ shoppingCartId, now }: CancelShoppingCart) => - this.on(shoppingCartId, (shoppingCart) => shoppingCart.cancel(now)); -}