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 " ) ]
13+ [ Trait ( "Category" , "Operation:Decide " ) ]
1214public class GettingStateTests ( 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 ( ) ;
@@ -23,16 +25,55 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
2325 var tShirt = new PricedProductItem ( tShirtId , 1 , 50 ) ;
2426
2527 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 )
28+ new Opened ( clientId , DateTime . UtcNow ) ,
29+ new ProductItemAdded ( twoPairsOfShoes , DateTime . UtcNow ) ,
30+ new ProductItemAdded ( tShirt , DateTime . UtcNow ) ,
31+ new ProductItemRemoved ( pairOfShoes , DateTime . UtcNow ) ,
32+ new Confirmed ( DateTime . UtcNow ) ,
33+ new Canceled ( DateTime . UtcNow )
3234 } ;
3335
3436 var streamName = $ "shopping_cart-{ shoppingCartId } ";
3537
38+ await Fixture . Streams . DecideAsync (
39+ streamName ,
40+ new Open ( clientId , DateTime . UtcNow ) ,
41+ Decider
42+ ) ;
43+
44+ await Fixture . Streams . DecideAsync (
45+ streamName ,
46+ new AddProductItem ( twoPairsOfShoes , DateTime . UtcNow ) ,
47+ Decider
48+ ) ;
49+
50+ await Fixture . Streams . DecideAsync (
51+ streamName ,
52+ new AddProductItem ( tShirt , DateTime . UtcNow ) ,
53+ Decider
54+ ) ;
55+
56+ await Fixture . Streams . DecideAsync (
57+ streamName ,
58+ new RemoveProductItem ( pairOfShoes , DateTime . UtcNow ) ,
59+ Decider
60+ ) ;
61+
62+ await Fixture . Streams . DecideAsync (
63+ streamName ,
64+ new Confirm ( DateTime . UtcNow ) ,
65+ Decider
66+ ) ;
67+
68+ await Assert . ThrowsAsync < InvalidOperationException > (
69+ ( ) =>
70+ Fixture . Streams . DecideAsync (
71+ streamName ,
72+ new Cancel ( DateTime . UtcNow ) ,
73+ Decider
74+ )
75+ ) ;
76+
3677 await Fixture . Streams . AppendToStreamAsync ( streamName , events ) ;
3778
3879 var stateBuilder = StateBuilder . For < ShoppingCart , Event > ( Evolve , ( ) => new Initial ( ) ) ;
@@ -44,16 +85,6 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
4485
4586 // Then
4687 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);
5788 }
5889}
5990
@@ -68,30 +99,25 @@ decimal UnitPrice
6899public abstract record ShoppingCart {
69100 public abstract record Event {
70101 public record Opened (
71- Guid ShoppingCartId ,
72102 Guid ClientId ,
73103 DateTimeOffset OpenedAt
74104 ) : Event ;
75105
76106 public record ProductItemAdded (
77- Guid ShoppingCartId ,
78107 PricedProductItem ProductItem ,
79108 DateTimeOffset AddedAt
80109 ) : Event ;
81110
82111 public record ProductItemRemoved (
83- Guid ShoppingCartId ,
84112 PricedProductItem ProductItem ,
85113 DateTimeOffset RemovedAt
86114 ) : Event ;
87115
88116 public record Confirmed (
89- Guid ShoppingCartId ,
90117 DateTimeOffset ConfirmedAt
91118 ) : Event ;
92119
93120 public record Canceled (
94- Guid ShoppingCartId ,
95121 DateTimeOffset CanceledAt
96122 ) : Event ;
97123
@@ -110,10 +136,10 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
110136 ( Initial , Opened ) =>
111137 new Pending ( ProductItems . Empty ) ,
112138
113- ( Pending ( var productItems ) , ProductItemAdded ( _ , var productItem , _ ) ) =>
139+ ( Pending ( var productItems ) , ProductItemAdded ( var productItem , _ ) ) =>
114140 new Pending ( productItems . Add ( productItem ) ) ,
115141
116- ( Pending ( var productItems ) , ProductItemRemoved ( _ , var productItem , _ ) ) =>
142+ ( Pending ( var productItems ) , ProductItemRemoved ( var productItem , _ ) ) =>
117143 new Pending ( productItems . Remove ( productItem ) ) ,
118144
119145 ( Pending , Confirmed ) =>
@@ -124,6 +150,61 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
124150
125151 _ => state
126152 } ;
153+
154+ public abstract record Command {
155+ public record Open (
156+ Guid ClientId ,
157+ DateTimeOffset Now
158+ ) : Command ;
159+
160+ public record AddProductItem (
161+ PricedProductItem ProductItem ,
162+ DateTimeOffset Now
163+ ) : Command ;
164+
165+ public record RemoveProductItem (
166+ PricedProductItem ProductItem ,
167+ DateTimeOffset Now
168+ ) : Command ;
169+
170+ public record Confirm (
171+ DateTimeOffset Now
172+ ) : Command ;
173+
174+ public record Cancel (
175+ DateTimeOffset Now
176+ ) : Command ;
177+
178+ Command ( ) { }
179+ }
180+
181+ public static Event [ ] Decide ( Command command , ShoppingCart state ) =>
182+ ( state , command ) switch {
183+ ( Pending , Open ) => [ ] ,
184+
185+ ( Initial , Open ( var clientId , var now ) ) => [ new Opened ( clientId , now ) ] ,
186+
187+ ( Pending , AddProductItem ( var productItem , var now ) ) => [ new ProductItemAdded ( productItem , now ) ] ,
188+
189+ ( Pending ( var productItems ) , RemoveProductItem ( var productItem , var now ) ) =>
190+ productItems . HasEnough ( productItem )
191+ ? [ new ProductItemRemoved ( productItem , now ) ]
192+ : throw new InvalidOperationException ( "Not enough product items to remove" ) ,
193+
194+ ( Pending , Confirm ( var now ) ) => [ new Confirmed ( now ) ] ,
195+
196+ ( Pending , Cancel ( var now ) ) => [ new Canceled ( now ) ] ,
197+
198+ _ => throw new InvalidOperationException (
199+ $ "Cannot { command . GetType ( ) . Name } for { state . GetType ( ) . Name } shopping cart"
200+ )
201+ } ;
202+
203+ public static readonly Decider < ShoppingCart , Command , Event > Decider = new Decider < ShoppingCart , Command , Event > (
204+ Decide ,
205+ Evolve ,
206+ ( ) => new Initial ( )
207+ ) ;
127208}
128209
129210public record ProductItems ( ImmutableDictionary < string , int > Items ) {
@@ -144,19 +225,3 @@ static string Key(PricedProductItem pricedProductItem) =>
144225 ProductItems IncrementQuantity ( string key , int quantity ) =>
145226 new ( Items . SetItem ( key , Items . TryGetValue ( key , out var current ) ? current + quantity : quantity ) ) ;
146227}
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