11using System . Collections . Immutable ;
22using EventStore . Client ;
3+ using Kurrent . Client . Streams . DecisionMaking ;
34using Kurrent . Client . Streams . GettingState ;
45
56namespace Kurrent . Client . Tests . Streams . DecisionMaking . UnionTypes ;
67
78using static ShoppingCart ;
89using static ShoppingCart . Event ;
10+ using static ShoppingCart . Command ;
911
1012[ Trait ( "Category" , "Target:Streams" ) ]
11- [ Trait ( "Category" , "Operation:GetState " ) ]
12- public class GettingStateTests ( ITestOutputHelper output , KurrentPermanentFixture fixture )
13+ [ Trait ( "Category" , "Operation:Decide " ) ]
14+ public class DecisionMakingWithDeciderTests ( ITestOutputHelper output , KurrentPermanentFixture fixture )
1315 : KurrentPermanentTests < KurrentPermanentFixture > ( output , fixture ) {
1416 [ RetryFact ]
15- public async Task gets_state_for_state_builder_with_evolve_function_and_typed_events ( ) {
17+ public async Task runs_business_logic_with_decider_and_typed_events ( ) {
1618 // Given
1719 var shoppingCartId = Guid . NewGuid ( ) ;
1820 var clientId = Guid . NewGuid ( ) ;
@@ -22,38 +24,56 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
2224 var pairOfShoes = new PricedProductItem ( shoesId , 1 , 100 ) ;
2325 var tShirt = new PricedProductItem ( tShirtId , 1 , 50 ) ;
2426
25- var events = new Event [ ] {
26- new Opened ( shoppingCartId , clientId , DateTime . UtcNow ) ,
27- new ProductItemAdded ( shoppingCartId , twoPairsOfShoes , DateTime . UtcNow ) ,
28- new ProductItemAdded ( shoppingCartId , tShirt , DateTime . UtcNow ) ,
29- new ProductItemRemoved ( shoppingCartId , pairOfShoes , DateTime . UtcNow ) ,
30- new Confirmed ( shoppingCartId , DateTime . UtcNow ) ,
31- new Canceled ( shoppingCartId , DateTime . UtcNow )
32- } ;
33-
3427 var streamName = $ "shopping_cart-{ shoppingCartId } ";
3528
36- await Fixture . Streams . AppendToStreamAsync ( streamName , events ) ;
37-
38- var stateBuilder = StateBuilder . For < ShoppingCart , Event > ( Evolve , ( ) => new Initial ( ) ) ;
39-
40- // When
41- var result = await Fixture . Streams . GetStateAsync ( streamName , stateBuilder ) ;
42-
43- var shoppingCart = result . State ;
44-
45- // Then
46- Assert . IsType < Closed > ( shoppingCart ) ;
47- // TODO: Add some time travelling
48- // Assert.Equal(2, shoppingCart.);
49- //
50- // Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId);
51- // Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity);
52- // Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice);
53- //
54- // Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId);
55- // Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity);
56- // Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice);
29+ var result = await Fixture . Streams . DecideAsync (
30+ streamName ,
31+ new Open ( clientId , DateTime . UtcNow ) ,
32+ Decider
33+ ) ;
34+
35+ Assert . IsType < SuccessResult > ( result ) ;
36+
37+ result = await Fixture . Streams . DecideAsync (
38+ streamName ,
39+ new AddProductItem ( twoPairsOfShoes , DateTime . UtcNow ) ,
40+ Decider
41+ ) ;
42+
43+ Assert . IsType < SuccessResult > ( result ) ;
44+
45+ result = await Fixture . Streams . DecideAsync (
46+ streamName ,
47+ new AddProductItem ( tShirt , DateTime . UtcNow ) ,
48+ Decider
49+ ) ;
50+
51+ Assert . IsType < SuccessResult > ( result ) ;
52+
53+ result = await Fixture . Streams . DecideAsync (
54+ streamName ,
55+ new RemoveProductItem ( pairOfShoes , DateTime . UtcNow ) ,
56+ Decider
57+ ) ;
58+
59+ Assert . IsType < SuccessResult > ( result ) ;
60+
61+ result = await Fixture . Streams . DecideAsync (
62+ streamName ,
63+ new Confirm ( DateTime . UtcNow ) ,
64+ Decider
65+ ) ;
66+
67+ Assert . IsType < SuccessResult > ( result ) ;
68+
69+ await Assert . ThrowsAsync < InvalidOperationException > (
70+ ( ) =>
71+ Fixture . Streams . DecideAsync (
72+ streamName ,
73+ new Cancel ( DateTime . UtcNow ) ,
74+ Decider
75+ )
76+ ) ;
5777 }
5878}
5979
@@ -68,30 +88,25 @@ decimal UnitPrice
6888public abstract record ShoppingCart {
6989 public abstract record Event {
7090 public record Opened (
71- Guid ShoppingCartId ,
7291 Guid ClientId ,
7392 DateTimeOffset OpenedAt
7493 ) : Event ;
7594
7695 public record ProductItemAdded (
77- Guid ShoppingCartId ,
7896 PricedProductItem ProductItem ,
7997 DateTimeOffset AddedAt
8098 ) : Event ;
8199
82100 public record ProductItemRemoved (
83- Guid ShoppingCartId ,
84101 PricedProductItem ProductItem ,
85102 DateTimeOffset RemovedAt
86103 ) : Event ;
87104
88105 public record Confirmed (
89- Guid ShoppingCartId ,
90106 DateTimeOffset ConfirmedAt
91107 ) : Event ;
92108
93109 public record Canceled (
94- Guid ShoppingCartId ,
95110 DateTimeOffset CanceledAt
96111 ) : Event ;
97112
@@ -110,10 +125,10 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
110125 ( Initial , Opened ) =>
111126 new Pending ( ProductItems . Empty ) ,
112127
113- ( Pending ( var productItems ) , ProductItemAdded ( _ , var productItem , _ ) ) =>
128+ ( Pending ( var productItems ) , ProductItemAdded ( var productItem , _ ) ) =>
114129 new Pending ( productItems . Add ( productItem ) ) ,
115130
116- ( Pending ( var productItems ) , ProductItemRemoved ( _ , var productItem , _ ) ) =>
131+ ( Pending ( var productItems ) , ProductItemRemoved ( var productItem , _ ) ) =>
117132 new Pending ( productItems . Remove ( productItem ) ) ,
118133
119134 ( Pending , Confirmed ) =>
@@ -124,6 +139,61 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
124139
125140 _ => state
126141 } ;
142+
143+ public abstract record Command {
144+ public record Open (
145+ Guid ClientId ,
146+ DateTimeOffset Now
147+ ) : Command ;
148+
149+ public record AddProductItem (
150+ PricedProductItem ProductItem ,
151+ DateTimeOffset Now
152+ ) : Command ;
153+
154+ public record RemoveProductItem (
155+ PricedProductItem ProductItem ,
156+ DateTimeOffset Now
157+ ) : Command ;
158+
159+ public record Confirm (
160+ DateTimeOffset Now
161+ ) : Command ;
162+
163+ public record Cancel (
164+ DateTimeOffset Now
165+ ) : Command ;
166+
167+ Command ( ) { }
168+ }
169+
170+ public static Event [ ] Decide ( Command command , ShoppingCart state ) =>
171+ ( state , command ) switch {
172+ ( Pending , Open ) => [ ] ,
173+
174+ ( Initial , Open ( var clientId , var now ) ) => [ new Opened ( clientId , now ) ] ,
175+
176+ ( Pending , AddProductItem ( var productItem , var now ) ) => [ new ProductItemAdded ( productItem , now ) ] ,
177+
178+ ( Pending ( var productItems ) , RemoveProductItem ( var productItem , var now ) ) =>
179+ productItems . HasEnough ( productItem )
180+ ? [ new ProductItemRemoved ( productItem , now ) ]
181+ : throw new InvalidOperationException ( "Not enough product items to remove" ) ,
182+
183+ ( Pending , Confirm ( var now ) ) => [ new Confirmed ( now ) ] ,
184+
185+ ( Pending , Cancel ( var now ) ) => [ new Canceled ( now ) ] ,
186+
187+ _ => throw new InvalidOperationException (
188+ $ "Cannot { command . GetType ( ) . Name } for { state . GetType ( ) . Name } shopping cart"
189+ )
190+ } ;
191+
192+ public static readonly Decider < ShoppingCart , Command , Event > Decider = new Decider < ShoppingCart , Command , Event > (
193+ Decide ,
194+ Evolve ,
195+ ( ) => new Initial ( )
196+ ) ;
127197}
128198
129199public record ProductItems ( ImmutableDictionary < string , int > Items ) {
@@ -144,19 +214,3 @@ static string Key(PricedProductItem pricedProductItem) =>
144214 ProductItems IncrementQuantity ( string key , int quantity ) =>
145215 new ( Items . SetItem ( key , Items . TryGetValue ( key , out var current ) ? current + quantity : quantity ) ) ;
146216}
147-
148- public static class DictionaryExtensions {
149- public static ImmutableDictionary < TKey , TValue > Set < TKey , TValue > (
150- this ImmutableDictionary < TKey , TValue > dictionary ,
151- TKey key ,
152- Func < TValue ? , TValue > set
153- ) where TKey : notnull =>
154- dictionary . SetItem ( key , set ( dictionary . TryGetValue ( key , out var current ) ? current : default ) ) ;
155-
156- public static void Set < TKey , TValue > (
157- this Dictionary < TKey , TValue > dictionary ,
158- TKey key ,
159- Func < TValue ? , TValue > set
160- ) where TKey : notnull =>
161- dictionary [ key ] = set ( dictionary . TryGetValue ( key , out var current ) ? current : default ) ;
162- }
0 commit comments