Unit test improvements.
This commit is contained in:
+20
-37
@@ -2,39 +2,15 @@
|
||||
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
|
||||
{
|
||||
[Fact]
|
||||
public void GivenValidAccounts_BasicFee_ShouldValidateEverything()
|
||||
{
|
||||
// Arrange
|
||||
var command = new ApplyFeeCommand(1, 100);
|
||||
var account = new Account(1, 100, AccountType.Debit);
|
||||
var revenueAccount = new Account(99999, 0, AccountType.Credit);
|
||||
var pair = new FeeContext(account, revenueAccount, TimeProvider.System.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var intents = ApplyFeeHandler.Handle(command, pair);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
intents.CustomerWrite.Entity.Balance.Should()
|
||||
.Be(account.Balance - command.Amount);
|
||||
intents.RevenueWrite.Entity.Balance.Should()
|
||||
.Be(revenueAccount.Balance + command.Amount);
|
||||
intents.JournalWrite.Entity.Lines.Count.Should()
|
||||
.BeGreaterThanOrEqualTo(2);
|
||||
intents.Event.AccountId.Should()
|
||||
.Be(account.Id);
|
||||
intents.Event.Amount.Should()
|
||||
.Be(command.Amount);
|
||||
}
|
||||
|
||||
public class LiabilityAccount
|
||||
{
|
||||
[Theory]
|
||||
@@ -49,17 +25,18 @@ public class ApplyFeeHandlerTests
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var command = new ApplyFeeCommand(1, feeAmount);
|
||||
var account = new Account(1, startingBalance, AccountType.Liability);
|
||||
var revenueAccount = new Account(99999, 0, AccountType.Credit);
|
||||
var pair = new FeeContext(account, revenueAccount, TimeProvider.System.GetUtcNow());
|
||||
var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var intents = ApplyFeeHandler.Handle(command, pair);
|
||||
var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
intents.CustomerWrite.Entity.Balance.Should()
|
||||
customerWrite.Entity.Balance.Should()
|
||||
.Be(expectedBalance);
|
||||
}
|
||||
|
||||
@@ -70,13 +47,15 @@ public class ApplyFeeHandlerTests
|
||||
[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.Liability);
|
||||
var revenueAccount = new Account(99999, 0, AccountType.Credit);
|
||||
var pair = new FeeContext(account, revenueAccount, TimeProvider.System.GetUtcNow());
|
||||
var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var act = () => ApplyFeeHandler.Handle(command, pair);
|
||||
var act = () => ApplyFeeHandler.Handle(command, feeContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
@@ -98,17 +77,19 @@ public class ApplyFeeHandlerTests
|
||||
decimal expectedBalance
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var command = new ApplyFeeCommand(1, feeAmount);
|
||||
var account = new Account(1, startingBalance, AccountType.Credit);
|
||||
var revenueAccount = new Account(99999, 0, AccountType.Credit);
|
||||
var pair = new FeeContext(account, revenueAccount, TimeProvider.System.GetUtcNow());
|
||||
var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var intents = ApplyFeeHandler.Handle(command, pair);
|
||||
var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
intents.CustomerWrite.Entity.Balance.Should()
|
||||
customerWrite.Entity.Balance.Should()
|
||||
.Be(expectedBalance);
|
||||
}
|
||||
|
||||
@@ -123,17 +104,19 @@ public class ApplyFeeHandlerTests
|
||||
decimal expectedBalance
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var command = new ApplyFeeCommand(1, feeAmount);
|
||||
var account = new Account(1, startingBalance, AccountType.Credit);
|
||||
var revenueAccount = new Account(99999, 0, AccountType.Credit);
|
||||
var pair = new FeeContext(account, revenueAccount, TimeProvider.System.GetUtcNow());
|
||||
var feeContext = new FeeContext(account, revenueAccount, timeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var intents = ApplyFeeHandler.Handle(command, pair);
|
||||
var (customerWrite, revenueWrite, journalWrite, @event) = ApplyFeeHandler.Handle(command, feeContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
intents.CustomerWrite.Entity.Balance.Should()
|
||||
customerWrite.Entity.Balance.Should()
|
||||
.Be(expectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using FinancialApi.Application.Commands;
|
||||
using FinancialApi.Application.Events;
|
||||
using FinancialApi.Application.Handlers;
|
||||
using FinancialApi.Application.Models;
|
||||
using FinancialApi.Domain.Entities;
|
||||
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<Account> SourceWrite, IStorageAction<Account> DestWrite, IStorageAction<BalancedJournalEntry>
|
||||
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.Liability);
|
||||
var destAccount = new Account(2, destStartingBalance, AccountType.Liability);
|
||||
var transferContext = new TransferContext(sourceAccount, destAccount, timeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
return TransferFundsHandler.Handle(command, transferContext);
|
||||
}
|
||||
}
|
||||
+37
-36
@@ -1,37 +1,38 @@
|
||||
using FinancialApi.Application.Commands;
|
||||
using FinancialApi.Application.Handlers;
|
||||
using FinancialApi.Application.Models;
|
||||
using FinancialApi.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
namespace FinancialApi.Application.UnitTests;
|
||||
|
||||
public class ApplyTransferHandlerTests
|
||||
{
|
||||
public class ApplyFeeHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void GivenValidAccounts_BasicFee_ShouldValidateEverything()
|
||||
{
|
||||
// Arrange
|
||||
var fakeTimeProvider = new FakeTimeProvider();
|
||||
|
||||
var command = new TransferFundsCommand(1, 2, 100);
|
||||
var sourceAccount = new Account(1, 100, AccountType.Liability);
|
||||
var destAccount = new Account(1, 100, AccountType.Liability);
|
||||
var transferContext = new TransferContext(sourceAccount, destAccount, fakeTimeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var intents = TransferFundsHandler.Handle(command, transferContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
intents.SourceWrite.Entity.Balance.Should()
|
||||
.Be(0);
|
||||
intents.DestWrite.Entity.Balance.Should()
|
||||
.Be(200);
|
||||
}
|
||||
}
|
||||
using FinancialApi.Application.Commands;
|
||||
using FinancialApi.Application.Handlers;
|
||||
using FinancialApi.Application.Models;
|
||||
using FinancialApi.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
namespace FinancialApi.Application.UnitTests;
|
||||
|
||||
public class TransferFundsHandlerTests
|
||||
{
|
||||
public class ApplyFeeHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void GivenValidAccounts_BasicFee_ShouldValidateEverything()
|
||||
{
|
||||
// Arrange
|
||||
var fakeTimeProvider = new FakeTimeProvider();
|
||||
var command = new TransferFundsCommand(1, 2, 100);
|
||||
var sourceAccount = new Account(1, 100, AccountType.Liability);
|
||||
var destAccount = new Account(1, 100, AccountType.Liability);
|
||||
var transferContext = new TransferContext(sourceAccount, destAccount, fakeTimeProvider.GetUtcNow());
|
||||
|
||||
// Act
|
||||
var (sourceWrite, destWrite, journalWrite, @event) = TransferFundsHandler.Handle(command, transferContext);
|
||||
|
||||
// Assert
|
||||
using var _ = new AssertionScope();
|
||||
sourceWrite.Entity.Balance.Should()
|
||||
.Be(0);
|
||||
destWrite.Entity.Balance.Should()
|
||||
.Be(200);
|
||||
journalWrite.Entity.Lines.Should()
|
||||
.HaveCountGreaterThanOrEqualTo(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public static class ApplyFeeHandler
|
||||
}
|
||||
|
||||
public static ( IStorageAction<Account> CustomerWrite, IStorageAction<Account> RevenueWrite,
|
||||
IStorageAction<JournalEntry> JournalWrite, FeeAppliedEvent Event ) Handle(
|
||||
IStorageAction<BalancedJournalEntry> JournalWrite, FeeAppliedEvent Event ) Handle(
|
||||
ApplyFeeCommand cmd,
|
||||
FeeContext context
|
||||
)
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ public static class ReverseJournalHandler
|
||||
throw new InvalidOperationException($"Journal Entry {cmd.OriginalJournalId} does not exist");
|
||||
}
|
||||
|
||||
public static ( IStorageAction<JournalEntry> JournalWrite, UnitOfWork<Account> AccountWrites, JournalReversedEvent
|
||||
public static (IStorageAction<BalancedJournalEntry> JournalWrite, UnitOfWork<Account> AccountWrites, JournalReversedEvent
|
||||
Event) Handle(ReverseJournalCommand cmd, JournalReversalContext context)
|
||||
{
|
||||
var reversalJournal = BalancedJournal.CreateReversal(
|
||||
context.OriginalJournalEntry,
|
||||
context.OriginalBalancedJournalEntry,
|
||||
$"Reversal: {cmd.Reason}",
|
||||
context.TimeStamp
|
||||
);
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ public static class TransferFundsHandler
|
||||
return new TransferContext(source, dest, timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
public static ( IStorageAction<Account> SourceWrite, IStorageAction<Account> DestWrite, IStorageAction<JournalEntry>
|
||||
JournalWrite, FundsTransferredEvent Event ) Handle(TransferFundsCommand cmd, TransferContext context)
|
||||
public static (IStorageAction<Account> SourceWrite, IStorageAction<Account> DestWrite, IStorageAction<BalancedJournalEntry>
|
||||
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);
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ namespace FinancialApi.Application.Interfaces;
|
||||
|
||||
public interface IReversalQuery
|
||||
{
|
||||
Task<(JournalEntry?, List<Account>?)> GetReversalDataAsync(Guid journalEntryId);
|
||||
Task<(BalancedJournalEntry?, List<Account>?)> GetReversalDataAsync(Guid journalEntryId);
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ using FinancialApi.Domain.Entities;
|
||||
namespace FinancialApi.Application.Models;
|
||||
|
||||
public record JournalReversalContext(
|
||||
JournalEntry OriginalJournalEntry,
|
||||
BalancedJournalEntry OriginalBalancedJournalEntry,
|
||||
IReadOnlyList<Account> AffectedAccounts,
|
||||
DateTimeOffset TimeStamp
|
||||
);
|
||||
+56
-55
@@ -1,56 +1,57 @@
|
||||
using FinancialApi.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
|
||||
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<InvalidStartingBalanceException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AccountType.Credit)]
|
||||
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.Debit)]
|
||||
[InlineData(AccountType.Revenue)]
|
||||
[InlineData(AccountType.Liability)]
|
||||
public void GivenDebitAccountType_ShouldNotAllowNegativeBalance(AccountType accountType)
|
||||
{
|
||||
var account = new Account(1, 0, accountType);
|
||||
|
||||
var act = () => account.ApplyPosting(100, EntryType.Debit);
|
||||
|
||||
act.Should()
|
||||
.Throw<InsufficientFundsException>();
|
||||
}
|
||||
using FinancialApi.Domain.Entities;
|
||||
using FinancialApi.Domain.Exceptions;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace FinancialApi.Domain.Tests;
|
||||
|
||||
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<InvalidStartingBalanceException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AccountType.Credit)]
|
||||
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.Debit)]
|
||||
[InlineData(AccountType.Revenue)]
|
||||
[InlineData(AccountType.Liability)]
|
||||
public void GivenDebitAccountType_ShouldNotAllowNegativeBalance(AccountType accountType)
|
||||
{
|
||||
var account = new Account(1, 0, accountType);
|
||||
|
||||
var act = () => account.ApplyPosting(100, EntryType.Debit);
|
||||
|
||||
act.Should()
|
||||
.Throw<InsufficientFundsException>();
|
||||
}
|
||||
}
|
||||
@@ -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<JournalLine> 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<JournalLine> 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<GaapViolationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GivenEmptyJournal_ShouldFail()
|
||||
{
|
||||
// Arrange
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
|
||||
// Act
|
||||
var act = () => BalancedJournal.Create(Guid.NewGuid(), "Transfer", timeProvider.GetUtcNow(), []);
|
||||
|
||||
// Assert
|
||||
act.Should()
|
||||
.Throw<GaapViolationException>();
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
using FinancialApi.Domain.Entities;
|
||||
using FinancialApi.Domain.Exceptions;
|
||||
|
||||
namespace FinancialApi.Domain.Aggregates;
|
||||
|
||||
public record BalancedJournal
|
||||
{
|
||||
public JournalEntry Entry { get; }
|
||||
public BalancedJournalEntry Entry { get; }
|
||||
|
||||
private BalancedJournal(JournalEntry entry)
|
||||
private BalancedJournal(BalancedJournalEntry entry)
|
||||
{
|
||||
Entry = entry;
|
||||
}
|
||||
|
||||
public static BalancedJournal Create(Guid id, string description, DateTimeOffset createdAt, List<JournalLine> 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);
|
||||
|
||||
if (debits != credits)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GAAP Violation: Total Debits ({debits}) must equal Total Credits ({credits})!"
|
||||
);
|
||||
}
|
||||
|
||||
return new BalancedJournal(new JournalEntry(id, description, createdAt, lines));
|
||||
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(JournalEntry original, string reason, DateTimeOffset now)
|
||||
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 }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace FinancialApi.Domain.Entities;
|
||||
using FinancialApi.Domain.Exceptions;
|
||||
|
||||
namespace FinancialApi.Domain.Entities;
|
||||
|
||||
public enum AccountType
|
||||
{
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
namespace FinancialApi.Domain.Entities;
|
||||
|
||||
public record JournalEntry
|
||||
public record BalancedJournalEntry
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Description { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public List<JournalLine> Lines { get; init; } = [];
|
||||
|
||||
internal JournalEntry() { }
|
||||
|
||||
internal JournalEntry(Guid id, string description, DateTimeOffset createdAt, List<JournalLine> lines)
|
||||
internal BalancedJournalEntry(Guid id, string description, DateTimeOffset createdAt, List<JournalLine> lines)
|
||||
{
|
||||
Lines = lines;
|
||||
Id = id;
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
namespace FinancialApi.Domain.Entities;
|
||||
|
||||
public class InsufficientFundsException(string message) : Exception(message);
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
namespace FinancialApi.Domain.Entities;
|
||||
|
||||
public class InvalidStartingBalanceException(string message) : Exception(message);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace FinancialApi.Domain.Exceptions;
|
||||
|
||||
public class GaapViolationException(string message) : Exception(message);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace FinancialApi.Domain.Exceptions;
|
||||
|
||||
public class InsufficientFundsException(string message) : GaapViolationException(message);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace FinancialApi.Domain.Exceptions;
|
||||
|
||||
public class InvalidStartingBalanceException(string message) : GaapViolationException(message);
|
||||
@@ -6,7 +6,7 @@ namespace FinancialApi.Infrastructure;
|
||||
public class AccountDbContext(DbContextOptions<AccountDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Account> Accounts => Set<Account>();
|
||||
public DbSet<JournalEntry> JournalEntries => Set<JournalEntry>();
|
||||
public DbSet<BalancedJournalEntry> JournalEntries => Set<BalancedJournalEntry>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -20,7 +20,7 @@ public class AccountDbContext(DbContextOptions<AccountDbContext> options) : DbCo
|
||||
new Account(99999, 0.00m, AccountType.Revenue)
|
||||
);
|
||||
|
||||
modelBuilder.Entity<JournalEntry>(builder =>
|
||||
modelBuilder.Entity<BalancedJournalEntry>(builder =>
|
||||
{
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.OwnsMany(
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace FinancialApi.Infrastructure;
|
||||
|
||||
public class ReversalQuery(AccountDbContext db) : IReversalQuery
|
||||
{
|
||||
public async Task<(JournalEntry?, List<Account>?)> GetReversalDataAsync(Guid journalEntryId)
|
||||
public async Task<(BalancedJournalEntry?, List<Account>?)> GetReversalDataAsync(Guid journalEntryId)
|
||||
{
|
||||
var entry = await db.JournalEntries.Include(x => x.Lines)
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Domain", "Fina
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -42,5 +44,9 @@ Global
|
||||
{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
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfiguredTaskAwaitable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe929cb9673c947ad9269facdac90006adb3400_003F14_003Fd939fe0a_003FConfiguredTaskAwaitable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConstructorBindingFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fda476e261fcf49b394c1074b25b4cdce2b2960_003Fd4_003Fb1aa95c1_003FConstructorBindingFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fda476e261fcf49b394c1074b25b4cdce2b2960_003F37_003F1b1010ed_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALateBoundTestFramework_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fcffa676d9bfc6c7a1c9d0d25ead15b3b0c9e12e3187bf397eaa52260e5fd91_003FLateBoundTestFramework_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMessageContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb263759e5fefb34bcd589e63aeba791cef6f591ecbc93d1a7e62befa9e8bc6_003FMessageContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARelationalLoggerExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb797502679b4aa8376708777b4db8d11e31a798d7e6bead76c15bdfa30834892_003FRelationalLoggerExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe929cb9673c947ad9269facdac90006adb3400_003Ff0_003F6b0e8a92_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWolverineRuntime_002EHostService_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F97bae8f1ffdd86c14edef353d7a85d3d3773b56c68e3a62e8815d1afaacbe797_003FWolverineRuntime_002EHostService_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=ea7499a6_002Ddeba_002D4139_002Dac76_002D505e3f7f2642/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<Solution />
|
||||
|
||||
Reference in New Issue
Block a user