From f8b37815b0adcb1de63241f887b62eddb13c7fa1 Mon Sep 17 00:00:00 2001 From: Brian Johnson Date: Thu, 30 Jul 2026 08:17:18 +0200 Subject: [PATCH] Some fixes to the queries and context records. --- .../ApplyFeeHandlerTests.cs | 4 +- .../Handlers/ApplyFeeHandler.cs | 1 - .../Handlers/ReverseJournalHandler.cs | 16 +- .../Handlers/TransferFundsHandler.cs | 1 - .../Interfaces/IAccountQuery.cs | 1 - .../Interfaces/IReversalQuery.cs | 6 +- .../Models/FeeContext.cs | 1 - .../Models/JournalReversalContext.cs | 1 - .../Models/TransferContext.cs | 1 - .../FinancialApi.Domain/FinancialResult.cs | 221 ++++++++++++++++++ .../AccountDbContext.cs | 3 +- .../AccountQuery.cs | 1 - .../ReversalQuery.cs | 12 +- .../a-frame-architecture.sln.DotSettings.user | 7 +- wolverine/a-frame-architecture/mise.toml | 8 +- 15 files changed, 255 insertions(+), 29 deletions(-) create mode 100644 wolverine/a-frame-architecture/FinancialApi.Domain/FinancialResult.cs diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs index e4ea6b9..1d531f2 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs @@ -1,8 +1,6 @@ -using System.Security.Cryptography; -using FinancialApi.Application.Commands; +using FinancialApi.Application.Commands; using FinancialApi.Application.Handlers; using FinancialApi.Application.Models; -using FinancialApi.Domain; using FinancialApi.Domain.Entities; using FluentAssertions; using FluentAssertions.Execution; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs index b046064..98b0827 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs @@ -2,7 +2,6 @@ using FinancialApi.Application.Commands; using FinancialApi.Application.Events; using FinancialApi.Application.Interfaces; using FinancialApi.Application.Models; -using FinancialApi.Domain; using FinancialApi.Domain.Aggregates; using FinancialApi.Domain.Entities; using Wolverine.Persistence; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs index 860e85e..ae5f200 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs @@ -2,7 +2,6 @@ using FinancialApi.Application.Commands; using FinancialApi.Application.Events; using FinancialApi.Application.Interfaces; using FinancialApi.Application.Models; -using FinancialApi.Domain; using FinancialApi.Domain.Aggregates; using FinancialApi.Domain.Entities; using Wolverine.Persistence; @@ -17,8 +16,19 @@ public static class ReverseJournalHandler TimeProvider timeProvider ) { - var data = await query.GetReversalDataAsync(cmd.OriginalJournalId); - return data ?? throw new InvalidOperationException($"Journal Entry {cmd.OriginalJournalId} does not exist"); + var (journalEnty, accounts) = await query.GetReversalDataAsync(cmd.OriginalJournalId); + if (journalEnty == null) + { + throw new InvalidOperationException("Journal not found"); + } + + if (accounts == null) + { + throw new InvalidOperationException("Journal accounts not found"); + } + + return new JournalReversalContext(journalEnty, accounts, timeProvider.GetUtcNow()) ?? + throw new InvalidOperationException($"Journal Entry {cmd.OriginalJournalId} does not exist"); } public static ( IStorageAction JournalWrite, UnitOfWork AccountWrites, JournalReversedEvent diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs index d112af5..f434e71 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs @@ -2,7 +2,6 @@ using FinancialApi.Application.Commands; using FinancialApi.Application.Events; using FinancialApi.Application.Interfaces; using FinancialApi.Application.Models; -using FinancialApi.Domain; using FinancialApi.Domain.Aggregates; using FinancialApi.Domain.Entities; using Wolverine.Persistence; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs index b7d11a3..b074d23 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs @@ -1,4 +1,3 @@ -using FinancialApi.Domain; using FinancialApi.Domain.Entities; namespace FinancialApi.Application.Interfaces; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs index 4c345f1..d988783 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs @@ -1,10 +1,8 @@ -using FinancialApi.Application.Commands; -using FinancialApi.Application.Handlers; -using FinancialApi.Application.Models; +using FinancialApi.Domain.Entities; namespace FinancialApi.Application.Interfaces; public interface IReversalQuery { - Task GetReversalDataAsync(Guid journalEntryId); + Task<(JournalEntry?, List?)> GetReversalDataAsync(Guid journalEntryId); } \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Models/FeeContext.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Models/FeeContext.cs index 8f15968..bfc40f5 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Models/FeeContext.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/FeeContext.cs @@ -1,4 +1,3 @@ -using FinancialApi.Domain; using FinancialApi.Domain.Entities; namespace FinancialApi.Application.Models; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs index 886dfd2..19627f5 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs @@ -1,4 +1,3 @@ -using FinancialApi.Domain; using FinancialApi.Domain.Entities; namespace FinancialApi.Application.Models; diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs index 6f857a8..c517ed4 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs @@ -1,4 +1,3 @@ -using FinancialApi.Domain; using FinancialApi.Domain.Entities; namespace FinancialApi.Application.Models; diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialResult.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialResult.cs new file mode 100644 index 0000000..1e27886 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialResult.cs @@ -0,0 +1,221 @@ +namespace FinancialApi.Domain; + +public interface IFinancialError +{ + int ErrorCode { get; } + string Message { get; } + Exception? Exception { get; } +} + +public sealed class FinancialError : IFinancialError +{ + public FinancialError(int errorCode, string message, Exception? exception = null) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(errorCode); + + ErrorCode = errorCode; + Message = message ?? throw new ArgumentNullException(nameof(message)); + Exception = exception; + } + + public int ErrorCode { get; } + + public string Message { get; } + + public Exception? Exception { get; } + + public override string ToString() => $"Error: {ErrorCode} - {Message}{(Exception == null ? "" : $"\n{Exception}")}"; +} + +public sealed class FinancialResult +{ + private readonly T? _resultValue; + private readonly FinancialError? _resultError; + + public FinancialResult(T resultValue) + { + IsError = false; + _resultValue = resultValue; + _resultError = null; + } + + public FinancialResult(FinancialError resultError) + { + IsError = true; + _resultValue = default; + _resultError = resultError; + } + + public bool IsError { get; } + + /// + /// Error indicator + /// + public bool IsSuccess => !IsError; + + /// + /// Result Values + /// + public T? Value => _resultValue; + + public FinancialError? Error => _resultError; + + public static implicit operator FinancialResult(T resultValue) => new(resultValue); + + public static implicit operator FinancialResult(FinancialError resultError) => new(resultError); + + public static FinancialResult Success(T successValue) + { + return new FinancialResult(successValue); + } + + public static FinancialResult Failure(FinancialError failureValue) + { + return new FinancialResult(failureValue); + } + + public TResult Match( + Func success, + Func failure, + Func nullValue + ) + { + return IsError switch + { + false when _resultValue == null => nullValue(), + true => failure(_resultError!), + _ => success(_resultValue) + }; + } + + public TResult Match(Func success, Func failure) + { + return IsError switch + { + true => failure(_resultError!), + _ => success(_resultValue!) + }; + } + + public TResult? Match(Func success) + { + return IsError switch + { + true => default, + _ => success(_resultValue!) + }; + } + + public void Match(Action success, Action failure, Action nullValue) + { + switch (IsError) + { + case false when _resultValue == null: + nullValue.Invoke(); + return; + case true: + failure.Invoke(_resultError!); + return; + default: + success(_resultValue); + break; + } + } + + public void Match(Action success, Action failure) + { + switch (IsError) + { + case true: + failure.Invoke(_resultError!); + return; + default: + success(_resultValue!); + break; + } + } + + public void Match(Action success) + { + if (IsError) + { + return; + } + + success(_resultValue!); + } + + public async Task MatchAsync( + Func> success, + Func> failure, + Func> nullValue + ) + { + return IsError switch + { + false when _resultValue == null => await nullValue(), + true => await failure(_resultError!), + _ => await success(_resultValue) + }; + } + + public async Task MatchAsync( + Func> success, + Func> failure + ) + { + return IsError switch + { + true => await failure(_resultError!), + _ => await success(_resultValue!) + }; + } + + public async Task MatchAsync(Func> success) + { + return IsError switch + { + true => default, + _ => await success(_resultValue!) + }; + } + + public async Task MatchAsync(Func success, Func failure, Func nullValue) + { + switch (IsError) + { + case false when _resultValue == null: + await nullValue.Invoke(); + return; + case true: + await failure.Invoke(_resultError!); + return; + default: + await success(_resultValue!); + break; + } + } + + public async Task MatchAsync(Func success, Func failure) + { + switch (IsError) + { + case true: + await failure.Invoke(_resultError!); + return; + default: + await success(_resultValue!); + break; + } + } + + public async Task MatchAsync(Func success) + { + if (IsError) + { + return; + } + + await success(_resultValue!); + } +} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs index 45ad50a..db4bf30 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs @@ -1,5 +1,4 @@ -using FinancialApi.Domain; -using FinancialApi.Domain.Entities; +using FinancialApi.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace FinancialApi.Infrastructure; diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs index 8f4e774..74a390f 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs @@ -1,5 +1,4 @@ using FinancialApi.Application.Interfaces; -using FinancialApi.Domain; using FinancialApi.Domain.Entities; using Microsoft.EntityFrameworkCore; diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs index 3c72954..abc9f84 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs @@ -1,14 +1,12 @@ -using FinancialApi.Application.Commands; -using FinancialApi.Application.Handlers; using FinancialApi.Application.Interfaces; -using FinancialApi.Application.Models; +using FinancialApi.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace FinancialApi.Infrastructure; -public class ReversalQuery(AccountDbContext db, TimeProvider timeProvider) : IReversalQuery +public class ReversalQuery(AccountDbContext db) : IReversalQuery { - public async Task GetReversalDataAsync(Guid journalEntryId) + public async Task<(JournalEntry?, List?)> GetReversalDataAsync(Guid journalEntryId) { var entry = await db.JournalEntries.Include(x => x.Lines) .AsNoTracking() @@ -16,7 +14,7 @@ public class ReversalQuery(AccountDbContext db, TimeProvider timeProvider) : IRe if (entry == null) { - return null; + return (null, null); } var accountIds = entry.Lines.Select(l => l.AccountId) @@ -27,6 +25,6 @@ public class ReversalQuery(AccountDbContext db, TimeProvider timeProvider) : IRe .Where(a => accountIds.Contains(a.Id)) .ToListAsync(); - return new JournalReversalContext(entry, accounts, timeProvider.GetUtcNow()); + return (entry, accounts); } } \ No newline at end of file diff --git a/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user index e5be057..e360ee3 100644 --- a/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user +++ b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user @@ -1,8 +1,11 @@  + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> -</SessionState> \ No newline at end of file +</SessionState> + \ No newline at end of file diff --git a/wolverine/a-frame-architecture/mise.toml b/wolverine/a-frame-architecture/mise.toml index b81001a..66d7e4d 100644 --- a/wolverine/a-frame-architecture/mise.toml +++ b/wolverine/a-frame-architecture/mise.toml @@ -1,4 +1,4 @@ -[tools] +[tools] dotnet = "10" [tasks.restore] @@ -12,3 +12,9 @@ run = "dotnet build" [tasks.clean] run = "fd -I \"bin|obj\" -td -X rm -r && dotnet clean" + +[tasks.migrate] +usage = ''' +arg "" help="Migration Name" +''' +run = "dotnet ef migrations add \"${name?}\" --startup-project FinancialApi --project FinancialApi.Infrastructure"