Skip to content

Commit

Permalink
Added Business Processes starting point and refactored exercises solu…
Browse files Browse the repository at this point in the history
…tions
  • Loading branch information
oskardudycz committed Sep 3, 2024
1 parent e779157 commit 44e910c
Show file tree
Hide file tree
Showing 20 changed files with 1,217 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public void GuestStayAccountEventTypes_AreDefined()

// Then
const int minimumExpectedEventTypesCount = 5;
events.Should().HaveCountGreaterThan(minimumExpectedEventTypesCount);
events.GroupBy(e => e.GetType()).Should().HaveCount(minimumExpectedEventTypesCount);
events.Should().HaveCountGreaterOrEqualTo(minimumExpectedEventTypesCount);
events.GroupBy(e => e.GetType()).Should().HaveCountGreaterOrEqualTo(minimumExpectedEventTypesCount);
}

[Fact]
Expand All @@ -37,7 +37,7 @@ public void GroupCheckoutEventTypes_AreDefined()

// Then
const int minimumExpectedEventTypesCount = 3;
events.Should().HaveCountGreaterThan(minimumExpectedEventTypesCount);
events.GroupBy(e => e.GetType()).Should().HaveCount(minimumExpectedEventTypesCount);
events.Should().HaveCountGreaterOrEqualTo(minimumExpectedEventTypesCount);
events.GroupBy(e => e.GetType()).Should().HaveCountGreaterOrEqualTo(minimumExpectedEventTypesCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public record GroupCheckoutInitiated(
DateTimeOffset InitiatedAt
): GroupCheckoutEvent;

// Ignore those events below during this exercise
public record GuestCheckoutsInitiated(
Guid GroupCheckoutId,
Guid[] InitiatedGuestStayIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>_03_BusinessProcesses</RootNamespace>
<RootNamespace>BusinessProcesses</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="35.5.1" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Ogooreck" Version="0.8.2" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.6" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json;

namespace BusinessProcesses.Core;

public class Database
{
private readonly Dictionary<string, object> storage = new();

public ValueTask Store<T>(Guid id, T obj, CancellationToken _) where T : class
{
storage[GetId<T>(id)] = obj;

return ValueTask.CompletedTask;
}

public ValueTask Delete<T>(Guid id, CancellationToken _)
{
storage.Remove(GetId<T>(id));
return ValueTask.CompletedTask;
}

public ValueTask<T?> Get<T>(Guid id, CancellationToken _) where T : class =>
ValueTask.FromResult(
storage.TryGetValue(GetId<T>(id), out var result)
?
// Clone to simulate getting new instance on loading
JsonSerializer.Deserialize<T>(JsonSerializer.Serialize((T)result))
: null
);

private static string GetId<T>(Guid id) => $"{typeof(T).Name}-{id}";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Collections.Concurrent;
using FluentAssertions;

namespace BusinessProcesses.Core;

public class EventBus
{
public ValueTask Publish(object[] events, CancellationToken _)
{
foreach (var @event in events)
{
if (!eventHandlers.TryGetValue(@event.GetType(), out var handlers))
continue;

foreach (var middleware in middlewares)
middleware(@event);

foreach (var handler in handlers)
handler(@event);
}

return ValueTask.CompletedTask;
}

public void Subscribe<T>(Action<T> eventHandler)
{
Action<object> handler = x => eventHandler((T)x);

eventHandlers.AddOrUpdate(
typeof(T),
_ => [handler],
(_, handlers) =>
{
handlers.Add(handler);
return handlers;
}
);
}

public void Use(Action<object> middleware) =>
middlewares.Add(middleware);

private readonly ConcurrentDictionary<Type, List<Action<object>>> eventHandlers = new();
private readonly List<Action<object>> middlewares = [];
}

public class EventCatcher
{
public List<object> Published { get; } = [];

public void Catch(object @event) =>
Published.Add(@event);

public void Reset() => Published.Clear();

public void ShouldNotReceiveAnyEvent() =>
Published.Should().BeEmpty();

public void ShouldReceiveSingleEvent<T>(T @event)
{
Published.Should().HaveCount(1);
Published.OfType<T>().Should().HaveCount(1);
Published.Single().Should().Be(@event);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;

namespace BusinessProcesses.Version1_Aggregates.Core;

public abstract class Aggregate<TEvent, TId>
where TEvent : class
where TId : notnull
{
[JsonInclude] public TId Id { get; protected set; } = default!;

[NonSerialized] private readonly Queue<TEvent> uncommittedEvents = new();

public virtual void Apply(TEvent @event) { }

public object[] DequeueUncommittedEvents()
{
var dequeuedEvents = uncommittedEvents.Cast<object>().ToArray();;

uncommittedEvents.Clear();

return dequeuedEvents;
}

protected void Enqueue(TEvent @event)
{
uncommittedEvents.Enqueue(@event);
Apply(@event);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using Bogus;
using BusinessProcesses.Core;
using BusinessProcesses.Version1_Aggregates.GroupCheckouts;
using BusinessProcesses.Version1_Aggregates.GuestStayAccounts;
using Xunit;
using Xunit.Abstractions;
using Database = BusinessProcesses.Core.Database;

namespace BusinessProcesses.Version1_Aggregates;

using static GuestStayAccountEvent;
using static GuestStayAccountCommand;
using static GroupCheckoutCommand;

public class EntityDefinitionTests
{
private readonly ITestOutputHelper testOutputHelper;

[Fact]
public async Task CheckingInGuest_Succeeds()
{
// Given
var guestStayId = Guid.NewGuid();
var command = new CheckInGuest(guestStayId, now);
publishedEvents.Reset();

// When
await guestStayFacade.CheckInGuest(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckedIn(guestStayId, now));
}

[Fact]
public async Task RecordingChargeForCheckedInGuest_Succeeds()
{
// Given
var guestStayId = Guid.NewGuid();
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1)));
publishedEvents.Reset();
// And
var amount = generate.Finance.Amount();
var command = new RecordCharge(guestStayId, amount, now);

// When
await guestStayFacade.RecordCharge(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new ChargeRecorded(guestStayId, amount, now));
}

[Fact]
public async Task RecordingPaymentForCheckedInGuest_Succeeds()
{
// Given
var guestStayId = Guid.NewGuid();
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1)));
publishedEvents.Reset();
// And
var amount = generate.Finance.Amount();
var command = new RecordPayment(guestStayId, amount, now);

// When
await guestStayFacade.RecordPayment(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new PaymentRecorded(guestStayId, amount, now));
}

[Fact]
public async Task RecordingPaymentForCheckedInGuestWithCharge_Succeeds()
{
// Given
var guestStayId = Guid.NewGuid();
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1)));
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, generate.Finance.Amount(), now.AddHours(-1)));
publishedEvents.Reset();
// And
var amount = generate.Finance.Amount();
var command = new RecordPayment(guestStayId, amount, now);

// When
await guestStayFacade.RecordPayment(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new PaymentRecorded(guestStayId, amount, now));
}

[Fact]
public async Task CheckingOutGuestWithSettledBalance_Succeeds()
{
// Given
var guestStayId = Guid.NewGuid();

var amount = generate.Finance.Amount();
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1)));
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, amount, now.AddHours(-2)));
await guestStayFacade.RecordPayment(new RecordPayment(guestStayId, amount, now.AddHours(-1)));
publishedEvents.Reset();
// And
var command = new CheckOutGuest(guestStayId, now);

// When
await guestStayFacade.CheckOutGuest(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckedOut(guestStayId, now));
}

[Fact]
public async Task CheckingOutGuestWithSettledBalance_FailsWithGuestCheckoutFailed()
{
// Given
var guestStayId = Guid.NewGuid();

var amount = generate.Finance.Amount();
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStayId, now.AddDays(-1)));
await guestStayFacade.RecordCharge(new RecordCharge(guestStayId, amount + 10, now.AddHours(-2)));
await guestStayFacade.RecordPayment(new RecordPayment(guestStayId, amount, now.AddHours(-1)));
publishedEvents.Reset();
// And
var command = new CheckOutGuest(guestStayId, now);

// When
try
{
await guestStayFacade.CheckOutGuest(command);
}
catch (Exception exc)
{
testOutputHelper.WriteLine(exc.Message);
}

// Then
publishedEvents.ShouldReceiveSingleEvent(new GuestCheckoutFailed(guestStayId, GuestCheckoutFailed.FailureReason.BalanceNotSettled, now));
}

[Fact]
public async Task GroupCheckoutForMultipleGuestStay_ShouldBeInitiated()
{
// Given;
Guid[] guestStays = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()];

await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[0], now.AddDays(-1)));
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[1], now.AddDays(-1)));
await guestStayFacade.CheckInGuest(new CheckInGuest(guestStays[2], now.AddDays(-1)));
publishedEvents.Reset();
// And
var groupCheckoutId = Guid.NewGuid();
var clerkId = Guid.NewGuid();
var command = new InitiateGroupCheckout(groupCheckoutId, clerkId, guestStays, now);

// When
await guestStayFacade.InitiateGroupCheckout(command);

// Then
publishedEvents.ShouldReceiveSingleEvent(new GroupCheckoutEvent.GroupCheckoutInitiated(groupCheckoutId, clerkId, guestStays, now));
}

private readonly Database database = new();
private readonly EventBus eventBus = new();
private readonly EventCatcher publishedEvents = new();
private readonly GuestStayFacade guestStayFacade;
private readonly Faker generate = new();
private readonly DateTimeOffset now = DateTimeOffset.Now;

public EntityDefinitionTests(ITestOutputHelper testOutputHelper)
{
this.testOutputHelper = testOutputHelper;
guestStayFacade = new GuestStayFacade(database, eventBus);
eventBus.Use(publishedEvents.Catch);
}
}
Loading

0 comments on commit 44e910c

Please sign in to comment.