diff --git a/wolverine/a-frame-architecture/.editorconfig b/wolverine/a-frame-architecture/.editorconfig
new file mode 100644
index 0000000..4706336
--- /dev/null
+++ b/wolverine/a-frame-architecture/.editorconfig
@@ -0,0 +1,146 @@
+[*]
+charset = utf-8-bom
+end_of_line = crlf
+trim_trailing_whitespace = false
+insert_final_newline = false
+indent_style = space
+indent_size = 4
+
+# Microsoft .NET properties
+csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+dotnet_naming_rule.interfaces_rule.import_to_resharper = True
+dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces
+dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7
+dotnet_naming_rule.interfaces_rule.severity = warning
+dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style
+dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
+dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style
+dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols
+dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_1.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style
+dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1
+dotnet_naming_rule.unity_serialized_field_rule_2.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_2.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_2.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_2.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_2.style = lower_camel_case_style
+dotnet_naming_rule.unity_serialized_field_rule_2.symbols = unity_serialized_field_symbols_2
+dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_style.i_upper_camel_case_style.required_prefix = I
+dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
+dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
+dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface
+dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any
+dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_required_modifiers = instance
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# ReSharper properties
+resharper_align_linq_query = true
+resharper_autodetect_indent_settings = true
+resharper_braces_for_for = required
+resharper_braces_for_foreach = required
+resharper_braces_for_ifelse = required
+resharper_braces_for_while = required
+resharper_cpp_insert_final_newline = true
+resharper_csharp_empty_block_style = together_same_line
+resharper_csharp_int_align_comments = true
+resharper_csharp_wrap_after_declaration_lpar = true
+resharper_csharp_wrap_after_invocation_lpar = true
+resharper_csharp_wrap_arguments_style = chop_if_long
+resharper_csharp_wrap_before_declaration_rpar = true
+resharper_csharp_wrap_before_invocation_rpar = true
+resharper_csharp_wrap_parameters_style = chop_if_long
+resharper_formatter_off_tag = @formatter:off
+resharper_formatter_on_tag = @formatter:on
+resharper_formatter_tags_enabled = true
+resharper_keep_existing_expr_member_arrangement = false
+resharper_keep_existing_initializer_arrangement = false
+resharper_keep_existing_linebreaks = false
+resharper_place_field_attribute_on_same_line = if_owner_is_single_line
+resharper_place_method_attribute_on_same_line = if_owner_is_single_line
+resharper_show_autodetect_configure_formatting_tip = false
+resharper_use_indent_from_vs = false
+resharper_wrap_before_primary_constructor_declaration_rpar = true
+resharper_wrap_chained_method_calls = chop_always
+
+# ReSharper inspection severities
+resharper_arrange_redundant_parentheses_highlighting = hint
+resharper_arrange_this_qualifier_highlighting = hint
+resharper_arrange_type_member_modifiers_highlighting = hint
+resharper_arrange_type_modifiers_highlighting = hint
+resharper_built_in_type_reference_style_for_member_access_highlighting = hint
+resharper_built_in_type_reference_style_highlighting = hint
+resharper_enforce_do_while_statement_braces_highlighting = warning
+resharper_enforce_fixed_statement_braces_highlighting = warning
+resharper_enforce_foreach_statement_braces_highlighting = warning
+resharper_enforce_for_statement_braces_highlighting = warning
+resharper_enforce_if_statement_braces_highlighting = warning
+resharper_enforce_lock_statement_braces_highlighting = warning
+resharper_enforce_using_statement_braces_highlighting = warning
+resharper_enforce_while_statement_braces_highlighting = warning
+resharper_mvc_action_not_resolved_highlighting = warning
+resharper_mvc_area_not_resolved_highlighting = warning
+resharper_mvc_controller_not_resolved_highlighting = warning
+resharper_mvc_masterpage_not_resolved_highlighting = warning
+resharper_mvc_partial_view_not_resolved_highlighting = warning
+resharper_mvc_template_not_resolved_highlighting = warning
+resharper_mvc_view_component_not_resolved_highlighting = warning
+resharper_mvc_view_component_view_not_resolved_highlighting = warning
+resharper_mvc_view_not_resolved_highlighting = warning
+resharper_razor_assembly_not_resolved_highlighting = warning
+resharper_redundant_base_qualifier_highlighting = warning
+resharper_suggest_var_or_type_built_in_types_highlighting = hint
+resharper_suggest_var_or_type_elsewhere_highlighting = hint
+resharper_suggest_var_or_type_simple_types_highlighting = hint
+resharper_web_config_module_not_resolved_highlighting = warning
+resharper_web_config_type_not_resolved_highlighting = warning
+resharper_web_config_wrong_module_highlighting = warning
+
+[{*.yaml,*.yml}]
+indent_style = space
+indent_size = 2
+
+[{tsconfig.app.json,tsconfig.base.json,tsconfig.e2e.json,tsconfig.editor.json,tsconfig.json,tsconfig.lib.json,tsconfig.spec.json,tsconfig.test.json}]
+indent_style = space
+indent_size = 2
+
+[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}]
+indent_style = space
+indent_size = 2
+
+[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,pssl,pssli,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}]
+indent_style = space
+indent_size = 4
+tab_width = 4
diff --git a/wolverine/a-frame-architecture/.gitignore b/wolverine/a-frame-architecture/.gitignore
new file mode 100644
index 0000000..add57be
--- /dev/null
+++ b/wolverine/a-frame-architecture/.gitignore
@@ -0,0 +1,5 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/.gitignore b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/.gitignore
new file mode 100644
index 0000000..6d7db21
--- /dev/null
+++ b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/.gitignore
@@ -0,0 +1,15 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/projectSettingsUpdater.xml
+/modules.xml
+/.idea.a-frame-architecture.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/encodings.xml b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/indexLayout.xml b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/mise.xml b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/mise.xml
new file mode 100644
index 0000000..a5fff2e
--- /dev/null
+++ b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/mise.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/vcs.xml b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/vcs.xml
new file mode 100644
index 0000000..b2bdec2
--- /dev/null
+++ b/wolverine/a-frame-architecture/.idea/.idea.a-frame-architecture/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs
new file mode 100644
index 0000000..8739c27
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs
@@ -0,0 +1,129 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Handlers;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Exceptions;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.Extensions.Time.Testing;
+
+namespace FinancialApi.Application.UnitTests;
+
+public class ApplyFeeHandlerTests
+{
+ public class SpendAccount
+ {
+ [Theory]
+ [InlineData(100.0, 20.0, 80.0)]
+ [InlineData(199.78, 10.0, 189.78)]
+ [InlineData(200000, 0.09, 199999.91)]
+ [InlineData(1000, 1000, 0)]
+ public void GivenStartingBalanceGreaterThanFee_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new ApplyFeeCommand(1, feeAmount);
+ var account = new Account(1, startingBalance, AccountType.Spend);
+ var revenueAccount = new Account(99999, 0, AccountType.Revenue);
+ var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ }
+
+ [Theory]
+ [InlineData(100.0, 110.0)]
+ [InlineData(199.78, 200.0)]
+ [InlineData(200000, 200000.01)]
+ [InlineData(1000, 1999999)]
+ public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldFail(decimal startingBalance, decimal feeAmount)
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new ApplyFeeCommand(1, feeAmount);
+ var account = new Account(1, startingBalance, AccountType.Spend);
+ var revenueAccount = new Account(99999, 0, AccountType.Revenue);
+ var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var act = () => ApplyFeeHandler.Handle(command, feeContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ act.Should()
+ .Throw();
+ }
+ }
+
+ public class BorrowAccount
+ {
+ [Theory]
+ [InlineData(100.0, 20.0, 80.0)]
+ [InlineData(199.78, 10.0, 189.78)]
+ [InlineData(200000, 0.09, 199999.91)]
+ [InlineData(1000, 1000, 0)]
+ public void GivenStartingBalanceGreaterThanFee_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new ApplyFeeCommand(1, feeAmount);
+ var account = new Account(1, startingBalance, AccountType.Borrow);
+ var revenueAccount = new Account(99999, 0, AccountType.Revenue);
+ var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ }
+
+ [Theory]
+ [InlineData(100.0, 110.0, -10.0)]
+ [InlineData(199.78, 200.0, -0.22)]
+ [InlineData(200000, 200000.01, -0.01)]
+ [InlineData(1000, 1999999, -1998999)]
+ public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new ApplyFeeCommand(1, feeAmount);
+ var account = new Account(1, startingBalance, AccountType.Borrow);
+ var revenueAccount = new Account(99999, 0, AccountType.Revenue);
+ var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ revenueWrite.Entity.Balance.Should()
+ .Be(feeAmount);
+ journalWrite.Should()
+ .NotBeNull();
+ @event.Should()
+ .NotBeNull();
+ }
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/FinancialApi.Application.UnitTests.csproj b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/FinancialApi.Application.UnitTests.csproj
new file mode 100644
index 0000000..59ef367
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/FinancialApi.Application.UnitTests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\wolverinefx\6.18.0\lib\net10.0\Wolverine.dll
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ReverseJournalHandlerTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ReverseJournalHandlerTests.cs
new file mode 100644
index 0000000..c03449d
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ReverseJournalHandlerTests.cs
@@ -0,0 +1,67 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Handlers;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Events;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.Extensions.Time.Testing;
+using Wolverine.Persistence;
+
+namespace FinancialApi.Application.UnitTests;
+
+public class ReverseJournalHandlerTests
+{
+ [Fact]
+ public void GivenValidJournalEntry_Reversal_ShouldResetAccountBalances()
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var expectedSourceAccountBalance = 100.0m;
+ var expectedDestAccountBalance = 100.0m;
+ var (initialSourceAccountWrite, initialDestAccountWrite, initialJournalWrite, initialEvent) =
+ CreateInitialTransferTransaction(
+ expectedSourceAccountBalance,
+ expectedDestAccountBalance,
+ 10,
+ timeProvider
+ );
+ var command = new ReverseJournalCommand(initialJournalWrite.Entity.Id, "Invalid Transaction");
+ var context = new JournalReversalContext(
+ initialJournalWrite.Entity,
+ [initialSourceAccountWrite.Entity, initialDestAccountWrite.Entity],
+ timeProvider.GetUtcNow()
+ );
+
+ // Act
+ var (journalWrite, accountWrites, @event) = ReverseJournalHandler.Handle(command, context);
+
+ // Assert
+ using var _ = new AssertionScope();
+ accountWrites[0]
+ .Entity.Balance.Should()
+ .Be(expectedSourceAccountBalance);
+ accountWrites[1]
+ .Entity.Balance.Should()
+ .Be(expectedDestAccountBalance);
+ journalWrite.Entity.Lines.Should()
+ .HaveCount(initialJournalWrite.Entity.Lines.Count);
+ }
+
+ private (IStorageAction SourceWrite, IStorageAction DestWrite, IStorageAction
+ JournalWrite, FundsTransferredEvent Event) CreateInitialTransferTransaction(
+ decimal sourceStartingBalance,
+ decimal destStartingBalance,
+ decimal transferAmount,
+ TimeProvider timeProvider
+ )
+ {
+ var command = new TransferFundsCommand(1, 2, transferAmount);
+ var sourceAccount = new Account(1, sourceStartingBalance, AccountType.Spend);
+ var destAccount = new Account(2, destStartingBalance, AccountType.Spend);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ return TransferFundsHandler.Handle(command, transferContext);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/TransferFundsHandlerTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/TransferFundsHandlerTests.cs
new file mode 100644
index 0000000..781d83a
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/TransferFundsHandlerTests.cs
@@ -0,0 +1,154 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Handlers;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Exceptions;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.Extensions.Time.Testing;
+
+namespace FinancialApi.Application.UnitTests;
+
+public class TransferFundsHandlerTests
+{
+ public class LiabilityAccount
+ {
+ [Theory]
+ [InlineData(100.0, 20.0, 80.0)]
+ [InlineData(199.78, 10.0, 189.78)]
+ [InlineData(200000, 0.09, 199999.91)]
+ [InlineData(1000, 1000, 0)]
+ public void GivenStartingBalanceGreaterThanTransfer_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new TransferFundsCommand(1, 2, feeAmount);
+ var sourceAccount = new Account(1, startingBalance, AccountType.Spend);
+ var destAccount = new Account(2, 0, AccountType.Spend);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) =
+ TransferFundsHandler.Handle(command, transferContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ }
+
+ [Theory]
+ [InlineData(100.0, 110.0)]
+ [InlineData(199.78, 200.0)]
+ [InlineData(200000, 200000.01)]
+ [InlineData(1000, 1999999)]
+ public void GivenStartingBalanceLessThanTransfer_ApplyFee_ShouldFail(decimal startingBalance, decimal feeAmount)
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new TransferFundsCommand(1, 2, feeAmount);
+ var sourceAccount = new Account(1, startingBalance, AccountType.Spend);
+ var destAccount = new Account(2, 0, AccountType.Spend);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var act = () => TransferFundsHandler.Handle(command, transferContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ act.Should()
+ .Throw();
+ }
+ }
+
+ public class CreditAccount
+ {
+ [Theory]
+ [InlineData(100.0, 20.0, 80.0)]
+ [InlineData(199.78, 10.0, 189.78)]
+ [InlineData(200000, 0.09, 199999.91)]
+ [InlineData(1000, 1000, 0)]
+ public void GivenStartingBalanceGreaterThanFee_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new TransferFundsCommand(1, 2, feeAmount);
+ var sourceAccount = new Account(1, startingBalance, AccountType.Borrow);
+ var destAccount = new Account(2, 0, AccountType.Borrow);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) =
+ TransferFundsHandler.Handle(command, transferContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ }
+
+ [Theory]
+ [InlineData(100.0, 110.0, -10.0)]
+ [InlineData(199.78, 200.0, -0.22)]
+ [InlineData(200000, 200000.01, -0.01)]
+ [InlineData(1000, 1999999, -1998999)]
+ public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldSucceed(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new TransferFundsCommand(1, 2, feeAmount);
+ var sourceAccount = new Account(1, startingBalance, AccountType.Borrow);
+ var destAccount = new Account(2, 0, AccountType.Borrow);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var act = () => TransferFundsHandler.Handle(command, transferContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ act.Should()
+ .NotThrow();
+ }
+
+
+ [Theory]
+ [InlineData(100.0, 110.0, -10.0)]
+ [InlineData(199.78, 200.0, -0.22)]
+ [InlineData(200000, 200000.01, -0.01)]
+ [InlineData(1000, 1999999, -1998999)]
+ public void GivenStartingBalanceLessThanFee_ApplyFee_ShouldBeExpectedBalance(
+ decimal startingBalance,
+ decimal feeAmount,
+ decimal expectedBalance
+ )
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ var command = new TransferFundsCommand(1, 2, feeAmount);
+ var sourceAccount = new Account(1, startingBalance, AccountType.Borrow);
+ var destAccount = new Account(2, 0, AccountType.Borrow);
+ var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
+
+ // Act
+ var (customerWrite, revenueWrite, journalWrite, @event) =
+ TransferFundsHandler.Handle(command, transferContext);
+
+ // Assert
+ using var _ = new AssertionScope();
+ customerWrite.Entity.Balance.Should()
+ .Be(expectedBalance);
+ }
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ApplyFeeCommand.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ApplyFeeCommand.cs
new file mode 100644
index 0000000..92fd480
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ApplyFeeCommand.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Application.Commands;
+
+public record ApplyFeeCommand(int AccountId, decimal Amount);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ReverseJournalCommand.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ReverseJournalCommand.cs
new file mode 100644
index 0000000..66e954b
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/ReverseJournalCommand.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Application.Commands;
+
+public record ReverseJournalCommand(Guid OriginalJournalId, string Reason);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Commands/TransferFundsCommand.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/TransferFundsCommand.cs
new file mode 100644
index 0000000..6f39544
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Commands/TransferFundsCommand.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Application.Commands;
+
+public record TransferFundsCommand(int SourceAccountId, int DestinationAccountId, decimal Amount);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj b/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj
new file mode 100644
index 0000000..53a91b8
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\microsoft.extensions.logging.abstractions\10.0.9\lib\net10.0\Microsoft.Extensions.Logging.Abstractions.dll
+
+
+ ..\..\..\..\..\..\.nuget\packages\wolverinefx\6.18.0\lib\net10.0\Wolverine.dll
+
+
+
+
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs
new file mode 100644
index 0000000..c31a65a
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ApplyFeeHandler.cs
@@ -0,0 +1,51 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Aggregates;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Events;
+using Wolverine.Persistence;
+
+namespace FinancialApi.Application.Handlers;
+
+public static class ApplyFeeHandler
+{
+ public static async Task LoadAsync(ApplyFeeCommand cmd, IAccountQuery query, TimeProvider timeProvider)
+ {
+ var source = await query.FindByIdAsync(cmd.AccountId);
+ var dest = await query.FindByIdAsync(99999);
+ if (source == null || dest == null)
+ {
+ throw new InvalidOperationException("Cannot process transfer. Account(s) not found.");
+ }
+
+ return new FeeContext(source, dest, timeProvider.GetUtcNow());
+ }
+
+ public static ( IStorageAction CustomerWrite, IStorageAction RevenueWrite,
+ IStorageAction JournalWrite, FeeAppliedEvent Event ) Handle(
+ ApplyFeeCommand cmd,
+ FeeContext context
+ )
+ {
+ var updatedSource = context.Source.ApplyPosting(cmd.Amount, EntryType.Debit);
+ var updatedRevenue = context.Destination.ApplyPosting(cmd.Amount, EntryType.Credit);
+
+ var lines = new List
+ {
+ new(updatedSource.Id, cmd.Amount, EntryType.Debit), new(updatedRevenue.Id, cmd.Amount, EntryType.Credit)
+ };
+
+ var journalEntry = BalancedJournal.Create(
+ Guid.NewGuid(),
+ $"Service Fee Applied: {cmd.Amount} to Account {cmd.AccountId}",
+ context.TimeStamp,
+ lines
+ );
+
+ var @event = new FeeAppliedEvent(cmd.AccountId, cmd.Amount);
+
+ return (Storage.Update(updatedSource), Storage.Update(updatedRevenue), Storage.Insert(journalEntry.Entry),
+ @event);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/FinancialEventHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/FinancialEventHandler.cs
new file mode 100644
index 0000000..ede771f
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/FinancialEventHandler.cs
@@ -0,0 +1,30 @@
+using FinancialApi.Domain.Events;
+using Microsoft.Extensions.Logging;
+
+namespace FinancialApi.Application.Handlers;
+
+public static class FinancialEventHandler
+{
+ public static void Handle(FeeAppliedEvent @event, IEventTracker store, ILogger logger)
+ {
+ var message = $"FeeApplied: ${@event.Amount} moved from Account {@event.AccountId} to Account 99999.";
+ logger.LogInformation("BACKGROUND EVENT FIRED: {Message}", message);
+ store.Add(message);
+ }
+
+ public static void Handle(FundsTransferredEvent @event, IEventTracker store, ILogger logger)
+ {
+ var message =
+ $"FundsTransferred: ${@event.Amount} moved from Account {@event.SourceAccountId} to Account {@event.DestinationAccountId}.";
+ logger.LogInformation("BACKGROUND EVENT FIRED: {Message}", message);
+ store.Add(message);
+ }
+
+ public static void Handle(JournalReversedEvent @event, IEventTracker store, ILogger logger)
+ {
+ var message =
+ $"JournalReversed: Original Entry {@event.OriginalJournalId} was reversed by Entry {@event.ReversalJournalId}.";
+ logger.LogInformation("BACKGROUND EVENT FIRED: {Message}", message);
+ store.Add(message);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs
new file mode 100644
index 0000000..df8260e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/ReverseJournalHandler.cs
@@ -0,0 +1,59 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Aggregates;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Events;
+using Wolverine.Persistence;
+
+namespace FinancialApi.Application.Handlers;
+
+public static class ReverseJournalHandler
+{
+ public static async Task LoadAsync(
+ ReverseJournalCommand cmd,
+ IReversalQuery query,
+ TimeProvider timeProvider
+ )
+ {
+ 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
+ Event) Handle(ReverseJournalCommand cmd, JournalReversalContext context)
+ {
+ var reversalJournal = BalancedJournal.CreateReversal(
+ context.OriginalBalancedJournalEntry,
+ $"Reversal: {cmd.Reason}",
+ context.TimeStamp
+ );
+ var accountState = context.AffectedAccounts.ToDictionary(a => a.Id);
+
+ reversalJournal.Entry.Lines.ForEach(l =>
+ {
+ var currentAccount = accountState[l.AccountId];
+ accountState[l.AccountId] = currentAccount.ApplyPosting(l.Amount, l.Type);
+ }
+ );
+ var accountUow = new UnitOfWork();
+ foreach (var account in accountState.Values)
+ {
+ accountUow.Update(account);
+ }
+
+ return (Storage.Insert(reversalJournal.Entry), accountUow,
+ new JournalReversedEvent(cmd.OriginalJournalId, reversalJournal.Entry.Id));
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs
new file mode 100644
index 0000000..4fc117f
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Handlers/TransferFundsHandler.cs
@@ -0,0 +1,53 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Application.Models;
+using FinancialApi.Domain.Aggregates;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Events;
+using Wolverine.Persistence;
+
+namespace FinancialApi.Application.Handlers;
+
+public static class TransferFundsHandler
+{
+ public static async Task LoadAsync(
+ TransferFundsCommand cmd,
+ IAccountQuery query,
+ TimeProvider timeProvider
+ )
+ {
+ var source = await query.FindByIdAsync(cmd.SourceAccountId);
+ var dest = await query.FindByIdAsync(cmd.DestinationAccountId);
+
+ if (source == null || dest == null)
+ {
+ return null;
+ }
+
+ return new TransferContext(source, dest, timeProvider.GetUtcNow());
+ }
+
+ public static (IStorageAction SourceWrite, IStorageAction DestWrite, IStorageAction
+ JournalWrite, FundsTransferredEvent Event) Handle(TransferFundsCommand cmd, TransferContext context)
+ {
+ var updatedSource = context.Source.ApplyPosting(cmd.Amount, EntryType.Debit);
+ var updatedDest = context.Destination.ApplyPosting(cmd.Amount, EntryType.Credit);
+
+ var lines = new List
+ {
+ new(context.Source.Id, cmd.Amount, EntryType.Debit),
+ new(context.Destination.Id, cmd.Amount, EntryType.Credit)
+ };
+
+ var journalEntry = BalancedJournal.Create(
+ Guid.NewGuid(),
+ $"Transfer: {cmd.Amount} from {cmd.SourceAccountId} to {cmd.DestinationAccountId}",
+ context.TimeStamp,
+ lines
+ );
+
+ var @event = new FundsTransferredEvent(cmd.SourceAccountId, cmd.DestinationAccountId, cmd.Amount);
+
+ return (Storage.Update(updatedSource), Storage.Update(updatedDest), Storage.Insert(journalEntry.Entry), @event);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs
new file mode 100644
index 0000000..b074d23
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IAccountQuery.cs
@@ -0,0 +1,8 @@
+using FinancialApi.Domain.Entities;
+
+namespace FinancialApi.Application.Interfaces;
+
+public interface IAccountQuery
+{
+ Task FindByIdAsync(int id);
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs
new file mode 100644
index 0000000..d663373
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Interfaces/IReversalQuery.cs
@@ -0,0 +1,8 @@
+using FinancialApi.Domain.Entities;
+
+namespace FinancialApi.Application.Interfaces;
+
+public interface IReversalQuery
+{
+ Task<(BalancedJournalEntry?, 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
new file mode 100644
index 0000000..bfc40f5
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/FeeContext.cs
@@ -0,0 +1,5 @@
+using FinancialApi.Domain.Entities;
+
+namespace FinancialApi.Application.Models;
+
+public record FeeContext(Account Source, Account Destination, DateTimeOffset TimeStamp);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs
new file mode 100644
index 0000000..9a9ed61
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/JournalReversalContext.cs
@@ -0,0 +1,9 @@
+using FinancialApi.Domain.Entities;
+
+namespace FinancialApi.Application.Models;
+
+public record JournalReversalContext(
+ BalancedJournalEntry OriginalBalancedJournalEntry,
+ IReadOnlyList AffectedAccounts,
+ DateTimeOffset TimeStamp
+);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs
new file mode 100644
index 0000000..c517ed4
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Application/Models/TransferContext.cs
@@ -0,0 +1,5 @@
+using FinancialApi.Domain.Entities;
+
+namespace FinancialApi.Application.Models;
+
+public record TransferContext(Account Source, Account Destination, DateTimeOffset TimeStamp);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/AccountTests.cs b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/AccountTests.cs
new file mode 100644
index 0000000..c669abd
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/AccountTests.cs
@@ -0,0 +1,55 @@
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Exceptions;
+using FluentAssertions;
+
+namespace FinancialApi.Domain.Tests;
+
+public class AccountTests
+{
+ [Theory]
+ [InlineData(AccountType.Borrow)]
+ public void CreateBorrowAccountType_ShouldAllowNegativeBalance(AccountType accountType)
+ {
+ var act = () => new Account(1, -100, accountType);
+
+ act()
+ .Balance.Should()
+ .BeNegative();
+ }
+
+ [Theory]
+ [InlineData(AccountType.Revenue)]
+ [InlineData(AccountType.Spend)]
+ public void CreateDebitAccountType_ShouldNotAllowNegativeBalance(AccountType accountType)
+ {
+ var act = () => new Account(1, -100, accountType);
+
+ act.Should()
+ .Throw();
+ }
+
+ [Theory]
+ [InlineData(AccountType.Borrow)]
+ public void GivenCreditAccountType_ShouldAllowNegativeBalance(AccountType accountType)
+ {
+ var account = new Account(1, 0, accountType);
+
+ var updatedAccount = account.ApplyPosting(100, EntryType.Debit);
+
+ updatedAccount.Balance.Should()
+ .BeNegative();
+ }
+
+ [Theory]
+ [InlineData(AccountType.Revenue)]
+ [InlineData(AccountType.Spend)]
+ public void GivenDebitAccountType_ShouldNotAllowNegativeBalance(AccountType accountType)
+ {
+ var account = new Account(1, 0, accountType);
+
+ var act = () => account.ApplyPosting(100, EntryType.Debit);
+
+ act.Should()
+ .Throw();
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/BalancedJournalEntryTests.cs b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/BalancedJournalEntryTests.cs
new file mode 100644
index 0000000..8ae24f9
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/BalancedJournalEntryTests.cs
@@ -0,0 +1,54 @@
+using FinancialApi.Domain.Aggregates;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Exceptions;
+using FluentAssertions;
+using Microsoft.Extensions.Time.Testing;
+
+namespace FinancialApi.Domain.Tests;
+
+public class BalancedJournalEntryTests
+{
+ [Fact]
+ public void GivenBalanceJournal_ShouldSucceed()
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ List lines = [new(1, 100, EntryType.Debit), new(1, 100, EntryType.Credit)];
+
+ // Act
+ var act = () => BalancedJournal.Create(Guid.NewGuid(), "Transfer", timeProvider.GetUtcNow(), lines);
+
+ // Assert
+ act.Should()
+ .NotThrow();
+ }
+
+ [Fact]
+ public void GivenUnbalancedJournal_ShouldFail()
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+ List lines = [new(1, 10, EntryType.Debit), new(1, 100, EntryType.Credit)];
+
+ // Act
+ var act = () => BalancedJournal.Create(Guid.NewGuid(), "Transfer", timeProvider.GetUtcNow(), lines);
+
+ // Assert
+ act.Should()
+ .Throw();
+ }
+
+ [Fact]
+ public void GivenEmptyJournal_ShouldFail()
+ {
+ // Arrange
+ var timeProvider = new FakeTimeProvider();
+
+ // Act
+ var act = () => BalancedJournal.Create(Guid.NewGuid(), "Transfer", timeProvider.GetUtcNow(), []);
+
+ // Assert
+ act.Should()
+ .Throw();
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/FinancialApi.Domain.Tests.csproj b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/FinancialApi.Domain.Tests.csproj
new file mode 100644
index 0000000..d2a332c
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain.Tests/FinancialApi.Domain.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\microsoft.extensions.timeprovider.testing\10.6.0\lib\net10.0\Microsoft.Extensions.TimeProvider.Testing.dll
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Aggregates/BalancedJournal.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Aggregates/BalancedJournal.cs
new file mode 100644
index 0000000..8bc5af1
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Aggregates/BalancedJournal.cs
@@ -0,0 +1,46 @@
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Exceptions;
+
+namespace FinancialApi.Domain.Aggregates;
+
+public record BalancedJournal
+{
+ public BalancedJournalEntry Entry { get; }
+
+ private BalancedJournal(BalancedJournalEntry entry)
+ {
+ Entry = entry;
+ }
+
+ public static BalancedJournal Create(Guid id, string description, DateTimeOffset createdAt, List lines)
+ {
+ if (lines.Count(l => l.Type == EntryType.Debit) == 0)
+ {
+ throw new GaapViolationException($"GAAP Violation: A journal entry needs at least one debit line.");
+ }
+
+ if (lines.Count(l => l.Type == EntryType.Credit) == 0)
+ {
+ throw new GaapViolationException($"A journal entry needs at least one credit line.");
+ }
+
+ var debits = lines.Where(l => l.Type == EntryType.Debit)
+ .Sum(l => l.Amount);
+ var credits = lines.Where(l => l.Type == EntryType.Credit)
+ .Sum(l => l.Amount);
+
+ return debits != credits
+ ? throw new GaapViolationException($"Total Debits ({debits}) must equal Total Credits ({credits})!")
+ : new BalancedJournal(new BalancedJournalEntry(id, description, createdAt, lines));
+ }
+
+ public static BalancedJournal CreateReversal(BalancedJournalEntry original, string reason, DateTimeOffset now)
+ {
+ var reversedLines = original.Lines.Select(l =>
+ l with { Type = l.Type == EntryType.Debit ? EntryType.Credit : EntryType.Debit }
+ )
+ .ToList();
+
+ return Create(Guid.NewGuid(), reason, now, reversedLines);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs
new file mode 100644
index 0000000..29d5c8f
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs
@@ -0,0 +1,53 @@
+using FinancialApi.Domain.Exceptions;
+
+namespace FinancialApi.Domain.Entities;
+
+public enum AccountType
+{
+ Spend,
+ Borrow,
+ Revenue
+}
+
+public record Account
+{
+ public Account(int Id, decimal Balance, AccountType Type)
+ {
+ this.Id = Id;
+ this.Balance = Balance;
+ this.Type = Type;
+
+ if (Balance < 0 && Type is AccountType.Spend or AccountType.Revenue)
+ {
+ throw new InvalidStartingBalanceException("Insufficient funds.");
+ }
+ }
+
+ public Account ApplyPosting(decimal amount, EntryType entryType)
+ {
+ if (amount <= 0)
+ {
+ throw new ArgumentException("Amount must be positive.");
+ }
+
+ var newBalance = entryType == EntryType.Credit ? Balance + amount : Balance - amount;
+
+ if (newBalance < 0 && Type is AccountType.Spend or AccountType.Revenue)
+ {
+ throw new InsufficientFundsException("Insufficient funds.");
+ }
+
+ return this with { Balance = newBalance };
+ }
+
+ public int Id { get; init; }
+ public decimal Balance { get; init; }
+ public AccountType Type { get; init; }
+
+ public void Deconstruct(out int Id, out decimal Balance, out AccountType Type)
+ {
+ Id = this.Id;
+ Balance = this.Balance;
+ Type = this.Type;
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/BalancedJournalEntry.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/BalancedJournalEntry.cs
new file mode 100644
index 0000000..7e5cd8e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/BalancedJournalEntry.cs
@@ -0,0 +1,19 @@
+namespace FinancialApi.Domain.Entities;
+
+public record BalancedJournalEntry
+{
+ public Guid Id { get; init; }
+ public string Description { get; init; } = null!;
+ public DateTimeOffset CreatedAt { get; init; }
+ public List Lines { get; init; } = [];
+
+ internal BalancedJournalEntry() { }
+
+ internal BalancedJournalEntry(Guid id, string description, DateTimeOffset createdAt, List lines)
+ {
+ Lines = lines;
+ Id = id;
+ Description = description;
+ CreatedAt = createdAt;
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/EntryType.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/EntryType.cs
new file mode 100644
index 0000000..491289e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/EntryType.cs
@@ -0,0 +1,7 @@
+namespace FinancialApi.Domain.Entities;
+
+public enum EntryType
+{
+ Debit,
+ Credit
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/JournalLine.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/JournalLine.cs
new file mode 100644
index 0000000..03d4e3b
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/JournalLine.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Entities;
+
+public record JournalLine(int AccountId, decimal Amount, EntryType Type);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FeeAppliedEvent.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FeeAppliedEvent.cs
new file mode 100644
index 0000000..5fdaa23
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FeeAppliedEvent.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Events;
+
+public record FeeAppliedEvent(int AccountId, decimal Amount);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FundsTransferredEvent.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FundsTransferredEvent.cs
new file mode 100644
index 0000000..8e28286
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/FundsTransferredEvent.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Events;
+
+public record FundsTransferredEvent(int SourceAccountId, int DestinationAccountId, decimal Amount);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Events/IEventTracker.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/IEventTracker.cs
new file mode 100644
index 0000000..264d02f
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/IEventTracker.cs
@@ -0,0 +1,6 @@
+namespace FinancialApi.Domain.Events;
+
+public interface IEventTracker
+{
+ void Add(string eventMessage);
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Events/JournalReversedEvent.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/JournalReversedEvent.cs
new file mode 100644
index 0000000..cf33e38
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Events/JournalReversedEvent.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Events;
+
+public record JournalReversedEvent(Guid OriginalJournalId, Guid ReversalJournalId);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/GaapViolationException.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/GaapViolationException.cs
new file mode 100644
index 0000000..9e44719
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/GaapViolationException.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Exceptions;
+
+public class GaapViolationException(string message) : Exception(message);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InsufficientFundsException.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InsufficientFundsException.cs
new file mode 100644
index 0000000..f8500be
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InsufficientFundsException.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Exceptions;
+
+public class InsufficientFundsException(string message) : GaapViolationException(message);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InvalidStartingBalanceException.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InvalidStartingBalanceException.cs
new file mode 100644
index 0000000..34dabb9
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Exceptions/InvalidStartingBalanceException.cs
@@ -0,0 +1,3 @@
+namespace FinancialApi.Domain.Exceptions;
+
+public class InvalidStartingBalanceException(string message) : GaapViolationException(message);
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialApi.Domain.csproj b/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialApi.Domain.csproj
new file mode 100644
index 0000000..237d661
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Domain/FinancialApi.Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
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.Tests/ApplyFeeIntegrationTests.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/ApplyFeeIntegrationTests.cs
new file mode 100644
index 0000000..8f89e2e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/ApplyFeeIntegrationTests.cs
@@ -0,0 +1,35 @@
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Domain.Entities;
+using FinancialApi.Domain.Events;
+using FluentAssertions;
+using Wolverine;
+using Wolverine.Tracking;
+
+namespace FinancialApi.Infrastructure.Tests;
+
+internal class FakeAccountQuery : IAccountQuery
+{
+ public Task FindByIdAsync(int id) => Task.FromResult(new Account(id, 100.00m, AccountType.Spend))!;
+}
+
+public class ApplyFeeIntegrationTests(WolverineTestFixture fixture) : IClassFixture
+{
+ [Fact]
+ public async Task GivenValidCommand_WhenApplyingFee_ShouldPublishFeeAppliedEvent()
+ {
+ // Arrange
+ var command = new ApplyFeeCommand(1, 100);
+
+ // Act
+ var session = await fixture.Host.InvokeMessageAndWaitAsync(command);
+
+ // Assert
+ session.Sent.AllMessages()
+ .ShouldHaveMessageOfType();
+ session.Sent.MessagesOf()
+ .Count()
+ .Should()
+ .Be(1);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/FinancialApi.Infrastructure.Tests.csproj b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/FinancialApi.Infrastructure.Tests.csproj
new file mode 100644
index 0000000..4a800f9
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/FinancialApi.Infrastructure.Tests.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\microsoft.extensions.hosting\10.0.0\lib\net10.0\Microsoft.Extensions.Hosting.dll
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\microsoft.extensions.timeprovider.testing\10.6.0\lib\net10.0\Microsoft.Extensions.TimeProvider.Testing.dll
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/WolverineTestFixture.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/WolverineTestFixture.cs
new file mode 100644
index 0000000..91e3b3e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure.Tests/WolverineTestFixture.cs
@@ -0,0 +1,34 @@
+using FinancialApi.Application.Handlers;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Domain.Events;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Time.Testing;
+using Wolverine;
+
+namespace FinancialApi.Infrastructure.Tests;
+
+public class WolverineTestFixture : IDisposable
+{
+ public IHost Host { get; }
+
+ public WolverineTestFixture()
+ {
+ // We build and start the host ONCE here
+ Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
+ .ConfigureServices((context, services) =>
+ {
+ services.AddSingleton(_ => new FakeAccountQuery());
+ services.AddSingleton(_ => new FakeTimeProvider());
+ services.AddSingleton(_ => new DemoEventStore());
+ }
+ )
+ .UseWolverine(opts => { opts.Discovery.IncludeAssembly(typeof(ApplyFeeHandler).Assembly); })
+ .Start();
+ }
+
+ public void Dispose()
+ {
+ Host.Dispose();
+ }
+}
\ 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
new file mode 100644
index 0000000..5ed211a
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountDbContext.cs
@@ -0,0 +1,42 @@
+using FinancialApi.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace FinancialApi.Infrastructure;
+
+public class AccountDbContext(DbContextOptions options) : DbContext(options)
+{
+ public DbSet Accounts => Set();
+ public DbSet JournalEntries => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasKey(x => x.Id);
+
+ modelBuilder.Entity()
+ .HasData(
+ new Account(1, 10000000.00m, AccountType.Borrow),
+ new Account(2, 50000000.00m, AccountType.Spend),
+ new Account(99999, 0.00m, AccountType.Revenue)
+ );
+
+ modelBuilder.Entity(builder =>
+ {
+ builder.HasKey(x => x.Id);
+ builder.OwnsMany(
+ x => x.Lines,
+ line =>
+ {
+ line.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ line.Property("Id")
+ .ValueGeneratedOnAdd();
+ line.HasKey("Id");
+ line.Property(x => x.Type)
+ .HasConversion();
+ }
+ );
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs
new file mode 100644
index 0000000..13901db
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/AccountQuery.cs
@@ -0,0 +1,14 @@
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace FinancialApi.Infrastructure;
+
+public class AccountQuery(AccountDbContext db) : IAccountQuery
+{
+ public async Task FindByIdAsync(int id)
+ {
+ return await db.Accounts.AsNoTracking()
+ .FirstOrDefaultAsync(x => x.Id == id);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/DemoEventStore.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/DemoEventStore.cs
new file mode 100644
index 0000000..c00b0be
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/DemoEventStore.cs
@@ -0,0 +1,13 @@
+using FinancialApi.Domain.Events;
+
+namespace FinancialApi.Infrastructure;
+
+public class DemoEventStore : IEventTracker
+{
+ public List CapturedEvents { get; } = new();
+
+ public void Add(string eventMessage)
+ {
+ CapturedEvents.Add($"[{DateTimeOffset.UtcNow:HH:mm:ss}] {eventMessage}");
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj
new file mode 100644
index 0000000..af19738
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs
new file mode 100644
index 0000000..b101970
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs
@@ -0,0 +1,50 @@
+//
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ [Migration("20260713152041_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Application.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Balance = 100.00m
+ },
+ new
+ {
+ Id = 2,
+ Balance = 500.00m
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs
new file mode 100644
index 0000000..b5cfec2
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs
@@ -0,0 +1,45 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Accounts",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Balance = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Accounts", x => x.Id);
+ });
+
+ migrationBuilder.InsertData(
+ table: "Accounts",
+ columns: new[] { "Id", "Balance" },
+ values: new object[,]
+ {
+ { 1, 100.00m },
+ { 2, 500.00m }
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Accounts");
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.Designer.cs
new file mode 100644
index 0000000..b67f0c2
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.Designer.cs
@@ -0,0 +1,115 @@
+//
+using System;
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ [Migration("20260715093924_AddJournal")]
+ partial class AddJournal
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Application.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccountType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ AccountType = "Credit",
+ Balance = 100.00m
+ },
+ new
+ {
+ Id = 2,
+ AccountType = "Liability",
+ Balance = 500.00m
+ },
+ new
+ {
+ Id = 99999,
+ AccountType = "Revenue",
+ Balance = 0.00m
+ });
+ });
+
+ modelBuilder.Entity("FinancialApi.Application.JournalEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JournalEntries");
+ });
+
+ modelBuilder.Entity("FinancialApi.Application.JournalEntry", b =>
+ {
+ b.OwnsMany("FinancialApi.Application.JournalLine", "Lines", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b1.Property("AccountId")
+ .HasColumnType("INTEGER");
+
+ b1.Property("Amount")
+ .HasColumnType("TEXT");
+
+ b1.Property("JournalEntryId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("JournalEntryId");
+
+ b1.ToTable("JournalLine");
+
+ b1.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ });
+
+ b.Navigation("Lines");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.cs
new file mode 100644
index 0000000..316b75c
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715093924_AddJournal.cs
@@ -0,0 +1,100 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ ///
+ public partial class AddJournal : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AccountType",
+ table: "Accounts",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.CreateTable(
+ name: "JournalEntries",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ Description = table.Column(type: "TEXT", nullable: false),
+ CreatedAt = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_JournalEntries", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "JournalLine",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ AccountId = table.Column(type: "INTEGER", nullable: false),
+ Amount = table.Column(type: "TEXT", nullable: false),
+ Type = table.Column(type: "TEXT", nullable: false),
+ JournalEntryId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_JournalLine", x => x.Id);
+ table.ForeignKey(
+ name: "FK_JournalLine_JournalEntries_JournalEntryId",
+ column: x => x.JournalEntryId,
+ principalTable: "JournalEntries",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "AccountType",
+ value: "Credit");
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 2,
+ column: "AccountType",
+ value: "Liability");
+
+ migrationBuilder.InsertData(
+ table: "Accounts",
+ columns: new[] { "Id", "AccountType", "Balance" },
+ values: new object[] { 99999, "Revenue", 0.00m });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_JournalLine_JournalEntryId",
+ table: "JournalLine",
+ column: "JournalEntryId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "JournalLine");
+
+ migrationBuilder.DropTable(
+ name: "JournalEntries");
+
+ migrationBuilder.DeleteData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 99999);
+
+ migrationBuilder.DropColumn(
+ name: "AccountType",
+ table: "Accounts");
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.Designer.cs
new file mode 100644
index 0000000..00ec55f
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.Designer.cs
@@ -0,0 +1,115 @@
+//
+using System;
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ [Migration("20260715100428_UpdateAccountBalances")]
+ partial class UpdateAccountBalances
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Application.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccountType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ AccountType = "Credit",
+ Balance = 10000000.00m
+ },
+ new
+ {
+ Id = 2,
+ AccountType = "Liability",
+ Balance = 50000000.00m
+ },
+ new
+ {
+ Id = 99999,
+ AccountType = "Revenue",
+ Balance = 0.00m
+ });
+ });
+
+ modelBuilder.Entity("FinancialApi.Application.JournalEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JournalEntries");
+ });
+
+ modelBuilder.Entity("FinancialApi.Application.JournalEntry", b =>
+ {
+ b.OwnsMany("FinancialApi.Application.JournalLine", "Lines", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b1.Property("AccountId")
+ .HasColumnType("INTEGER");
+
+ b1.Property("Amount")
+ .HasColumnType("TEXT");
+
+ b1.Property("JournalEntryId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("JournalEntryId");
+
+ b1.ToTable("JournalLine");
+
+ b1.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ });
+
+ b.Navigation("Lines");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.cs
new file mode 100644
index 0000000..5cb1170
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260715100428_UpdateAccountBalances.cs
@@ -0,0 +1,46 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ ///
+ public partial class UpdateAccountBalances : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "Balance",
+ value: 10000000.00m);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 2,
+ column: "Balance",
+ value: 50000000.00m);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "Balance",
+ value: 100.00m);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 2,
+ column: "Balance",
+ value: 500.00m);
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.Designer.cs
new file mode 100644
index 0000000..76231fa
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.Designer.cs
@@ -0,0 +1,114 @@
+//
+using System;
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ [Migration("20260729103021_AccountTypes")]
+ partial class AccountTypes
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Balance = 10000000.00m,
+ Type = 2
+ },
+ new
+ {
+ Id = 2,
+ Balance = 50000000.00m,
+ Type = 0
+ },
+ new
+ {
+ Id = 99999,
+ Balance = 0.00m,
+ Type = 3
+ });
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.JournalEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JournalEntries");
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.JournalEntry", b =>
+ {
+ b.OwnsMany("FinancialApi.Domain.Entities.JournalLine", "Lines", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b1.Property("AccountId")
+ .HasColumnType("INTEGER");
+
+ b1.Property("Amount")
+ .HasColumnType("TEXT");
+
+ b1.Property("JournalEntryId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("JournalEntryId");
+
+ b1.ToTable("JournalLine");
+
+ b1.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ });
+
+ b.Navigation("Lines");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.cs
new file mode 100644
index 0000000..2e06369
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260729103021_AccountTypes.cs
@@ -0,0 +1,82 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ ///
+ public partial class AccountTypes : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "AccountType",
+ table: "Accounts");
+
+ migrationBuilder.AddColumn(
+ name: "Type",
+ table: "Accounts",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "Type",
+ value: 2);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 2,
+ column: "Type",
+ value: 0);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 99999,
+ column: "Type",
+ value: 3);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Type",
+ table: "Accounts");
+
+ migrationBuilder.AddColumn(
+ name: "AccountType",
+ table: "Accounts",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "AccountType",
+ value: "Credit");
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 2,
+ column: "AccountType",
+ value: "Liability");
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 99999,
+ column: "AccountType",
+ value: "Revenue");
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.Designer.cs
new file mode 100644
index 0000000..8601062
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.Designer.cs
@@ -0,0 +1,114 @@
+//
+using System;
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ [Migration("20260731073046_UpdateAccountTypes")]
+ partial class UpdateAccountTypes
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Balance = 10000000.00m,
+ Type = 1
+ },
+ new
+ {
+ Id = 2,
+ Balance = 50000000.00m,
+ Type = 0
+ },
+ new
+ {
+ Id = 99999,
+ Balance = 0.00m,
+ Type = 2
+ });
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.BalancedJournalEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JournalEntries");
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.BalancedJournalEntry", b =>
+ {
+ b.OwnsMany("FinancialApi.Domain.Entities.JournalLine", "Lines", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b1.Property("AccountId")
+ .HasColumnType("INTEGER");
+
+ b1.Property("Amount")
+ .HasColumnType("TEXT");
+
+ b1.Property("JournalEntryId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("JournalEntryId");
+
+ b1.ToTable("JournalLine");
+
+ b1.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ });
+
+ b.Navigation("Lines");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.cs
new file mode 100644
index 0000000..ce3ebb9
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260731073046_UpdateAccountTypes.cs
@@ -0,0 +1,46 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ ///
+ public partial class UpdateAccountTypes : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "Type",
+ value: 1);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 99999,
+ column: "Type",
+ value: 2);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 1,
+ column: "Type",
+ value: 2);
+
+ migrationBuilder.UpdateData(
+ table: "Accounts",
+ keyColumn: "Id",
+ keyValue: 99999,
+ column: "Type",
+ value: 3);
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs
new file mode 100644
index 0000000..51fd1ec
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs
@@ -0,0 +1,111 @@
+//
+using System;
+using FinancialApi.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FinancialApi.Infrastructure.Migrations
+{
+ [DbContext(typeof(AccountDbContext))]
+ partial class AccountDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Balance")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Accounts");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Balance = 10000000.00m,
+ Type = 1
+ },
+ new
+ {
+ Id = 2,
+ Balance = 50000000.00m,
+ Type = 0
+ },
+ new
+ {
+ Id = 99999,
+ Balance = 0.00m,
+ Type = 2
+ });
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.BalancedJournalEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JournalEntries");
+ });
+
+ modelBuilder.Entity("FinancialApi.Domain.Entities.BalancedJournalEntry", b =>
+ {
+ b.OwnsMany("FinancialApi.Domain.Entities.JournalLine", "Lines", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b1.Property("AccountId")
+ .HasColumnType("INTEGER");
+
+ b1.Property("Amount")
+ .HasColumnType("TEXT");
+
+ b1.Property("JournalEntryId")
+ .HasColumnType("TEXT");
+
+ b1.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("JournalEntryId");
+
+ b1.ToTable("JournalLine");
+
+ b1.WithOwner()
+ .HasForeignKey("JournalEntryId");
+ });
+
+ b.Navigation("Lines");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs
new file mode 100644
index 0000000..2b0a51b
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/ReversalQuery.cs
@@ -0,0 +1,30 @@
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace FinancialApi.Infrastructure;
+
+public class ReversalQuery(AccountDbContext db) : IReversalQuery
+{
+ public async Task<(BalancedJournalEntry?, List?)> GetReversalDataAsync(Guid journalEntryId)
+ {
+ var entry = await db.JournalEntries.Include(x => x.Lines)
+ .AsNoTracking()
+ .FirstOrDefaultAsync(x => x.Id == journalEntryId);
+
+ if (entry == null)
+ {
+ return (null, null);
+ }
+
+ var accountIds = entry.Lines.Select(l => l.AccountId)
+ .Distinct()
+ .ToList();
+
+ var accounts = await db.Accounts.AsNoTracking()
+ .Where(a => accountIds.Contains(a.Id))
+ .ToListAsync();
+
+ return (entry, accounts);
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/Extensions.cs b/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..7f05ac7
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/Extensions.cs
@@ -0,0 +1,135 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace FinancialApi.ServiceDefaults;
+
+// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/aspire/service-defaults
+public static class Extensions
+{
+ private const string HealthEndpointPath = "/health";
+ private const string AlivenessEndpointPath = "/alive";
+
+ public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ }
+ );
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder)
+ where TBuilder : IHostApplicationBuilder
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ }
+ );
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ }
+ )
+ .WithTracing(tracing =>
+ {
+ tracing.AddSource(builder.Environment.ApplicationName)
+ .AddAspNetCoreInstrumentation(tracing =>
+ // Exclude health check requests from tracing
+ tracing.Filter = context =>
+ !context.Request.Path.StartsWithSegments(HealthEndpointPath) &&
+ !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
+ )
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ }
+ );
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder)
+ where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry()
+ .UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static TBuilder AddDefaultHealthChecks(this TBuilder builder)
+ where TBuilder : IHostApplicationBuilder
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks(HealthEndpointPath);
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks(
+ AlivenessEndpointPath,
+ new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }
+ );
+ }
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/FinancialApi.ServiceDefaults.csproj b/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/FinancialApi.ServiceDefaults.csproj
new file mode 100644
index 0000000..37c4266
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi.ServiceDefaults/FinancialApi.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wolverine/a-frame-architecture/FinancialApi/DomainExceptionHandler.cs b/wolverine/a-frame-architecture/FinancialApi/DomainExceptionHandler.cs
new file mode 100644
index 0000000..35cd90e
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/DomainExceptionHandler.cs
@@ -0,0 +1,33 @@
+using FinancialApi.Domain.Exceptions;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+
+namespace FinancialApi;
+
+public class DomainExceptionHandler : IExceptionHandler
+{
+ public async ValueTask TryHandleAsync(
+ HttpContext httpContext,
+ Exception exception,
+ CancellationToken cancellationToken
+ )
+ {
+ if (exception is not (InvalidOperationException or ArgumentException or GaapViolationException))
+ {
+ return false;
+ }
+
+ var problemDetails = new ProblemDetails
+ {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "Domain Validation Error",
+ Detail = exception.Message,
+ Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"
+ };
+
+ httpContext.Response.StatusCode = problemDetails.Status.Value;
+ await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj
new file mode 100644
index 0000000..481994a
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http
new file mode 100644
index 0000000..e6ba523
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http
@@ -0,0 +1,38 @@
+@FinancialApi_HostAddress = https://localhost:7010
+
+### Apply Fee to an Account
+
+POST {{FinancialApi_HostAddress}}/accounts/apply-fee
+Accept: application/json
+Content-Type: application/json
+
+{
+ "accountId": 2,
+ "amount": 100
+}
+
+### Transfer Between Accounts
+
+POST {{FinancialApi_HostAddress}}/accounts/transfer
+Content-Type: application/json
+
+{
+ "sourceAccountId": 1,
+ "destinationAccountId": 2,
+ "amount": 1000.00
+}
+
+### Reverse a Journal Entry
+
+POST {{FinancialApi_HostAddress}}/journal/reverse
+Content-Type: application/json
+
+{
+ "originalJournalId": "E7FB25DB-406F-4063-A4B0-DE6BB8292437",
+ "reason": "Incorrect Account Fee"
+}
+
+### Events
+
+GET {{FinancialApi_HostAddress}}/events
+Content-Type: application/json
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi/Program.cs b/wolverine/a-frame-architecture/FinancialApi/Program.cs
new file mode 100644
index 0000000..d08c147
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/Program.cs
@@ -0,0 +1,103 @@
+using FinancialApi;
+using FinancialApi.Application.Commands;
+using FinancialApi.Application.Handlers;
+using FinancialApi.Application.Interfaces;
+using FinancialApi.Domain.Events;
+using FinancialApi.Infrastructure;
+using FinancialApi.ServiceDefaults;
+using JasperFx.Resources;
+using Microsoft.EntityFrameworkCore;
+using Wolverine;
+using Wolverine.EntityFrameworkCore;
+using Wolverine.Sqlite;
+
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddExceptionHandler();
+builder.Services.AddProblemDetails();
+
+var connectionString = "Data Source=demo.db";
+
+builder.Services.AddDbContext(options => options.UseSqlite(connectionString));
+
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddSingleton();
+
+// Add services to the container.
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+
+builder.AddServiceDefaults();
+
+builder.Host.UseWolverine(opts =>
+ {
+ opts.Discovery.IncludeAssembly(typeof(DemoEventStore).Assembly);
+ opts.Discovery.IncludeAssembly(typeof(ApplyFeeHandler).Assembly);
+ opts.PersistMessagesWithSqlite(connectionString);
+ opts.UseEntityFrameworkCoreTransactions();
+ opts.Policies.AutoApplyTransactions();
+ }
+);
+builder.Host.UseResourceSetupOnStartup();
+
+var app = builder.Build();
+
+app.UseExceptionHandler();
+
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ await db.Database.MigrateAsync();
+}
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+app.UseHttpsRedirection();
+
+app.MapPost(
+ "/accounts/apply-fee",
+ async (ApplyFeeCommand cmd, IMessageBus bus) =>
+ {
+ await bus.InvokeAsync(cmd);
+ return Results.Accepted();
+ }
+);
+
+app.MapPost(
+ "/accounts/transfer",
+ async (TransferFundsCommand cmd, IMessageBus bus) =>
+ {
+ await bus.InvokeAsync(cmd);
+ return Results.Accepted();
+ }
+);
+
+app.MapPost(
+ "/journal/reverse",
+ async (ReverseJournalCommand cmd, IMessageBus bus) =>
+ {
+ await bus.InvokeAsync(cmd);
+ return Results.Accepted();
+ }
+);
+
+app.MapGet(
+ "/events",
+ (IEventTracker tracker) =>
+ {
+ if (tracker is DemoEventStore store)
+ {
+ return Results.Ok(store.CapturedEvents);
+ }
+
+ return Results.NotFound();
+ }
+);
+
+app.Run();
\ No newline at end of file
diff --git a/wolverine/a-frame-architecture/FinancialApi/Properties/launchSettings.json b/wolverine/a-frame-architecture/FinancialApi/Properties/launchSettings.json
new file mode 100644
index 0000000..a91c327
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5115",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7010;http://localhost:5115",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi/appsettings.Development.json b/wolverine/a-frame-architecture/FinancialApi/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/wolverine/a-frame-architecture/FinancialApi/appsettings.json b/wolverine/a-frame-architecture/FinancialApi/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/wolverine/a-frame-architecture/FinancialApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/wolverine/a-frame-architecture/a-frame-architecture.sln b/wolverine/a-frame-architecture/a-frame-architecture.sln
new file mode 100644
index 0000000..03833ff
--- /dev/null
+++ b/wolverine/a-frame-architecture/a-frame-architecture.sln
@@ -0,0 +1,58 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.ServiceDefaults", "FinancialApi.ServiceDefaults\FinancialApi.ServiceDefaults.csproj", "{D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi", "FinancialApi\FinancialApi.csproj", "{9EFD8279-869B-485C-BA02-C983E96348A0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Application", "FinancialApi.Application\FinancialApi.Application.csproj", "{B7EB1ECE-9F7F-440B-8818-28A127A99915}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Infrastructure", "FinancialApi.Infrastructure\FinancialApi.Infrastructure.csproj", "{6AB36AB5-05BD-41C8-9498-63EBDFAF8114}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Domain", "FinancialApi.Domain\FinancialApi.Domain.csproj", "{2DB493D2-1C96-4F7C-9AB6-2846A92860B9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Application.UnitTests", "FinancialApi.Application.UnitTests\FinancialApi.Application.UnitTests.csproj", "{93DBA3AA-3C03-431C-A41C-DDADFAC964D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Domain.Tests", "FinancialApi.Domain.Tests\FinancialApi.Domain.Tests.csproj", "{C15E3FE0-6E3D-4FFC-892E-D469536ADC13}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Infrastructure.Tests", "FinancialApi.Infrastructure.Tests\FinancialApi.Infrastructure.Tests.csproj", "{8C1BA932-16F2-42B3-A07C-C589C31044B3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9EFD8279-869B-485C-BA02-C983E96348A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9EFD8279-869B-485C-BA02-C983E96348A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9EFD8279-869B-485C-BA02-C983E96348A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9EFD8279-869B-485C-BA02-C983E96348A0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2DB493D2-1C96-4F7C-9AB6-2846A92860B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2DB493D2-1C96-4F7C-9AB6-2846A92860B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2DB493D2-1C96-4F7C-9AB6-2846A92860B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2DB493D2-1C96-4F7C-9AB6-2846A92860B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {93DBA3AA-3C03-431C-A41C-DDADFAC964D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {93DBA3AA-3C03-431C-A41C-DDADFAC964D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {93DBA3AA-3C03-431C-A41C-DDADFAC964D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {93DBA3AA-3C03-431C-A41C-DDADFAC964D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C15E3FE0-6E3D-4FFC-892E-D469536ADC13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C15E3FE0-6E3D-4FFC-892E-D469536ADC13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C15E3FE0-6E3D-4FFC-892E-D469536ADC13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C15E3FE0-6E3D-4FFC-892E-D469536ADC13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C1BA932-16F2-42B3-A07C-C589C31044B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C1BA932-16F2-42B3-A07C-C589C31044B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C1BA932-16F2-42B3-A07C-C589C31044B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C1BA932-16F2-42B3-A07C-C589C31044B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user
new file mode 100644
index 0000000..d52150c
--- /dev/null
+++ b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user
@@ -0,0 +1,15 @@
+
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ ForceIncluded
+ <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
diff --git a/wolverine/a-frame-architecture/mise.toml b/wolverine/a-frame-architecture/mise.toml
new file mode 100644
index 0000000..a7a0331
--- /dev/null
+++ b/wolverine/a-frame-architecture/mise.toml
@@ -0,0 +1,20 @@
+[tools]
+dotnet = "10"
+
+[tasks.restore]
+description = "Restore the Project"
+run = "dotnet restore"
+
+[tasks.build]
+depends = ["restore"]
+description = "Build the Project"
+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 \"${usage_name?}\" --startup-project FinancialApi --project FinancialApi.Infrastructure"
\ No newline at end of file