Skip to content

Commit

Permalink
Added an example of closing and opening as one operation
Browse files Browse the repository at this point in the history
Adjusted the closing the books sample to align with article content
  • Loading branch information
oskardudycz committed Feb 17, 2024
1 parent 0c70ed9 commit 906ee6b
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
using Marten;

namespace Helpdesk.Api.Core.Marten;
namespace PointOfSales.Api.Core.Marten;

public static class DocumentSessionExtensions
{
public static Task Add<T>(this IDocumentSession documentSession, string id, object[] events, CancellationToken ct)
where T : class =>
documentSession.Add<T, object>(id, events, ct);

public static Task Add<T, TEvent>(this IDocumentSession documentSession, string id, TEvent[] events,
CancellationToken ct)
where T : class
{
if (events.Length == 0)
return Task.CompletedTask;

documentSession.Events.StartStream<T>(id, events);
documentSession.Events.StartStream<T>(id, events.Cast<object>().ToArray());
return documentSession.SaveChangesAsync(token: ct);
}

public static Task GetAndUpdate<T>(
public static Task GetAndUpdate<T, TEvent>(
this IDocumentSession documentSession,
string id,
int version,
Func<T, object[]> handle,
Func<T, TEvent[]> handle,
CancellationToken ct
) where T : class =>
documentSession.Events.WriteToAggregate<T>(id, version, stream =>
{
var result = handle(stream.Aggregate);
if (result.Length != 0)
stream.AppendMany(result);
stream.AppendMany(result.Cast<object>().ToArray());
}, ct);
}
49 changes: 43 additions & 6 deletions Sample/ClosingTheBooks/PointOfSales.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using Helpdesk.Api.Core.Http.Middlewares.ExceptionHandling;
using Helpdesk.Api.Core.Marten;
using JasperFx.CodeGeneration;
using Marten;
using Marten.AspNetCore;
Expand All @@ -12,6 +11,7 @@
using Marten.Services.Json;
using Oakton;
using PointOfSales.Api.Core;
using PointOfSales.Api.Core.Marten;
using PointOfSales.CashierShifts;
using PointOfSales.CashRegister;
using Weasel.Core;
Expand All @@ -22,6 +22,7 @@
using static PointOfSales.CashierShifts.CashierShiftEvent;
using static PointOfSales.Api.Core.ETagExtensions;
using static System.DateTimeOffset;
using static PointOfSales.CashierShifts.CloseAndOpenShift;
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -100,10 +101,12 @@ CancellationToken ct
var lastClosedShift = await documentSession.GetLastCashierShift(cashRegisterId);
var result = Decide(new OpenShift(cashRegisterId, body.CashierId, Now), lastClosedShift);
if (result.Length == 0 || result[0] is not ShiftOpened opened)
var opened = result.OfType<ShiftOpened>().SingleOrDefault();
if (opened == null)
throw new InvalidOperationException("Cannot Open Shift");
await documentSession.Add<CashierShift>(opened.CashierShiftId, result, ct);
await documentSession.Add<CashierShift, CashierShiftEvent>(opened.CashierShiftId, result, ct);
return Created(
$"/api/cash-registers/{cashRegisterId}/cashier-shifts/{opened.CashierShiftId.ShiftNumber}",
Expand All @@ -125,7 +128,7 @@ CancellationToken ct
var cashierShiftId = new CashierShiftId(cashRegisterId, shiftNumber);
var transactionId = CombGuidIdGeneration.NewGuid().ToString();
return documentSession.GetAndUpdate<CashierShift>(cashierShiftId, ToExpectedVersion(eTag),
return documentSession.GetAndUpdate<CashierShift, CashierShiftEvent>(cashierShiftId, ToExpectedVersion(eTag),
state => Decide(new RegisterTransaction(cashierShiftId, transactionId, body.Amount, Now), state), ct);
}
);
Expand All @@ -142,14 +145,43 @@ CancellationToken ct
{
var cashierShiftId = new CashierShiftId(cashRegisterId, shiftNumber);
return documentSession.GetAndUpdate<CashierShift>(cashierShiftId, ToExpectedVersion(eTag),
return documentSession.GetAndUpdate<CashierShift, CashierShiftEvent>(cashierShiftId, ToExpectedVersion(eTag),
state => Decide(new CloseShift(cashierShiftId, body.DeclaredTender, Now), state), ct);
}
);


// alternative showing how you could handle closing and opening in the same method
// that require tho that shifts are continuous
app.MapPost("/api/cash-registers/{cashRegisterId}/cashier-shifts/{shiftNumber:int}/close-and-open",
async (
IDocumentSession documentSession,
string cashRegisterId,
int shiftNumber,
CloseAndOpenShiftRequest body,
[FromIfMatchHeader] string eTag,
CancellationToken ct
) =>
{
var command = new CloseAndOpenCommand(
new CashierShiftId(cashRegisterId, shiftNumber),
body.CashierId,
body.DeclaredTender,
Now
);
var openedCashierId = await documentSession.CloseAndOpenCashierShift(command, ToExpectedVersion(eTag), ct);
return Created(
$"/api/cash-registers/{cashRegisterId}/cashier-shifts/{openedCashierId.ShiftNumber}",
cashRegisterId
);
}
);

app.MapGet("/api/cash-registers/{cashRegisterId}/cashier-shifts/{shiftNumber:int}",
(HttpContext context, IQuerySession querySession, string cashRegisterId, int shiftNumber) =>
querySession.Json.WriteById<CashierShiftDetails>(new CashierShiftId(cashRegisterId, shiftNumber), context)
querySession.Json.WriteById<CurrentCashierShift>(new CashierShiftId(cashRegisterId, shiftNumber), context)
);


Expand All @@ -174,6 +206,11 @@ public record CloseShiftRequest(
decimal DeclaredTender
);

public record CloseAndOpenShiftRequest(
decimal DeclaredTender,
string CashierId
);


public partial class Program
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
namespace PointOfSales.CashRegister;

public static class CashRegisterId
{
public static string From(string workstation) =>
$"urn:cash_register:{workstation}";
}

public record CashRegister(string Id)
{
public static CashRegister Create(CashRegisterInitialized @event) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Text.Json.Serialization;

namespace PointOfSales.CashierShifts;

using System;
Expand Down Expand Up @@ -47,21 +45,21 @@ public record CashierShiftId(string CashRegisterId, int ShiftNumber)
public override string ToString() => $"urn:cashier_shift:{CashRegisterId}:{ShiftNumber}";
}

public abstract record CashierShiftEvent(CashierShiftId CashierShiftId)
public abstract record CashierShiftEvent
{
public record ShiftOpened(
CashierShiftId CashierShiftId,
string CashierId,
decimal Float,
DateTimeOffset StartedAt
): CashierShiftEvent(CashierShiftId);
): CashierShiftEvent;

public record TransactionRegistered(
CashierShiftId CashierShiftId,
string TransactionId,
decimal Amount,
DateTimeOffset RegisteredAt
): CashierShiftEvent(CashierShiftId);
): CashierShiftEvent;

