diff --git a/Commands.Test/Commands.Test.csproj b/Commands.Test/Commands.Test.csproj index 5c83fd9f..4e91c910 100644 --- a/Commands.Test/Commands.Test.csproj +++ b/Commands.Test/Commands.Test.csproj @@ -6,7 +6,10 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Commands.Test/FinancialYear/When_adding_a_financial_year.cs b/Commands.Test/FinancialYear/When_adding_a_financial_year.cs new file mode 100644 index 00000000..13dc7620 --- /dev/null +++ b/Commands.Test/FinancialYear/When_adding_a_financial_year.cs @@ -0,0 +1,108 @@ +using Commands.Handlers.FinancialYear.AddFinancialYear; + +using Domain; +using Domain.Interfaces; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +using Moq; + +using Persistence; +using Persistence.Repositories; + +using Xunit; + +namespace Commands.Test.FinancialYear; + +public class When_adding_a_financial_year +{ + private readonly Mock _financialYearRepositoryMock; + private readonly HaSpManContext _haspmanDbContext; + + public When_adding_a_financial_year() + { + _financialYearRepositoryMock = new Mock(); + + var financialYearConfigurationOptions = Options.Create(new FinancialYearConfiguration() + { + StartDate = new DateTimeOffset(new DateTime(2022,9,1)), + EndDate = new DateTimeOffset(new DateTime(2023,8,31)) + }); + + _haspmanDbContext = new HaSpManContext(new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); + SUT = new AddFinancialYearHandler(_financialYearRepositoryMock.Object, financialYearConfigurationOptions, + _haspmanDbContext); + } + + public AddFinancialYearHandler SUT { get; set; } + + + + + public class And_no_financial_year_already_exists : When_adding_a_financial_year + { + [Fact] + public async Task It_should_add_a_new_financial_year() + { + var startDate = new DateTimeOffset(new DateTime(DateTime.Now.Year, 9, 1)); + var endDate = new DateTimeOffset(new DateTime(DateTime.Now.AddYears(1).Year, 8, 31)); + _financialYearRepositoryMock + .Setup(x => + x.Add(It.Is(financialYear => financialYear.StartDate == startDate))) + .Verifiable(); + _financialYearRepositoryMock + .Setup(x => + x.Add(It.Is(financialYear => financialYear.EndDate == endDate))) + .Verifiable(); + + await SUT.Handle(new AddFinancialYearCommand(), CancellationToken.None); + + _financialYearRepositoryMock + .Verify(x => + x.Add(It.Is(financialYear => financialYear.StartDate == startDate))); + _financialYearRepositoryMock + .Verify(x => + x.Add(It.Is(financialYear => financialYear.EndDate == endDate))); + + } + + + public class And_a_financial_year_already_exists : When_adding_a_financial_year + { + [Fact] + public async Task It_should_add_a_new_financial_year_following_the_last_year() + { + + var startDate = new DateTimeOffset(new DateTime(2023, 9, 1)); + + var endDate = new DateTimeOffset(new DateTime(2024, 8, 31)); + _financialYearRepositoryMock + .Setup(x => + x.GetMostRecentAsync(It.IsAny())) + .ReturnsAsync(new Domain.FinancialYear(new DateTimeOffset(new DateTime(2022, 9, 1)), + new DateTimeOffset(new DateTime(2023,8,31)), new List())); + + _financialYearRepositoryMock + .Setup(x => + x.Add(It.Is(financialYear => financialYear.StartDate == startDate))) + .Verifiable(); + _financialYearRepositoryMock + .Setup(x => + x.Add(It.Is(financialYear => financialYear.EndDate == endDate))) + .Verifiable(); + + await SUT.Handle(new AddFinancialYearCommand(), CancellationToken.None); + + _financialYearRepositoryMock + .Verify(x => + x.Add(It.Is(financialYear => financialYear.StartDate == startDate))); + _financialYearRepositoryMock + .Verify(x => + x.Add(It.Is(financialYear => financialYear.EndDate == endDate))); + + } + } + } +} \ No newline at end of file diff --git a/Commands.Test/FinancialYear/When_closing_a_financial_year.cs b/Commands.Test/FinancialYear/When_closing_a_financial_year.cs new file mode 100644 index 00000000..9f1ff10d --- /dev/null +++ b/Commands.Test/FinancialYear/When_closing_a_financial_year.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Commands.Handlers.FinancialYear.CloseFinancialYear; + +using Domain; +using Domain.Interfaces; + +using FluentAssertions; + +using Moq; + +using Persistence.Repositories; + +using Xunit; + +namespace Commands.Test.FinancialYear; + +public class When_closing_a_financial_year +{ + private readonly Mock _financialYearRepositoryMock; + + public When_closing_a_financial_year() + { + _financialYearRepositoryMock = new Mock(); + SUT = new CloseFinancialYearHandler(_financialYearRepositoryMock.Object); + } + + public CloseFinancialYearHandler SUT { get; set; } + + [Fact] + public async Task It_should_mark_the_year_as_closed() + { + var startDate = DateTimeOffset.Now; + var financialYear = new Domain.FinancialYear(startDate, startDate.AddYears(1).AddDays(-1), new List()); + _financialYearRepositoryMock + .Setup(x => x.GetByIdAsync(financialYear.Id, CancellationToken.None)) + .ReturnsAsync(financialYear); +; await SUT.Handle(new CloseFinancialYearCommand(financialYear.Id), CancellationToken.None); + + financialYear.IsClosed.Should().BeTrue(); + } + + [Fact] + public async Task It_should_mark_all_related_transactions_as_as_locked() + { + var startDate = DateTimeOffset.Now; + var financialYear = new Domain.FinancialYear(startDate, startDate.AddYears(1).AddDays(-1), new List() + { + new CreditTransaction("Random counter party", Guid.NewGuid(), 20,DateTimeOffset.Now, "A description", new List(), null, new List()) + }); + _financialYearRepositoryMock + .Setup(x => x.GetByIdAsync(financialYear.Id, CancellationToken.None)) + .ReturnsAsync(financialYear); + ; + await SUT.Handle(new CloseFinancialYearCommand(financialYear.Id), CancellationToken.None); + + financialYear.IsClosed.Should().BeTrue(); + } + + [Fact] + public async Task It_should_throw_an_exception_when_no_year_is_found() + { + var newGuid = Guid.NewGuid(); + var exception = await Assert.ThrowsAsync(async () => + { + await SUT.Handle(new CloseFinancialYearCommand(newGuid), CancellationToken.None); + }); + Assert.Equal($"No financial year found by Id {newGuid} (Parameter 'Id')", exception.Message); + } +} \ No newline at end of file diff --git a/Commands.Test/Transaction/When_editing_a_transaction.cs b/Commands.Test/Transaction/When_editing_a_transaction.cs new file mode 100644 index 00000000..4a8ddd95 --- /dev/null +++ b/Commands.Test/Transaction/When_editing_a_transaction.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Commands.Handlers.Transaction.EditTransaction; + +using Domain; +using Domain.Interfaces; + +using FluentAssertions; + +using MediatR; + +using Moq; + +using Persistence.Repositories; + +using Xunit; + +using AttachmentFile = Commands.Handlers.AttachmentFile; + +namespace Commands.Test.Transaction; +public class When_editing_a_transaction +{ + private readonly Mock _financialYearRepositoryMock; + + public When_editing_a_transaction() + { + _financialYearRepositoryMock = new Mock(); + var mediatorMock = new Mock(); + SUT = new EditTransactionHandler(_financialYearRepositoryMock.Object, mediatorMock.Object); + } + + public EditTransactionHandler SUT { get; set; } + + [Fact] + public async Task It_should_throw_an_exception_when_locked() + { + var bankAccountId = Guid.NewGuid(); + + var transaction = new CreditTransaction("Random counter party", bankAccountId, 20m, DateTimeOffset.Now, + "A description", new List(), null, new List()); + var financialYear = new Domain.FinancialYear(new DateTimeOffset(new DateTime(2022,9,1)), new DateTimeOffset(new DateTime(2023,8,31)), new List() + { + transaction + }); + financialYear.Close(); + + _financialYearRepositoryMock + .Setup(x => x.GetFinancialYearByTransactionId(transaction.Id, CancellationToken.None)) + .ReturnsAsync(financialYear); + + var exception = await Assert.ThrowsAsync(async () => await SUT.Handle( + new EditTransactionCommand(transaction.Id, "Random counter party name", null, bankAccountId, + DateTimeOffset.Now, "Another description", new List(), + new List()), CancellationToken.None)); + + exception.Message.Should().Be("Financial year is already closed"); + } +} diff --git a/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearCommand.cs b/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearCommand.cs new file mode 100644 index 00000000..8beea30d --- /dev/null +++ b/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearCommand.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Commands.Handlers.FinancialYear.AddFinancialYear; + +public record AddFinancialYearCommand() : IRequest; + + +public class AddFinancialYearCommandValidator : AbstractValidator +{ + public AddFinancialYearCommandValidator() + { + } +} \ No newline at end of file diff --git a/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearHandler.cs b/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearHandler.cs new file mode 100644 index 00000000..ae40728d --- /dev/null +++ b/Commands/Handlers/FinancialYear/AddFinancialYear/AddFinancialYearHandler.cs @@ -0,0 +1,46 @@ +using Domain; +using Domain.Interfaces; + +using Microsoft.Extensions.Options; + +using Persistence; +using Persistence.Repositories; + +namespace Commands.Handlers.FinancialYear.AddFinancialYear; + +public class AddFinancialYearHandler : IRequestHandler +{ + private readonly IFinancialYearRepository _financialYearRepository; + private readonly FinancialYearConfiguration _financialYearOptions; + + private readonly HaSpManContext _haSpManContext; + + public AddFinancialYearHandler(IFinancialYearRepository financialYearRepository, + IOptions financialYearOptions, + HaSpManContext haSpManContext) + { + _financialYearRepository = financialYearRepository; + _financialYearOptions = financialYearOptions.Value; + _haSpManContext = haSpManContext; + } + public async Task Handle(AddFinancialYearCommand request, CancellationToken cancellationToken) + { + + var lastFinancialYear = await _financialYearRepository.GetMostRecentAsync(cancellationToken); + + // In case this is the first year we create, assume we want it to be this year. + // Otherwise, just add a new year + var year = lastFinancialYear == null ? DateTime.Now.Year : lastFinancialYear.EndDate.Year; + + var startDate = new DateTimeOffset(new DateTime(year, _financialYearOptions.StartDate.Month, _financialYearOptions.StartDate.Day)); + + var financialYear = new Domain.FinancialYear( + startDate, + startDate.AddYears(1).AddDays(-1), + new List()); + + _financialYearRepository.Add(financialYear); + await _financialYearRepository.SaveChangesAsync(cancellationToken); + return financialYear; + } +} \ No newline at end of file diff --git a/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearCommand.cs b/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearCommand.cs new file mode 100644 index 00000000..7d467c30 --- /dev/null +++ b/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using FluentValidation; + +namespace Commands.Handlers.FinancialYear.CloseFinancialYear; + +public record CloseFinancialYearCommand(Guid Id) : IRequest; + +public class CloseFinancialYearCommandValidator : AbstractValidator +{ + public CloseFinancialYearCommandValidator() + { + RuleFor(x => x.Id).NotEmpty(); + } +} \ No newline at end of file diff --git a/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearHandler.cs b/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearHandler.cs new file mode 100644 index 00000000..e1520721 --- /dev/null +++ b/Commands/Handlers/FinancialYear/CloseFinancialYear/CloseFinancialYearHandler.cs @@ -0,0 +1,24 @@ +using Domain.Interfaces; + +using Persistence.Repositories; + +namespace Commands.Handlers.FinancialYear.CloseFinancialYear; + +public class CloseFinancialYearHandler : IRequestHandler +{ + private readonly IFinancialYearRepository _financialYearRepository; + + public CloseFinancialYearHandler(IFinancialYearRepository financialYearRepository) + { + _financialYearRepository = financialYearRepository; + } + public async Task Handle(CloseFinancialYearCommand request, CancellationToken cancellationToken) + { + var financialYear = await _financialYearRepository.GetByIdAsync(request.Id, cancellationToken) + ?? throw new ArgumentException($"No financial year found by Id {request.Id}", nameof(request.Id)); + financialYear.Close(); + + await _financialYearRepository.SaveChangesAsync(cancellationToken); + + } +} \ No newline at end of file diff --git a/Commands/Handlers/Transaction/AddAttachments/AddAttachmentsCommand.cs b/Commands/Handlers/Transaction/AddAttachments/AddAttachmentsCommand.cs index 621e91d5..4fc948dd 100644 --- a/Commands/Handlers/Transaction/AddAttachments/AddAttachmentsCommand.cs +++ b/Commands/Handlers/Transaction/AddAttachments/AddAttachmentsCommand.cs @@ -30,25 +30,27 @@ public AttachmentValidator() public class AddAttachmentsHandler : IRequestHandler { - private readonly ITransactionRepository _transactionRepository; + private readonly IFinancialYearRepository _financialYearRepository; private readonly IAttachmentStorage _attachmentStorage; - public AddAttachmentsHandler(ITransactionRepository transactionRepository, IAttachmentStorage attachmentStorage) + public AddAttachmentsHandler(IFinancialYearRepository financialYearRepository, IAttachmentStorage attachmentStorage) { - _transactionRepository = transactionRepository; + _financialYearRepository = financialYearRepository; _attachmentStorage = attachmentStorage; } public async Task Handle(AddAttachmentsCommand request, CancellationToken cancellationToken) { - var transaction = await _transactionRepository.GetByIdAsync(request.TransactionId, cancellationToken) - ?? throw new ArgumentException($"No transaction found for Id {request.TransactionId}", nameof(request.TransactionId)); + var financialYear = await _financialYearRepository.GetFinancialYearByTransactionId(request.TransactionId, cancellationToken) + ?? throw new ArgumentException($"No financial year found for transactionId {request.TransactionId}", nameof(request.TransactionId)); + + var transaction = financialYear.Transactions.First(x => x.Id == request.TransactionId); var transactionAttachments = await StoreAttachmentsAsync(request, cancellationToken); - + transaction.AddAttachments(transactionAttachments); - await _transactionRepository.SaveAsync(cancellationToken); + await _financialYearRepository.SaveChangesAsync(cancellationToken); return Unit.Value; } diff --git a/Commands/Handlers/Transaction/AddCreditTransaction/AddCreditTransactionHandler.cs b/Commands/Handlers/Transaction/AddCreditTransaction/AddCreditTransactionHandler.cs index 673c2c45..3a590454 100644 --- a/Commands/Handlers/Transaction/AddCreditTransaction/AddCreditTransactionHandler.cs +++ b/Commands/Handlers/Transaction/AddCreditTransaction/AddCreditTransactionHandler.cs @@ -1,7 +1,9 @@  +using Commands.Handlers.FinancialYear.AddFinancialYear; using Commands.Handlers.Transaction.AddAttachments; using Domain; +using Domain.Interfaces; using Persistence.Repositories; @@ -9,22 +11,29 @@ namespace Commands.Handlers.Transaction.AddCreditTransaction; public class AddCreditTransactionHandler : IRequestHandler { - private readonly ITransactionRepository _transactionRepository; + private readonly IFinancialYearRepository _financialYearRepository; private readonly IMediator _mediator; - public AddCreditTransactionHandler(ITransactionRepository transactionRepository, IMediator mediator) + public AddCreditTransactionHandler(IFinancialYearRepository financialYearRepository, IMediator mediator) { - _transactionRepository = transactionRepository; + _financialYearRepository = financialYearRepository; _mediator = mediator; } public async Task Handle(AddCreditTransactionCommand request, CancellationToken cancellationToken) { + + var financialYear = + await _financialYearRepository.GetFinancialYearByDateAsync(request.ReceivedDateTime, cancellationToken) + ?? await _mediator.Send(new AddFinancialYearCommand(), cancellationToken); + var totalAmount = request.TransactionTypeAmounts.Sum(x => x.Amount); var transaction = new CreditTransaction(request.CounterPartyName, request.BankAccountId, totalAmount, request.ReceivedDateTime, request.Description, new List(), request.MemberId, request.TransactionTypeAmounts); - _transactionRepository.Add(transaction); - await _transactionRepository.SaveAsync(cancellationToken); + + financialYear.AddTransaction(transaction); +; + await _financialYearRepository.SaveChangesAsync(cancellationToken); await _mediator.Send(new AddAttachmentsCommand(transaction.Id, request.NewAttachmentFiles), cancellationToken); diff --git a/Commands/Handlers/Transaction/AddDebitTransaction/AddDebitTransactionHandler.cs b/Commands/Handlers/Transaction/AddDebitTransaction/AddDebitTransactionHandler.cs index 1b8d392a..7a9a0c02 100644 --- a/Commands/Handlers/Transaction/AddDebitTransaction/AddDebitTransactionHandler.cs +++ b/Commands/Handlers/Transaction/AddDebitTransaction/AddDebitTransactionHandler.cs @@ -1,31 +1,36 @@ -using Commands.Handlers.Transaction.AddAttachments; +using Commands.Handlers.FinancialYear.AddFinancialYear; +using Commands.Handlers.Transaction.AddAttachments; using Domain; - -using Persistence.Repositories; +using Domain.Interfaces; namespace Commands.Handlers.Transaction.AddDebitTransaction; public class AddDebitTransactionHandler : IRequestHandler { - private readonly ITransactionRepository _transactionRepository; + private readonly IFinancialYearRepository _financialYearRepository; private readonly IMediator _mediator; - public AddDebitTransactionHandler(ITransactionRepository transactionRepository, IMediator mediator) + public AddDebitTransactionHandler(IFinancialYearRepository financialYearRepository, IMediator mediator) { - _transactionRepository = transactionRepository; + _financialYearRepository = financialYearRepository; _mediator = mediator; } public async Task Handle(AddDebitTransactionCommand request, CancellationToken cancellationToken) { + var financialYear = + await _financialYearRepository.GetFinancialYearByDateAsync(request.ReceivedDateTime, cancellationToken) + ?? await _mediator.Send(new AddFinancialYearCommand(), cancellationToken); + var totalAmount = request.TransactionTypeAmounts.Sum(x => x.Amount); var transaction = new DebitTransaction(request.CounterPartyName, request.BankAccountId, totalAmount, request.ReceivedDateTime, request.Description, new List(), request.MemberId, request.TransactionTypeAmounts); - _transactionRepository.Add(transaction); - await _transactionRepository.SaveAsync(cancellationToken); + financialYear.AddTransaction(transaction); + + await _financialYearRepository.SaveChangesAsync(cancellationToken); await _mediator.Send(new AddAttachmentsCommand(transaction.Id, request.NewAttachmentFiles), cancellationToken); diff --git a/Commands/Handlers/Transaction/EditTransaction/EditTransactionHandler.cs b/Commands/Handlers/Transaction/EditTransaction/EditTransactionHandler.cs index 26a34eaa..ae603092 100644 --- a/Commands/Handlers/Transaction/EditTransaction/EditTransactionHandler.cs +++ b/Commands/Handlers/Transaction/EditTransaction/EditTransactionHandler.cs @@ -1,37 +1,54 @@ -using Commands.Handlers.Transaction.AddAttachments; +using Commands.Handlers.FinancialYear.AddFinancialYear; +using Commands.Handlers.Transaction.AddAttachments; + +using Domain.Interfaces; using Persistence.Repositories; namespace Commands.Handlers.Transaction.EditTransaction; -public class EditTransactionHandler : IRequestHandler +public class EditTransactionHandler : IRequestHandler { - private readonly ITransactionRepository _transactionRepository; + private readonly IFinancialYearRepository _financialYearRepository; private readonly IMediator _mediator; - public EditTransactionHandler(ITransactionRepository transactionRepository, IMediator mediator) + public EditTransactionHandler(IFinancialYearRepository financialYearRepository, IMediator mediator) { - _transactionRepository = transactionRepository; + _financialYearRepository = financialYearRepository; _mediator = mediator; } - public async Task Handle(EditTransactionCommand request, CancellationToken cancellationToken) + public async Task Handle(EditTransactionCommand request, CancellationToken cancellationToken) { - var transaction = await _transactionRepository.GetByIdAsync(request.Id, cancellationToken) + var financialYear = + await _financialYearRepository.GetFinancialYearByTransactionId(request.Id, cancellationToken) ?? throw new ArgumentException($"No transaction found for Id {request.Id}", nameof(request.Id)); - var totalAmount = request.TransactionTypeAmounts.Sum(x => x.Amount); + if (financialYear.StartDate <= request.ReceivedDateTime && + financialYear.EndDate >= request.ReceivedDateTime) + { + financialYear.ChangeTransaction(request.Id, request.CounterPartyName, request.MemberId, request.BankAccountId, + request.ReceivedDateTime, request.TransactionTypeAmounts, request.Description); + } + else + { + var transaction = financialYear.Transactions.First(x => x.Id == request.Id); + + var matchingFinancialYear = + await _financialYearRepository.GetFinancialYearByDateAsync(request.ReceivedDateTime, + cancellationToken) + ?? await _mediator.Send(new AddFinancialYearCommand(), cancellationToken); - transaction.ChangeCounterParty(request.CounterPartyName, request.MemberId); - transaction.ChangeBankAccountId(request.BankAccountId); - transaction.ChangeReceivedDateTime(request.ReceivedDateTime); - transaction.ChangeAmount(totalAmount, request.TransactionTypeAmounts); - transaction.ChangeDescription(request.Description); + matchingFinancialYear.AddTransaction(transaction); + matchingFinancialYear.ChangeTransaction(request.Id, request.CounterPartyName, request.MemberId, request.BankAccountId, + request.ReceivedDateTime, request.TransactionTypeAmounts, request.Description); + financialYear.Transactions.Remove(transaction); + } - await _transactionRepository.SaveAsync(cancellationToken); - await _mediator.Send(new AddAttachmentsCommand(transaction.Id, request.NewAttachmentFiles), cancellationToken); + await _financialYearRepository.SaveChangesAsync(cancellationToken); - return transaction.Id; + await _mediator.Send(new AddAttachmentsCommand(request.Id, request.NewAttachmentFiles), cancellationToken); + } } diff --git a/Commands/Handlers/Transaction/EditTransaction/EditTransationCommand.cs b/Commands/Handlers/Transaction/EditTransaction/EditTransationCommand.cs index dd91b276..65c3020b 100644 --- a/Commands/Handlers/Transaction/EditTransaction/EditTransationCommand.cs +++ b/Commands/Handlers/Transaction/EditTransaction/EditTransationCommand.cs @@ -14,7 +14,7 @@ public record EditTransactionCommand( DateTimeOffset ReceivedDateTime, string Description, ICollection TransactionTypeAmounts, - ICollection NewAttachmentFiles) : IRequest; + ICollection NewAttachmentFiles) : IRequest; public class EditTransactionCommandValidator : AbstractValidator diff --git a/Domain/FinancialYear.cs b/Domain/FinancialYear.cs new file mode 100644 index 00000000..65097db1 --- /dev/null +++ b/Domain/FinancialYear.cs @@ -0,0 +1,60 @@ +using Azure.Core; + +namespace Domain; + +public class FinancialYear +{ +#pragma warning disable CS8618 + public FinancialYear(){ } // Make EFCore happy +#pragma warning restore CS8618 + public FinancialYear(DateTimeOffset startDate, DateTimeOffset endDate, ICollection transactions) + { + StartDate = startDate; + EndDate = endDate; + Transactions = transactions; + } + + public Guid Id { get; private set; } + public DateTimeOffset StartDate { get; set; } + public DateTimeOffset EndDate { get; private set;} + + public bool IsClosed { get; private set; } + + public ICollection Transactions { get; private set; } + + public void Close() + { + if (IsClosed) + { + throw new InvalidOperationException("Financial year is already closed"); + } + IsClosed = true; + } + + public void AddTransaction(Transaction transaction) + { + if (IsClosed) + { + throw new InvalidOperationException("Financial year is closed"); + } + Transactions.Add(transaction); + } + + public void ChangeTransaction(Guid transactionId, string counterPartyName, Guid? memberId, Guid bankAccountId, + DateTimeOffset receivedDateTime, ICollection transactionTypeAmounts, string description) + { + if (IsClosed) + { + throw new InvalidOperationException("Financial year is already closed"); + } + + var transaction = Transactions.First(x => x.Id == transactionId); + var totalAmount = transactionTypeAmounts.Sum(x => x.Amount); + + transaction.ChangeCounterParty(counterPartyName, memberId); + transaction.ChangeBankAccountId(bankAccountId); + transaction.ChangeReceivedDateTime(receivedDateTime); + transaction.ChangeAmount(totalAmount, transactionTypeAmounts); + transaction.ChangeDescription(description); + } +} \ No newline at end of file diff --git a/Domain/FinancialYearConfiguration.cs b/Domain/FinancialYearConfiguration.cs new file mode 100644 index 00000000..37b693e4 --- /dev/null +++ b/Domain/FinancialYearConfiguration.cs @@ -0,0 +1,7 @@ +namespace Domain; + +public class FinancialYearConfiguration +{ + public DateTimeOffset StartDate { get; set; } + public DateTimeOffset EndDate { get; set;} +} \ No newline at end of file diff --git a/Domain/Interfaces/IFinancialYearRepository.cs b/Domain/Interfaces/IFinancialYearRepository.cs new file mode 100644 index 00000000..9153806e --- /dev/null +++ b/Domain/Interfaces/IFinancialYearRepository.cs @@ -0,0 +1,12 @@ +namespace Domain.Interfaces; + +public interface IFinancialYearRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken); + void Add(FinancialYear financialYear); + Task SaveChangesAsync(CancellationToken cancellationToken); + Task GetMostRecentAsync(CancellationToken cancellationToken); + + Task GetFinancialYearByTransactionId(Guid transactionId, CancellationToken cancellationToken); + Task GetFinancialYearByDateAsync(DateTimeOffset dateTime, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Domain/Transaction.cs b/Domain/Transaction.cs index 3bf3dcb9..cd574ddd 100644 --- a/Domain/Transaction.cs +++ b/Domain/Transaction.cs @@ -131,6 +131,7 @@ public void AddAttachments(ICollection attachments) Attachments.Add(attachment); } } + } public class DebitTransaction : Transaction { diff --git a/Persistence/EntityConfigurations/FinancialYearEntityTypeConfiguration.cs b/Persistence/EntityConfigurations/FinancialYearEntityTypeConfiguration.cs new file mode 100644 index 00000000..91e2a0a6 --- /dev/null +++ b/Persistence/EntityConfigurations/FinancialYearEntityTypeConfiguration.cs @@ -0,0 +1,15 @@ +using Domain; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Persistence.EntityConfigurations; + +public class FinancialYearEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Owned entity types cannot have inheritance hierarchies https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities#current-shortcomings + builder.HasMany(x => x.Transactions).WithOne(); + } +} \ No newline at end of file diff --git a/Persistence/EntityConfigurations/TransactionConfigurations.cs b/Persistence/EntityConfigurations/TransactionConfigurations.cs index 4e6d2529..0a5e1910 100644 --- a/Persistence/EntityConfigurations/TransactionConfigurations.cs +++ b/Persistence/EntityConfigurations/TransactionConfigurations.cs @@ -5,6 +5,18 @@ namespace Persistence.EntityConfigurations; +public class TransactionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + + builder.ToTable("Transactions"); + builder.HasDiscriminator("Discriminator") + .HasValue(nameof(DebitTransaction)) + .HasValue(nameof(CreditTransaction)); + } +} + public class CreditTransactionConfiguration : IEntityTypeConfiguration { @@ -59,7 +71,6 @@ public class DebitTransactionConfiguration : IEntityTypeConfiguration builder) { - builder.Property(x => x.Amount).IsRequired(); builder.Property(x => x.BankAccountId).IsRequired(); builder.Property(x => x.DateFiled).IsRequired(); diff --git a/Persistence/HaSpManContext.cs b/Persistence/HaSpManContext.cs index 124fcf64..72ba52f4 100644 --- a/Persistence/HaSpManContext.cs +++ b/Persistence/HaSpManContext.cs @@ -21,10 +21,14 @@ public HaSpManContext(DbContextOptions options) public DbSet Members { get; set; } = null!; public DbSet BankAccounts { get; set; } = null!; public DbSet BankAccountsWithTotals { get; set; } = null!; - public DbSet Transactions { get; set; } = null!; - + + public DbSet FinancialYears { get;set; } = null!; + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseSqlServer(x => x.MigrationsHistoryTable("__EFMigrationsHistory", Schema.HaSpMan)); + { + optionsBuilder.UseSqlServer(x => x.MigrationsHistoryTable("__EFMigrationsHistory", Schema.HaSpMan)); + + } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.Designer.cs b/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.Designer.cs new file mode 100644 index 00000000..df9424d6 --- /dev/null +++ b/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.Designer.cs @@ -0,0 +1,398 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(HaSpManContext))] + [Migration("20230728183144_AddFinancialYearAsAggregateRoot")] + partial class AddFinancialYearAsAggregateRoot + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("HaSpMan") + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar"); + + b.HasKey("Id"); + + b.ToTable("BankAccounts", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("IsClosed") + .HasColumnType("bit"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYears", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.FinancialYearConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYearConfigurations", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("MembershipExpiryDate") + .HasColumnType("datetimeoffset"); + + b.Property("MembershipFee") + .HasColumnType("float"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.HasKey("Id"); + + b.ToTable("Members", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("CounterPartyName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("DateFiled") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FinancialYearId") + .HasColumnType("uniqueidentifier"); + + b.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReceivedDateTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("FinancialYearId"); + + b.ToTable("Transactions", "HaSpMan"); + + b.HasDiscriminator("Discriminator").HasValue("Transaction"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("NumberOfTransactions") + .HasColumnType("bigint"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.HasKey("BankAccountId"); + + b.ToTable((string)null); + + b.ToView("vwBankAccountTotals", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.CreditTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("CreditTransaction"); + }); + + modelBuilder.Entity("Domain.DebitTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("DebitTransaction"); + }); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("BankAccountId", "Id"); + + b1.ToTable("BankAccount_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("BankAccountId"); + }); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("MemberId", "Id"); + + b1.ToTable("Member_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.OwnsOne("Types.Address", "Address", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("HouseNumber") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("varchar"); + + b1.Property("Street") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar"); + + b1.HasKey("MemberId"); + + b1.ToTable("Members", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.HasOne("Domain.FinancialYear", null) + .WithMany("Transactions") + .HasForeignKey("FinancialYearId"); + + b.OwnsMany("Domain.TransactionAttachment", "Attachments", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("FullPath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_Attachments", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.OwnsMany("Domain.TransactionTypeAmount", "TransactionTypeAmounts", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b1.Property("TransactionType") + .HasColumnType("int"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_TransactionTypeAmounts", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.Navigation("Attachments"); + + b.Navigation("TransactionTypeAmounts"); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.HasOne("Domain.BankAccount", "Account") + .WithOne() + .HasForeignKey("Persistence.Views.BankAccountsWithTotals", "BankAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.cs b/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.cs new file mode 100644 index 00000000..bd61d717 --- /dev/null +++ b/Persistence/Migrations/20230728183144_AddFinancialYearAsAggregateRoot.cs @@ -0,0 +1,96 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class AddFinancialYearAsAggregateRoot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FinancialYearId", + schema: "HaSpMan", + table: "Transactions", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.CreateTable( + name: "FinancialYearConfigurations", + schema: "HaSpMan", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + StartDate = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FinancialYearConfigurations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "FinancialYears", + schema: "HaSpMan", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + StartDate = table.Column(type: "datetimeoffset", nullable: false), + EndDate = table.Column(type: "datetimeoffset", nullable: false), + IsClosed = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FinancialYears", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_FinancialYearId", + schema: "HaSpMan", + table: "Transactions", + column: "FinancialYearId"); + + migrationBuilder.AddForeignKey( + name: "FK_Transactions_FinancialYears_FinancialYearId", + schema: "HaSpMan", + table: "Transactions", + column: "FinancialYearId", + principalSchema: "HaSpMan", + principalTable: "FinancialYears", + principalColumn: "Id"); + + + + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Transactions_FinancialYears_FinancialYearId", + schema: "HaSpMan", + table: "Transactions"); + + migrationBuilder.DropTable( + name: "FinancialYearConfigurations", + schema: "HaSpMan"); + + migrationBuilder.DropTable( + name: "FinancialYears", + schema: "HaSpMan"); + + migrationBuilder.DropIndex( + name: "IX_Transactions_FinancialYearId", + schema: "HaSpMan", + table: "Transactions"); + + migrationBuilder.DropColumn( + name: "FinancialYearId", + schema: "HaSpMan", + table: "Transactions"); + } + } +} diff --git a/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.Designer.cs b/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.Designer.cs new file mode 100644 index 00000000..98826678 --- /dev/null +++ b/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.Designer.cs @@ -0,0 +1,398 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(HaSpManContext))] + [Migration("20230728184205_TransactionsBelongToFinancialYear")] + partial class TransactionsBelongToFinancialYear + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("HaSpMan") + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar"); + + b.HasKey("Id"); + + b.ToTable("BankAccounts", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("IsClosed") + .HasColumnType("bit"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYears", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.FinancialYearConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYearConfigurations", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("MembershipExpiryDate") + .HasColumnType("datetimeoffset"); + + b.Property("MembershipFee") + .HasColumnType("float"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.HasKey("Id"); + + b.ToTable("Members", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("CounterPartyName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("DateFiled") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FinancialYearId") + .HasColumnType("uniqueidentifier"); + + b.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReceivedDateTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("FinancialYearId"); + + b.ToTable("Transactions", "HaSpMan"); + + b.HasDiscriminator("Discriminator").HasValue("Transaction"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("NumberOfTransactions") + .HasColumnType("bigint"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.HasKey("BankAccountId"); + + b.ToTable((string)null); + + b.ToView("vwBankAccountTotals", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.CreditTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("CreditTransaction"); + }); + + modelBuilder.Entity("Domain.DebitTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("DebitTransaction"); + }); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("BankAccountId", "Id"); + + b1.ToTable("BankAccount_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("BankAccountId"); + }); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("MemberId", "Id"); + + b1.ToTable("Member_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.OwnsOne("Types.Address", "Address", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("HouseNumber") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("varchar"); + + b1.Property("Street") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar"); + + b1.HasKey("MemberId"); + + b1.ToTable("Members", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.HasOne("Domain.FinancialYear", null) + .WithMany("Transactions") + .HasForeignKey("FinancialYearId"); + + b.OwnsMany("Domain.TransactionAttachment", "Attachments", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("FullPath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_Attachments", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.OwnsMany("Domain.TransactionTypeAmount", "TransactionTypeAmounts", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b1.Property("TransactionType") + .HasColumnType("int"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_TransactionTypeAmounts", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.Navigation("Attachments"); + + b.Navigation("TransactionTypeAmounts"); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.HasOne("Domain.BankAccount", "Account") + .WithOne() + .HasForeignKey("Persistence.Views.BankAccountsWithTotals", "BankAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.cs b/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.cs new file mode 100644 index 00000000..50bfb36a --- /dev/null +++ b/Persistence/Migrations/20230728184205_TransactionsBelongToFinancialYear.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations; + +/// +public partial class TransactionsBelongToFinancialYear : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("INSERT INTO " + + "HaspMan.FinancialYears (Id, StartDate, EndDate, IsClosed) " + + "VALUES (newId(), CAST('2022-09-01 00:00:00.0000000 +02:00' AS DATETIMEOFFSET),CAST('2023-08-31 00:00:00.0000000 +02:00' AS DATETIMEOFFSET), 0)"); + + migrationBuilder.DropIndex("IX_Transactions_FinancialYearId", schema: "HaspMan", table: "Transactions"); + migrationBuilder.AlterColumn( + "FinancialYearId", + schema:"HaspMan", + table: "Transactions", + nullable: false); + migrationBuilder.CreateIndex( + name: "IX_Transactions_FinancialYearId", + schema: "HaSpMan", + table: "Transactions", + column: "FinancialYearId"); + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} \ No newline at end of file diff --git a/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.Designer.cs b/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.Designer.cs new file mode 100644 index 00000000..45278d87 --- /dev/null +++ b/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.Designer.cs @@ -0,0 +1,384 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(HaSpManContext))] + [Migration("20230728215040_RemoveFinancialConfiguration")] + partial class RemoveFinancialConfiguration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("HaSpMan") + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar"); + + b.HasKey("Id"); + + b.ToTable("BankAccounts", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("IsClosed") + .HasColumnType("bit"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYears", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.Property("MembershipExpiryDate") + .HasColumnType("datetimeoffset"); + + b.Property("MembershipFee") + .HasColumnType("float"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar"); + + b.HasKey("Id"); + + b.ToTable("Members", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("CounterPartyName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("DateFiled") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FinancialYearId") + .HasColumnType("uniqueidentifier"); + + b.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReceivedDateTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("FinancialYearId"); + + b.ToTable("Transactions", "HaSpMan"); + + b.HasDiscriminator("Discriminator").HasValue("Transaction"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("NumberOfTransactions") + .HasColumnType("bigint"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.HasKey("BankAccountId"); + + b.ToTable((string)null); + + b.ToView("vwBankAccountTotals", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.CreditTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("CreditTransaction"); + }); + + modelBuilder.Entity("Domain.DebitTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("DebitTransaction"); + }); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("BankAccountId", "Id"); + + b1.ToTable("BankAccount_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("BankAccountId"); + }); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("MemberId", "Id"); + + b1.ToTable("Member_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.OwnsOne("Types.Address", "Address", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar"); + + b1.Property("HouseNumber") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("varchar"); + + b1.Property("Street") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar"); + + b1.HasKey("MemberId"); + + b1.ToTable("Members", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.HasOne("Domain.FinancialYear", null) + .WithMany("Transactions") + .HasForeignKey("FinancialYearId"); + + b.OwnsMany("Domain.TransactionAttachment", "Attachments", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("FullPath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_Attachments", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.OwnsMany("Domain.TransactionTypeAmount", "TransactionTypeAmounts", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b1.Property("TransactionType") + .HasColumnType("int"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_TransactionTypeAmounts", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.Navigation("Attachments"); + + b.Navigation("TransactionTypeAmounts"); + }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.HasOne("Domain.BankAccount", "Account") + .WithOne() + .HasForeignKey("Persistence.Views.BankAccountsWithTotals", "BankAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.cs b/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.cs new file mode 100644 index 00000000..de1ac50a --- /dev/null +++ b/Persistence/Migrations/20230728215040_RemoveFinancialConfiguration.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class RemoveFinancialConfiguration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FinancialYearConfigurations", + schema: "HaSpMan"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "FinancialYearConfigurations", + schema: "HaSpMan", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + StartDate = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FinancialYearConfigurations", x => x.Id); + }); + } + } +} diff --git a/Persistence/Migrations/HaSpManContextModelSnapshot.cs b/Persistence/Migrations/HaSpManContextModelSnapshot.cs index a83d9550..94619d0f 100644 --- a/Persistence/Migrations/HaSpManContextModelSnapshot.cs +++ b/Persistence/Migrations/HaSpManContextModelSnapshot.cs @@ -1,6 +1,10 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; #nullable disable @@ -40,6 +44,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("BankAccounts", "HaSpMan"); }); + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("IsClosed") + .HasColumnType("bit"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("FinancialYears", "HaSpMan"); + }); + modelBuilder.Entity("Domain.Member", b => { b.Property("Id") @@ -104,6 +128,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("FinancialYearId") + .HasColumnType("uniqueidentifier"); + b.Property("MemberId") .HasColumnType("uniqueidentifier"); @@ -112,6 +139,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("FinancialYearId"); + b.ToTable("Transactions", "HaSpMan"); b.HasDiscriminator("Discriminator").HasValue("Transaction"); @@ -119,6 +148,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.UseTphMappingStrategy(); }); + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("NumberOfTransactions") + .HasColumnType("bigint"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.HasKey("BankAccountId"); + + b.ToTable((string)null); + + b.ToView("vwBankAccountTotals", "HaSpMan"); + }); + modelBuilder.Entity("Domain.CreditTransaction", b => { b.HasBaseType("Domain.Transaction"); @@ -250,6 +297,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Transaction", b => { + b.HasOne("Domain.FinancialYear", null) + .WithMany("Transactions") + .HasForeignKey("FinancialYearId"); + b.OwnsMany("Domain.TransactionAttachment", "Attachments", b1 => { b1.Property("TransactionId") @@ -308,6 +359,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("TransactionTypeAmounts"); }); + + modelBuilder.Entity("Persistence.Views.BankAccountsWithTotals", b => + { + b.HasOne("Domain.BankAccount", "Account") + .WithOne() + .HasForeignKey("Persistence.Views.BankAccountsWithTotals", "BankAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("Domain.FinancialYear", b => + { + b.Navigation("Transactions"); + }); #pragma warning restore 612, 618 } } diff --git a/Persistence/Repositories/FinancialYearRepository.cs b/Persistence/Repositories/FinancialYearRepository.cs new file mode 100644 index 00000000..91892901 --- /dev/null +++ b/Persistence/Repositories/FinancialYearRepository.cs @@ -0,0 +1,58 @@ +using Domain; +using Domain.Interfaces; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Persistence.Repositories; + +public class FinancialYearRepository : IFinancialYearRepository +{ + + private readonly HaSpManContext _context; + + public FinancialYearRepository(IDbContextFactory contextFactory) + { + + _context = contextFactory.CreateDbContext(); + } + + public Task GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + return _context.FinancialYears.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + } + + public Task GetTransactionAsync(Guid transactionId, CancellationToken cancellationToken) + { + return _context.FinancialYears + .Include(x => x.Transactions.Where(t => t.Id == transactionId)) + .FirstOrDefaultAsync(x => x.Transactions.Any(t => t.Id == transactionId), cancellationToken); + } + + public void Add(FinancialYear financialYear) + { + _context.FinancialYears.Add(financialYear); + } + + public async Task SaveChangesAsync(CancellationToken cancellationToken) + { + await _context.SaveChangesAsync(cancellationToken); + } + + public Task GetMostRecentAsync(CancellationToken cancellationToken) + { + return _context.FinancialYears.OrderByDescending(x => x.StartDate).FirstOrDefaultAsync(cancellationToken); + } + + public Task GetFinancialYearByTransactionId(Guid transactionId, CancellationToken cancellationToken) + { + return _context.FinancialYears + .SingleOrDefaultAsync(x => x.Transactions.Any(t => t.Id == transactionId), cancellationToken); + } + + public Task GetFinancialYearByDateAsync(DateTimeOffset dateTime, CancellationToken cancellationToken) + { + return _context.FinancialYears.SingleOrDefaultAsync(x => x.StartDate <= dateTime && x.EndDate >= dateTime, + cancellationToken); + } +} \ No newline at end of file diff --git a/Persistence/Repositories/TransactionRepository.cs b/Persistence/Repositories/TransactionRepository.cs deleted file mode 100644 index e2997db9..00000000 --- a/Persistence/Repositories/TransactionRepository.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Domain; - -using Microsoft.EntityFrameworkCore; - -namespace Persistence.Repositories; - -public class TransactionRepository : ITransactionRepository -{ - private readonly HaSpManContext _context; - - public TransactionRepository(IDbContextFactory haSpManContext) - { - _context = haSpManContext.CreateDbContext(); - } - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) - { - return await _context.Transactions - .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); - } - - - - public async Task> GetAllAsync(CancellationToken cancellationToken) - { - return await _context.Transactions.ToListAsync(cancellationToken); - } - - public void AddRange(IEnumerable transactions) - { - _context.Transactions.AddRange(transactions); - } - - public void Add(Transaction member) - { - _context.Transactions.Add(member); - } - - public void Remove(Transaction member) - { - _context.Transactions.Remove(member); - } - - public async Task SaveAsync(CancellationToken cancellationToken) - { - await _context.SaveChangesAsync(cancellationToken); - } -} -public interface ITransactionRepository -{ - Task GetByIdAsync(Guid id, CancellationToken cancellationToken); - Task> GetAllAsync(CancellationToken cancellationToken); - void AddRange(IEnumerable transactions); - void Add(Transaction member); - void Remove(Transaction member); - Task SaveAsync(CancellationToken cancellationToken); -} diff --git a/Queries/BankAccounts/GetBankAccountById.cs b/Queries/BankAccounts/GetBankAccountById.cs index 0e4912e2..fa6710f6 100644 --- a/Queries/BankAccounts/GetBankAccountById.cs +++ b/Queries/BankAccounts/GetBankAccountById.cs @@ -19,7 +19,7 @@ public GetBankAccountByIdHandler(IMapper mapper, IDbContextFactory Handle(GetBankAccountByIdQuery request, CancellationToken cancellationToken) { - var context = _contextFactory.CreateDbContext(); + var context = await _contextFactory.CreateDbContextAsync(cancellationToken); var bankAccount = await context.BankAccounts.SingleAsync(b => b.Id == request.Id, cancellationToken: cancellationToken); return _mapper.Map(bankAccount); diff --git a/Queries/BankAccounts/SearchBankAccounts.cs b/Queries/BankAccounts/SearchBankAccounts.cs index 66392d6e..86c1782b 100644 --- a/Queries/BankAccounts/SearchBankAccounts.cs +++ b/Queries/BankAccounts/SearchBankAccounts.cs @@ -38,7 +38,7 @@ public SearchBankAccountsHandler(IDbContextFactory contextFactor public async Task> Handle(SearchBankAccountsQuery request, CancellationToken cancellationToken) { - var context = _contextFactory.CreateDbContext(); + var context = await _contextFactory.CreateDbContextAsync(cancellationToken); var bankAccountsQueryable = context.BankAccountsWithTotals .AsNoTracking() .Where(GetFilterCriteria(request.SearchString)); diff --git a/Queries/FinancialYears/GetFinancialYearsQuery.cs b/Queries/FinancialYears/GetFinancialYearsQuery.cs new file mode 100644 index 00000000..d1334d12 --- /dev/null +++ b/Queries/FinancialYears/GetFinancialYearsQuery.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; + +using Persistence; + +namespace Queries.FinancialYears; + +public record GetFinancialYearsQuery() : IRequest>; +public record FinancialYear(Guid Id, DateTimeOffset StartDateTimeOffset, DateTimeOffset EndDateTimeOffset, bool IsCloded); + + +public class GetFinancialYearsHandler : IRequestHandler> +{ + private readonly IDbContextFactory _contextFactory; + private readonly IMapper _mapper; + + public GetFinancialYearsHandler(IDbContextFactory contextFactory, IMapper mapper) + { + _contextFactory = contextFactory; + _mapper = mapper; + } + public async Task> Handle(GetFinancialYearsQuery request, + CancellationToken cancellationToken) + { + var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + var financialYears = await context.FinancialYears.ToListAsync(cancellationToken); + + return financialYears + .OrderByDescending(x => x.StartDate) + .Select(x => new FinancialYear(x.Id, x.StartDate, x.EndDate, x.IsClosed)) + .ToList(); + } +} \ No newline at end of file diff --git a/Queries/Members/Handlers/AutocompleteMember/AutocompleteCounterpartyHandler.cs b/Queries/Members/Handlers/AutocompleteMember/AutocompleteCounterpartyHandler.cs index feff0120..4b0ae895 100644 --- a/Queries/Members/Handlers/AutocompleteMember/AutocompleteCounterpartyHandler.cs +++ b/Queries/Members/Handlers/AutocompleteMember/AutocompleteCounterpartyHandler.cs @@ -30,7 +30,9 @@ public async Task Handle(AutocompleteCounterpa } - var counterParties = await context.Transactions + var counterParties = await context.FinancialYears + .SelectMany(x => x.Transactions) + .AsNoTracking() .Where(x => x.MemberId == null && x.CounterPartyName.ToLower().Contains(request.SearchString.ToLower())) diff --git a/Queries/Transactions/GetAttachment/GetAttachmentCommand.cs b/Queries/Transactions/GetAttachment/GetAttachmentCommand.cs index d64e5525..e0de9ee5 100644 --- a/Queries/Transactions/GetAttachment/GetAttachmentCommand.cs +++ b/Queries/Transactions/GetAttachment/GetAttachmentCommand.cs @@ -1,28 +1,34 @@ using Domain.Interfaces; -using Persistence.Repositories; +using Microsoft.EntityFrameworkCore; + +using Persistence; using Types; -namespace Commands.Handlers.Transaction.GetAttachment; +namespace Queries.Transactions.GetAttachment; public record GetAttachmentQuery(Guid TransactionId, string FileName) : IRequest; public class GetAttachmentHandler : IRequestHandler { - private readonly ITransactionRepository _transactionRepository; + private readonly IDbContextFactory _dbContextFactory; private readonly IAttachmentStorage _attachmentStorage; - public GetAttachmentHandler(ITransactionRepository transactionRepository, IAttachmentStorage attachmentStorage) + public GetAttachmentHandler(IDbContextFactory dbContextFactory, IAttachmentStorage attachmentStorage) { - _transactionRepository = transactionRepository; + _dbContextFactory = dbContextFactory; _attachmentStorage = attachmentStorage; } public async Task Handle(GetAttachmentQuery request, CancellationToken cancellationToken) { - + var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); var transactionId = request.TransactionId; - var transaction = await _transactionRepository.GetByIdAsync(transactionId, cancellationToken) + var transaction = + await context.FinancialYears + .SelectMany(x => x.Transactions) + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Id == transactionId, cancellationToken) ?? throw new ArgumentException($"No transaction found for Id {request.TransactionId}", nameof(request.TransactionId)); var attachment = transaction.Attachments.SingleOrDefault(x => x.Name == request.FileName) diff --git a/Queries/Transactions/Handlers/GetTransactionByIdHandler.cs b/Queries/Transactions/Handlers/GetTransactionByIdHandler.cs index bd6f4e37..219ed7cc 100644 --- a/Queries/Transactions/Handlers/GetTransactionByIdHandler.cs +++ b/Queries/Transactions/Handlers/GetTransactionByIdHandler.cs @@ -23,7 +23,9 @@ public GetTransactionByIdHandler(IDbContextFactory contextFactor public async Task Handle(GetTransactionByIdQuery request, CancellationToken cancellationToken) { var context = await _contextFactory.CreateDbContextAsync(cancellationToken); - var transaction = await context.Transactions.SingleAsync(x => x.Id == request.Id, cancellationToken); + var transaction = await context.FinancialYears + .SelectMany(x => x.Transactions) + .AsNoTracking().SingleAsync(x => x.Id == request.Id, cancellationToken); return _mapper.Map(transaction); } diff --git a/Queries/Transactions/Handlers/GetTransactionsHandler.cs b/Queries/Transactions/Handlers/GetTransactionsHandler.cs index e24086db..70dd6e00 100644 --- a/Queries/Transactions/Handlers/GetTransactionsHandler.cs +++ b/Queries/Transactions/Handlers/GetTransactionsHandler.cs @@ -25,8 +25,11 @@ public GetTransactionsHandler(IDbContextFactory contextFactory, } public async Task> Handle(GetTransactionQuery request, CancellationToken cancellationToken) { - var context = _contextFactory.CreateDbContext(); - var transactions = context.Transactions.AsNoTracking() + var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + var transactions = + context.FinancialYears + .SelectMany(x => x.Transactions) + .AsNoTracking() .Where(GetFilterCriteria(request.SearchString)); var totalCount = await transactions.CountAsync(cancellationToken); diff --git a/Web/Pages/FinancialYears/CloseFinancialYearDialog.razor b/Web/Pages/FinancialYears/CloseFinancialYearDialog.razor new file mode 100644 index 00000000..d77c6af7 --- /dev/null +++ b/Web/Pages/FinancialYears/CloseFinancialYearDialog.razor @@ -0,0 +1,32 @@ +@using Commands.Handlers.FinancialYear.CloseFinancialYear +@using Queries.FinancialYears + +@inject IMediator _mediatr + + + + Are you sure you want to close financial year @FinancialYear.StartDateTimeOffset.Year - @FinancialYear.EndDateTimeOffset.Year? + + + Cancel + Ok + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public FinancialYear FinancialYear { get; set; } = null!; + + private async Task Submit() + { + + await _mediatr.Send(new CloseFinancialYearCommand(FinancialYear.Id)); + MudDialog.Close(true); + } + + void Cancel() => MudDialog.Cancel(); +} \ No newline at end of file diff --git a/Web/Pages/FinancialYears/FinancialYears.razor b/Web/Pages/FinancialYears/FinancialYears.razor new file mode 100644 index 00000000..0d0c226f --- /dev/null +++ b/Web/Pages/FinancialYears/FinancialYears.razor @@ -0,0 +1,103 @@ +@page "/financialyears" +@attribute [Authorize] +@using Queries.FinancialYears +@using Commands.Handlers.FinancialYear.CloseFinancialYear + +@inject IMediator _mediator +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Financial years + + + + + + Start date + + + End date + + + Is closed + + + + + + + @context.StartDateTimeOffset.Date.ToString("dd-MM-yyyy") + @context.EndDateTimeOffset.Date.ToString("dd-MM-yyyy") + @context.IsCloded + + + + + + + + + + + + + + No matching records found + + + Loading... + + + + + + +@code { + private MudTable? table; + + private async Task> ServerReload(TableState state) + { + StateHasChanged(); + + var query = new GetFinancialYearsQuery(); + var data = await _mediator.Send(query); + + return new TableData() + { + TotalItems = data.Count, + Items = data + }; + } + + private void OnSearch(string text) + { + table?.ReloadServerData(); + } + + private async Task CloseFinancialYear(FinancialYear financialYear) + { + + var options = new DialogOptions { }; + var parameters = new DialogParameters() + { + ["financialYear"] = financialYear + }; + var reference = await DialogService.ShowAsync($"Close financial year", parameters, options); + var dialog = await reference.Result; + if (!dialog.Canceled) + { + + Snackbar.Clear(); + Snackbar.Add($"Closed financial year {financialYear.StartDateTimeOffset.Year} - {financialYear.EndDateTimeOffset.Year}"); + table?.ReloadServerData(); + } + } + +} \ No newline at end of file diff --git a/Web/Pages/Transactions/EditTransaction.razor b/Web/Pages/Transactions/EditTransaction.razor index 67070328..b235b5cf 100644 --- a/Web/Pages/Transactions/EditTransaction.razor +++ b/Web/Pages/Transactions/EditTransaction.razor @@ -70,7 +70,7 @@ .Select(x => new AttachmentFile(x.FileName, x.ContentType, x.UnsafePath)) .ToList()); - var response = await Mediator.Send(command); + await Mediator.Send(command); if (memberId.HasValue && expirationDate.HasValue && applyCalculation) { await Mediator.Send(new ExtendMembershipCommand(memberId.Value, expirationDate.Value)); diff --git a/Web/Pages/Transactions/TransactionForm.razor b/Web/Pages/Transactions/TransactionForm.razor index f4fd3b03..8de63217 100644 --- a/Web/Pages/Transactions/TransactionForm.razor +++ b/Web/Pages/Transactions/TransactionForm.razor @@ -4,7 +4,7 @@ @using Queries.Members.Handlers.GetMemberById @using Queries.Members.ViewModels @using Queries.Members.Handlers.AutocompleteMember -@using Commands.Handlers.Transaction.GetAttachment +@using Queries.Transactions.GetAttachment @inject IWebHostEnvironment Environment @inject IDialogService dialogService diff --git a/Web/Shared/NavMenu.razor b/Web/Shared/NavMenu.razor index b5184970..700d7cd4 100644 --- a/Web/Shared/NavMenu.razor +++ b/Web/Shared/NavMenu.razor @@ -8,8 +8,11 @@ New transaction - List bankaccounts - New bankaccount + List bankaccounts + New bankaccount + + + List financial years About \ No newline at end of file diff --git a/Web/Startup.cs b/Web/Startup.cs index fda45774..3a3c55b0 100644 --- a/Web/Startup.cs +++ b/Web/Startup.cs @@ -1,6 +1,7 @@ using Commands.Handlers.Transaction.AddDebitTransaction; using Commands.Services; +using Domain; using Domain.Interfaces; using FluentValidation; @@ -52,11 +53,12 @@ public void ConfigureServices(IServiceCollection services) DbContextExtensions.MigrateHaSpManContext(dbConnectionString); services.Configure(Configuration.GetSection("Storage")); + services.Configure(Configuration.GetSection("FinancialYear")); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddAutoMapper( typeof(MapperProfiles.MemberProfile), typeof(MapperProfiles.TransactionProfile), diff --git a/Web/Web.csproj b/Web/Web.csproj index cbb96e27..beba806a 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -14,8 +14,7 @@ all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Web/appsettings.json b/Web/appsettings.json index d9d9a9bf..0e51e7df 100644 --- a/Web/appsettings.json +++ b/Web/appsettings.json @@ -6,5 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "FinancialYear": { + "StartDate": "2022-09-01", + "EndDate": "2023-08-31" + } }