Skip to content

Commit 2ae9faf

Browse files
committed
Vending Machine sample app
Added sample app
1 parent 181ce88 commit 2ae9faf

20 files changed

+1357
-0
lines changed

FunctionalStateMachine.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalStateMachine.Comm
3939
EndProject
4040
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalStateMachine.CommandRunner.Generator", "FunctionalStateMachine.CommandRunner.Generator\FunctionalStateMachine.CommandRunner.Generator.csproj", "{F2EB10B5-AD83-47A7-BFA3-3A552B06AC76}"
4141
EndProject
42+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VendingMachineSampleApp", "VendingMachineSampleApp\VendingMachineSampleApp.csproj", "{E6646DD6-3BA5-4A57-B644-59D670F81002}"
43+
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VendingMachineSampleApp.Tests", "VendingMachineSampleApp.Tests\VendingMachineSampleApp.Tests.csproj", "{62D20B6C-490F-4616-A311-18D52A4E8F8C}"
45+
EndProject
4246
Global
4347
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4448
Debug|Any CPU = Debug|Any CPU
@@ -77,5 +81,13 @@ Global
7781
{F2EB10B5-AD83-47A7-BFA3-3A552B06AC76}.Debug|Any CPU.Build.0 = Debug|Any CPU
7882
{F2EB10B5-AD83-47A7-BFA3-3A552B06AC76}.Release|Any CPU.ActiveCfg = Release|Any CPU
7983
{F2EB10B5-AD83-47A7-BFA3-3A552B06AC76}.Release|Any CPU.Build.0 = Release|Any CPU
84+
{E6646DD6-3BA5-4A57-B644-59D670F81002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
85+
{E6646DD6-3BA5-4A57-B644-59D670F81002}.Debug|Any CPU.Build.0 = Debug|Any CPU
86+
{E6646DD6-3BA5-4A57-B644-59D670F81002}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{E6646DD6-3BA5-4A57-B644-59D670F81002}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{62D20B6C-490F-4616-A311-18D52A4E8F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89+
{62D20B6C-490F-4616-A311-18D52A4E8F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
90+
{62D20B6C-490F-4616-A311-18D52A4E8F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
91+
{62D20B6C-490F-4616-A311-18D52A4E8F8C}.Release|Any CPU.Build.0 = Release|Any CPU
8092
EndGlobalSection
8193
EndGlobal
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
13+
<PackageReference Include="xunit" Version="2.9.2" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<Using Include="Xunit" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\VendingMachineSampleApp\VendingMachineSampleApp.csproj" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using VendingMachineSampleApp.Configuration;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.Tests;
5+
6+
public class VendingMachineSampleTests
7+
{
8+
[Fact]
9+
public void SelectItem_InStock_TransitionsToItemSelected()
10+
{
11+
var machine = VendingMachineBuilder.BuildMachine();
12+
var data = VendingMachineData.Initialize(CreateInventory());
13+
14+
var (newState, newData, _) = machine.Fire(new SelectItemTrigger("A1"), VendingMachineState.Idle, data);
15+
16+
Assert.Equal(VendingMachineState.ItemSelected, newState);
17+
Assert.Equal("A1", newData.SelectedItemCode);
18+
Assert.Equal(0m, newData.MoneyInserted);
19+
}
20+
21+
[Fact]
22+
public void SelectItem_OutOfStock_TransitionsToOutOfStock()
23+
{
24+
var machine = VendingMachineBuilder.BuildMachine();
25+
var data = VendingMachineData.Initialize(CreateInventory(stock: 0));
26+
27+
var (newState, newData, _) = machine.Fire(new SelectItemTrigger("A1"), VendingMachineState.Idle, data);
28+
29+
Assert.Equal(VendingMachineState.OutOfStock, newState);
30+
Assert.Null(newData.SelectedItemCode);
31+
}
32+
33+
[Fact]
34+
public void InsertMoney_Enough_TransitionsToDispensing()
35+
{
36+
var machine = VendingMachineBuilder.BuildMachine();
37+
var data = VendingMachineData.Initialize(CreateInventory());
38+
var (selectedState, selectedData, _) = machine.Fire(new SelectItemTrigger("A1"), VendingMachineState.Idle, data);
39+
40+
var (newState, newData, _) = machine.Fire(new InsertMoneyTrigger(2.00m), selectedState, selectedData);
41+
42+
Assert.Equal(VendingMachineState.DispensingItem, newState);
43+
Assert.Equal(2.00m, newData.MoneyInserted);
44+
}
45+
46+
[Fact]
47+
public void DispenseComplete_WithChange_GoesToReturningChangeThenIdle()
48+
{
49+
var machine = VendingMachineBuilder.BuildMachine();
50+
var data = VendingMachineData.Initialize(CreateInventory());
51+
var (selectedState, selectedData, _) = machine.Fire(new SelectItemTrigger("A1"), VendingMachineState.Idle, data);
52+
var (dispenseState, dispenseData, _) = machine.Fire(new InsertMoneyTrigger(2.00m), selectedState, selectedData);
53+
54+
var (returningState, returningData, _) = machine.Fire(new DispenseCompleteTrigger(), dispenseState, dispenseData);
55+
var (idleState, idleData, _) = machine.Fire(new DispenseCompleteTrigger(), returningState, returningData);
56+
57+
Assert.Equal(VendingMachineState.ReturningChange, returningState);
58+
Assert.Equal(VendingMachineState.Idle, idleState);
59+
Assert.Null(idleData.SelectedItemCode);
60+
Assert.Equal(0m, idleData.MoneyInserted);
61+
}
62+
63+
[Fact]
64+
public void DispenseComplete_NoChange_GoesDirectlyToIdle()
65+
{
66+
var machine = VendingMachineBuilder.BuildMachine();
67+
var data = VendingMachineData.Initialize(CreateInventory());
68+
var (selectedState, selectedData, _) = machine.Fire(new SelectItemTrigger("A1"), VendingMachineState.Idle, data);
69+
var (dispenseState, dispenseData, _) = machine.Fire(new InsertMoneyTrigger(1.50m), selectedState, selectedData);
70+
71+
var (idleState, idleData, _) = machine.Fire(new DispenseCompleteTrigger(), dispenseState, dispenseData);
72+
73+
Assert.Equal(VendingMachineState.Idle, idleState);
74+
Assert.Null(idleData.SelectedItemCode);
75+
Assert.Equal(0m, idleData.MoneyInserted);
76+
}
77+
78+
private static Dictionary<string, VendingItem> CreateInventory(int stock = 5)
79+
{
80+
return new Dictionary<string, VendingItem>
81+
{
82+
["A1"] = new("A1", "Chips", 1.50m, stock)
83+
};
84+
}
85+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles DispenseItemCommand by simulating the physical dispensing of an item.
8+
/// In a real system, this would trigger motor control hardware to dispense the product.
9+
/// </summary>
10+
public class DispenseItemHandler : ICommandRunner<DispenseItemCommand>
11+
{
12+
public void Run(DispenseItemCommand command)
13+
{
14+
Console.ForegroundColor = ConsoleColor.Yellow;
15+
Console.WriteLine($"🔊 *motor whirring sounds*");
16+
Console.WriteLine($"📦 Dispensing {command.ItemCode}...");
17+
18+
// Simulate mechanical delay
19+
Thread.Sleep(500);
20+
21+
Console.WriteLine($"✓ Item dispensed successfully");
22+
Console.ResetColor();
23+
}
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles DisplayMessageCommand by outputting messages to the console.
8+
/// Simulates updating a vending machine display screen.
9+
/// </summary>
10+
public class DisplayMessageHandler : ICommandRunner<DisplayMessageCommand>
11+
{
12+
public void Run(DisplayMessageCommand command)
13+
{
14+
Console.ForegroundColor = ConsoleColor.Cyan;
15+
Console.WriteLine($"🖥️ {command.Message}");
16+
Console.ResetColor();
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles LogTransactionCommand by writing transaction records to console.
8+
/// In a real system, this would write to a file, database, or logging service.
9+
/// </summary>
10+
public class LogTransactionHandler : ICommandRunner<LogTransactionCommand>
11+
{
12+
public void Run(LogTransactionCommand command)
13+
{
14+
var status = command.Success ? "✓ SUCCESS" : "✗ FAILED";
15+
Console.ForegroundColor = ConsoleColor.Gray;
16+
Console.WriteLine($"💾 [LOG] Transaction: {command.ItemCode} | Amount: ${command.AmountPaid:F2} | {status}");
17+
Console.ResetColor();
18+
}
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles PlaySoundCommand by playing different sound effects to the user.
8+
/// In a real system, this would interface with the machine's audio system.
9+
/// Here we just print text representations of the sounds.
10+
/// </summary>
11+
public class PlaySoundHandler : ICommandRunner<PlaySoundCommand>
12+
{
13+
public void Run(PlaySoundCommand command)
14+
{
15+
var sound = command.Sound switch
16+
{
17+
VendingSound.SelectionConfirmed => "🔊 *beep*",
18+
VendingSound.TransactionComplete => "🔊 *ding!*",
19+
VendingSound.ErrorSound => "🔊 *buzz - error sound*",
20+
VendingSound.DispensingSound => "🔊 *whirrrr*",
21+
VendingSound.CoinReturn => "🔊 *coin return chime*",
22+
VendingSound.JamAlert => "🔊 *ALERT - machine jam detected*",
23+
_ => "🔊 *sound*"
24+
};
25+
26+
Console.WriteLine(sound);
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles ReturnMoneyCommand by simulating the coin return slot dispensing money.
8+
/// In a real system, this would trigger the coin mechanism to dispense physical coins or bills.
9+
/// </summary>
10+
public class ReturnMoneyHandler : ICommandRunner<ReturnMoneyCommand>
11+
{
12+
public void Run(ReturnMoneyCommand command)
13+
{
14+
if (command.Amount <= 0)
15+
return;
16+
17+
Console.ForegroundColor = ConsoleColor.Green;
18+
Console.WriteLine($"💰 Returning change: ${command.Amount:F2}");
19+
Console.WriteLine($"🪙 *coins/bills dispensed*");
20+
21+
// Simulate coin return mechanism
22+
Thread.Sleep(300);
23+
24+
Console.ResetColor();
25+
}
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles UpdateInventoryCommand by decrementing the stock of a dispensed item.
8+
/// Maintains the in-memory inventory state as items are sold.
9+
/// </summary>
10+
public class UpdateInventoryHandler : ICommandRunner<UpdateInventoryCommand>
11+
{
12+
private readonly Dictionary<string, VendingItem> _inventory;
13+
14+
public UpdateInventoryHandler(Dictionary<string, VendingItem> inventory)
15+
{
16+
_inventory = inventory;
17+
}
18+
19+
public void Run(UpdateInventoryCommand command)
20+
{
21+
if (_inventory.TryGetValue(command.ItemCode, out var item))
22+
{
23+
var newQuantity = item.Quantity - command.QuantityDispensed;
24+
_inventory[command.ItemCode] = item with { Quantity = newQuantity };
25+
26+
Console.ForegroundColor = ConsoleColor.Magenta;
27+
Console.WriteLine($"📊 [INVENTORY] {item.Name}: {item.Quantity}{newQuantity}");
28+
Console.ResetColor();
29+
}
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using FunctionalStateMachine.CommandRunner;
2+
using VendingMachineSampleApp.Domain;
3+
4+
namespace VendingMachineSampleApp.CommandRunners;
5+
6+
/// <summary>
7+
/// Handles UpdateSalesMetricsCommand by tracking sales analytics.
8+
/// In a real system, this would send data to a monitoring/analytics service.
9+
/// </summary>
10+
public class UpdateSalesMetricsHandler : ICommandRunner<UpdateSalesMetricsCommand>
11+
{
12+
private decimal _totalRevenue = 0m;
13+
private int _successfulTransactions = 0;
14+
private int _failedTransactions = 0;
15+
16+
public void Run(UpdateSalesMetricsCommand command)
17+
{
18+
_totalRevenue += command.Revenue;
19+
20+
if (command.Success)
21+
_successfulTransactions++;
22+
else
23+
_failedTransactions++;
24+
25+
var successRate = _successfulTransactions + _failedTransactions > 0
26+
? (decimal)_successfulTransactions / (_successfulTransactions + _failedTransactions) * 100
27+
: 0;
28+
29+
Console.ForegroundColor = ConsoleColor.DarkGreen;
30+
Console.WriteLine($"📈 [METRICS] Revenue: ${_totalRevenue:F2} | Success Rate: {successRate:F1}% | Successful: {_successfulTransactions} | Failed: {_failedTransactions}");
31+
Console.ResetColor();
32+
}
33+
}

0 commit comments

Comments
 (0)