From 0de38ef2df5442ae7ff39aa9790eff8cf424349a Mon Sep 17 00:00:00 2001 From: Brian Johnson Date: Tue, 14 Jul 2026 16:57:03 +0200 Subject: [PATCH] Added a transfer funds handler using a-frame and wolverine. --- .../FinancialApi.Application/Account.cs | 43 +++++++++++++++++++ .../FinancialApi.Application/Class1.cs | 16 ------- .../{Class1.cs => AccountDbContext.cs} | 6 +-- .../ApplyFeeHandler.cs | 3 +- .../TransferFundsHandler.cs | 43 +++++++++++++++++++ .../FinancialApi/FinancialApi.http | 13 +++++- .../FinancialApi/Program.cs | 6 +++ 7 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 wolverine/a-frame-architecture/FinancialApi.Application/Account.cs delete mode 100644 wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs rename wolverine/a-frame-architecture/FinancialApi.Infrastructure/{Class1.cs => AccountDbContext.cs} (76%) rename wolverine/a-frame-architecture/{FinancialApi => FinancialApi.Infrastructure}/ApplyFeeHandler.cs (93%) create mode 100644 wolverine/a-frame-architecture/FinancialApi.Infrastructure/TransferFundsHandler.cs diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Account.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Account.cs new file mode 100644 index 0000000..8c73567 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Account.cs @@ -0,0 +1,43 @@ +namespace FinancialApi.Application; + +public record Account(int Id, decimal Balance) +{ + public Account ApplyFee(decimal amount) + { + if (amount <= 0) + { + throw new ArgumentException("Fee must be positive."); + } + + if (Balance - amount < 0) + { + throw new InvalidOperationException("Insufficient funds."); + } + + return this with { Balance = Balance - amount }; + } + + public Account Debit(decimal amount) + { + if (amount <= 0) + { + throw new ArgumentException("Debit amount must be positive."); + } + if (Balance - amount < 0) + { + throw new InvalidOperationException("Insufficient funds."); + } + + return this with { Balance = Balance - amount }; + } + + public Account Credit(decimal amount) + { + if (amount <= 0) + { + throw new ArgumentException("Credit amount must be positive."); + } + + return this with { Balance = Balance + amount }; + } +} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs deleted file mode 100644 index 2687d92..0000000 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace FinancialApi.Application; - -public record Account(int Id, decimal Balance) -{ - public Account ApplyFee(decimal amount) - { - if (amount <= 0) - throw new ArgumentException("Fee must be positive."); - - if (Balance - amount < 0) - throw new InvalidOperationException("Insufficient funds."); - - // Returns a brand new record with the updated balance - return this with { Balance = Balance - amount }; - } -} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs similarity index 76% rename from wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs rename to wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs index f0a2233..ae3d0ff 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs @@ -3,12 +3,8 @@ using Microsoft.EntityFrameworkCore; namespace FinancialApi.Infrastructure; -public class AccountDbContext : DbContext +public class AccountDbContext(DbContextOptions options) : DbContext(options) { - public AccountDbContext(DbContextOptions options) : base(options) - { - } - public DbSet Accounts => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ApplyFeeHandler.cs similarity index 93% rename from wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs rename to wolverine/a-frame-architecture/FinancialApi.Infrastructure/ApplyFeeHandler.cs index 358527e..a9cfc66 100644 --- a/wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ApplyFeeHandler.cs @@ -1,10 +1,9 @@ using FinancialApi.Application; -using FinancialApi.Infrastructure; using Microsoft.EntityFrameworkCore; using Wolverine.Attributes; using Wolverine.Persistence; -namespace FinancialApi; +namespace FinancialApi.Infrastructure; public static class ApplyFeeHandler { diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/TransferFundsHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/TransferFundsHandler.cs new file mode 100644 index 0000000..548714a --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/TransferFundsHandler.cs @@ -0,0 +1,43 @@ +using FinancialApi.Application; +using Microsoft.EntityFrameworkCore; +using Wolverine.Attributes; +using Wolverine.Persistence; + +namespace FinancialApi.Infrastructure; + +public record TransferFundsCommand(int SourceAccountId, int DestinationAccountId, decimal Amount); + +public record FundsTransferredEvent(int SourceAccountId, int DestinationAccountId, decimal Amount); + +// A simple container to pass our loaded entities into the pure function +public record TransferPair(Account Source, Account Destination); + +public static class TransferFundsHandler +{ + public static async Task LoadAsync(TransferFundsCommand cmd, AccountDbContext db) + { + var source = await db.Accounts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == cmd.SourceAccountId); + var dest = await db.Accounts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == cmd.DestinationAccountId); + + if (source == null || dest == null) return null; // Wolverine drops into a 404/Problem if null + + return new TransferPair(source, dest); + } + + [Transactional] + public static ( + IStorageAction SourceWrite, + IStorageAction DestWrite, + FundsTransferredEvent Event + ) Handle(TransferFundsCommand cmd, TransferPair pair) + { + var updatedSource = pair.Source.Debit(cmd.Amount); + var updatedDest = pair.Destination.Credit(cmd.Amount); + + var sourcePersistence = Storage.Update(updatedSource); + var destPersistence = Storage.Update(updatedDest); + var @event = new FundsTransferredEvent(cmd.SourceAccountId, cmd.DestinationAccountId, cmd.Amount); + + return (sourcePersistence, destPersistence, @event); + } +} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http index c4c3e4c..bfac6b8 100644 --- a/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http +++ b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http @@ -3,7 +3,7 @@ GET {{FinancialApi_HostAddress}}/weatherforecast/ Accept: application/json -### +### Apply Fee to an Account POST {{FinancialApi_HostAddress}}/accounts/apply-fee Accept: application/json @@ -14,4 +14,13 @@ Content-Type: application/json "amount": 5.0 } -### +### Transfer Between Accounts + +POST {{FinancialApi_HostAddress}}/accounts/transfer +Content-Type: application/json + +{ + "sourceAccountId": 1, + "destinationAccountId": 2, + "amount": 30.00 +} diff --git a/wolverine/a-frame-architecture/FinancialApi/Program.cs b/wolverine/a-frame-architecture/FinancialApi/Program.cs index f8f18ae..d6e5348 100644 --- a/wolverine/a-frame-architecture/FinancialApi/Program.cs +++ b/wolverine/a-frame-architecture/FinancialApi/Program.cs @@ -53,4 +53,10 @@ app.MapPost("/accounts/apply-fee", async (ApplyFeeCommand cmd, IMessageBus bus) return Results.Accepted(); }); +app.MapPost("/accounts/transfer", async (TransferFundsCommand cmd, IMessageBus bus) => +{ + await bus.InvokeAsync(cmd); + return Results.Accepted(); +}); + app.Run();