public record ShiftClosed(
CashierShiftId CashierShiftId,
Expand All @@ -70,5 +68,7 @@ public record ShiftClosed(
decimal ShortageAmount,
decimal FinalFloat,
DateTimeOffset ClosedAt
): CashierShiftEvent(CashierShiftId);
): CashierShiftEvent;

private CashierShiftEvent(){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,21 @@ public record CloseShift(
decimal DeclaredTender,
DateTimeOffset Now
): CashierShiftCommand;

private CashierShiftCommand() { }
}

public static class CashierShiftDecider
{
public static object[] Decide(CashierShiftCommand command, CashierShift state) =>
public static CashierShiftEvent[] Decide(CashierShiftCommand command, CashierShift state) =>
(command, state) switch
{
(OpenShift open, NonExisting) =>
[
new ShiftOpened(
new CashierShiftId(open.CashRegisterId, 1),
open.CashierId,
0,
open.Now
)
],
[Open(open.CashierId, open.CashRegisterId, 1, 0, open.Now)],

(OpenShift open, Closed closed) =>
[

new ShiftOpened(
new CashierShiftId(open.CashRegisterId, closed.ShiftId.ShiftNumber + 1),
open.CashierId,
closed.FinalFloat,
open.Now
)
Open(open.CashierId, open.CashRegisterId, closed.ShiftId.ShiftNumber + 1, closed.FinalFloat, open.Now)
],

(OpenShift, Opened) => [],
Expand Down Expand Up @@ -79,4 +68,20 @@ public static class CashierShiftDecider

_ => throw new InvalidOperationException($"Cannot run {command.GetType().Name} on {state.GetType().Name}")
};

private static ShiftOpened Open(
string cashierId,
string cashRegisterId,
int shiftNumber,
decimal @float,
DateTimeOffset now) =>
new(
new CashierShiftId(cashRegisterId, shiftNumber),
cashierId,
@float,
now
);
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Marten;

namespace PointOfSales.CashierShifts;

using static CashierShiftEvent;
using static CashierShiftCommand;
using static CashierShiftDecider;
using CommandResult = (CashierShiftId StreamId, CashierShiftEvent[] Events);

public record CloseAndOpenCommand(
CashierShiftId CashierShiftId,
string CashierId,
decimal DeclaredTender,
DateTimeOffset Now
);

public static class CloseAndOpenShift
{
public static (CommandResult, CommandResult) Handle(CloseAndOpenCommand command, CashierShift currentShift)
{
var (currentShiftId, cashierId, declaredTender, now) = command;
var closingResult = Decide(new CloseShift(currentShiftId, declaredTender, now), currentShift);

currentShift = closingResult.Aggregate(currentShift, (current, @event) => current.Apply(@event));

var openResult = Decide(new OpenShift(currentShiftId, cashierId, now), currentShift);

// double check if it was actually
var opened = openResult.OfType<ShiftOpened>().SingleOrDefault();
if (opened == null)
throw new InvalidOperationException("Cannot open new shift!");

return ((currentShiftId, closingResult), (opened.CashierShiftId, openResult));
}

public static async Task<CashierShiftId> CloseAndOpenCashierShift(
this IDocumentSession documentSession,
CloseAndOpenCommand command,
int version,
CancellationToken ct
)
{
var currentShift =
await documentSession.Events.AggregateStreamAsync<CashierShift>(command.CashierShiftId, token: ct) ??
new CashierShift.NonExisting();

var (closingResult, openResult) = Handle(command, currentShift);

// Append Closing result to the old stream
if (closingResult.Events.Length > 0)
documentSession.Events.Append(closingResult.StreamId, version, closingResult.Events.AsEnumerable());

if (openResult.Events.Length > 0)
documentSession.Events.StartStream<CashierShift>(openResult.StreamId, openResult.Events.AsEnumerable());

await documentSession.SaveChangesAsync(ct);

return openResult.StreamId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace PointOfSales.CashierShifts;

using static CashierShiftEvent;

public record CashierShiftDetails(
public record CurrentCashierShift(
string Id,
string CashierRegisterId,
int ShiftNumber,
Expand All @@ -13,7 +13,10 @@ public record CashierShiftDetails(
int TransactionsCount,
DateTimeOffset StartedAt,
ClosingDetails? ClosingDetails = null
);
)
{
public int Version { get; set; }
}

public record ClosingDetails(
decimal? DeclaredTender = null,
Expand All @@ -23,9 +26,9 @@ public record ClosingDetails(
DateTimeOffset? ClosedAt = null
);

public class CashierShiftDetailsProjection: SingleStreamProjection<CashierShiftDetails>
public class CurrentCashierShiftProjection: SingleStreamProjection<CurrentCashierShift>
{
public static CashierShiftDetails Create(ShiftOpened @event) =>
public static CurrentCashierShift Create(ShiftOpened @event) =>
new(
@event.CashierShiftId,
@event.CashierShiftId.CashRegisterId,
Expand All @@ -36,10 +39,10 @@ public class CashierShiftDetailsProjection: SingleStreamProjection<CashierShiftD
@event.StartedAt
);

public CashierShiftDetails Apply(TransactionRegistered @event, CashierShiftDetails details) =>
public CurrentCashierShift Apply(TransactionRegistered @event, CurrentCashierShift details) =>
details with { Float = details.Float + @event.Amount, TransactionsCount = details.TransactionsCount + 1 };

public CashierShiftDetails Apply(ShiftClosed @event, CashierShiftDetails details) =>
public CurrentCashierShift Apply(ShiftClosed @event, CurrentCashierShift details) =>
details with
{
ClosingDetails = new ClosingDetails(
Expand Down

0 comments on commit 906ee6b

Please sign in to comment.