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/FinancialApi.Application.UnitTests/AccountTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/AccountTests.cs index 7c39c9a..43f749d 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/AccountTests.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/AccountTests.cs @@ -5,6 +5,29 @@ namespace FinancialApi.Application.UnitTests; public class AccountTests { + [Theory] + [InlineData(AccountType.Credit)] + public void CreateCreditAccountType_ShouldAllowNegativeBalance(AccountType accountType) + { + var act = () => new Account(1, -100, accountType); + + act() + .Balance.Should() + .BeNegative(); + } + + [Theory] + [InlineData(AccountType.Debit)] + [InlineData(AccountType.Revenue)] + [InlineData(AccountType.Liability)] + public void CreateDebitAccountType_ShouldNotAllowNegativeBalance(AccountType accountType) + { + var act = () => new Account(1, -100, accountType); + + act.Should() + .Throw(); + } + [Theory] [InlineData(AccountType.Credit)] public void GivenCreditAccountType_ShouldAllowNegativeBalance(AccountType accountType) diff --git a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs index 0aead9d..e4ea6b9 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Application.UnitTests/ApplyFeeHandlerTests.cs @@ -50,6 +50,7 @@ public class ApplyFeeHandlerTests decimal expectedBalance ) { + // Arrange var command = new ApplyFeeCommand(1, feeAmount); var account = new Account(1, startingBalance, AccountType.Liability); var revenueAccount = new Account(99999, 0, AccountType.Credit); @@ -83,10 +84,6 @@ public class ApplyFeeHandlerTests using var _ = new AssertionScope(); act.Should() .Throw(); - account.Balance.Should() - .Be(startingBalance); - revenueAccount.Balance.Should() - .Be(0); } } diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs index 361efac..f275278 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/Account.cs @@ -8,8 +8,20 @@ public enum AccountType Revenue } -public record Account(int Id, decimal Balance, AccountType Type) +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.Liability or AccountType.Debit or AccountType.Revenue) + { + throw new InvalidStartingBalanceException("Insufficient funds."); + } + } + public Account ApplyPosting(decimal amount, EntryType entryType) { if (amount <= 0) @@ -18,7 +30,6 @@ public record Account(int Id, decimal Balance, AccountType Type) } var newBalance = entryType == EntryType.Credit ? Balance + amount : Balance - amount; - ; if (newBalance < 0 && Type is AccountType.Liability or AccountType.Debit or AccountType.Revenue) { @@ -27,6 +38,15 @@ public record Account(int Id, decimal Balance, AccountType Type) return this with { Balance = newBalance }; } -} -public class InsufficientFundsException(string insufficientFunds) : Exception(insufficientFunds); \ No newline at end of file + 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/InsufficientFundsException.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/InsufficientFundsException.cs new file mode 100644 index 0000000..96be158 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/InsufficientFundsException.cs @@ -0,0 +1,3 @@ +namespace FinancialApi.Domain.Entities; + +public class InsufficientFundsException(string message) : Exception(message); \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/InvalidStartingBalanceException.cs b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/InvalidStartingBalanceException.cs new file mode 100644 index 0000000..7e9a058 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Domain/Entities/InvalidStartingBalanceException.cs @@ -0,0 +1,3 @@ +namespace FinancialApi.Domain.Entities; + +public class InvalidStartingBalanceException(string message) : Exception(message); \ No newline at end of file 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/AccountDbContextModelSnapshot.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs index 420b8fa..1bafe3a 100644 --- a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs @@ -17,19 +17,18 @@ namespace FinancialApi.Infrastructure.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); - modelBuilder.Entity("FinancialApi.Application.Account", b => + modelBuilder.Entity("FinancialApi.Domain.Entities.Account", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AccountType") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("Balance") .HasColumnType("TEXT"); + b.Property("Type") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.ToTable("Accounts"); @@ -38,24 +37,24 @@ namespace FinancialApi.Infrastructure.Migrations new { Id = 1, - AccountType = "Credit", - Balance = 10000000.00m + Balance = 10000000.00m, + Type = 2 }, new { Id = 2, - AccountType = "Liability", - Balance = 50000000.00m + Balance = 50000000.00m, + Type = 0 }, new { Id = 99999, - AccountType = "Revenue", - Balance = 0.00m + Balance = 0.00m, + Type = 3 }); }); - modelBuilder.Entity("FinancialApi.Application.JournalEntry", b => + modelBuilder.Entity("FinancialApi.Domain.Entities.JournalEntry", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -73,9 +72,9 @@ namespace FinancialApi.Infrastructure.Migrations b.ToTable("JournalEntries"); }); - modelBuilder.Entity("FinancialApi.Application.JournalEntry", b => + modelBuilder.Entity("FinancialApi.Domain.Entities.JournalEntry", b => { - b.OwnsMany("FinancialApi.Application.JournalLine", "Lines", b1 => + b.OwnsMany("FinancialApi.Domain.Entities.JournalLine", "Lines", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() diff --git a/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user index 40f1a89..e5be057 100644 --- a/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user +++ b/wolverine/a-frame-architecture/a-frame-architecture.sln.DotSettings.user @@ -1,6 +1,7 @@  ForceIncluded ForceIncluded + ForceIncluded ForceIncluded <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution />