From 65cc97ddd4a438ecf9431ebf722553d2271522dc Mon Sep 17 00:00:00 2001 From: Valters Melnalksnis Date: Sun, 29 Sep 2024 12:36:41 +0300 Subject: [PATCH] pit-stop --- source/Gnomeshade.Avalonia.Core/Converters.cs | 3 + .../DesignTime/DesignTimeData.cs | 40 +- .../DesignTime/DesignTimeGnomeshadeClient.cs | 258 +- .../Loans/LoanPaymentRow.cs | 2 +- .../LocalDateConverter.cs | 2 +- .../LocalDateTimeConverter.cs | 2 +- .../MainWindowViewModel.cs | 5 + .../NodaTimeValueClassConverter.cs | 77 + ...ter.cs => NodaTimeValueStructConverter.cs} | 2 +- .../PeriodConverter.cs | 22 + .../Reports/BalanceReportViewModel.cs | 69 +- .../Reports/CategoryReportViewModel.cs | 106 +- .../ServiceCollectionExtensions.cs | 3 +- .../Controls/TransactionFilter.cs | 4 + .../PlannedLoanPaymentUpsertionViewModel.cs | 173 + .../Loans/PlannedLoanPaymentViewModel.cs | 103 + .../PlannedTransactionUpsertionViewModel.cs | 88 + .../PlannedPurchaseUpsertionViewModel.cs | 224 + .../Purchases/PlannedPurchaseViewModel.cs | 175 + .../Purchases/PurchaseExtensions.cs | 25 + .../Transactions/TransactionOverview.cs | 18 +- .../TransactionScheduleOverview.cs | 41 + .../TransactionScheduleUpsertionViewModel.cs | 120 + .../TransactionScheduleViewModel.cs | 171 + .../Transactions/TransactionUpsertionBase.cs | 23 + .../TransactionUpsertionViewModel.cs | 7 +- .../Transactions/TransactionViewModel.cs | 61 +- .../PlannedTransferUpsertionViewModel.cs | 307 + .../Transfers/PlannedTransferViewModel.cs | 92 + .../Transfers/TransferExtensions.cs | 139 + .../Transfers/TransferUpsertionViewModel.cs | 13 +- .../Transfers/TransferViewModel.cs | 9 +- .../UpsertionViewModel.cs | 4 + .../Gnomeshade.Avalonia.Core/ViewLocator.cs | 9 +- .../DetailedPlannedTransaction2Entity.cs | 38 + .../Entities/DetailedTransactionEntity.cs | 8 +- .../Entities/LoanPaymentEntity.cs | 4 +- .../Entities/PlannedPurchaseEntity.cs | 8 + .../Entities/PlannedTransactionEntity.cs | 15 + .../Entities/PlannedTransferEntity.cs | 48 + .../Gnomeshade.Data/Entities/PurchaseBase.cs | 49 + .../Entities/PurchaseEntity.cs | 42 +- .../Entities/TransactionBase.cs | 27 + .../Entities/TransactionEntity.cs | 13 +- .../Entities/TransactionScheduleEntity.cs | 39 + .../Gnomeshade.Data/Entities/TransferBase.cs | 43 + .../Entities/TransferEntity.cs | 29 +- .../00000037_transaction_schedules.sql | 33 + .../AccountInCurrencyRepository.cs | 2 +- .../Repositories/AccountRepository.cs | 11 +- .../Repositories/CategoryRepository.cs | 2 +- .../Repositories/CounterpartyRepository.cs | 4 +- .../Repositories/LinkRepository.cs | 2 +- .../Repositories/Loan2Repository.cs | 2 +- .../Repositories/LoanPaymentRepository.cs | 2 +- .../Repositories/LoanRepository.cs | 2 +- .../Repositories/OwnerRepository.cs | 2 +- .../Repositories/OwnershipRepository.cs | 2 +- .../Repositories/PlannedPurchaseRepository.cs | 102 + .../PlannedTransactionRepository.cs | 73 + .../Repositories/PlannedTransferRepository.cs | 59 + .../Repositories/ProductRepository.cs | 2 +- .../Repositories/ProjectRepository.cs | 2 +- .../Repositories/PurchaseRepository.cs | 2 +- .../Gnomeshade.Data/Repositories/Queries.cs | 39 + .../Queries/PlannedTransaction/SelectAll.sql | 14 + .../Queries/TransactionSchedule/Delete.sql | 16 + .../Queries/TransactionSchedule/Insert.sql | 24 + .../Queries/TransactionSchedule/Select.sql | 24 + .../Queries/TransactionSchedule/SelectAll.sql | 14 + .../Queries/TransactionSchedule/Update.sql | 21 + .../Repositories/Repository.cs | 43 +- .../Repositories/TransactionRepository.cs | 2 +- .../TransactionScheduleRepository.cs | 59 + .../Repositories/TransferRepository.cs | 2 +- .../Repositories/UnitRepository.cs | 2 +- source/Gnomeshade.Desktop/App.axaml.cs | 2 + .../Gnomeshade.Desktop/Views/MainWindow.axaml | 3 + .../Views/MainWindow.axaml.cs | 13 +- .../Views/PlaceholderView.axaml | 13 + .../Views/PlaceholderView.axaml.cs | 23 + .../Views/Reports/BalanceReportView.axaml | 4 + .../Views/Reports/CategoryReportView.axaml | 5 +- .../Controls/TransactionFilterView.axaml | 5 +- .../Loans/LoanUpsertionView.axaml.cs | 6 +- .../Transactions/Loans/LoanView.axaml.cs | 6 +- .../Loans/PlannedLoanUpsertionView.axaml | 28 + .../Loans/PlannedLoanUpsertionView.axaml.cs | 17 + .../Transactions/Loans/PlannedLoanView.axaml | 70 + .../Loans/PlannedLoanView.axaml.cs | 17 + .../PlannedTransactionUpsertionView.axaml | 18 + .../PlannedTransactionUpsertionView.axaml.cs | 17 + .../Views/Transactions/ProjectionConverter.cs | 22 + .../PlannedPurchaseUpsertionView.axaml | 53 + .../PlannedPurchaseUpsertionView.axaml.cs | 17 + .../Purchases/PlannedPurchaseView.axaml | 96 + .../Purchases/PlannedPurchaseView.axaml.cs | 17 + .../Transactions/Purchases/PurchaseView.axaml | 7 + .../TransactionScheduleUpsertionView.axaml | 39 + .../TransactionScheduleUpsertionView.axaml.cs | 18 + .../TransactionScheduleView.axaml | 233 + .../TransactionScheduleView.axaml.cs | 17 + .../TransactionUpsertionView.axaml | 4 +- .../TransactionUpsertionView.axaml.cs | 9 +- .../Views/Transactions/TransactionView.axaml | 13 +- .../Transactions/TransactionView.axaml.cs | 6 +- .../PlannedTransferUpsertionView.axaml | 96 + .../PlannedTransferUpsertionView.axaml.cs | 17 + .../Transfers/PlannedTransferView.axaml | 104 + .../Transfers/PlannedTransferView.axaml.cs | 17 + .../GnomeshadeClient.cs | 120 + .../ITransactionClient.cs | 172 + source/Gnomeshade.WebApi.Client/Routes.cs | 95 +- .../GnomeshadeSerializerContext.cs | 18 +- .../Loans/LoanPayment.cs | 41 +- .../Loans/LoanPaymentBase.cs | 48 + .../Loans/LoanPaymentCreation.cs | 8 +- .../Loans/PlannedLoanPayment.cs | 12 + .../Transactions/PlannedPurchase.cs | 13 + .../Transactions/PlannedPurchaseCreation.cs | 12 + .../Transactions/PlannedTransaction.cs | 17 + .../PlannedTransactionCreation.cs | 20 + .../Transactions/PlannedTransfer.cs | 50 + .../Transactions/PlannedTransferCreation.cs | 50 + .../Transactions/Purchase.cs | 50 +- .../Transactions/PurchaseBase.cs | 59 + .../Transactions/PurchaseCreation.cs | 28 +- .../Transactions/PurchaseCreationBase.cs | 36 + .../Transactions/Transaction.cs | 26 +- .../Transactions/TransactionBase.cs | 34 + .../Transactions/TransactionSchedule.cs | 47 + .../TransactionScheduleCreation.cs | 33 + .../Transactions/Transfer.cs | 36 +- .../Transactions/TransferBase.cs | 47 + .../Transactions/TransferCreation.cs | 25 +- .../Transactions/TransferCreationBase.cs | 41 + .../Controllers/PlannedPurchasesController.cs | 57 + .../PlannedTransactionsController.cs | 80 + .../Controllers/PlannedTransfersController.cs | 57 + .../V1/Controllers/PurchasesController.cs | 2 +- .../Controllers/TransactionItemController.cs | 6 +- .../TransactionSchedulesController.cs | 130 + .../V1/Controllers/TransactionsController.cs | 26 + .../V1/Controllers/TransfersController.cs | 2 +- source/Gnomeshade.WebApi/V1/CreatableBase.cs | 3 +- .../PlannedLoanPaymentsController.cs | 92 + .../Accounts/AccountViewModelTests.cs | 2 +- .../NodaTimeValueConverterTests.cs | 2 +- .../Products/ProductViewModelTests.cs | 7 +- .../Reports/BalanceReportViewModelTests.cs | 33 +- .../Controls/TransactionFilterTests.cs | 3 +- .../Transactions/TransactionViewModelTests.cs | 16 +- ...cted._swagger_v1_swagger.json.verified.txt | 6025 ++++++++++++----- ...cted._swagger_v2_swagger.json.verified.txt | 553 +- .../Scenarios/TransactionPlanningTests.cs | 77 + 155 files changed, 10398 insertions(+), 2269 deletions(-) create mode 100644 source/Gnomeshade.Avalonia.Core/NodaTimeValueClassConverter.cs rename source/Gnomeshade.Avalonia.Core/{NodaTimeValueConverter.cs => NodaTimeValueStructConverter.cs} (96%) create mode 100644 source/Gnomeshade.Avalonia.Core/PeriodConverter.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentUpsertionViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/PlannedTransactionUpsertionViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseUpsertionViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleOverview.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleUpsertionViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionBase.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferUpsertionViewModel.cs create mode 100644 source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferViewModel.cs create mode 100644 source/Gnomeshade.Data/Entities/DetailedPlannedTransaction2Entity.cs create mode 100644 source/Gnomeshade.Data/Entities/PlannedPurchaseEntity.cs create mode 100644 source/Gnomeshade.Data/Entities/PlannedTransactionEntity.cs create mode 100644 source/Gnomeshade.Data/Entities/PlannedTransferEntity.cs create mode 100644 source/Gnomeshade.Data/Entities/PurchaseBase.cs create mode 100644 source/Gnomeshade.Data/Entities/TransactionBase.cs create mode 100644 source/Gnomeshade.Data/Entities/TransactionScheduleEntity.cs create mode 100644 source/Gnomeshade.Data/Entities/TransferBase.cs create mode 100644 source/Gnomeshade.Data/Migrations/00000037_transaction_schedules.sql create mode 100644 source/Gnomeshade.Data/Repositories/PlannedPurchaseRepository.cs create mode 100644 source/Gnomeshade.Data/Repositories/PlannedTransactionRepository.cs create mode 100644 source/Gnomeshade.Data/Repositories/PlannedTransferRepository.cs create mode 100644 source/Gnomeshade.Data/Repositories/Queries/PlannedTransaction/SelectAll.sql create mode 100644 source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Delete.sql create mode 100644 source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Insert.sql create mode 100644 source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Select.sql create mode 100644 source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/SelectAll.sql create mode 100644 source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Update.sql create mode 100644 source/Gnomeshade.Data/Repositories/TransactionScheduleRepository.cs create mode 100644 source/Gnomeshade.Desktop/Views/PlaceholderView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/PlaceholderView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/ProjectionConverter.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml.cs create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml create mode 100644 source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml.cs create mode 100644 source/Gnomeshade.WebApi.Models/Loans/LoanPaymentBase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Loans/PlannedLoanPayment.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchaseCreation.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedTransaction.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedTransactionCreation.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedTransfer.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PlannedTransferCreation.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PurchaseBase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreationBase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/TransactionBase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/TransactionSchedule.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/TransactionScheduleCreation.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/TransferBase.cs create mode 100644 source/Gnomeshade.WebApi.Models/Transactions/TransferCreationBase.cs create mode 100644 source/Gnomeshade.WebApi/V1/Controllers/PlannedPurchasesController.cs create mode 100644 source/Gnomeshade.WebApi/V1/Controllers/PlannedTransactionsController.cs create mode 100644 source/Gnomeshade.WebApi/V1/Controllers/PlannedTransfersController.cs create mode 100644 source/Gnomeshade.WebApi/V1/Controllers/TransactionSchedulesController.cs create mode 100644 source/Gnomeshade.WebApi/V2/Controllers/PlannedLoanPaymentsController.cs create mode 100644 tests/Gnomeshade.WebApi.Tests.Integration/Scenarios/TransactionPlanningTests.cs diff --git a/source/Gnomeshade.Avalonia.Core/Converters.cs b/source/Gnomeshade.Avalonia.Core/Converters.cs index d88a99a83..7ca1c33ed 100644 --- a/source/Gnomeshade.Avalonia.Core/Converters.cs +++ b/source/Gnomeshade.Avalonia.Core/Converters.cs @@ -18,6 +18,9 @@ public static class Converters /// Gets a / converter. public static LocalDateTimeConverter LocalDateTime { get; } = new(); + /// Gets a / converter. + public static PeriodConverter Period { get; } = new(); + /// Gets a converter that checks whether the collection is not empty. public static IValueConverter NotEmpty { get; } = new FuncValueConverter(collection => collection?.Count > 0); diff --git a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs index 6f06f11ca..2b256dde8 100644 --- a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs +++ b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeData.cs @@ -131,7 +131,7 @@ public static class DesignTimeData /// Gets an instance of for use during design time. public static TransactionViewModel TransactionViewModel { get; } = - InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, Clock, DateTimeZoneProvider)); + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, Clock, DateTimeZoneProvider)); /// Gets an instance of for use during design time. public static TransactionFilter TransactionFilter { get; } = new(ActivityService, Clock, DateTimeZoneProvider); @@ -148,7 +148,7 @@ public static class DesignTimeData public static LinkViewModel LinkViewModel { get; } = InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty)); - /// Gets an instance of for use during design time. + /// Gets an instance of for use during design time. public static LoanPaymentUpsertionViewModel LoanPaymentUpsertionViewModel { get; } = InitializeViewModel(new LoanPaymentUpsertionViewModel(ActivityService, GnomeshadeClient, Guid.Empty, null)); @@ -240,6 +240,42 @@ public static class DesignTimeData public static DashboardViewModel DashboardViewModel { get; } = InitializeViewModel(new(ActivityService, GnomeshadeClient, Clock, DateTimeZoneProvider)); + /// Gets an instance of for use during design time. + public static PlannedTransactionUpsertionViewModel PlannedTransactionUpsertionViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, null)); + + /// Gets an instance of for use during design time. + public static TransactionScheduleUpsertionViewModel TransactionScheduleUpsertionViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, null)); + + /// Gets an instance of for use during design time. + public static TransactionScheduleViewModel TransactionScheduleViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider)); + + /// Gets an instance of for use during design time. + public static PlannedTransferViewModel PlannedTransferViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, Guid.Empty)); + + /// Gets an instance of for use during design time. + public static PlannedTransferUpsertionViewModel PlannedTransferUpsertionViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, Guid.Empty, null)); + + /// Gets an instance of for use during design time. + public static PlannedPurchaseViewModel PlannedPurchaseViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, Guid.Empty)); + + /// Gets an instance of for use during design time. + public static PlannedPurchaseUpsertionViewModel PlannedPurchaseUpsertionViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, DialogService, DateTimeZoneProvider, Guid.Empty, null)); + + /// Gets an instance of for use during design time. + public static PlannedLoanPaymentUpsertionViewModel PlannedLoanPaymentUpsertionViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty, null)); + + /// Gets an instance of for use during design time. + public static PlannedLoanPaymentViewModel PlannedLoanPaymentViewModel { get; } = + InitializeViewModel(new(ActivityService, GnomeshadeClient, Guid.Empty)); + [UnconditionalSuppressMessage( "Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", diff --git a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeGnomeshadeClient.cs b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeGnomeshadeClient.cs index 71665d400..a69c9f054 100644 --- a/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeGnomeshadeClient.cs +++ b/source/Gnomeshade.Avalonia.Core/DesignTime/DesignTimeGnomeshadeClient.cs @@ -47,6 +47,12 @@ public sealed class DesignTimeGnomeshadeClient : IGnomeshadeClient private static readonly List _loanPayments; private static readonly List _projects; + private static readonly List _transactionSchedules; + private static readonly List _plannedTransactions; + private static readonly List _plannedTransfers; + private static readonly List _plannedPurchases; + private static readonly List _plannedLoanPayments; + static DesignTimeGnomeshadeClient() { var euro = new Currency { Id = Guid.NewGuid(), Name = "Euro", AlphabeticCode = "EUR" }; @@ -55,7 +61,8 @@ static DesignTimeGnomeshadeClient() var counterparty = new Counterparty { Id = Guid.Empty, Name = "John Doe" }; var otherCounterparty = new Counterparty { Id = Guid.NewGuid(), Name = "Jane Doe" }; - _counterparties = [counterparty, otherCounterparty]; + var bankCounterparty = new Counterparty { Id = Guid.NewGuid(), Name = "Bank" }; + _counterparties = [counterparty, otherCounterparty, bankCounterparty]; var cash = new Account { @@ -69,27 +76,39 @@ static DesignTimeGnomeshadeClient() new() { Id = Guid.NewGuid(), CurrencyId = usd.Id, CurrencyAlphabeticCode = usd.AlphabeticCode } ], }; + var spending = new Account { Id = Guid.NewGuid(), Name = "Spending", CounterpartyId = counterparty.Id, PreferredCurrencyId = euro.Id, - Currencies = - [new() { Id = Guid.NewGuid(), CurrencyId = euro.Id, CurrencyAlphabeticCode = euro.AlphabeticCode }], + Currencies = [new() { Id = Guid.NewGuid(), CurrencyId = euro.Id, CurrencyAlphabeticCode = euro.AlphabeticCode }], + }; + + var bankAccount = new Account + { + Id = Guid.NewGuid(), + Name = "Bank", + CounterpartyId = bankCounterparty.Id, + PreferredCurrencyId = euro.Id, + Currencies = [new() { Id = Guid.NewGuid(), CurrencyId = euro.Id, CurrencyAlphabeticCode = euro.AlphabeticCode }], }; - _accounts = [cash, spending]; + + _accounts = [cash, spending, bankAccount]; var kilogram = new Unit { Id = Guid.NewGuid(), Name = "Kilogram" }; var gram = new Unit { Id = Guid.NewGuid(), Name = "Gram", ParentUnitId = kilogram.Id, Multiplier = 1000m }; _units = [kilogram, gram]; var food = new Category { Id = Guid.Empty, Name = "Food" }; - _categories = [food]; + var liabilities = new Category { Id = Guid.NewGuid(), Name = "Liabilities" }; + _categories = [food, liabilities]; var bread = new Product { Id = Guid.NewGuid(), Name = "Bread", CategoryId = food.Id, UnitId = kilogram.Id }; var milk = new Product { Id = Guid.NewGuid(), Name = "Milk", CategoryId = food.Id }; - _products = [bread, milk]; + var loan = new Product { Id = Guid.NewGuid(), Name = "Loan", CategoryId = liabilities.Id }; + _products = [bread, milk, loan]; var transaction = new Transaction { @@ -217,10 +236,94 @@ static DesignTimeGnomeshadeClient() Name = "Home improvement", }, ]; + + var transactionSchedule = new TransactionSchedule + { + Id = Guid.Empty, + Name = "Monthly", + StartingAt = SystemClock.Instance.GetCurrentInstant() + Duration.FromDays(15), + Period = Period.FromMonths(1), + Count = 12, + }; + + _transactionSchedules = [transactionSchedule]; + _plannedTransactions = []; + _plannedTransfers = []; + _plannedPurchases = []; + _plannedLoanPayments = []; + + var timeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault(); + for (var i = 0; i < transactionSchedule.Count; i++) + { + var startingAt = transactionSchedule.StartingAt.InZone(timeZone).LocalDateTime; + for (var j = 0; j < i; j++) + { + startingAt += transactionSchedule.Period; + } + + var bookedAt = startingAt.InZoneStrictly(timeZone).ToInstant(); + + var plannedTransaction = new PlannedTransaction + { + Id = i is 0 ? Guid.Empty : Guid.NewGuid(), + ScheduleId = transactionSchedule.Id, + }; + + var plannedPrincipalTransfer = new PlannedTransfer + { + Id = i is 0 ? Guid.Empty : Guid.NewGuid(), + TransactionId = plannedTransaction.Id, + SourceAmount = 500, + SourceAccountId = spending.Currencies.Single(account => account.CurrencyId == euro.Id).Id, + TargetAmount = 500, + TargetCounterpartyId = bankCounterparty.Id, + TargetCurrencyId = euro.Id, + BookedAt = bookedAt, + Order = 1, + }; + + var plannedInterestTransfer = new PlannedTransfer + { + Id = Guid.NewGuid(), + TransactionId = plannedTransaction.Id, + SourceAmount = 150, + SourceAccountId = spending.Currencies.Single(account => account.CurrencyId == euro.Id).Id, + TargetAmount = 150, + TargetCounterpartyId = bankCounterparty.Id, + TargetCurrencyId = euro.Id, + BookedAt = bookedAt, + Order = 2, + }; + + var plannedPurchase = new PlannedPurchase + { + Id = i is 0 ? Guid.Empty : Guid.NewGuid(), + TransactionId = plannedTransaction.Id, + Price = plannedPrincipalTransfer.SourceAmount + plannedInterestTransfer.SourceAmount, + CurrencyId = euro.Id, + ProductId = loan.Id, + Amount = 1, + ProjectIds = [], + }; + + var plannedLoanPayment = new PlannedLoanPayment + { + Id = i is 0 ? Guid.Empty : Guid.NewGuid(), + LoanId = _loans.Single().Id, + TransactionId = plannedTransaction.Id, + Amount = plannedPrincipalTransfer.SourceAmount, + Interest = plannedInterestTransfer.SourceAmount, + }; + + _plannedTransactions.Add(plannedTransaction); + _plannedTransfers.AddRange([plannedPrincipalTransfer, plannedInterestTransfer]); + _plannedPurchases.Add(plannedPurchase); + _plannedLoanPayments.Add(plannedLoanPayment); + } } /// - public Task LogInAsync(Login login) => throw new NotImplementedException(); + public Task LogInAsync(Login login) => Task.FromResult(new SuccessfulLogin()); /// public Task SocialRegister() => throw new NotImplementedException(); @@ -486,6 +589,147 @@ public Task AddRelatedTransactionAsync(Guid id, Guid relatedId) => public Task RemoveRelatedTransactionAsync(Guid id, Guid relatedId) => throw new NotImplementedException(); + /// + public Task> GetTransactionSchedules(CancellationToken cancellationToken = default) => + Task.FromResult(_transactionSchedules.ToList()); + + /// + public Task GetTransactionSchedule(Guid id, CancellationToken cancellationToken = default) => + Task.FromResult(_transactionSchedules.Single(schedule => schedule.Id == id)); + + /// + public async Task CreateTransactionSchedule(TransactionScheduleCreation schedule) + { + var id = Guid.NewGuid(); + await PutTransactionSchedule(id, schedule); + return id; + } + + /// + public Task PutTransactionSchedule(Guid id, TransactionScheduleCreation schedule) + { + var existing = _transactionSchedules.SingleOrDefault(transactionSchedule => transactionSchedule.Id == id); + existing ??= new(); + + existing.Name = schedule.Name; + existing.StartingAt = schedule.StartingAt; + existing.Period = schedule.Period; + existing.Count = schedule.Count; + + return Task.CompletedTask; + } + + /// + public async Task DeleteTransactionSchedule(Guid id) + { + var schedule = await GetTransactionSchedule(id); + _transactionSchedules.Remove(schedule); + } + + /// + public Task> GetPlannedTransactions(Interval interval, CancellationToken cancellationToken = default) + { + var transactions = _plannedTransactions + .Where(transaction => + { + var date = _plannedTransfers + .Where(transfer => transfer.TransactionId == transaction.Id) + .Select(transfer => transfer.BookedAt) + .Max(); + + return date is { } instant && interval.Contains(instant); + }) + .ToList(); + + return Task.FromResult(transactions); + } + + /// + public Task> GetPlannedTransactions(CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransactions.ToList()); + + /// + public Task> GetPlannedTransactions( + Guid scheduleId, + CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransactions.Where(transaction => transaction.ScheduleId == scheduleId).ToList()); + + /// + public Task GetPlannedTransaction(Guid id, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransactions.Single(transaction => transaction.Id == id)); + + /// + public Task CreatePlannedTransaction(PlannedTransactionCreation transaction) => throw new NotImplementedException(); + + /// + public Task PutPlannedTransaction(Guid id, PlannedTransactionCreation transaction) => throw new NotImplementedException(); + + /// + public Task DeletePlannedTransaction(Guid id) => throw new NotImplementedException(); + + /// + public Task> GetPlannedTransfers(CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransfers.ToList()); + + /// + public Task> GetPlannedTransfers(Guid transactionId, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransfers.Where(transfer => transfer.TransactionId == transactionId).ToList()); + + /// + public Task GetPlannedTransfer(Guid id, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedTransfers.Single(transfer => transfer.Id == id)); + + /// + public Task CreatePlannedTransfer(PlannedTransferCreation transfer) => throw new NotImplementedException(); + + /// + public Task PutPlannedTransfer(Guid id, PlannedTransferCreation transfer) => throw new NotImplementedException(); + + /// + public Task DeletePlannedTransfer(Guid id) => throw new NotImplementedException(); + + /// + public Task> GetPlannedPurchases(CancellationToken cancellationToken = default) => + Task.FromResult(_plannedPurchases.ToList()); + + /// + public Task> GetPlannedPurchases(Guid transactionId, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedPurchases.Where(transfer => transfer.TransactionId == transactionId).ToList()); + + /// + public Task GetPlannedPurchase(Guid id, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedPurchases.Single(purchase => purchase.Id == id)); + + /// + public Task CreatePlannedPurchase(PlannedPurchaseCreation purchase) => throw new NotImplementedException(); + + /// + public Task PutPlannedPurchase(Guid id, PlannedPurchaseCreation purchase) => throw new NotImplementedException(); + + /// + public Task DeletePlannedPurchase(Guid id) => throw new NotImplementedException(); + + /// + public Task> GetPlannedLoanPayments(CancellationToken cancellationToken = default) => + Task.FromResult(_plannedLoanPayments.ToList()); + + /// + public Task> GetPlannedLoanPayments(Guid transactionId, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedLoanPayments.Where(transfer => transfer.TransactionId == transactionId).ToList()); + + /// + public Task GetPlannedLoanPayment(Guid id, CancellationToken cancellationToken = default) => + Task.FromResult(_plannedLoanPayments.Single(payment => payment.Id == id)); + + /// + public Task CreatePlannedLoanPayment(LoanPaymentCreation loanPayment) => throw new NotImplementedException(); + + /// + public Task PutPlannedLoanPayment(Guid id, LoanPaymentCreation transfer) => throw new NotImplementedException(); + + /// + public Task DeletePlannedLoanPayment(Guid id) => throw new NotImplementedException(); + /// [Obsolete] public Task> GetLegacyLoans(CancellationToken cancellationToken = default) => diff --git a/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs b/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs index cddb9cfa7..3fdbc43ab 100644 --- a/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs +++ b/source/Gnomeshade.Avalonia.Core/Loans/LoanPaymentRow.cs @@ -18,7 +18,7 @@ public sealed class LoanPaymentRow : PropertyChangedBase /// The payment which this row will represent. /// All available loans. /// All available currencies. - public LoanPaymentRow(LoanPayment payment, IEnumerable loans, IEnumerable currencies) + public LoanPaymentRow(LoanPaymentBase payment, IEnumerable loans, IEnumerable currencies) { var loan = loans.Single(loan => loan.Id == payment.LoanId); Loan = loan.Name; diff --git a/source/Gnomeshade.Avalonia.Core/LocalDateConverter.cs b/source/Gnomeshade.Avalonia.Core/LocalDateConverter.cs index 270e9fb02..cfe1d7312 100644 --- a/source/Gnomeshade.Avalonia.Core/LocalDateConverter.cs +++ b/source/Gnomeshade.Avalonia.Core/LocalDateConverter.cs @@ -10,7 +10,7 @@ namespace Gnomeshade.Avalonia.Core; /// Converts a binding value of type . -public sealed class LocalDateConverter : NodaTimeValueConverter +public sealed class LocalDateConverter : NodaTimeValueStructConverter { /// protected override LocalDate TemplateValue { get; } = new(2000, 12, 31); diff --git a/source/Gnomeshade.Avalonia.Core/LocalDateTimeConverter.cs b/source/Gnomeshade.Avalonia.Core/LocalDateTimeConverter.cs index a205ce7c7..c207f1831 100644 --- a/source/Gnomeshade.Avalonia.Core/LocalDateTimeConverter.cs +++ b/source/Gnomeshade.Avalonia.Core/LocalDateTimeConverter.cs @@ -10,7 +10,7 @@ namespace Gnomeshade.Avalonia.Core; /// Converts a binding value of type . -public sealed class LocalDateTimeConverter : NodaTimeValueConverter +public sealed class LocalDateTimeConverter : NodaTimeValueStructConverter { /// protected override LocalDateTime TemplateValue { get; } = new(2000, 12, 31, 13, 20); diff --git a/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs b/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs index 63f5ac9de..01fb78e66 100644 --- a/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/MainWindowViewModel.cs @@ -79,6 +79,7 @@ public MainWindowViewModel(IServiceProvider serviceProvider) About = activityService.Create(ShowAboutWindow, "Waiting for About window to be closed"); License = activityService.Create(ShowLicenseWindow, "Waiting for License window to be closed"); NavigateBack = activityService.Create(NavigateBackAsync, () => _navigationHistory.Count is not 0, "Navigating back"); + TransactionSchedules = activityService.Create(SwitchTo, "Switching to transaction schedules"); PropertyChanging += OnPropertyChanging; } @@ -95,6 +96,9 @@ public MainWindowViewModel(IServiceProvider serviceProvider) /// Gets a command for switching to the previous . public CommandBase NavigateBack { get; } + /// Gets a command for switching the to . + public CommandBase TransactionSchedules { get; } + /// Gets a value indicating whether it's possible to log out. public bool CanLogOut => ActiveView is not null and not LoginViewModel and not ConfigurationWizardViewModel; @@ -241,6 +245,7 @@ private static Exception CurrentLifetimeIsNull() private async Task InitializeActiveViewAsync() { // The first notification does not show up, and subsequent calls work after some delay + // todo if the app starts too fast the first notification still does not show up ActivityService.ShowNotification(new(null, null, expiration: TimeSpan.FromMilliseconds(1))); if (ActiveView is not null) diff --git a/source/Gnomeshade.Avalonia.Core/NodaTimeValueClassConverter.cs b/source/Gnomeshade.Avalonia.Core/NodaTimeValueClassConverter.cs new file mode 100644 index 000000000..710b6b442 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/NodaTimeValueClassConverter.cs @@ -0,0 +1,77 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Globalization; + +using Avalonia; +using Avalonia.Data; + +using NodaTime.Text; + +namespace Gnomeshade.Avalonia.Core; + +/// +public abstract class NodaTimeValueClassConverter : NodaTimeValueConverterBase + where TTime : class +{ + /// Gets a value to use for displaying example value on validation error. + protected abstract TTime TemplateValue { get; } + + /// + public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + { + return null; + } + + if (!targetType.IsAssignableTo(typeof(string)) || value is not TTime time) + { + return InvalidCastNotification; + } + + return GetPattern(culture).Format(time); + } + + /// + public override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is UnsetValueType or null) + { + return null; + } + + if (value is not string text) + { + return InvalidCastNotification; + } + + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + if (!targetType.IsAssignableTo(typeof(TTime))) + { + return InvalidCastNotification; + } + + var parseResult = GetPattern(culture).Parse(text); + if (parseResult.Success) + { + return parseResult.Value; + } + + var exception = new DataValidationException($"Expected format is {GetPatternText(culture)}"); + return new BindingNotification(exception, BindingErrorType.DataValidationError); + } + + /// Gets the pattern using which will be convert to and from text. + /// The culture to use. + /// A pattern that can convert to and form text. + protected abstract IPattern GetPattern(CultureInfo culture); + + private string GetPatternText(CultureInfo culture) => GetPattern(culture).Format(TemplateValue); +} diff --git a/source/Gnomeshade.Avalonia.Core/NodaTimeValueConverter.cs b/source/Gnomeshade.Avalonia.Core/NodaTimeValueStructConverter.cs similarity index 96% rename from source/Gnomeshade.Avalonia.Core/NodaTimeValueConverter.cs rename to source/Gnomeshade.Avalonia.Core/NodaTimeValueStructConverter.cs index 3f09a783b..65f0046e1 100644 --- a/source/Gnomeshade.Avalonia.Core/NodaTimeValueConverter.cs +++ b/source/Gnomeshade.Avalonia.Core/NodaTimeValueStructConverter.cs @@ -13,7 +13,7 @@ namespace Gnomeshade.Avalonia.Core; /// -public abstract class NodaTimeValueConverter : NodaTimeValueConverterBase +public abstract class NodaTimeValueStructConverter : NodaTimeValueConverterBase where TTime : struct { /// Gets a value to use for displaying example value on validation error. diff --git a/source/Gnomeshade.Avalonia.Core/PeriodConverter.cs b/source/Gnomeshade.Avalonia.Core/PeriodConverter.cs new file mode 100644 index 000000000..0a095a3e9 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/PeriodConverter.cs @@ -0,0 +1,22 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System.Globalization; + +using NodaTime; +using NodaTime.Text; + +namespace Gnomeshade.Avalonia.Core; + +/// Converts a binding value of type . +public sealed class PeriodConverter : NodaTimeValueClassConverter +{ + /// + protected override Period TemplateValue { get; } = + Period.FromYears(1) + Period.FromMonths(6) + Period.FromWeeks(2) + Period.FromDays(5); + + /// + protected override PeriodPattern GetPattern(CultureInfo culture) => + PeriodPattern.NormalizingIso; +} diff --git a/source/Gnomeshade.Avalonia.Core/Reports/BalanceReportViewModel.cs b/source/Gnomeshade.Avalonia.Core/Reports/BalanceReportViewModel.cs index 7553b5410..261b17a67 100644 --- a/source/Gnomeshade.Avalonia.Core/Reports/BalanceReportViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Reports/BalanceReportViewModel.cs @@ -7,11 +7,13 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Gnomeshade.Avalonia.Core.Reports.Splits; using Gnomeshade.WebApi.Client; using Gnomeshade.WebApi.Models.Accounts; +using Gnomeshade.WebApi.Models.Transactions; using LiveChartsCore.Defaults; using LiveChartsCore.Kernel.Sketches; @@ -54,6 +56,10 @@ public sealed partial class BalanceReportViewModel : ViewModelBase [Notify] private IReportSplit? _selectedSplit = SplitProvider.MonthlySplit; + /// Gets or sets a value indicating whether to include projections in the report. + [Notify] + private bool _includeProjections = true; + /// Gets the y axes for . [Notify(Setter.Private)] private List _yAxes; @@ -103,11 +109,16 @@ public void ResetZoom() /// protected override async Task Refresh() { - var transfersTask = _gnomeshadeClient.GetTransfersAsync(); + var cancellation = new CancellationTokenSource(); + var task = ( + _gnomeshadeClient.GetTransfersAsync(cancellation.Token), + _gnomeshadeClient.GetPlannedTransfers(cancellation.Token)) + .WhenAll(); + var (counterparty, allAccounts, currencies) = await - (_gnomeshadeClient.GetMyCounterpartyAsync(), - _gnomeshadeClient.GetAccountsAsync(), - _gnomeshadeClient.GetCurrenciesAsync()) + (_gnomeshadeClient.GetMyCounterpartyAsync(cancellation.Token), + _gnomeshadeClient.GetAccountsAsync(cancellation.Token), + _gnomeshadeClient.GetCurrenciesAsync(cancellation.Token)) .WhenAll(); var selected = SelectedAccounts.Select(account => account.Id).ToArray(); @@ -123,6 +134,7 @@ protected override async Task Refresh() if (SelectedSplit is not { } reportSplit) { + await cancellation.CancelAsync(); return; } @@ -132,7 +144,22 @@ protected override async Task Refresh() .SelectMany(account => account.Currencies.Where(aic => aic.CurrencyId == (SelectedCurrency?.Id ?? account.PreferredCurrencyId)).Select(aic => aic.Id)) .ToArray(); - var transfers = (await transfersTask) + var (actualTransfers, plannedTransfers) = await task; + var timeZone = _dateTimeZoneProvider.GetSystemDefault(); + + IEnumerable allTransfers = actualTransfers; + if (IncludeProjections) + { + var filtered = plannedTransfers + .Select(PlannedTransferSelector) + .Where(tuple => + (tuple.Planned.SourceAccountId is { } sourceId && inCurrencyIds.Contains(sourceId)) || + (tuple.Planned.TargetAccountId is { } targetId && inCurrencyIds.Contains(targetId))) + .Select(tuple => tuple.Transfer); + allTransfers = allTransfers.Concat(filtered); + } + + var transfers = allTransfers .Where(transfer => inCurrencyIds.Contains(transfer.SourceAccountId) || inCurrencyIds.Contains(transfer.TargetAccountId)) @@ -141,7 +168,6 @@ protected override async Task Refresh() .ThenBy(transfer => transfer.ModifiedAt) .ToArray(); - var timeZone = _dateTimeZoneProvider.GetSystemDefault(); var currentTime = _clock.GetCurrentInstant(); var dates = transfers .Select(transfer => transfer.ValuedAt ?? transfer.BookedAt!.Value) @@ -185,6 +211,32 @@ protected override async Task Refresh() XAxes = [reportSplit.GetXAxis(startTime, endTime)]; } + private static (PlannedTransfer Planned, Transfer Transfer) PlannedTransferSelector(PlannedTransfer plannedTransfer) + { + var transfer = new Transfer + { + Id = plannedTransfer.Id, + CreatedAt = plannedTransfer.CreatedAt, + OwnerId = plannedTransfer.OwnerId, + CreatedByUserId = plannedTransfer.CreatedByUserId, + ModifiedAt = plannedTransfer.ModifiedAt, + ModifiedByUserId = plannedTransfer.ModifiedByUserId, + TransactionId = plannedTransfer.TransactionId, + SourceAmount = plannedTransfer.SourceAmount, + SourceAccountId = plannedTransfer.SourceAccountId ?? default, + TargetAmount = plannedTransfer.TargetAmount, + TargetAccountId = plannedTransfer.TargetAccountId ?? default, + BankReference = null, + ExternalReference = null, + InternalReference = null, + Order = plannedTransfer.Order, + BookedAt = plannedTransfer.BookedAt, + ValuedAt = null, + }; + + return (plannedTransfer, transfer); + } + private void OnPropertyChanging(object? sender, PropertyChangingEventArgs e) { if (e.PropertyName is nameof(SelectedAccounts)) @@ -205,6 +257,11 @@ private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) await RefreshAsync(); } + if (e.PropertyName is nameof(IncludeProjections)) + { + await RefreshAsync(); + } + if (!IsBusy && e.PropertyName is nameof(SelectedCurrency)) { await RefreshAsync(); diff --git a/source/Gnomeshade.Avalonia.Core/Reports/CategoryReportViewModel.cs b/source/Gnomeshade.Avalonia.Core/Reports/CategoryReportViewModel.cs index 9e3ed9cf3..70650a921 100644 --- a/source/Gnomeshade.Avalonia.Core/Reports/CategoryReportViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Reports/CategoryReportViewModel.cs @@ -44,6 +44,10 @@ public sealed partial class CategoryReportViewModel : ViewModelBase [Notify] private IReportSplit? _selectedSplit = SplitProvider.MonthlySplit; + /// Gets or sets a value indicating whether to include projections in the report. + [Notify] + private bool _includeProjections = true; + /// Gets the data series of amount spent per month per category. [Notify(Setter.Private)] private List> _series = []; @@ -86,9 +90,14 @@ protected override async Task Refresh() return; } - var accounts = await _gnomeshadeClient.GetAccountsAsync(); + var (accounts, allTransactions, plannedTransactions, products) = await + (_gnomeshadeClient.GetAccountsAsync(), + _gnomeshadeClient.GetDetailedTransactionsAsync(new(Instant.MinValue, Instant.MaxValue)), + _gnomeshadeClient.GetPlannedTransactions(new Interval(Instant.MinValue, Instant.MaxValue)), + _gnomeshadeClient.GetProductsAsync()) + .WhenAll(); + var accountsInCurrency = accounts.SelectMany(account => account.Currencies).ToArray(); - var allTransactions = await _gnomeshadeClient.GetDetailedTransactionsAsync(new(Instant.MinValue, Instant.MaxValue)); var displayableTransactions = allTransactions .Select(transaction => transaction with { TransferBalance = -transaction.TransferBalance }) .Where(transaction => transaction.TransferBalance > 0) @@ -96,6 +105,12 @@ protected override async Task Refresh() var timeZone = _dateTimeZoneProvider.GetSystemDefault(); + if (IncludeProjections) + { + var planned = await Task.WhenAll(plannedTransactions.Select(Selector)); + displayableTransactions = displayableTransactions.Concat(planned).ToArray(); + } + var transactions = new TransactionData[displayableTransactions.Length]; for (var i = 0; i < displayableTransactions.Length; i++) { @@ -133,11 +148,9 @@ protected override async Task Refresh() var endTime = new ZonedDateTime(dates.Max(), timeZone); var splits = reportSplit.GetSplits(startTime, endTime).ToArray(); - var products = await _gnomeshadeClient.GetProductsAsync(); - var categories = await _gnomeshadeClient.GetCategoriesAsync(); - var nodes = categories + var nodes = Categories .Where(category => category.CategoryId == SelectedCategory?.Id || category.Id == SelectedCategory?.Id) - .Select(category => CategoryNode.FromCategory(category, categories)) + .Select(category => CategoryNode.FromCategory(category, Categories)) .ToList(); var uncategorizedTransfers = transactions @@ -185,19 +198,15 @@ protected override async Task Refresh() for (var purchaseIndex = 0; purchaseIndex < purchasesToSum.Length; purchaseIndex++) { var purchase = purchasesToSum[purchaseIndex]; - var sourceCurrencyIds = purchase.SourceCurrencyIds; - var targetCurrencyIds = purchase.TargetCurrencyIds; - if (sourceCurrencyIds.Length is not 1 || targetCurrencyIds.Length is not 1) + if (purchase.SourceCurrencyIds is not [var sourceCurrency] || + purchase.TargetCurrencyIds is not [var targetCurrency]) { // todo cannot handle multiple currencies (#686) sum += purchase.Purchase.Price; continue; } - var sourceCurrency = sourceCurrencyIds.Single(); - var targetCurrency = targetCurrencyIds.Single(); - if (sourceCurrency == targetCurrency) { sum += purchase.Purchase.Price; @@ -219,12 +228,85 @@ protected override async Task Refresh() XAxes = [reportSplit.GetXAxis(startTime, endTime)]; } + private async Task Selector(PlannedTransaction transaction) + { + var plannedTransfers = await _gnomeshadeClient.GetPlannedTransfers(transaction.Id); + var plannedPurchases = await _gnomeshadeClient.GetPlannedPurchases(transaction.Id); + var startTime = plannedTransfers.Select(transfer => transfer.BookedAt).Max()!.Value; + + var transfers = plannedTransfers.Select(plannedTransfer => new Transfer + { + Id = plannedTransfer.Id, + CreatedAt = plannedTransfer.CreatedAt, + OwnerId = plannedTransfer.OwnerId, + CreatedByUserId = plannedTransfer.CreatedByUserId, + ModifiedAt = plannedTransfer.ModifiedAt, + ModifiedByUserId = plannedTransfer.ModifiedByUserId, + TransactionId = plannedTransfer.TransactionId, + SourceAmount = plannedTransfer.SourceAmount, + SourceAccountId = plannedTransfer.SourceAccountId ?? default, + TargetAmount = plannedTransfer.TargetAmount, + TargetAccountId = plannedTransfer.TargetAccountId ?? default, + BankReference = null, + ExternalReference = null, + InternalReference = null, + Order = plannedTransfer.Order, + BookedAt = startTime, + ValuedAt = null, + }).ToList(); + + var purchases = plannedPurchases.Select(plannedPurchase => new Purchase + { + Id = plannedPurchase.Id, + CreatedAt = plannedPurchase.CreatedAt, + OwnerId = plannedPurchase.OwnerId, + CreatedByUserId = plannedPurchase.CreatedByUserId, + ModifiedAt = plannedPurchase.ModifiedAt, + ModifiedByUserId = plannedPurchase.ModifiedByUserId, + TransactionId = plannedPurchase.TransactionId, + Price = plannedPurchase.Price, + CurrencyId = plannedPurchase.CurrencyId, + ProductId = plannedPurchase.ProductId, + Amount = plannedPurchase.Amount, + DeliveryDate = null, + Order = plannedPurchase.Order, + }).ToList(); + + return new() + { + Id = transaction.Id, + OwnerId = transaction.OwnerId, + CreatedAt = transaction.CreatedAt, + CreatedByUserId = transaction.CreatedByUserId, + ModifiedAt = transaction.ModifiedAt, + ModifiedByUserId = transaction.ModifiedByUserId, + Description = null, + ImportedAt = null, + ReconciledAt = null, + RefundedBy = null, + BookedAt = startTime, + ValuedAt = null, + Transfers = transfers, + TransferBalance = transfers.Select(transfer => transfer.SourceAmount).Sum(), // todo + Purchases = purchases, + PurchaseTotal = purchases.Select(purchase => purchase.Price).Sum(), + LoanPayments = [], + LoanTotal = 0, + Links = [], + }; + } + private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(SelectedSplit) && SelectedSplit is not null) { await RefreshAsync(); } + + if (e.PropertyName is nameof(IncludeProjections)) + { + await RefreshAsync(); + } } internal readonly struct PurchaseData(Purchase purchase, CategoryNode? node, TransactionData transaction) diff --git a/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs b/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs index a3f26acd3..2dee3dcec 100644 --- a/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs +++ b/source/Gnomeshade.Avalonia.Core/ServiceCollectionExtensions.cs @@ -56,5 +56,6 @@ public static IServiceCollection AddViewModels(this IServiceCollection serviceCo .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Controls/TransactionFilter.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Controls/TransactionFilter.cs index 54bb93d1e..bd45a0adc 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Controls/TransactionFilter.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Controls/TransactionFilter.cs @@ -108,6 +108,10 @@ public sealed partial class TransactionFilter : FilterBase [Notify] private string? _transferReferenceFilter; + /// Gets or sets a value indicating whether to include planned transactions. + [Notify] + private bool _includeProjections = true; + /// Initializes a new instance of the class. /// Service for indicating the activity of the application to the user. /// Clock which can provide the current instant. diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentUpsertionViewModel.cs new file mode 100644 index 000000000..5f48c89fd --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentUpsertionViewModel.cs @@ -0,0 +1,173 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Controls; + +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Accounts; +using Gnomeshade.WebApi.Models.Loans; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Loans; + +/// Creates or updates a single loan payment. +public sealed partial class PlannedLoanPaymentUpsertionViewModel : UpsertionViewModel +{ + private readonly Guid _transactionId; + + /// Gets all available loans. + [Notify(Setter.Private)] + private List _loans = []; + + /// Gets or sets the loan that this payment is a part of. + [Notify] + private Loan? _loan; + + /// Gets or sets the amount of the loan payment. + /// + [Notify] + private decimal? _amount; + + /// Gets or sets the interest amount of this loan payment. + [Notify] + private decimal? _interest; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// The id of the transaction to which to add the loan. + /// The id of the loan payment to edit. + public PlannedLoanPaymentUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + Guid transactionId, + Guid? id) + : base(activityService, gnomeshadeClient) + { + _transactionId = transactionId; + Id = id; + + PropertyChanged += OnPropertyChanged; + } + + /// + public AutoCompleteSelector LoanSelector => AutoCompleteSelectors.Loan; + + /// + public override bool CanSave => + Loan is not null && + Amount is not null && + Interest is not null; + + /// + protected override async Task Refresh() + { + Loans = await GnomeshadeClient.GetLoansAsync(); + + if (Id is not { } id) + { + return; + } + + var payment = await GnomeshadeClient.GetPlannedLoanPayment(id); + Loan = Loans.Single(loan => loan.Id == payment.LoanId); + Amount = payment.Amount; + Interest = payment.Interest; + } + + /// + protected override async Task SaveValidatedAsync() + { + var loanPayment = new LoanPaymentCreation + { + LoanId = Loan?.Id, + TransactionId = _transactionId, + Amount = Amount, + Interest = Interest, + }; + + if (Id is { } existingId) + { + await GnomeshadeClient.PutPlannedLoanPayment(existingId, loanPayment); + } + else + { + Id = await GnomeshadeClient.CreatePlannedLoanPayment(loanPayment); + } + + return Id.Value; + } + + private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not nameof(Loan) || Loan is not { } loan) + { + return; + } + + var accounts = await GnomeshadeClient.GetAccountsAsync(); + var transfers = await GnomeshadeClient.GetPlannedTransfers(_transactionId); + + var sourceCounterparties = transfers + .Select(transfer => + { + if (transfer.IsSourceAccount) + { + return accounts + .Single(account => account.Currencies.Any(currency => currency.Id == transfer.SourceAccountId)) + .CounterpartyId; + } + + return transfer.SourceCounterpartyId.Value; + }) + .Distinct() + .ToArray(); + + var targetCounterparties = transfers + .Select(transfer => + { + if (transfer.IsTargetAccount) + { + return accounts + .Single(account => account.Currencies.Any(currency => currency.Id == transfer.TargetAccountId)) + .CounterpartyId; + } + + return transfer.TargetCounterpartyId.Value; + }) + .Distinct() + .ToArray(); + + if (sourceCounterparties is not [var source] || targetCounterparties is not [var target]) + { + return; + } + + if (loan.IssuingCounterpartyId == source && loan.ReceivingCounterpartyId == target) + { + Amount = transfers.Sum(transfer => transfer.SourceAmount); + Interest = 0; + } + else if (loan.IssuingCounterpartyId == target && loan.ReceivingCounterpartyId == source) + { + if (transfers.OrderByDescending(transfer => transfer.SourceAmount).ToArray() is [var amount, var interest]) + { + Amount = -amount.SourceAmount; + Interest = -interest.SourceAmount; + } + else if (transfers is [var transfer]) + { + Amount = -transfer.SourceAmount; + Interest = 0; + } + } + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentViewModel.cs new file mode 100644 index 000000000..509a99d85 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Loans/PlannedLoanPaymentViewModel.cs @@ -0,0 +1,103 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.Avalonia.Core.Loans; +using Gnomeshade.WebApi.Client; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Loans; + +/// Overview of all loan payments for a single transaction. +public sealed partial class PlannedLoanPaymentViewModel : OverviewViewModel +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + private readonly Guid _transactionId; + private PlannedLoanPaymentUpsertionViewModel _details; + + /// Gets the total loaned amount. + [Notify(Setter.Private)] + private decimal _total; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// The transaction for which to create a loan overview. + public PlannedLoanPaymentViewModel(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid transactionId) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + _transactionId = transactionId; + _details = new(activityService, gnomeshadeClient, transactionId, null); + + PropertyChanged += OnPropertyChanged; + _details.Upserted += DetailsOnUpserted; + } + + /// + public override PlannedLoanPaymentUpsertionViewModel Details + { + get => _details; + set + { + _details.Upserted -= DetailsOnUpserted; + SetAndNotify(ref _details, value); + _details.Upserted += DetailsOnUpserted; + } + } + + /// + public override async Task UpdateSelection() + { + Details = new(ActivityService, _gnomeshadeClient, _transactionId, Selected?.Id); + await Details.RefreshAsync(); + } + + /// + protected override async Task Refresh() + { + var payments = await _gnomeshadeClient.GetPlannedLoanPayments(_transactionId); + var loans = await _gnomeshadeClient.GetLoansAsync(); + var currencies = await _gnomeshadeClient.GetCurrenciesAsync(); + + var overviews = payments + .Select(payment => new LoanPaymentRow(payment, loans, currencies)) + .ToList(); + + var selected = Selected; + Total = payments.Select(payment => payment.Amount + payment.Interest).Sum(); // todo + Rows = new(overviews); + Selected = Rows.SingleOrDefault(row => row.Id == selected?.Id); + + if (Selected is null) + { + await Details.RefreshAsync(); + } + } + + /// + protected override async Task DeleteAsync(LoanPaymentRow row) + { + await _gnomeshadeClient.DeletePlannedLoanPayment(row.Id); + Details = new(ActivityService, _gnomeshadeClient, _transactionId, null); + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(Rows)) + { + OnPropertyChanged(nameof(Total)); + } + } + + private async void DetailsOnUpserted(object? sender, UpsertedEventArgs e) + { + await RefreshAsync(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/PlannedTransactionUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/PlannedTransactionUpsertionViewModel.cs new file mode 100644 index 000000000..4f1fa3466 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/PlannedTransactionUpsertionViewModel.cs @@ -0,0 +1,88 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.Avalonia.Core.Transactions.Loans; +using Gnomeshade.Avalonia.Core.Transactions.Purchases; +using Gnomeshade.Avalonia.Core.Transactions.Transfers; +using Gnomeshade.WebApi.Client; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions; + +/// Create or update a planned transaction. +public sealed partial class PlannedTransactionUpsertionViewModel : TransactionUpsertionBase +{ + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + + /// Gets view model of all transfers of this transaction. + [Notify(Setter.Private)] + private PlannedTransferViewModel? _transfers; + + /// Gets view model of all purchases of this transaction. + [Notify(Setter.Private)] + private PlannedPurchaseViewModel? _purchases; + + /// Gets view model of all loans of this transaction. + [Notify(Setter.Private)] + private PlannedLoanPaymentViewModel? _loans; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// Gnomeshade API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The id of the transaction to edit. + public PlannedTransactionUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid? id) + : base(activityService, gnomeshadeClient, id) + { + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + } + + /// + public override bool CanSave => false; + + /// + protected override async Task Refresh() + { + if (Id is not { } transactionId) + { + IsReadOnly = false; + return; + } + + Transfers ??= new(ActivityService, GnomeshadeClient, _dialogService, _dateTimeZoneProvider, transactionId); + Purchases ??= new(ActivityService, GnomeshadeClient, _dialogService, _dateTimeZoneProvider, transactionId); + Loans ??= new(ActivityService, GnomeshadeClient, transactionId); + + await Task.WhenAll( + Transfers.RefreshAsync(), + Purchases.RefreshAsync(), + Loans.RefreshAsync()); + + if (!Loans.Rows.Any()) + { + Loans = null; + } + } + + /// + protected override Task SaveValidatedAsync() + { + throw new NotImplementedException(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseUpsertionViewModel.cs new file mode 100644 index 000000000..37f45ebc8 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseUpsertionViewModel.cs @@ -0,0 +1,224 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core.Commands; +using Gnomeshade.Avalonia.Core.Products; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Accounts; +using Gnomeshade.WebApi.Models.Products; +using Gnomeshade.WebApi.Models.Projects; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Purchases; + +/// Create or update a purchase. +public sealed partial class PlannedPurchaseUpsertionViewModel : UpsertionViewModel +{ + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + private readonly Guid _transactionId; + + private PlannedPurchase? _purchase; + + /// Gets or sets the amount paid to purchase an of . + [Notify] + private decimal? _price; + + /// Gets or sets the id of the currency of . + [Notify] + private Currency? _currency; + + /// Gets or sets the id of the purchased product. + [Notify] + private Product? _product; + + /// Gets or sets the amount of that was purchased. + [Notify] + private decimal? _amount; + + /// Gets or sets the project that this purchase is a part of. + [Notify] + private Project? _project; + + /// Gets a collection of all currencies. + [Notify(Setter.Private)] + private List _currencies = []; + + /// Gets a collection of all products. + [Notify(Setter.Private)] + private List _products = []; + + /// Gets a collection of all projects. + [Notify(Setter.Private)] + private List _projects = []; + + /// Gets the name of the unit of the . + [Notify(Setter.Private)] + private string? _unitName; + + /// Gets or sets the order of the purchase within a transaction. + [Notify] + private uint? _order; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// Gnomeshade API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The id of the transaction to which to add the purchase to. + /// The id of the purchase to edit. + public PlannedPurchaseUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid transactionId, + Guid? id) + : base(activityService, gnomeshadeClient) + { + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + _transactionId = transactionId; + Id = id; + + CreateProduct = activityService.Create(ShowNewProductDialog, "Waiting for product creation"); + PropertyChanged += OnPropertyChanged; + } + + /// + public AutoCompleteSelector CurrencySelector => AutoCompleteSelectors.Currency; + + /// + public AutoCompleteSelector ProductSelector => AutoCompleteSelectors.Product; + + /// + public AutoCompleteSelector ProjectSelector => AutoCompleteSelectors.Project; + + /// + public override bool CanSave => + Price is not null && + Currency is not null && + Product is not null && + Amount is not null; + + /// Gets a command for showing a modal dialog for creating a new product. + public CommandBase CreateProduct { get; } + + /// + protected override async Task Refresh() + { + var (currencies, products, projects) = await ( + GnomeshadeClient.GetCurrenciesAsync(), + GnomeshadeClient.GetProductsAsync(), + GnomeshadeClient.GetProjectsAsync()) + .WhenAll(); + + Currencies = currencies; + Products = products; + Projects = projects; + + if (Id is not { } purchaseId) + { + return; + } + + _purchase = await GnomeshadeClient.GetPlannedPurchase(purchaseId); + + Price = _purchase.Price; + Currency = Currencies.Single(currency => currency.Id == _purchase.CurrencyId); + Amount = _purchase.Amount; + Product = Products.Single(product => product.Id == _purchase.ProductId); + Order = _purchase.Order; + Project = _purchase.ProjectIds switch + { + [] => null, + [var projectId] => Projects.Single(project => project.Id == projectId), + _ => throw new NotSupportedException("Purchases that are a part of multiple projects are not supported"), + }; + } + + /// + protected override async Task SaveValidatedAsync() + { + var purchaseCreation = new PlannedPurchaseCreation + { + TransactionId = _transactionId, + Price = Price, + CurrencyId = Currency!.Id, + Amount = Amount, + ProductId = Product!.Id, + Order = Order, + }; + + if (Id is { } existingId) + { + await GnomeshadeClient.PutPlannedPurchase(existingId, purchaseCreation); + } + else + { + Id = await GnomeshadeClient.CreatePlannedPurchase(purchaseCreation); + } + + if (_purchase?.ProjectIds is [var projectId]) + { + if (Project is null || Project.Id != projectId) + { + await GnomeshadeClient.RemovePurchaseFromProjectAsync(projectId, Id.Value); + } + + if (Project is not null && Project.Id != projectId) + { + await GnomeshadeClient.AddPurchaseToProjectAsync(Project.Id, Id.Value); + } + } + else if (Project is not null) + { + await GnomeshadeClient.AddPurchaseToProjectAsync(Project.Id, Id.Value); + } + + return Id.Value; + } + + private async Task ShowNewProductDialog(Window window) + { + var viewModel = new ProductUpsertionViewModel(ActivityService, GnomeshadeClient, _dateTimeZoneProvider, null); + await viewModel.RefreshAsync(); + + var result = await _dialogService.ShowDialogValue(window, viewModel, dialog => + { + dialog.Title = "Create product"; + viewModel.Upserted += (_, args) => dialog.Close(args.Id); + }); + + await RefreshAsync(); + if (result is not { } createdId) + { + return; + } + + Product = Products.SingleOrDefault(product => product.Id == createdId); + } + + private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(Product)) + { + UnitName = Product?.UnitId is null + ? null + : (await GnomeshadeClient.GetUnitAsync(Product.UnitId.Value)).Name; + } + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseViewModel.cs new file mode 100644 index 000000000..4d0e140da --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PlannedPurchaseViewModel.cs @@ -0,0 +1,175 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Purchases; + +/// Overview of all s of a single . +public sealed partial class PlannedPurchaseViewModel : OverviewViewModel +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + private readonly Guid _transactionId; + + private PlannedPurchaseUpsertionViewModel _details; + + /// Gets the total purchased amount. + [Notify(Setter.Private)] + private decimal _total; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The transaction for which to create a purchase overview. + public PlannedPurchaseViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid transactionId) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + _transactionId = transactionId; + _details = new(activityService, gnomeshadeClient, _dialogService, dateTimeZoneProvider, transactionId, null); + + PropertyChanged += OnPropertyChanged; + _details.Upserted += DetailsOnUpserted; + } + + /// + public override PlannedPurchaseUpsertionViewModel Details + { + get => _details; + set + { + _details.Upserted -= DetailsOnUpserted; + SetAndNotify(ref _details, value); + _details.Upserted += DetailsOnUpserted; + } + } + + /// + public override async Task UpdateSelection() + { + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, _transactionId, Selected?.Id); + await Details.RefreshAsync(); + await SetDefaultCurrency(); + } + + /// + protected override async Task Refresh() + { + var purchases = await _gnomeshadeClient.GetPlannedPurchases(_transactionId); + var productIds = purchases.Select(purchase => purchase.ProductId).Distinct(); + + var (products, currencies, units, projects) = await ( + Task.WhenAll(productIds.Select(id => _gnomeshadeClient.GetProductAsync(id))), + _gnomeshadeClient.GetCurrenciesAsync(), + _gnomeshadeClient.GetUnitsAsync(), + _gnomeshadeClient.GetProjectsAsync()) + .WhenAll(); + + var overviews = purchases + .OrderBy(purchase => purchase.Order) + .ThenBy(purchase => purchase.ModifiedAt) + .Select(purchase => purchase.ToOverview(currencies, products, units, projects)); + + var sort = DataGridView.SortDescriptions; + var selected = Selected; + + Rows.CollectionChanged -= RowsOnCollectionChanged; + Rows = new(overviews); + Rows.CollectionChanged += RowsOnCollectionChanged; + DataGridView.SortDescriptions.AddRange(sort); + Selected = Rows.SingleOrDefault(overview => overview.Id == selected?.Id); + + if (Selected is null) + { + await Details.RefreshAsync(); + await SetDefaultCurrency(); + } + } + + /// + protected override async Task DeleteAsync(PurchaseOverview row) + { + await _gnomeshadeClient.DeletePurchaseAsync(row.Id); + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, _transactionId, null); + } + + private async Task SetDefaultCurrency() + { + var currencyNames = Rows.Select(overview => overview.CurrencyName).Distinct().ToList(); + if (currencyNames.Count > 1) + { + return; + } + + var currencyName = currencyNames.FirstOrDefault(); + var transfers = await _gnomeshadeClient.GetTransfersAsync(_transactionId); + if (string.IsNullOrWhiteSpace(currencyName)) + { + var accounts = + (await _gnomeshadeClient.GetAccountsAsync()).SelectMany(account => + account.Currencies); + var c = transfers + .Select(transfer => transfer.SourceAccountId) + .Concat(transfers.Select(transfer => transfer.TargetAccountId)) + .Select(id => accounts.Single(ac => ac.Id == id).CurrencyAlphabeticCode) + .Distinct() + .ToList(); + + if (c.Count is 1) + { + currencyName = c.Single(); + } + else + { + return; + } + } + + Details.Currency = Details.Currencies.Single(currency => currency.AlphabeticCode == currencyName); + Details.Price ??= transfers.Sum(transfer => transfer.SourceAmount) - Rows.Sum(row => row.Price); + + var lastOrder = Rows.Select(purchase => purchase.Order).Max() ?? default; + Details.Order ??= lastOrder + 1; + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(Rows)) + { + OnPropertyChanged(nameof(Total)); + } + } + + private void RowsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(nameof(Total)); + } + + private async void DetailsOnUpserted(object? sender, UpsertedEventArgs e) + { + await RefreshAsync(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PurchaseExtensions.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PurchaseExtensions.cs index 6c3da54c3..90f8d86b9 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PurchaseExtensions.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Purchases/PurchaseExtensions.cs @@ -41,4 +41,29 @@ internal static PurchaseOverview ToOverview( purchase.Order, project); } + + internal static PurchaseOverview ToOverview( + this PlannedPurchase purchase, + IEnumerable currencies, + IEnumerable products, + IEnumerable units, + IEnumerable projects) + { + var product = products.Single(product => product.Id == purchase.ProductId); + var unit = units.SingleOrDefault(unit => unit.Id == product.UnitId); + var project = purchase.ProjectIds is [var projectId, ..] + ? projects.Single(project => project.Id == projectId).Name + : null; + + return new( + purchase.Id, + purchase.Price, + currencies.Single(currency => currency.Id == purchase.CurrencyId).AlphabeticCode, + product.Name, + purchase.Amount, + unit?.Name, + null, + purchase.Order, + project); + } } diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionOverview.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionOverview.cs index d27140093..51781ed3c 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionOverview.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionOverview.cs @@ -21,14 +21,16 @@ public sealed class TransactionOverview : PropertyChangedBase /// All transfers of the transaction. /// All purchases of the transaction. /// All loan payments of the transaction. + /// Whether this transaction is a projection. public TransactionOverview( Guid id, DateTimeOffset? bookedAt, DateTimeOffset? valuedAt, DateTimeOffset? reconciledAt, - List transfers, - List purchases, - List loanPayments) + IReadOnlyCollection transfers, + IReadOnlyCollection purchases, + IReadOnlyCollection loanPayments, + bool projection = false) { Id = id; BookedAt = bookedAt; @@ -37,6 +39,7 @@ public TransactionOverview( Transfers = transfers; Purchases = purchases; LoanPayments = loanPayments; + Projection = projection; } /// Gets the id of the transactions. @@ -58,9 +61,12 @@ public TransactionOverview( public bool Reconciled => ReconciledAt is not null; /// Gets all transfers of the transaction. - public List Transfers { get; } + public IReadOnlyCollection Transfers { get; } - internal List Purchases { get; } + /// Gets a value indicating whether this transaction is a projection. + public bool Projection { get; } - internal List LoanPayments { get; } + internal IReadOnlyCollection Purchases { get; } + + internal IReadOnlyCollection LoanPayments { get; } } diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleOverview.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleOverview.cs new file mode 100644 index 000000000..191a6a0d1 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleOverview.cs @@ -0,0 +1,41 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +namespace Gnomeshade.Avalonia.Core.Transactions; + +/// An overview of a single . +public sealed class TransactionScheduleOverview : PropertyChangedBase +{ + /// Initializes a new instance of the class. + /// The schedule this overview will represent. + /// The current time zone. + public TransactionScheduleOverview(TransactionSchedule schedule, DateTimeZone timeZone) + { + Name = schedule.Name; + StartingAt = schedule.StartingAt.InZone(timeZone).LocalDateTime; + Period = schedule.Period; + Count = schedule.Count; + Id = schedule.Id; + } + + /// Gets the name of the schedule. + public string Name { get; } + + /// Gets the time of the first planned transaction. + public LocalDateTime StartingAt { get; } + + /// Gets the period between each planned transaction. + public Period Period { get; } + + /// Gets the number of planned transactions. + public int Count { get; } + + internal Guid Id { get; } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleUpsertionViewModel.cs new file mode 100644 index 000000000..aebd7a22d --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleUpsertionViewModel.cs @@ -0,0 +1,120 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions; + +/// Creates or updates . +public sealed partial class TransactionScheduleUpsertionViewModel : UpsertionViewModel +{ + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + + /// + [Notify] + private string? _name; + + /// + [Notify] + private LocalDateTime? _startingAt; + + /// + [Notify] + private Period? _period; + + /// + [Notify] + private int? _count; + + /// Gets or sets the transaction to use as a template for all planned transactions for the schedule. + [Notify] + private PlannedTransactionUpsertionViewModel? _templateTransaction; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The id of the transaction schedule to edit. + public TransactionScheduleUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid? id) + : base(activityService, gnomeshadeClient) + { + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + Id = id; + } + + /// + public override bool CanSave => + !string.IsNullOrWhiteSpace(Name) && + StartingAt is not null && + Period is not null && + Count is not null; + + /// + protected override async Task Refresh() + { + if (Id is not { } id) + { + TemplateTransaction = null; + return; + } + + var schedule = await GnomeshadeClient.GetTransactionSchedule(id); + var transactions = await GnomeshadeClient.GetPlannedTransactions(); + var timeZone = _dateTimeZoneProvider.GetSystemDefault(); + + Name = schedule.Name; + StartingAt = schedule.StartingAt.InZone(timeZone).LocalDateTime; + Period = schedule.Period; + Count = schedule.Count; + TemplateTransaction = transactions.FirstOrDefault() is { } transaction + ? new PlannedTransactionUpsertionViewModel(ActivityService, GnomeshadeClient, _dialogService, _dateTimeZoneProvider, transaction.Id) + : null; + + if (TemplateTransaction is not null) + { + await TemplateTransaction.RefreshAsync(); + } + } + + /// + protected override async Task SaveValidatedAsync() + { + var timeZone = _dateTimeZoneProvider.GetSystemDefault(); + var schedule = new TransactionScheduleCreation + { + Name = Name!, + StartingAt = StartingAt!.Value.InZoneStrictly(timeZone).ToInstant(), + Period = Period!, + Count = Count!.Value, + }; + + if (Id is { } existingId) + { + await GnomeshadeClient.PutTransactionSchedule(existingId, schedule); + } + else + { + Id = await GnomeshadeClient.CreateTransactionSchedule(schedule); + } + + return Id.Value; + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleViewModel.cs new file mode 100644 index 000000000..2dc5271f3 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionScheduleViewModel.cs @@ -0,0 +1,171 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.Avalonia.Core.Transactions.Transfers; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions; + +/// Overview of all transaction schedules. +/// +public sealed partial class TransactionScheduleViewModel + : OverviewViewModel +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + + private TransactionScheduleUpsertionViewModel _details; + + /// Gets a collection of all the planned transactions for the schedule. + [Notify(Setter.Private)] + private List? _plannedTransactions; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// A strongly typed API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + public TransactionScheduleViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + _details = new(activityService, gnomeshadeClient, dialogService, dateTimeZoneProvider, null); + + _details.Upserted += DetailsOnUpserted; + } + + /// + public override TransactionScheduleUpsertionViewModel Details + { + get => _details; + set + { + _details.Upserted -= DetailsOnUpserted; + SetAndNotify(ref _details, value); + _details.Upserted += DetailsOnUpserted; + } + } + + /// Gets a value indicating whether the column needs to be shown. + public bool ShowUserCurrency => _plannedTransactions? + .SelectMany(transaction => transaction.Transfers) + .Any(transfer => !string.IsNullOrWhiteSpace(transfer.UserCurrency)) ?? false; + + /// + /// Gets a value indicating whether the and + /// columns needs to be shown. + /// + public bool ShowOtherAmount => _plannedTransactions? + .SelectMany(transaction => transaction.Transfers) + .Any(transfer => transfer.DisplayTarget) ?? false; + + /// + public override async Task UpdateSelection() + { + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, Selected?.Id); + await Details.RefreshAsync(); + + if (Selected is null) + { + PlannedTransactions = null; + } + else + { + var currentTimeZone = _dateTimeZoneProvider.GetSystemDefault(); + await RefreshTransactions(currentTimeZone); + } + } + + /// + protected override async Task Refresh() + { + var schedules = await _gnomeshadeClient.GetTransactionSchedules(); + var currentTimeZone = _dateTimeZoneProvider.GetSystemDefault(); + var overviews = schedules + .Select(schedule => new TransactionScheduleOverview(schedule, currentTimeZone)) + .ToArray(); + + Rows = new(overviews); + + if (Selected is null) + { + await Details.RefreshAsync(); + _plannedTransactions = null; + } + else + { + await RefreshTransactions(currentTimeZone); + } + } + + /// + protected override async Task DeleteAsync(TransactionScheduleOverview row) + { + await _gnomeshadeClient.DeleteProjectAsync(row.Id); + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, null); + } + + private async void DetailsOnUpserted(object? sender, UpsertedEventArgs e) + { + await RefreshAsync(); + } + + private async Task RefreshTransactions(DateTimeZone currentTimeZone) + { + var (counterparties, counterparty, accounts) = await ( + _gnomeshadeClient.GetCounterpartiesAsync(), + _gnomeshadeClient.GetMyCounterpartyAsync(), + _gnomeshadeClient.GetAccountsAsync()) + .WhenAll(); + + var accountsInCurrency = accounts + .SelectMany(account => account.Currencies.Select(currency => (AccountInCurrency: currency, Account: account))) + .ToArray(); + + var plannedTransactions = await _gnomeshadeClient.GetPlannedTransactions(); + var overviews = await Task.WhenAll(plannedTransactions + .Select(async transaction => + { + var plannedTransfers = await _gnomeshadeClient.GetPlannedTransfers(transaction.Id); + var transfers = plannedTransfers + .Select(plannedTransfer => plannedTransfer.ToSummary(plannedTransfer.BookedAt!.Value, counterparties, counterparty, accountsInCurrency)) + .ToList(); + + var date = plannedTransfers + .Select(transfer => transfer.BookedAt) + .Max(); + + var purchases = await _gnomeshadeClient.GetPlannedPurchases(transaction.Id); + var loanPayments = await _gnomeshadeClient.GetPlannedLoanPayments(transaction.Id); + + return new TransactionOverview( + transaction.Id, + date!.Value.InZone(currentTimeZone).ToDateTimeOffset(), + null, + null, + transfers, + purchases, + loanPayments, + true); + })); + + PlannedTransactions = overviews.ToList(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionBase.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionBase.cs new file mode 100644 index 000000000..93f2f7007 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionBase.cs @@ -0,0 +1,23 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.WebApi.Client; + +namespace Gnomeshade.Avalonia.Core.Transactions; + +/// Base class for transaction upsertion view models. +public abstract class TransactionUpsertionBase : UpsertionViewModel +{ + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// Gnomeshade API client. + /// The id of the transaction to edit. + protected TransactionUpsertionBase(IActivityService activityService, IGnomeshadeClient gnomeshadeClient, Guid? id) + : base(activityService, gnomeshadeClient) + { + Id = id; + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs index 8c18c0059..fcc652c8c 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionUpsertionViewModel.cs @@ -22,7 +22,7 @@ namespace Gnomeshade.Avalonia.Core.Transactions; /// Create or update a transaction. -public sealed partial class TransactionUpsertionViewModel : UpsertionViewModel +public sealed partial class TransactionUpsertionViewModel : TransactionUpsertionBase { private readonly IDialogService _dialogService; private readonly IClock _clock; @@ -58,12 +58,11 @@ public TransactionUpsertionViewModel( IClock clock, IDateTimeZoneProvider dateTimeZoneProvider, Guid? id) - : base(activityService, gnomeshadeClient) + : base(activityService, gnomeshadeClient, id) { _dialogService = dialogService; _clock = clock; _dateTimeZoneProvider = dateTimeZoneProvider; - Id = id; Properties = new(activityService); Properties.PropertyChanged += PropertiesOnPropertyChanged; @@ -123,10 +122,12 @@ protected override async Task Refresh() { if (Id is not { } transactionId) { + IsReadOnly = false; return; } var transaction = await GnomeshadeClient.GetTransactionAsync(transactionId); + IsReadOnly = transaction.Reconciled; var defaultZone = _dateTimeZoneProvider.GetSystemDefault(); diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionViewModel.cs index 898441d81..f67c1f31c 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/TransactionViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/TransactionViewModel.cs @@ -25,7 +25,7 @@ namespace Gnomeshade.Avalonia.Core.Transactions; /// Overview of all s. -public sealed partial class TransactionViewModel : OverviewViewModel +public sealed partial class TransactionViewModel : OverviewViewModel { private static readonly DataGridSortDescription[] _sortDescriptions = [ @@ -41,7 +41,7 @@ public sealed partial class TransactionViewModel : OverviewViewModelGets all selected transactions. [Notify(Setter.Private)] @@ -66,7 +66,7 @@ public TransactionViewModel( _clock = clock; _dateTimeZoneProvider = dateTimeZoneProvider; _merge = new(ActivityService, _gnomeshadeClient); - _details = new(activityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, null); + _details = new TransactionUpsertionViewModel(activityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, null); _details.Upserted += DetailsOnUpserted; PropertyChanging += OnPropertyChanging; @@ -116,7 +116,7 @@ public TransactionViewModel( .Any(transfer => transfer.DisplayTarget); /// - public override TransactionUpsertionViewModel Details + public override TransactionUpsertionBase Details { get => _details; set @@ -138,7 +138,15 @@ public override TransactionUpsertionViewModel Details /// public override async Task UpdateSelection() { - Details = new(ActivityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, SelectedItem?.Id); + if (Selected is { Projection: true }) + { + Details = new PlannedTransactionUpsertionViewModel(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, Selected?.Id); + } + else + { + Details = new TransactionUpsertionViewModel(ActivityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, SelectedItem?.Id); + } + await Details.RefreshAsync(); } @@ -172,8 +180,9 @@ public void ClearMerge() /// protected override async Task Refresh() { - var (transactions, accounts, counterparties, products, categories, counterparty, loans) = await ( + var (transactions, plannedTransactions, accounts, counterparties, products, categories, counterparty, loans) = await ( _gnomeshadeClient.GetDetailedTransactionsAsync(Filter.Interval), + _gnomeshadeClient.GetPlannedTransactions(Filter.Interval), _gnomeshadeClient.GetAccountsAsync(), _gnomeshadeClient.GetCounterpartiesAsync(), _gnomeshadeClient.GetProductsAsync(), @@ -203,6 +212,37 @@ protected override async Task Refresh() transaction.LoanPayments); }).ToList(); + if (Filter.IncludeProjections) + { + var currentTimeZone = _dateTimeZoneProvider.GetSystemDefault(); + var plannedOverviews = await Task.WhenAll(plannedTransactions + .Select(async transaction => + { + var plannedTransfers = await _gnomeshadeClient.GetPlannedTransfers(transaction.Id); + var transfers = plannedTransfers + .Select(plannedTransfer => plannedTransfer.ToSummary(plannedTransfer.BookedAt!.Value, counterparties, counterparty, accountsInCurrency)) + .ToList(); + + var date = plannedTransfers + .Select(transfer => transfer.BookedAt) + .Max(); + + var purchases = await _gnomeshadeClient.GetPlannedPurchases(transaction.Id); + var loanPayments = await _gnomeshadeClient.GetPlannedLoanPayments(transaction.Id); + + return new TransactionOverview( + transaction.Id, + date!.Value.InZone(currentTimeZone).ToDateTimeOffset(), + null, + null, + transfers, + purchases, + loanPayments, + true); + })); + overviews = overviews.Concat(plannedOverviews).ToList(); + } + var selected = SelectedItem; var sort = DataGridView.SortDescriptions; Rows = new(overviews); @@ -221,7 +261,7 @@ protected override async Task Refresh() protected override async Task DeleteAsync(TransactionOverview row) { await _gnomeshadeClient.DeleteTransactionAsync(row.Id); - Details = new(ActivityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, null); + Details = new TransactionUpsertionViewModel(ActivityService, _gnomeshadeClient, _dialogService, _clock, _dateTimeZoneProvider, null); } /// @@ -285,13 +325,18 @@ private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) } } - private void FilterOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + private async void FilterOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(TransactionFilter.IsValid)) { OnPropertyChanged(nameof(CanRefresh)); } + if (e.PropertyName is nameof(TransactionFilter.IncludeProjections)) + { + await RefreshAsync(); + } + if (e.PropertyName is nameof(TransactionFilter.SelectedAccount) or nameof(TransactionFilter.InvertAccount) or diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferUpsertionViewModel.cs new file mode 100644 index 000000000..c012249d8 --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferUpsertionViewModel.cs @@ -0,0 +1,307 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core.Accounts; +using Gnomeshade.Avalonia.Core.Commands; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Accounts; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +using PropertyChanged.SourceGenerator; + +namespace Gnomeshade.Avalonia.Core.Transactions.Transfers; + +/// Create or update a transfer. +public sealed partial class PlannedTransferUpsertionViewModel : UpsertionViewModel +{ + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + private readonly Guid _transactionId; + + /// Gets or sets the amount withdrawn from . + [Notify] + private decimal? _sourceAmount; + + /// + [Notify] + private bool _isSourceAccount = true; + + /// Gets or sets the source account of the transfer. + [Notify] + private Account? _sourceAccount; + + /// Gets or sets the source counterparty of the transfer. + [Notify] + private Counterparty? _sourceCounterparty; + + /// Gets or sets the currency of . + [Notify] + private Currency? _sourceCurrency; + + /// Gets or sets the amount deposited to . + [Notify] + private decimal? _targetAmount; + + /// + [Notify] + private bool _isTargetAccount = true; + + /// Gets or sets the target account of the transaction item. + [Notify] + private Account? _targetAccount; + + /// Gets or sets the target counterparty of the transfer. + [Notify] + private Counterparty? _targetCounterparty; + + /// Gets or sets the currency of . + [Notify] + private Currency? _targetCurrency; + + /// Gets or sets the date on which the transfer was posted to an account on the account servicer accounting books. + [Notify] + private LocalDateTime? _bookingDate; + + /// Gets a collection of all active accounts. + [Notify(Setter.Private)] + private List _accounts = []; + + /// Gets a collection of all active counterparties. + [Notify(Setter.Private)] + private List _counterparties = []; + + /// Gets a collection of all currencies. + [Notify(Setter.Private)] + private List _currencies = []; + + /// Gets a collection of currencies available for . + [Notify(Setter.Private)] + private List _sourceCurrencies = []; + + /// Gets a collection of currencies available for . + [Notify(Setter.Private)] + private List _targetCurrencies = []; + + /// Gets or sets the order of the item within a transaction. + [Notify] + private uint? _order; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// Gnomeshade API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The id of the transaction to which to add the transfer to. + /// The id of the transfer to edit. + public PlannedTransferUpsertionViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid transactionId, + Guid? id) + : base(activityService, gnomeshadeClient) + { + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + _transactionId = transactionId; + Id = id; + + CreateAccount = activityService.Create(window => ShowAccountDialog(window, null), _ => CanCreate, "Waiting for account creation"); + PropertyChanged += OnPropertyChanged; + } + + /// + public AutoCompleteSelector AccountSelector => AutoCompleteSelectors.Account; + + /// + public AutoCompleteSelector CounterpartySelector => AutoCompleteSelectors.Counterparty; + + /// + public AutoCompleteSelector CurrencySelector => AutoCompleteSelectors.Currency; + + /// Gets a value indicating whether should not be editable. + public bool IsTargetAmountReadOnly => SourceCurrency == TargetCurrency; + + /// Gets a value indicating whether can be invoked. + public bool CanCreate => SourceAccount is null || TargetAccount is null; + + /// + public override bool CanSave => + SourceAmount is not null && + (IsSourceAccount ? SourceAccount is not null : SourceCounterparty is not null) && + SourceCurrency is not null && + TargetAmount is not null && + (IsTargetAccount ? TargetAccount is not null : TargetCounterparty is not null) && + TargetCurrency is not null && + BookingDate.HasValue; + + /// Gets a command for showing a dialog for creating a new account. + public CommandBase CreateAccount { get; } + + /// + protected override async Task Refresh() + { + (Accounts, Currencies, Counterparties) = await ( + GnomeshadeClient.GetAccountsAsync(), + GnomeshadeClient.GetCurrenciesAsync(), + GnomeshadeClient.GetCounterpartiesAsync()) + .WhenAll(); + + if (Id is not { } transferId) + { + return; + } + + var transfer = await GnomeshadeClient.GetPlannedTransfer(transferId); + var defaultZone = _dateTimeZoneProvider.GetSystemDefault(); + + SourceAmount = transfer.SourceAmount; + IsSourceAccount = transfer.IsSourceAccount; + if (transfer.IsSourceAccount) + { + SourceAccount = Accounts.Single(a => a.Currencies.Any(c => c.Id == transfer.SourceAccountId.Value)); + SourceCurrency = Currencies.Single(cur => cur.Id == SourceAccount.Currencies.Single(c => c.Id == transfer.SourceAccountId.Value).CurrencyId); + } + else + { + SourceCounterparty = Counterparties.Single(counterparty => counterparty.Id == transfer.SourceCounterpartyId.Value); + SourceCurrency = Currencies.Single(currency => currency.Id == transfer.SourceCurrencyId.Value); + } + + TargetAmount = transfer.TargetAmount; + IsTargetAccount = transfer.IsTargetAccount; + if (transfer.IsTargetAccount) + { + TargetAccount = Accounts.Single(a => a.Currencies.Any(c => c.Id == transfer.TargetAccountId.Value)); + TargetCurrency = Currencies.Single(cur => cur.Id == TargetAccount.Currencies.Single(c => c.Id == transfer.TargetAccountId.Value).CurrencyId); + } + else + { + TargetCounterparty = Counterparties.Single(counterparty => counterparty.Id == transfer.TargetCounterpartyId.Value); + TargetCurrency = Currencies.Single(currency => currency.Id == transfer.TargetCurrencyId.Value); + } + + BookingDate = transfer.BookedAt?.InZone(defaultZone).LocalDateTime; + Order = transfer.Order; + } + + /// + protected override async Task SaveValidatedAsync() + { + var transfer = new PlannedTransferCreation + { + TransactionId = _transactionId, + SourceAmount = SourceAmount, + TargetAmount = TargetAmount, + Order = Order, + BookedAt = BookingDate?.InZoneStrictly(_dateTimeZoneProvider.GetSystemDefault()).ToInstant(), + }; + + if (IsSourceAccount) + { + transfer.SourceAccountId = SourceAccount?.Currencies.Single(c => c.CurrencyId == SourceCurrency?.Id).Id; + } + else + { + transfer.SourceCounterpartyId = SourceCounterparty?.Id; + transfer.SourceCurrencyId = SourceCurrency?.Id; + } + + if (IsTargetAccount) + { + transfer.TargetAccountId = TargetAccount?.Currencies.Single(c => c.CurrencyId == TargetCurrency?.Id).Id; + } + else + { + transfer.TargetCounterpartyId = TargetCounterparty?.Id; + transfer.TargetCurrencyId = TargetCurrency?.Id; + } + + if (Id is { } existingId) + { + await GnomeshadeClient.PutPlannedTransfer(existingId, transfer); + } + else + { + Id = await GnomeshadeClient.CreatePlannedTransfer(transfer); + } + + return Id.Value; + } + + private async Task ShowAccountDialog(Window window, Guid? id) + { + var viewModel = new AccountUpsertionViewModel(ActivityService, GnomeshadeClient, id); + await viewModel.RefreshAsync(); + + _ = await _dialogService.ShowDialogValue(window, viewModel, dialog => + { + dialog.Title = id.HasValue ? "Edit account" : "Create account"; + viewModel.Upserted += (_, args) => dialog.Close(args.Id); + }); + + await RefreshAsync(); + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(CanCreate)) + { + CreateAccount.InvokeExecuteChanged(); + } + + if (e.PropertyName is nameof(SourceAccount) or nameof(SourceCounterparty) or nameof(IsSourceAccount)) + { + if (SourceAccount is null || !IsSourceAccount) + { + SourceCurrencies = Currencies.ToList(); + } + else + { + var ids = SourceAccount.Currencies.Select(currency => currency.CurrencyId).ToArray(); + SourceCurrencies = Currencies.Where(currency => ids.Contains(currency.Id)).ToList(); + } + } + + if (e.PropertyName is nameof(TargetAccount) or nameof(TargetCounterparty) or nameof(IsTargetAccount)) + { + if (TargetAccount is null || !IsTargetAccount) + { + TargetCurrencies = Currencies.ToList(); + } + else + { + var ids = TargetAccount.Currencies.Select(currency => currency.CurrencyId).ToArray(); + TargetCurrencies = Currencies.Where(currency => ids.Contains(currency.Id)).ToList(); + } + } + + if (!IsTargetAmountReadOnly) + { + return; + } + + switch (e.PropertyName) + { + case nameof(SourceAmount): + TargetAmount = SourceAmount; + return; + + case nameof(SourceCurrency): + TargetCurrency = SourceCurrency; + return; + } + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferViewModel.cs new file mode 100644 index 000000000..0c612718b --- /dev/null +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/PlannedTransferViewModel.cs @@ -0,0 +1,92 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +namespace Gnomeshade.Avalonia.Core.Transactions.Transfers; + +/// Overview of all planned transfers for a single . +public sealed class PlannedTransferViewModel : OverviewViewModel +{ + private readonly IGnomeshadeClient _gnomeshadeClient; + private readonly IDialogService _dialogService; + private readonly IDateTimeZoneProvider _dateTimeZoneProvider; + private readonly Guid _transactionId; + + private PlannedTransferUpsertionViewModel _details; + + /// Initializes a new instance of the class. + /// Service for indicating the activity of the application to the user. + /// Gnomeshade API client. + /// Service for creating dialog windows. + /// Time zone provider for localizing instants to local time. + /// The transaction for which to create a planned transfer overview. + public PlannedTransferViewModel( + IActivityService activityService, + IGnomeshadeClient gnomeshadeClient, + IDialogService dialogService, + IDateTimeZoneProvider dateTimeZoneProvider, + Guid transactionId) + : base(activityService) + { + _gnomeshadeClient = gnomeshadeClient; + _dialogService = dialogService; + _dateTimeZoneProvider = dateTimeZoneProvider; + _transactionId = transactionId; + _details = new(activityService, gnomeshadeClient, _dialogService, _dateTimeZoneProvider, transactionId, null); + + _details.Upserted += DetailsOnUpserted; + } + + /// + public override PlannedTransferUpsertionViewModel Details + { + get => _details; + set + { + _details.Upserted -= DetailsOnUpserted; + SetAndNotify(ref _details, value); + _details.Upserted += DetailsOnUpserted; + } + } + + /// + public override async Task UpdateSelection() + { + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, _transactionId, Selected?.Id); + await Details.RefreshAsync(); + } + + /// + protected override async Task Refresh() + { + var (transfers, accounts, counterparties, currencies) = await ( + _gnomeshadeClient.GetPlannedTransfers(_transactionId), + _gnomeshadeClient.GetAccountsAsync(), + _gnomeshadeClient.GetCounterpartiesAsync(), + _gnomeshadeClient.GetCurrenciesAsync()) + .WhenAll(); + var zone = _dateTimeZoneProvider.GetSystemDefault(); + + Rows = new(transfers.Select(transfer => transfer.ToOverview(accounts, counterparties, currencies, zone))); + + Details = new(ActivityService, _gnomeshadeClient, _dialogService, _dateTimeZoneProvider, _transactionId, Selected?.Id); + await Details.RefreshAsync(); + } + + /// + protected override Task DeleteAsync(TransferOverview row) => throw new NotImplementedException(); + + private async void DetailsOnUpserted(object? sender, UpsertedEventArgs e) + { + await RefreshAsync(); + } +} diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferExtensions.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferExtensions.cs index 133423c22..dffdbf3af 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferExtensions.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. +using System; using System.Collections.Generic; using System.Linq; @@ -34,6 +35,57 @@ internal static TransferOverview ToOverview(this Transfer transfer, List accounts, + List counterparties, + List currencies, + DateTimeZone timeZone) + { + string? sourceAccount; + string? sourceCurrency; + string? targetAccount; + string? targetCurrency; + + if (transfer.IsSourceAccount) + { + var account = accounts.Single(a => a.Currencies.Any(c => c.Id == transfer.SourceAccountId)); + sourceAccount = account.Name; + sourceCurrency = account.Currencies.Single(c => c.Id == transfer.SourceAccountId).CurrencyAlphabeticCode; + } + else + { + var counterparty = counterparties.Single(c => c.Id == transfer.SourceCounterpartyId); + sourceAccount = counterparty.Name; + sourceCurrency = currencies.Single(currency => currency.Id == transfer.SourceCurrencyId).AlphabeticCode; + } + + if (transfer.IsTargetAccount) + { + var account = accounts.Single(a => a.Currencies.Any(c => c.Id == transfer.TargetAccountId)); + targetAccount = account.Name; + targetCurrency = account.Currencies.Single(c => c.Id == transfer.TargetAccountId).CurrencyAlphabeticCode; + } + else + { + var counterparty = counterparties.Single(c => c.Id == transfer.TargetCounterpartyId); + targetAccount = counterparty.Name; + targetCurrency = currencies.Single(currency => currency.Id == transfer.TargetCurrencyId).AlphabeticCode; + } + + return new( + transfer.Id, + transfer.SourceAmount, + sourceAccount, + sourceCurrency, + transfer.TargetAmount, + targetAccount, + targetCurrency, + transfer.Order, + transfer.BookedAt?.InZone(timeZone).ToDateTimeOffset(), + null); + } + internal static TransferSummary ToSummary( this Transfer transfer, List counterparties, @@ -71,4 +123,91 @@ internal static TransferSummary ToSummary( sourceCurrency.CurrencyAlphabeticCode, transfer.SourceAmount); } + + internal static TransferSummary ToSummary( + this PlannedTransfer plannedTransfer, + Instant bookedAt, + IEnumerable counterparties, + Counterparty userCounterparty, + (AccountInCurrency AccountInCurrency, Account Account)[] accounts) + { + var (sourceCounterpartyId, sourceCurrencyCode, sourcePreferredCurrencyId, sourceCurrencyId, sourceAccountName) = + (plannedTransfer.SourceCounterpartyId ?? accounts.Single(tuple => tuple.AccountInCurrency.Id == plannedTransfer.SourceAccountId).Account.CounterpartyId, + plannedTransfer.SourceAccountId is { } x + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == x).AccountInCurrency.CurrencyAlphabeticCode + : accounts.First(tuple => tuple.AccountInCurrency.CurrencyId == plannedTransfer.SourceCurrencyId) + .AccountInCurrency.CurrencyAlphabeticCode, + plannedTransfer.SourceAccountId is { } y + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == y).Account.PreferredCurrencyId + : Guid.Empty, + plannedTransfer.SourceAccountId is { } z + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == z).AccountInCurrency.CurrencyId + : plannedTransfer.SourceCurrencyId!.Value, + plannedTransfer.SourceAccountId is { } w + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == w).Account.Name + : string.Empty); + + var (targetCounterpartyId, targetCurrencyCode, targetPreferredCurrencyId, targetCurrencyId, targetAccountName) = + (plannedTransfer.TargetCounterpartyId ?? accounts.Single(tuple => tuple.AccountInCurrency.Id == plannedTransfer.TargetAccountId).Account.CounterpartyId, + plannedTransfer.TargetAccountId is { } a + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == a).AccountInCurrency.CurrencyAlphabeticCode + : accounts.First(tuple => tuple.AccountInCurrency.CurrencyId == plannedTransfer.TargetCurrencyId) + .AccountInCurrency.CurrencyAlphabeticCode, + plannedTransfer.TargetAccountId is { } b + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == b).Account.PreferredCurrencyId + : Guid.Empty, + plannedTransfer.TargetAccountId is { } c + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == c).AccountInCurrency.CurrencyId + : plannedTransfer.TargetCurrencyId!.Value, + plannedTransfer.TargetAccountId is { } d + ? accounts.Single(tuple => tuple.AccountInCurrency.Id == d).Account.Name + : string.Empty); + + var transfer = new Transfer + { + Id = plannedTransfer.Id, + CreatedAt = plannedTransfer.CreatedAt, + OwnerId = plannedTransfer.OwnerId, + CreatedByUserId = plannedTransfer.CreatedByUserId, + ModifiedAt = plannedTransfer.ModifiedAt, + ModifiedByUserId = plannedTransfer.ModifiedByUserId, + TransactionId = plannedTransfer.TransactionId, + SourceAmount = plannedTransfer.SourceAmount, + SourceAccountId = plannedTransfer.SourceAccountId ?? default, + TargetAmount = plannedTransfer.TargetAmount, + TargetAccountId = plannedTransfer.TargetAccountId ?? default, + BankReference = null, + ExternalReference = null, + InternalReference = null, + Order = plannedTransfer.Order, + BookedAt = bookedAt, + ValuedAt = null, + }; + + return sourceCounterpartyId == userCounterparty.Id + ? new( + transfer, + sourceCurrencyCode, + sourcePreferredCurrencyId != sourceCurrencyId, + transfer.SourceAmount, + sourceAccountName, + "→", + targetCounterpartyId == userCounterparty.Id, + counterparties.Single(counterparty => targetCounterpartyId == counterparty.Id).Name, + targetAccountName, + targetCurrencyCode, + transfer.TargetAmount) + : new( + transfer, + targetCurrencyCode, + targetPreferredCurrencyId != targetCurrencyId, + transfer.TargetAmount, + targetAccountName, + "←", + false, + counterparties.Single(counterparty => sourceCounterpartyId == counterparty.Id).Name, + sourceAccountName, + sourceCurrencyCode, + transfer.SourceAmount); + } } diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferUpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferUpsertionViewModel.cs index 85cd32951..4613aee41 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferUpsertionViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferUpsertionViewModel.cs @@ -124,7 +124,7 @@ public TransferUpsertionViewModel( /// public AutoCompleteSelector CurrencySelector => AutoCompleteSelectors.Currency; - /// + /// public ZonedDateTime? BookedAt => BookingDate?.InZoneStrictly(_dateTimeZoneProvider.GetSystemDefault()); /// @@ -152,13 +152,10 @@ TargetCurrency is not null && /// protected override async Task Refresh() { - var accountsTask = GnomeshadeClient.GetAccountsAsync(); - var currenciesTask = GnomeshadeClient.GetCurrenciesAsync(); - - await Task.WhenAll(accountsTask, currenciesTask); - - Accounts = accountsTask.Result; - Currencies = currenciesTask.Result; + (Accounts, Currencies) = await + (GnomeshadeClient.GetAccountsAsync(), + GnomeshadeClient.GetCurrenciesAsync()) + .WhenAll(); if (Id is not { } transferId) { diff --git a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferViewModel.cs b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferViewModel.cs index a18d07849..ee52527c6 100644 --- a/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/Transactions/Transfers/TransferViewModel.cs @@ -78,12 +78,11 @@ public override async Task UpdateSelection() /// protected override async Task Refresh() { - var transactionsTask = _gnomeshadeClient.GetDetailedTransactionAsync(_transactionId); - var accountsTask = _gnomeshadeClient.GetAccountsAsync(); - await Task.WhenAll(transactionsTask, accountsTask); + var (transaction, accounts) = await + (_gnomeshadeClient.GetDetailedTransactionAsync(_transactionId), + _gnomeshadeClient.GetAccountsAsync()) + .WhenAll(); - var accounts = accountsTask.Result; - var transaction = transactionsTask.Result; Total = transaction.TransferBalance; IsReadOnly = transaction.Reconciled; diff --git a/source/Gnomeshade.Avalonia.Core/UpsertionViewModel.cs b/source/Gnomeshade.Avalonia.Core/UpsertionViewModel.cs index 092b434b6..d8f9476d5 100644 --- a/source/Gnomeshade.Avalonia.Core/UpsertionViewModel.cs +++ b/source/Gnomeshade.Avalonia.Core/UpsertionViewModel.cs @@ -20,6 +20,10 @@ public abstract partial class UpsertionViewModel : ViewModelBase [Notify(Setter.Protected)] private Guid? _id; + /// Gets or sets a value indicating whether this model can be modified. + [Notify] + private bool _isReadOnly = true; + /// Initializes a new instance of the class. /// Service for indicating the activity of the application to the user. /// The strongly typed API client. diff --git a/source/Gnomeshade.Avalonia.Core/ViewLocator.cs b/source/Gnomeshade.Avalonia.Core/ViewLocator.cs index 714e15c25..ca1df78b1 100644 --- a/source/Gnomeshade.Avalonia.Core/ViewLocator.cs +++ b/source/Gnomeshade.Avalonia.Core/ViewLocator.cs @@ -26,10 +26,7 @@ public sealed class ViewLocator : IDataTemplate /// public Control Build(object? data) { - if (data is null) - { - throw new ArgumentNullException(nameof(data)); - } + ArgumentNullException.ThrowIfNull(data); var dataType = data.GetType(); if (_viewDictionary.TryGetValue(dataType, out var viewType)) @@ -45,6 +42,10 @@ public Control Build(object? data) i.GetGenericArguments()[1] == dataType) && type.IsAssignableTo(typeof(Control))); +#if DEBUG + viewType ??= _assembly.GetTypes().SingleOrDefault(type => type.Name is "PlaceholderView"); +#endif + if (viewType is null) { throw new InvalidOperationException($"Could not find view type for {dataType.FullName}"); diff --git a/source/Gnomeshade.Data/Entities/DetailedPlannedTransaction2Entity.cs b/source/Gnomeshade.Data/Entities/DetailedPlannedTransaction2Entity.cs new file mode 100644 index 000000000..5b2c4a085 --- /dev/null +++ b/source/Gnomeshade.Data/Entities/DetailedPlannedTransaction2Entity.cs @@ -0,0 +1,38 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System.Collections.Generic; + +namespace Gnomeshade.Data.Entities; + +/// A transaction with all related information. +public sealed record DetailedPlannedTransaction2Entity : PlannedTransactionEntity +{ + /// Initializes a new instance of the class. + /// The transaction from which to initialize. + public DetailedPlannedTransaction2Entity(PlannedTransactionEntity transaction) + { + Id = transaction.Id; + CreatedAt = transaction.CreatedAt; + CreatedByUserId = transaction.CreatedByUserId; + DeletedAt = transaction.DeletedAt; + DeletedByUserId = transaction.DeletedByUserId; + OwnerId = transaction.OwnerId; + ModifiedAt = transaction.ModifiedAt; + ModifiedByUserId = transaction.ModifiedByUserId; + ScheduleId = transaction.ScheduleId; + } + + /// Gets the transfers of the transaction. + public List Transfers { get; init; } = []; + + /// Gets the purchases of the transaction. + public List Purchases { get; init; } = []; + + /// Gets the loan payments associated with the transaction. + public List LoanPayments { get; init; } = []; + + /// Gets the links associated with the transaction. + public List Links { get; init; } = []; +} diff --git a/source/Gnomeshade.Data/Entities/DetailedTransactionEntity.cs b/source/Gnomeshade.Data/Entities/DetailedTransactionEntity.cs index 189e3c3ec..18d56ed54 100644 --- a/source/Gnomeshade.Data/Entities/DetailedTransactionEntity.cs +++ b/source/Gnomeshade.Data/Entities/DetailedTransactionEntity.cs @@ -33,14 +33,14 @@ public DetailedTransactionEntity(TransactionEntity transaction) } /// Gets the transfers of the transaction. - public List Transfers { get; init; } = new(); + public List Transfers { get; init; } = []; /// Gets the purchases of the transaction. - public List Purchases { get; init; } = new(); + public List Purchases { get; init; } = []; /// Gets the loan payments associated with the transaction. - public List Loans { get; init; } = new(); + public List Loans { get; init; } = []; /// Gets the links associated with the transaction. - public List Links { get; init; } = new(); + public List Links { get; init; } = []; } diff --git a/source/Gnomeshade.Data/Entities/LoanPaymentEntity.cs b/source/Gnomeshade.Data/Entities/LoanPaymentEntity.cs index f64d69e5d..78de4b7d9 100644 --- a/source/Gnomeshade.Data/Entities/LoanPaymentEntity.cs +++ b/source/Gnomeshade.Data/Entities/LoanPaymentEntity.cs @@ -22,11 +22,11 @@ public sealed record LoanPaymentEntity : Entity, IOwnableEntity, IModifiableEnti /// public Guid ModifiedByUserId { get; set; } - /// Gets or sets the id of the the loan this loan payment is a part of. + /// Gets or sets the id of the loan this loan payment is a part of. /// public Guid LoanId { get; set; } - /// Gets or sets the id of the the transaction this loan payment is a part of. + /// Gets or sets the id of the transaction this loan payment is a part of. /// public Guid TransactionId { get; set; } diff --git a/source/Gnomeshade.Data/Entities/PlannedPurchaseEntity.cs b/source/Gnomeshade.Data/Entities/PlannedPurchaseEntity.cs new file mode 100644 index 000000000..b80eada85 --- /dev/null +++ b/source/Gnomeshade.Data/Entities/PlannedPurchaseEntity.cs @@ -0,0 +1,8 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +namespace Gnomeshade.Data.Entities; + +/// Represents the purchasing of a product or a service. +public sealed record PlannedPurchaseEntity : PurchaseBase; diff --git a/source/Gnomeshade.Data/Entities/PlannedTransactionEntity.cs b/source/Gnomeshade.Data/Entities/PlannedTransactionEntity.cs new file mode 100644 index 000000000..a4f3817ee --- /dev/null +++ b/source/Gnomeshade.Data/Entities/PlannedTransactionEntity.cs @@ -0,0 +1,15 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +namespace Gnomeshade.Data.Entities; + +/// A that will happen in the future. +public record PlannedTransactionEntity : TransactionBase +{ + /// Gets or sets the id of the schedule of the planned transaction. + /// + public Guid ScheduleId { get; set; } +} diff --git a/source/Gnomeshade.Data/Entities/PlannedTransferEntity.cs b/source/Gnomeshade.Data/Entities/PlannedTransferEntity.cs new file mode 100644 index 000000000..c7b5b9fce --- /dev/null +++ b/source/Gnomeshade.Data/Entities/PlannedTransferEntity.cs @@ -0,0 +1,48 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Gnomeshade.Data.Entities; + +/// A that will happen in the future. +public sealed record PlannedTransferEntity : TransferBase +{ + /// Gets or sets the id of the account from which currency is withdrawn from. + /// + [MemberNotNullWhen(true, nameof(SourceUsesAccount))] + public Guid? SourceAccountId { get; set; } + + /// Gets or sets the id of the counterparty from which currency will be withdrawn from. + /// + [MemberNotNullWhen(false, nameof(SourceUsesAccount))] + public Guid? SourceCounterpartyId { get; set; } + + /// Gets or sets the id of the currency in which funds will be withdrawn from . + /// + [MemberNotNullWhen(false, nameof(SourceUsesAccount))] + public Guid? SourceCurrencyId { get; set; } + + /// Gets or sets the id of the to which currency is deposited to. + /// + [MemberNotNullWhen(true, nameof(TargetUsesAccount))] + public Guid? TargetAccountId { get; set; } + + /// Gets or sets the id of the counterparty to which currency will be deposited to. + /// + [MemberNotNullWhen(false, nameof(TargetUsesAccount))] + public Guid? TargetCounterpartyId { get; set; } + + /// Gets or sets the id of the currency in which funds will be deposited to . + /// + [MemberNotNullWhen(false, nameof(TargetUsesAccount))] + public Guid? TargetCurrencyId { get; set; } + + /// Gets a value indicating whether or is specified. + public bool SourceUsesAccount => SourceAccountId is not null; + + /// Gets a value indicating whether or is specified. + public bool TargetUsesAccount => TargetAccountId is not null; +} diff --git a/source/Gnomeshade.Data/Entities/PurchaseBase.cs b/source/Gnomeshade.Data/Entities/PurchaseBase.cs new file mode 100644 index 000000000..65446b407 --- /dev/null +++ b/source/Gnomeshade.Data/Entities/PurchaseBase.cs @@ -0,0 +1,49 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using Gnomeshade.Data.Entities.Abstractions; + +using NodaTime; + +namespace Gnomeshade.Data.Entities; + +/// Base model for purchases. +public abstract record PurchaseBase : Entity, IOwnableEntity, IModifiableEntity, ISortableEntity +{ + /// + public Guid OwnerId { get; set; } + + /// + public Instant ModifiedAt { get; set; } + + /// + public Guid ModifiedByUserId { get; set; } + + /// + public uint? Order { get; set; } + + /// Gets or sets the id of transaction this transfer is a part of. + /// + public Guid TransactionId { get; set; } + + /// Gets or sets the amount paid to purchase an of . + public decimal Price { get; set; } + + /// Gets or sets the id of the currency of . + /// + public Guid CurrencyId { get; set; } + + /// Gets or sets the id of the purchased product. + /// + public Guid ProductId { get; set; } + + /// Gets or sets the amount of that was purchased. + public decimal Amount { get; set; } + + /// Gets or sets the ids of all the projects this purchase is a part of. + public List ProjectIds { get; set; } = []; +} diff --git a/source/Gnomeshade.Data/Entities/PurchaseEntity.cs b/source/Gnomeshade.Data/Entities/PurchaseEntity.cs index d8e75682b..64ed2acd7 100644 --- a/source/Gnomeshade.Data/Entities/PurchaseEntity.cs +++ b/source/Gnomeshade.Data/Entities/PurchaseEntity.cs @@ -2,51 +2,13 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. -using System; -using System.Collections.Generic; - -using Gnomeshade.Data.Entities.Abstractions; - using NodaTime; namespace Gnomeshade.Data.Entities; /// Represents the purchasing of a product or a service. -public sealed record PurchaseEntity : Entity, IOwnableEntity, IModifiableEntity, ISortableEntity +public sealed record PurchaseEntity : PurchaseBase { - /// - public Guid OwnerId { get; set; } - - /// - public Instant ModifiedAt { get; set; } - - /// - public Guid ModifiedByUserId { get; set; } - - /// Gets or sets the id of transaction this transfer is a part of. - /// - public Guid TransactionId { get; set; } - - /// Gets or sets the amount paid to purchase an of . - public decimal Price { get; set; } - - /// Gets or sets the id of the currency of . - /// - public Guid CurrencyId { get; set; } - - /// Gets or sets the id of the purchased product. - /// - public Guid ProductId { get; set; } - - /// Gets or sets the amount of that was purchased. - public decimal Amount { get; set; } - - /// Gets or sets the date when the was delivered. + /// Gets or sets the date when the was delivered. public Instant? DeliveryDate { get; set; } - - /// - public uint? Order { get; set; } - - /// Gets or sets the ids of all the projects this purchase is a part of. - public List ProjectIds { get; set; } = []; } diff --git a/source/Gnomeshade.Data/Entities/TransactionBase.cs b/source/Gnomeshade.Data/Entities/TransactionBase.cs new file mode 100644 index 000000000..0707aade9 --- /dev/null +++ b/source/Gnomeshade.Data/Entities/TransactionBase.cs @@ -0,0 +1,27 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.Data.Entities.Abstractions; + +using NodaTime; + +namespace Gnomeshade.Data.Entities; + +/// Base model for transactions. +public abstract record TransactionBase : Entity, IOwnableEntity, IModifiableEntity +{ + /// + public Guid OwnerId { get; set; } + + /// + public Instant ModifiedAt { get; set; } + + /// + public Guid ModifiedByUserId { get; set; } + + /// Gets or sets a value indicating whether the transaction is planned or not. + public bool Planned { get; set; } +} diff --git a/source/Gnomeshade.Data/Entities/TransactionEntity.cs b/source/Gnomeshade.Data/Entities/TransactionEntity.cs index 01b1bea4d..6b73af6af 100644 --- a/source/Gnomeshade.Data/Entities/TransactionEntity.cs +++ b/source/Gnomeshade.Data/Entities/TransactionEntity.cs @@ -4,24 +4,13 @@ using System; -using Gnomeshade.Data.Entities.Abstractions; - using NodaTime; namespace Gnomeshade.Data.Entities; /// A single financial transaction. -public record TransactionEntity : Entity, IOwnableEntity, IModifiableEntity +public record TransactionEntity : TransactionBase { - /// - public Guid OwnerId { get; set; } - - /// - public Instant ModifiedAt { get; set; } - - /// - public Guid ModifiedByUserId { get; set; } - /// Gets or sets the point in time when this transaction was posted to an account on the account servicer accounting books. public Instant? BookedAt { get; set; } diff --git a/source/Gnomeshade.Data/Entities/TransactionScheduleEntity.cs b/source/Gnomeshade.Data/Entities/TransactionScheduleEntity.cs new file mode 100644 index 000000000..e40683411 --- /dev/null +++ b/source/Gnomeshade.Data/Entities/TransactionScheduleEntity.cs @@ -0,0 +1,39 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.Data.Entities.Abstractions; + +using NodaTime; + +namespace Gnomeshade.Data.Entities; + +/// A schedule for planned transactions. +public sealed record TransactionScheduleEntity : Entity, IOwnableEntity, IModifiableEntity, INamedEntity +{ + /// + public Guid OwnerId { get; set; } + + /// + public Instant ModifiedAt { get; set; } + + /// + public Guid ModifiedByUserId { get; set; } + + /// + public string Name { get; set; } = null!; + + /// + public string NormalizedName { get; set; } = null!; + + /// Gets or sets the start of the planned transaction. + public Instant StartingAt { get; set; } + + /// Gets or sets the period between transactions. + public Period Period { get; set; } = null!; + + /// Gets or sets the number of planned transactions created by the schedule. + public int Count { get; set; } +} diff --git a/source/Gnomeshade.Data/Entities/TransferBase.cs b/source/Gnomeshade.Data/Entities/TransferBase.cs new file mode 100644 index 000000000..0c19d1a7d --- /dev/null +++ b/source/Gnomeshade.Data/Entities/TransferBase.cs @@ -0,0 +1,43 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.Data.Entities.Abstractions; + +using NodaTime; + +namespace Gnomeshade.Data.Entities; + +/// Base model for transfers. +public abstract record TransferBase : Entity, IOwnableEntity, IModifiableEntity, ISortableEntity +{ + /// + public Guid OwnerId { get; set; } + + /// + public Instant ModifiedAt { get; set; } + + /// + public Guid ModifiedByUserId { get; set; } + + /// + public uint? Order { get; set; } + + /// Gets or sets the id of transaction this transfer is a part of. + /// + public Guid TransactionId { get; set; } + + /// Gets or sets the amount withdrawn from the source account. + public decimal SourceAmount { get; set; } + + /// Gets or sets the amount deposited in the target account. + public decimal TargetAmount { get; set; } + + /// Gets or sets the point in time when this transfer was posted to an account on the account servicer accounting books. + public Instant? BookedAt { get; set; } // todo BookedAt or ValuedAt in the base class? + + /// Gets or sets a value indicating whether the transfer is planned or not. + public bool Planned { get; set; } // todo needed? +} diff --git a/source/Gnomeshade.Data/Entities/TransferEntity.cs b/source/Gnomeshade.Data/Entities/TransferEntity.cs index 368da8a14..518095fff 100644 --- a/source/Gnomeshade.Data/Entities/TransferEntity.cs +++ b/source/Gnomeshade.Data/Entities/TransferEntity.cs @@ -4,38 +4,17 @@ using System; -using Gnomeshade.Data.Entities.Abstractions; - using NodaTime; namespace Gnomeshade.Data.Entities; /// Represents a transfer between two accounts. -public sealed record TransferEntity : Entity, IOwnableEntity, IModifiableEntity, ISortableEntity +public sealed record TransferEntity : TransferBase { - /// - public Guid OwnerId { get; set; } - - /// - public Instant ModifiedAt { get; set; } - - /// - public Guid ModifiedByUserId { get; set; } - - /// Gets or sets the id of transaction this transfer is a part of. - /// - public Guid TransactionId { get; set; } - - /// Gets or sets the amount withdrawn from the source account. - public decimal SourceAmount { get; set; } - /// Gets or sets the id of the account from which currency is withdrawn from. /// public Guid SourceAccountId { get; set; } - /// Gets or sets the amount deposited in the target account. - public decimal TargetAmount { get; set; } - /// Gets or sets the id of the to which currency is deposited to. /// public Guid TargetAccountId { get; set; } @@ -49,12 +28,6 @@ public sealed record TransferEntity : Entity, IOwnableEntity, IModifiableEntity, /// Gets or sets a reference id issued by the user. public string? InternalReference { get; set; } - /// - public uint? Order { get; set; } - - /// Gets or sets the point in time when this transfer was posted to an account on the account servicer accounting books. - public Instant? BookedAt { get; set; } - /// Gets or sets the point in time when assets become available in case of deposit, or when assets cease to be available in case of withdrawal. public Instant? ValuedAt { get; set; } } diff --git a/source/Gnomeshade.Data/Migrations/00000037_transaction_schedules.sql b/source/Gnomeshade.Data/Migrations/00000037_transaction_schedules.sql new file mode 100644 index 000000000..805fa4907 --- /dev/null +++ b/source/Gnomeshade.Data/Migrations/00000037_transaction_schedules.sql @@ -0,0 +1,33 @@ +CREATE TABLE transaction_schedules +( + id uuid DEFAULT (uuid_generate_v4()) NOT NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by_user_id uuid NOT NULL, + deleted_at timestamptz, + deleted_by_user_id uuid, + owner_id uuid NOT NULL, + modified_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + modified_by_user_id uuid NOT NULL, + name text NOT NULL, + normalized_name text NOT NULL, + + starting_at timestamptz NOT NULL, + period interval NOT NULL, + count int NOT NULL, + + CONSTRAINT "transaction_schedules_id" PRIMARY KEY (id), + CONSTRAINT "transaction_schedules_created_by_user_id_fkey" FOREIGN KEY (created_by_user_id) REFERENCES users (id) NOT DEFERRABLE, + CONSTRAINT "transaction_schedules_deleted_by_user_id_fkey" FOREIGN KEY (deleted_by_user_id) REFERENCES users (id) NOT DEFERRABLE, + CONSTRAINT "transaction_schedules_deleted_check" CHECK ((deleted_at IS NULL AND deleted_by_user_id IS NULL) OR + (deleted_at IS NOT NULL AND deleted_by_user_id IS NOT NULL)), + CONSTRAINT "transaction_schedules_owner_id_fkey" FOREIGN KEY (owner_id) REFERENCES owners (id) NOT DEFERRABLE, + CONSTRAINT "transaction_schedules_modified_by_user_id_fkey" FOREIGN KEY (modified_by_user_id) REFERENCES users (id) NOT DEFERRABLE, + + CONSTRAINT "transaction_schedules_normalized_name_unique" UNIQUE (normalized_name, owner_id) +); + +ALTER TABLE transactions + ADD COLUMN planned bool DEFAULT false NOT NULL; + +ALTER TABLE transactions + ADD COLUMN schedule_id uuid NULL REFERENCES transaction_schedules; diff --git a/source/Gnomeshade.Data/Repositories/AccountInCurrencyRepository.cs b/source/Gnomeshade.Data/Repositories/AccountInCurrencyRepository.cs index 392bcf7fa..66f10adad 100644 --- a/source/Gnomeshade.Data/Repositories/AccountInCurrencyRepository.cs +++ b/source/Gnomeshade.Data/Repositories/AccountInCurrencyRepository.cs @@ -25,7 +25,7 @@ public sealed class AccountInCurrencyRepository(ILogger Queries.AccountInCurrency.Insert; /// - protected override string SelectAllSql => Queries.AccountInCurrency.SelectAll; + protected override string TableName => "accounts_in_currency a"; /// protected override string SelectSql => Queries.AccountInCurrency.Select; diff --git a/source/Gnomeshade.Data/Repositories/AccountRepository.cs b/source/Gnomeshade.Data/Repositories/AccountRepository.cs index 954d8a29c..bdf570f5b 100644 --- a/source/Gnomeshade.Data/Repositories/AccountRepository.cs +++ b/source/Gnomeshade.Data/Repositories/AccountRepository.cs @@ -39,7 +39,16 @@ public AccountRepository(ILogger logger, DbConnection dbConne protected override string InsertSql => Queries.Account.Insert; /// - protected override string SelectAllSql => Queries.Account.SelectAll; + protected override string TableName => "accounts a"; + + /// + protected override string ExistsSql => + $""" + SELECT 1 + FROM {TableName} + LEFT JOIN accounts_in_currency aic ON a.id = aic.account_id + WHERE {FindSql} AND {NotDeleted} LIMIT 1; + """; /// protected override string FindSql => "a.id = @id"; diff --git a/source/Gnomeshade.Data/Repositories/CategoryRepository.cs b/source/Gnomeshade.Data/Repositories/CategoryRepository.cs index 6a4105f2d..ab39b9918 100644 --- a/source/Gnomeshade.Data/Repositories/CategoryRepository.cs +++ b/source/Gnomeshade.Data/Repositories/CategoryRepository.cs @@ -28,7 +28,7 @@ public CategoryRepository(ILogger logger, DbConnection dbCon protected override string InsertSql => Queries.Category.Insert; /// - protected override string SelectAllSql => Queries.Category.SelectAll; + protected override string TableName => "categories c"; /// protected override string UpdateSql => Queries.Category.Update; diff --git a/source/Gnomeshade.Data/Repositories/CounterpartyRepository.cs b/source/Gnomeshade.Data/Repositories/CounterpartyRepository.cs index b28ca1bed..eee263eb4 100644 --- a/source/Gnomeshade.Data/Repositories/CounterpartyRepository.cs +++ b/source/Gnomeshade.Data/Repositories/CounterpartyRepository.cs @@ -35,7 +35,7 @@ public CounterpartyRepository(ILogger logger, DbConnecti protected override string InsertSql => Queries.Counterparty.Insert; /// - protected override string SelectAllSql => Queries.Counterparty.SelectAll; + protected override string TableName => "counterparties c"; /// protected override string UpdateSql => Queries.Counterparty.Update; @@ -77,7 +77,7 @@ public async Task MergeAsync(Guid targetId, Guid sourceId, Guid userId, DbTransa public Task> GetAllAsync(CancellationToken cancellationToken = default) { Logger.GetAll(true); - var command = new CommandDefinition($"{SelectAllSql} {GroupBy};", cancellationToken: cancellationToken); + var command = new CommandDefinition($"{Queries.Counterparty.SelectAll} {GroupBy};", cancellationToken: cancellationToken); return GetEntitiesAsync(command); } } diff --git a/source/Gnomeshade.Data/Repositories/LinkRepository.cs b/source/Gnomeshade.Data/Repositories/LinkRepository.cs index 9fee01971..74303fc0b 100644 --- a/source/Gnomeshade.Data/Repositories/LinkRepository.cs +++ b/source/Gnomeshade.Data/Repositories/LinkRepository.cs @@ -28,7 +28,7 @@ public LinkRepository(ILogger logger, DbConnection dbConnection) protected override string InsertSql => Queries.Link.Insert; /// - protected override string SelectAllSql => Queries.Link.SelectAll; + protected override string TableName => "links"; /// protected override string UpdateSql => Queries.Link.Update; diff --git a/source/Gnomeshade.Data/Repositories/Loan2Repository.cs b/source/Gnomeshade.Data/Repositories/Loan2Repository.cs index 4a6a039bc..e2fa8ff10 100644 --- a/source/Gnomeshade.Data/Repositories/Loan2Repository.cs +++ b/source/Gnomeshade.Data/Repositories/Loan2Repository.cs @@ -28,7 +28,7 @@ public Loan2Repository(ILogger logger, DbConnection dbConnectio protected override string InsertSql => Queries.Loan2.Insert; /// - protected override string SelectAllSql => Queries.Loan2.SelectAll; + protected override string TableName => "loans2"; /// protected override string FindSql => "loans2.id = @id"; diff --git a/source/Gnomeshade.Data/Repositories/LoanPaymentRepository.cs b/source/Gnomeshade.Data/Repositories/LoanPaymentRepository.cs index c238ec505..c25d3f3eb 100644 --- a/source/Gnomeshade.Data/Repositories/LoanPaymentRepository.cs +++ b/source/Gnomeshade.Data/Repositories/LoanPaymentRepository.cs @@ -35,7 +35,7 @@ public LoanPaymentRepository(ILogger logger, DbConnection protected override string InsertSql => Queries.LoanPayment.Insert; /// - protected override string SelectAllSql => Queries.LoanPayment.SelectAll; + protected override string TableName => "loan_payments"; /// protected override string UpdateSql => Queries.LoanPayment.Update; diff --git a/source/Gnomeshade.Data/Repositories/LoanRepository.cs b/source/Gnomeshade.Data/Repositories/LoanRepository.cs index 79b8b15cf..2905f0b5a 100644 --- a/source/Gnomeshade.Data/Repositories/LoanRepository.cs +++ b/source/Gnomeshade.Data/Repositories/LoanRepository.cs @@ -36,7 +36,7 @@ public LoanRepository(ILogger logger, DbConnection dbConnection) protected override string InsertSql => Queries.Loan.Insert; /// - protected override string SelectAllSql => Queries.Loan.SelectAll; + protected override string TableName => "loans"; /// protected override string UpdateSql => Queries.Loan.Update; diff --git a/source/Gnomeshade.Data/Repositories/OwnerRepository.cs b/source/Gnomeshade.Data/Repositories/OwnerRepository.cs index cda5dca72..414b19019 100644 --- a/source/Gnomeshade.Data/Repositories/OwnerRepository.cs +++ b/source/Gnomeshade.Data/Repositories/OwnerRepository.cs @@ -30,7 +30,7 @@ public OwnerRepository(ILogger logger, DbConnection dbConnectio protected override string InsertSql => Queries.Owner.Insert; /// - protected override string SelectAllSql => Queries.Owner.SelectAll; + protected override string TableName => "owners o"; /// protected override string SelectSql => Queries.Owner.Select; diff --git a/source/Gnomeshade.Data/Repositories/OwnershipRepository.cs b/source/Gnomeshade.Data/Repositories/OwnershipRepository.cs index 2bbae3ee4..a9f015288 100644 --- a/source/Gnomeshade.Data/Repositories/OwnershipRepository.cs +++ b/source/Gnomeshade.Data/Repositories/OwnershipRepository.cs @@ -26,7 +26,7 @@ public sealed class OwnershipRepository(ILogger logger, DbC protected override string InsertSql => Queries.Ownership.Insert; /// - protected override string SelectAllSql => Queries.Ownership.SelectAll; + protected override string TableName => "ownerships o"; /// protected override string SelectSql => Queries.Ownership.Select; diff --git a/source/Gnomeshade.Data/Repositories/PlannedPurchaseRepository.cs b/source/Gnomeshade.Data/Repositories/PlannedPurchaseRepository.cs new file mode 100644 index 000000000..bad72cef8 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/PlannedPurchaseRepository.cs @@ -0,0 +1,102 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Logging; +using Gnomeshade.Data.Repositories.Extensions; + +using Microsoft.Extensions.Logging; + +using static Gnomeshade.Data.Repositories.AccessLevel; + +namespace Gnomeshade.Data.Repositories; + +/// Persistence store of . +public sealed class PlannedPurchaseRepository(ILogger logger, DbConnection dbConnection) + : TransactionItemRepository(logger, dbConnection) +{ + /// + protected override string DeleteSql => Queries.PlannedPurchase.Delete; + + /// + protected override string InsertSql => Queries.PlannedPurchase.Insert; + + /// + protected override string TableName => "purchases"; + + /// + protected override string UpdateSql => Queries.PlannedPurchase.Update; + + /// + protected override string FindSql => "purchases.id = @id"; + + protected override string GroupBy => "GROUP BY purchases.id, project_purchases.project_id"; + + protected override string NotDeleted => "purchases.deleted_at IS NULL"; + + /// + protected override string SelectSql => Queries.PlannedPurchase.Select; + + /// + public override Task> GetAllAsync( + Guid transactionId, + Guid userId, + CancellationToken cancellationToken = default) + { + Logger.GetAll(); + return GetEntitiesAsync(new( + $"{SelectActiveSql} AND purchases.transaction_id = @transactionId {GroupBy};", + new { transactionId, userId, access = Read.ToParam() }, + cancellationToken: cancellationToken)); + } + + /// + public override Task> GetAllAsync( + Guid transactionId, + Guid userId, + DbTransaction dbTransaction) + { + Logger.GetAll(); + return GetEntitiesAsync(new( + $"{SelectActiveSql} AND purchases.transaction_id = @transactionId {GroupBy};", + new { transactionId, userId, access = Read.ToParam() }, + dbTransaction)); + } + + /// + protected override async Task> GetEntitiesAsync(CommandDefinition command) + { + var purchases = await DbConnection + .QueryAsync( + command, + (purchase, container) => + { + if (container is not null) + { + purchase.ProjectIds = [container.Id]; + } + + return purchase; + }, + "Id,Id"); + + return purchases + .GroupBy(purchase => purchase.Id) + .Select(grouping => + { + var purchase = grouping.First(); + purchase.ProjectIds = grouping.SelectMany(entity => entity.ProjectIds).ToList(); + return purchase; + }); + } +} diff --git a/source/Gnomeshade.Data/Repositories/PlannedTransactionRepository.cs b/source/Gnomeshade.Data/Repositories/PlannedTransactionRepository.cs new file mode 100644 index 000000000..180519a97 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/PlannedTransactionRepository.cs @@ -0,0 +1,73 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Data.Common; +using System.Threading.Tasks; + +using Dapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Logging; + +using Microsoft.Extensions.Logging; + +namespace Gnomeshade.Data.Repositories; + +public sealed class PlannedTransactionRepository(ILogger logger, DbConnection dbConnection) + : Repository(logger, dbConnection) +{ + /// + protected override string DeleteSql => Queries.Transaction.Delete; + + /// + protected override string InsertSql => Queries.Transaction.Insert; + + /// + protected override string SelectSql => Queries.Transaction.Select; + + /// + protected override string TableName => "transactions t"; + + /// + protected override string FindSql => "t.id = @id"; + + protected override string GroupBy => "GROUP BY t.id"; + + /// + protected override string UpdateSql => Queries.Transaction.Update; + + /// + protected override string NotDeleted => "t.deleted_at IS NULL"; + + /// + public override Task AddAsync(PlannedTransactionEntity entity, DbTransaction dbTransaction) + { + Logger.AddingEntityWithTransaction(); + var command = new CommandDefinition(InsertSql, entity, dbTransaction); + return DbConnection.QuerySingleAsync(command); + + // todo Add other planned transactions ? + } + + /// + public override async Task UpdateAsync(PlannedTransactionEntity entity, DbTransaction dbTransaction) + { + Logger.UpdatingEntityWithTransaction(); + var count = await DbConnection.ExecuteAsync(UpdateSql, entity, dbTransaction); + Logger.UpdatedRows(count); + return count; + + // todo Update other planned transactions linked to the same schedule ? + } + + /// + public override async Task DeleteAsync(Guid id, Guid userId, DbTransaction dbTransaction) + { + Logger.DeletingEntityWithTransaction(id); + var count = await DbConnection.ExecuteAsync(DeleteSql, new { id, userId }, dbTransaction); + Logger.DeletedRows(count); + return count; + } +} diff --git a/source/Gnomeshade.Data/Repositories/PlannedTransferRepository.cs b/source/Gnomeshade.Data/Repositories/PlannedTransferRepository.cs new file mode 100644 index 000000000..d74f25cd4 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/PlannedTransferRepository.cs @@ -0,0 +1,59 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Logging; + +using Microsoft.Extensions.Logging; + +using static Gnomeshade.Data.Repositories.AccessLevel; + +namespace Gnomeshade.Data.Repositories; + +/// Persistence store of . +public sealed class PlannedTransferRepository(ILogger logger, DbConnection dbConnection) + : TransactionItemRepository(logger, dbConnection) +{ + /// + protected override string DeleteSql => Queries.PlannedTransfer.Delete; + + /// + protected override string InsertSql => Queries.PlannedTransfer.Insert; + + /// + protected override string TableName => "transfers"; + + /// + protected override string UpdateSql => Queries.PlannedTransfer.Update; + + /// + protected override string FindSql => "transfers.id = @id"; + + protected override string GroupBy => "GROUP BY transfers.id"; + + /// + protected override string NotDeleted => "transfers.deleted_at IS NULL"; + + /// + protected override string SelectSql => Queries.PlannedTransfer.Select; + + /// + public override Task> GetAllAsync( + Guid transactionId, + Guid userId, + CancellationToken cancellationToken = default) + { + Logger.GetAll(); + return GetEntitiesAsync(new( + $"{SelectActiveSql} AND transfers.transaction_id = @transactionId {GroupBy};", + new { transactionId, userId, access = Read.ToParam() }, + cancellationToken: cancellationToken)); + } +} diff --git a/source/Gnomeshade.Data/Repositories/ProductRepository.cs b/source/Gnomeshade.Data/Repositories/ProductRepository.cs index 0c11bcb0d..8afa6f9aa 100644 --- a/source/Gnomeshade.Data/Repositories/ProductRepository.cs +++ b/source/Gnomeshade.Data/Repositories/ProductRepository.cs @@ -28,7 +28,7 @@ public ProductRepository(ILogger logger, DbConnection dbConne protected override string InsertSql => Queries.Product.Insert; /// - protected override string SelectAllSql => Queries.Product.SelectAll; + protected override string TableName => "products p"; /// protected override string UpdateSql => Queries.Product.Update; diff --git a/source/Gnomeshade.Data/Repositories/ProjectRepository.cs b/source/Gnomeshade.Data/Repositories/ProjectRepository.cs index a2052ca42..9434bcd88 100644 --- a/source/Gnomeshade.Data/Repositories/ProjectRepository.cs +++ b/source/Gnomeshade.Data/Repositories/ProjectRepository.cs @@ -25,7 +25,7 @@ public sealed class ProjectRepository(ILogger logger, DbConne protected override string InsertSql => Queries.Project.Insert; /// - protected override string SelectAllSql => Queries.Project.SelectAll; + protected override string TableName => "projects"; /// protected override string UpdateSql => Queries.Project.Update; diff --git a/source/Gnomeshade.Data/Repositories/PurchaseRepository.cs b/source/Gnomeshade.Data/Repositories/PurchaseRepository.cs index b4143ec6f..080bbb2a5 100644 --- a/source/Gnomeshade.Data/Repositories/PurchaseRepository.cs +++ b/source/Gnomeshade.Data/Repositories/PurchaseRepository.cs @@ -39,7 +39,7 @@ public PurchaseRepository(ILogger logger, DbConnection dbCon protected override string InsertSql => Queries.Purchase.Insert; /// - protected override string SelectAllSql => Queries.Purchase.SelectAll; + protected override string TableName => "purchases"; /// protected override string UpdateSql => Queries.Purchase.Update; diff --git a/source/Gnomeshade.Data/Repositories/Queries.cs b/source/Gnomeshade.Data/Repositories/Queries.cs index 1f17f6ae0..42f358f11 100644 --- a/source/Gnomeshade.Data/Repositories/Queries.cs +++ b/source/Gnomeshade.Data/Repositories/Queries.cs @@ -202,6 +202,19 @@ internal static class Purchase internal static string Update { get; } = Read($"Queries.{nameof(Purchase)}.Update.sql"); } + internal static class PlannedPurchase + { + internal static string Delete { get; } = Read($"Queries.{nameof(PlannedPurchase)}.Delete.sql"); + + internal static string Insert { get; } = Read($"Queries.{nameof(PlannedPurchase)}.Insert.sql"); + + internal static string Select { get; } = Read($"Queries.{nameof(PlannedPurchase)}.Select.sql"); + + internal static string SelectAll { get; } = Read($"Queries.{nameof(PlannedPurchase)}.SelectAll.sql"); + + internal static string Update { get; } = Read($"Queries.{nameof(PlannedPurchase)}.Update.sql"); + } + internal static class Transaction { internal static string Delete { get; } = Read($"Queries.{nameof(Transaction)}.Delete.sql"); @@ -221,6 +234,19 @@ internal static class Transaction internal static string Merge { get; } = Read($"Queries.{nameof(Transaction)}.Merge.sql"); } + internal static class TransactionSchedule + { + internal static string Delete { get; } = Read($"Queries.{nameof(TransactionSchedule)}.Delete.sql"); + + internal static string Insert { get; } = Read($"Queries.{nameof(TransactionSchedule)}.Insert.sql"); + + internal static string Select { get; } = Read($"Queries.{nameof(TransactionSchedule)}.Select.sql"); + + internal static string SelectAll { get; } = Read($"Queries.{nameof(TransactionSchedule)}.SelectAll.sql"); + + internal static string Update { get; } = Read($"Queries.{nameof(TransactionSchedule)}.Update.sql"); + } + internal static class Transfer { internal static string Delete { get; } = Read($"Queries.{nameof(Transfer)}.Delete.sql"); @@ -234,6 +260,19 @@ internal static class Transfer internal static string Update { get; } = Read($"Queries.{nameof(Transfer)}.Update.sql"); } + internal static class PlannedTransfer + { + internal static string Delete { get; } = Read($"Queries.{nameof(PlannedTransfer)}.Delete.sql"); + + internal static string Insert { get; } = Read($"Queries.{nameof(PlannedTransfer)}.Insert.sql"); + + internal static string Select { get; } = Read($"Queries.{nameof(PlannedTransfer)}.Select.sql"); + + internal static string SelectAll { get; } = Read($"Queries.{nameof(PlannedTransfer)}.SelectAll.sql"); + + internal static string Update { get; } = Read($"Queries.{nameof(PlannedTransfer)}.Update.sql"); + } + internal static class Unit { internal static string Delete { get; } = Read($"Queries.{nameof(Unit)}.Delete.sql"); diff --git a/source/Gnomeshade.Data/Repositories/Queries/PlannedTransaction/SelectAll.sql b/source/Gnomeshade.Data/Repositories/Queries/PlannedTransaction/SelectAll.sql new file mode 100644 index 000000000..de648b160 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/PlannedTransaction/SelectAll.sql @@ -0,0 +1,14 @@ +SELECT t.id, + t.owner_id OwnerId, + t.created_at CreatedAt, + t.created_by_user_id CreatedByUserId, + t.modified_at ModifiedAt, + t.modified_by_user_id ModifiedByUserId, + t.deleted_at DeletedAt, + t.deleted_by_user_id DeletedByUserId, + t.description, + t.imported_at ImportedAt, + t.reconciled_at ReconciledAt, + t.reconciled_by_user_id ReconciledByUserId, + t.refunded_by RefundedBy +FROM transactions t diff --git a/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Delete.sql b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Delete.sql new file mode 100644 index 000000000..28f6ffbe4 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Delete.sql @@ -0,0 +1,16 @@ +WITH accessable AS + (SELECT transaction_schedules.id + FROM transaction_schedules + INNER JOIN owners ON owners.id = transaction_schedules.owner_id + INNER JOIN ownerships ON owners.id = ownerships.owner_id + INNER JOIN access ON access.id = ownerships.access_id + WHERE ownerships.user_id = @userId + AND (access.normalized_name = 'DELETE' OR access.normalized_name = 'OWNER') + AND transaction_schedules.deleted_at IS NULL + AND transaction_schedules.id = @id) + +UPDATE transaction_schedules +SET deleted_at = CURRENT_TIMESTAMP, + deleted_by_user_id = @userId +FROM accessable +WHERE transaction_schedules.id IN (SELECT id FROM accessable); diff --git a/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Insert.sql b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Insert.sql new file mode 100644 index 000000000..255bc60e4 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Insert.sql @@ -0,0 +1,24 @@ +INSERT INTO projects +(id, + created_at, + created_by_user_id, + deleted_at, + deleted_by_user_id, + owner_id, + modified_at, + modified_by_user_id, + name, + normalized_name, + parent_project_id) +VALUES (@Id, + @CreatedAt, + @CreatedByUserId, + @DeletedAt, + @DeletedByUserId, + @OwnerId, + @ModifiedAt, + @ModifiedByUserId, + @Name, + upper(@Name), + @ParentProjectId) +RETURNING id; diff --git a/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Select.sql b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Select.sql new file mode 100644 index 000000000..6b868f6ac --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Select.sql @@ -0,0 +1,24 @@ +WITH accessable AS + (SELECT transaction_schedules.id + FROM transaction_schedules + INNER JOIN owners ON owners.id = transaction_schedules.owner_id + INNER JOIN ownerships ON owners.id = ownerships.owner_id + INNER JOIN access ON access.id = ownerships.access_id + WHERE ownerships.user_id = @userId + AND (access.normalized_name = @access OR access.normalized_name = 'OWNER')) + +SELECT transaction_schedules.id AS Id, + transaction_schedules.created_at AS CreatedAt, + transaction_schedules.created_by_user_id AS CreatedByUserId, + transaction_schedules.deleted_at AS DeletedAt, + transaction_schedules.deleted_by_user_id AS DeletedByUserId, + transaction_schedules.owner_id AS OwnerId, + transaction_schedules.modified_at AS ModifiedAt, + transaction_schedules.modified_by_user_id AS ModifiedByUserId, + transaction_schedules.name AS Name, + transaction_schedules.normalized_name AS NormalizedName, + transaction_schedules.starting_at AS StartingAt, + transaction_schedules.period AS Period, + transaction_schedules.count AS Count +FROM transaction_schedules +WHERE transaction_schedules.id IN (SELECT id from accessable) diff --git a/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/SelectAll.sql b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/SelectAll.sql new file mode 100644 index 000000000..9d8aae50c --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/SelectAll.sql @@ -0,0 +1,14 @@ +SELECT transaction_schedules.id AS Id, + transaction_schedules.created_at AS CreatedAt, + transaction_schedules.created_by_user_id AS CreatedByUserId, + transaction_schedules.deleted_at AS DeletedAt, + transaction_schedules.deleted_by_user_id AS DeletedByUserId, + transaction_schedules.owner_id AS OwnerId, + transaction_schedules.modified_at AS ModifiedAt, + transaction_schedules.modified_by_user_id AS ModifiedByUserId, + transaction_schedules.name AS Name, + transaction_schedules.normalized_name AS NormalizedName, + transaction_schedules.starting_at AS StartingAt, + transaction_schedules.period AS Period, + transaction_schedules.count AS Count +FROM transaction_schedules diff --git a/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Update.sql b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Update.sql new file mode 100644 index 000000000..6b318583c --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/Queries/TransactionSchedule/Update.sql @@ -0,0 +1,21 @@ +WITH accessable AS + (SELECT transaction_schedules.id + FROM transaction_schedules + INNER JOIN owners ON owners.id = transaction_schedules.owner_id + INNER JOIN ownerships ON owners.id = ownerships.owner_id + INNER JOIN access ON access.id = ownerships.access_id + WHERE ownerships.user_id = @ModifiedByUserId + AND (access.normalized_name = 'WRITE' OR access.normalized_name = 'OWNER') + AND transaction_schedules.deleted_at IS NULL + AND transaction_schedules.id = @Id) + +UPDATE transaction_schedules +SET modified_at = CURRENT_TIMESTAMP, + modified_by_user_id = @ModifiedByUserId, + name = @Name, + normalized_name = upper(@Name), + starting_at = @StartingAt, + period = @Period, + count = @Count +FROM accessable +WHERE transaction_schedules.id IN (SELECT id FROM accessable); diff --git a/source/Gnomeshade.Data/Repositories/Repository.cs b/source/Gnomeshade.Data/Repositories/Repository.cs index cb7740a93..3a5058129 100644 --- a/source/Gnomeshade.Data/Repositories/Repository.cs +++ b/source/Gnomeshade.Data/Repositories/Repository.cs @@ -51,8 +51,9 @@ protected Repository(ILogger> logger, DbConnection dbConnect protected string SelectActiveSql => $"{SelectSql} AND {NotDeleted}"; - /// Gets the SQL query for getting entities. - protected abstract string SelectAllSql { get; } + protected abstract string TableName { get; } + + protected virtual string ExistsSql => $"SELECT 1 FROM {TableName} WHERE {FindSql} AND {NotDeleted} LIMIT 1;"; /// Gets SQL where clause that filters for specific entity by id. protected abstract string FindSql { get; } @@ -69,7 +70,7 @@ protected Repository(ILogger> logger, DbConnection dbConnect /// The entity to add. /// The database transaction to use for the query. /// The id of the created entity. - public Task AddAsync(TEntity entity, DbTransaction dbTransaction) + public virtual Task AddAsync(TEntity entity, DbTransaction dbTransaction) { Logger.AddingEntityWithTransaction(); var command = new CommandDefinition(InsertSql, entity, dbTransaction); @@ -82,7 +83,7 @@ public Task AddAsync(TEntity entity, DbTransaction dbTransaction) /// The database transaction to use for the query. /// The number of affected rows. [MustUseReturnValue] - public async Task DeleteAsync(Guid id, Guid userId, DbTransaction dbTransaction) + public virtual async Task DeleteAsync(Guid id, Guid userId, DbTransaction dbTransaction) { Logger.DeletingEntityWithTransaction(id); var count = await DbConnection.ExecuteAsync(DeleteSql, new { id, userId }, dbTransaction); @@ -189,30 +190,28 @@ public Task GetByIdAsync(Guid id, Guid userId, DbTransaction dbTransact dbTransaction)); } - /// Searches for an entity with the specified id. - /// The id to search by. - /// A to observe while waiting for the task to complete. - /// The entity if one exists, otherwise . - public Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default) + [MustUseReturnValue] + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) { Logger.FindId(id); - return FindAsync(new( - $"{SelectAllSql} WHERE {FindSql} AND {NotDeleted} {GroupBy};", + var count = await DbConnection.QuerySingleOrDefaultAsync(new( + ExistsSql, new { id }, cancellationToken: cancellationToken)); + + return count is not null; } - /// Searches for an entity with the specified id. - /// The id to search by. - /// The database transaction to use for the query. - /// The entity if one exists, otherwise . - public Task FindByIdAsync(Guid id, DbTransaction dbTransaction) + [MustUseReturnValue] + public async Task ExistsAsync(Guid id, DbTransaction dbTransaction) { Logger.FindIdWithTransaction(id); - return FindAsync(new( - $"{SelectAllSql} WHERE {FindSql} AND {NotDeleted} {GroupBy};", + var count = await DbConnection.QuerySingleOrDefaultAsync(new( + ExistsSql, new { id }, dbTransaction)); + + return count is not null; } /// Updates an existing entity with the specified id using the specified database transaction. @@ -220,7 +219,7 @@ public Task GetByIdAsync(Guid id, Guid userId, DbTransaction dbTransact /// The database transaction to use for the query. /// The number of affected rows. [MustUseReturnValue] - public async Task UpdateAsync(TEntity entity, DbTransaction dbTransaction) + public virtual async Task UpdateAsync(TEntity entity, DbTransaction dbTransaction) { Logger.UpdatingEntityWithTransaction(); var count = await DbConnection.ExecuteAsync(UpdateSql, entity, dbTransaction); @@ -252,11 +251,7 @@ protected virtual Task> GetEntitiesAsync(CommandDefinition .SingleOrDefault(); } - /// Executes the specified command and maps the resulting row to . - /// The command to execute. - /// The single entity returned by the query. - /// The query returned multiple or no results. - protected async Task GetAsync(CommandDefinition command) + private async Task GetAsync(CommandDefinition command) { var entities = await GetEntitiesAsync(command).ConfigureAwait(false); return entities.Single(); diff --git a/source/Gnomeshade.Data/Repositories/TransactionRepository.cs b/source/Gnomeshade.Data/Repositories/TransactionRepository.cs index 234a27c87..ed7cecd7c 100644 --- a/source/Gnomeshade.Data/Repositories/TransactionRepository.cs +++ b/source/Gnomeshade.Data/Repositories/TransactionRepository.cs @@ -34,7 +34,7 @@ public sealed class TransactionRepository(ILogger logger, protected override string InsertSql => Queries.Transaction.Insert; /// - protected override string SelectAllSql => Queries.Transaction.SelectAll; + protected override string TableName => "transactions t"; /// protected override string UpdateSql => Queries.Transaction.Update; diff --git a/source/Gnomeshade.Data/Repositories/TransactionScheduleRepository.cs b/source/Gnomeshade.Data/Repositories/TransactionScheduleRepository.cs new file mode 100644 index 000000000..d1bd9ea79 --- /dev/null +++ b/source/Gnomeshade.Data/Repositories/TransactionScheduleRepository.cs @@ -0,0 +1,59 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Data.Common; +using System.Threading.Tasks; + +using Dapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Logging; + +using Microsoft.Extensions.Logging; + +namespace Gnomeshade.Data.Repositories; + +/// Database backed repository. +public sealed class TransactionScheduleRepository(ILogger logger, DbConnection dbConnection) + : NamedRepository(logger, dbConnection) +{ + /// + protected override string DeleteSql => Queries.TransactionSchedule.Delete; + + /// + protected override string InsertSql => Queries.TransactionSchedule.Insert; + + /// + protected override string TableName => "transaction_schedules"; + + /// + protected override string GroupBy => string.Empty; + + /// + protected override string UpdateSql => Queries.TransactionSchedule.Update; + + /// + protected override string FindSql => "transaction_schedules.id = @id"; + + /// + protected override string NotDeleted => "schedules.deleted_at IS NULL"; + + /// + protected override string NameSql => "schedules.normalized_name = upper(@name)"; + + /// + protected override string SelectSql => Queries.TransactionSchedule.Select; + + /// + public override async Task DeleteAsync(Guid id, Guid userId, DbTransaction dbTransaction) + { + Logger.DeletingEntityWithTransaction(id); + var count = await DbConnection.ExecuteAsync(DeleteSql, new { id, userId }, dbTransaction); + Logger.DeletedRows(count); + + // todo Delete linked planned transactions ? + return count; + } +} diff --git a/source/Gnomeshade.Data/Repositories/TransferRepository.cs b/source/Gnomeshade.Data/Repositories/TransferRepository.cs index 1c70a00c1..821be8be0 100644 --- a/source/Gnomeshade.Data/Repositories/TransferRepository.cs +++ b/source/Gnomeshade.Data/Repositories/TransferRepository.cs @@ -35,7 +35,7 @@ public TransferRepository(ILogger logger, DbConnection dbCon protected override string InsertSql => Queries.Transfer.Insert; /// - protected override string SelectAllSql => Queries.Transfer.SelectAll; + protected override string TableName => "transfers"; /// protected override string UpdateSql => Queries.Transfer.Update; diff --git a/source/Gnomeshade.Data/Repositories/UnitRepository.cs b/source/Gnomeshade.Data/Repositories/UnitRepository.cs index 47bf8252c..418ad1ead 100644 --- a/source/Gnomeshade.Data/Repositories/UnitRepository.cs +++ b/source/Gnomeshade.Data/Repositories/UnitRepository.cs @@ -28,7 +28,7 @@ public UnitRepository(ILogger logger, DbConnection dbConnection) protected override string InsertSql => Queries.Unit.Insert; /// - protected override string SelectAllSql => Queries.Unit.SelectAll; + protected override string TableName => "units u"; /// protected override string UpdateSql => Queries.Unit.Update; diff --git a/source/Gnomeshade.Desktop/App.axaml.cs b/source/Gnomeshade.Desktop/App.axaml.cs index 92a9f8051..0febc1182 100644 --- a/source/Gnomeshade.Desktop/App.axaml.cs +++ b/source/Gnomeshade.Desktop/App.axaml.cs @@ -17,6 +17,7 @@ using Gnomeshade.Avalonia.Core; using Gnomeshade.Avalonia.Core.Authentication; using Gnomeshade.Avalonia.Core.Configuration; +using Gnomeshade.Avalonia.Core.DesignTime; using Gnomeshade.Desktop.Authentication; using Gnomeshade.Desktop.Views; using Gnomeshade.WebApi.Client; @@ -123,6 +124,7 @@ public App(string[] args) }); serviceCollection.AddGnomeshadeClient(configuration); + serviceCollection.AddSingleton(); // todo remove serviceCollection .AddViewModels() diff --git a/source/Gnomeshade.Desktop/Views/MainWindow.axaml b/source/Gnomeshade.Desktop/Views/MainWindow.axaml index 73cf64edc..1400a0a9f 100644 --- a/source/Gnomeshade.Desktop/Views/MainWindow.axaml +++ b/source/Gnomeshade.Desktop/Views/MainWindow.axaml @@ -71,6 +71,9 @@ + diff --git a/source/Gnomeshade.Desktop/Views/MainWindow.axaml.cs b/source/Gnomeshade.Desktop/Views/MainWindow.axaml.cs index 9b07bba7e..3905879c3 100644 --- a/source/Gnomeshade.Desktop/Views/MainWindow.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/MainWindow.axaml.cs @@ -2,11 +2,7 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. -#if DEBUG -using Avalonia; -#endif using Avalonia.Controls; -using Avalonia.Markup.Xaml; using Gnomeshade.Avalonia.Core; @@ -16,12 +12,5 @@ namespace Gnomeshade.Desktop.Views; public sealed partial class MainWindow : Window, IView { /// Initializes a new instance of the class. - public MainWindow() - { - AvaloniaXamlLoader.Load(this); - -#if DEBUG - this.AttachDevTools(); -#endif - } + public MainWindow() => InitializeComponent(); } diff --git a/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml b/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml new file mode 100644 index 000000000..c0b8d2833 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml @@ -0,0 +1,13 @@ + + + + + Missing view + + + diff --git a/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml.cs b/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml.cs new file mode 100644 index 000000000..e6d6497ca --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/PlaceholderView.axaml.cs @@ -0,0 +1,23 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +#if DEBUG +using System.Diagnostics.CodeAnalysis; +#endif + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; + +namespace Gnomeshade.Desktop.Views; + +/// Used by when could not locate a view in debug mode. +#if DEBUG +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] +#endif +public sealed partial class PlaceholderView : UserControl +{ + /// Initializes a new instance of the class. + public PlaceholderView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Reports/BalanceReportView.axaml b/source/Gnomeshade.Desktop/Views/Reports/BalanceReportView.axaml index 0d0e2e85d..7f35f73f8 100644 --- a/source/Gnomeshade.Desktop/Views/Reports/BalanceReportView.axaml +++ b/source/Gnomeshade.Desktop/Views/Reports/BalanceReportView.axaml @@ -28,6 +28,10 @@ SelectedItem="{Binding SelectedSplit, Mode=TwoWay}" IsEnabled="{Binding !IsBusy}" /> + + Include projections + + - + + Include projections + + - + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs index 7be88ad6b..cdea45c06 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanUpsertionView.axaml.cs @@ -3,7 +3,6 @@ // See LICENSE.txt file in the project root for full license information. using Avalonia.Controls; -using Avalonia.Markup.Xaml; using Gnomeshade.Avalonia.Core; using Gnomeshade.Avalonia.Core.Transactions.Loans; @@ -14,8 +13,5 @@ namespace Gnomeshade.Desktop.Views.Transactions.Loans; public sealed partial class LoanUpsertionView : UserControl, IView { /// Initializes a new instance of the class. - public LoanUpsertionView() - { - AvaloniaXamlLoader.Load(this); - } + public LoanUpsertionView() => InitializeComponent(); } diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs index 98ae176f1..0c56c77cc 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/LoanView.axaml.cs @@ -3,7 +3,6 @@ // See LICENSE.txt file in the project root for full license information. using Avalonia.Controls; -using Avalonia.Markup.Xaml; using Gnomeshade.Avalonia.Core; using Gnomeshade.Avalonia.Core.Transactions.Loans; @@ -14,8 +13,5 @@ namespace Gnomeshade.Desktop.Views.Transactions.Loans; public sealed partial class LoanView : UserControl, IView { /// Initializes a new instance of the class. - public LoanView() - { - AvaloniaXamlLoader.Load(this); - } + public LoanView() => InitializeComponent(); } diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml new file mode 100644 index 000000000..29b161a61 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml.cs new file mode 100644 index 000000000..3c97dc28b --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanUpsertionView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Loans; + +namespace Gnomeshade.Desktop.Views.Transactions.Loans; + +/// +public sealed partial class PlannedLoanUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedLoanUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml new file mode 100644 index 000000000..4e194973a --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml.cs new file mode 100644 index 000000000..028c91d25 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Loans/PlannedLoanView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Loans; + +namespace Gnomeshade.Desktop.Views.Transactions.Loans; + +/// +public sealed partial class PlannedLoanView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedLoanView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml new file mode 100644 index 000000000..f139ad65f --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml.cs new file mode 100644 index 000000000..7503e7139 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/PlannedTransactionUpsertionView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions; + +namespace Gnomeshade.Desktop.Views.Transactions; + +/// +public sealed partial class PlannedTransactionUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedTransactionUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/ProjectionConverter.cs b/source/Gnomeshade.Desktop/Views/Transactions/ProjectionConverter.cs new file mode 100644 index 000000000..9f7cd817e --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/ProjectionConverter.cs @@ -0,0 +1,22 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Globalization; + +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Gnomeshade.Desktop.Views.Transactions; + +internal sealed class ProjectionConverter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is true + ? Brushes.DimGray + : Brushes.Transparent; + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => null; +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml new file mode 100644 index 000000000..25a81d7af --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml.cs new file mode 100644 index 000000000..cd76f5a00 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseUpsertionView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Purchases; + +namespace Gnomeshade.Desktop.Views.Transactions.Purchases; + +/// +public sealed partial class PlannedPurchaseUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedPurchaseUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml new file mode 100644 index 000000000..df65795e8 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml.cs new file mode 100644 index 000000000..63ba8e917 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PlannedPurchaseView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Purchases; + +namespace Gnomeshade.Desktop.Views.Transactions.Purchases; + +/// +public sealed partial class PlannedPurchaseView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedPurchaseView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PurchaseView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PurchaseView.axaml index fafa8a2e8..d4018f7c5 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PurchaseView.axaml +++ b/source/Gnomeshade.Desktop/Views/Transactions/Purchases/PurchaseView.axaml @@ -12,6 +12,13 @@ x:DataType="purchases:PurchaseViewModel"> + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml new file mode 100644 index 000000000..14ea8f974 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml.cs new file mode 100644 index 000000000..6f693063f --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleUpsertionView.axaml.cs @@ -0,0 +1,18 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions; +using Gnomeshade.Avalonia.Core.Transactions.Purchases; + +namespace Gnomeshade.Desktop.Views.Transactions; + +/// +public sealed partial class TransactionScheduleUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public TransactionScheduleUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml new file mode 100644 index 000000000..7b3bc5dcf --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml.cs new file mode 100644 index 000000000..abc1d3782 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionScheduleView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions; + +namespace Gnomeshade.Desktop.Views.Transactions; + +/// +public sealed partial class TransactionScheduleView : UserControl, IView +{ + /// Initializes a new instance of the class. + public TransactionScheduleView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml index 1a512f24f..a6ff3631e 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml @@ -24,7 +24,7 @@ diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml.cs index 8110151b3..ac1f2d3cd 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml.cs +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionUpsertionView.axaml.cs @@ -3,7 +3,6 @@ // See LICENSE.txt file in the project root for full license information. using Avalonia.Controls; -using Avalonia.Markup.Xaml; using Gnomeshade.Avalonia.Core; using Gnomeshade.Avalonia.Core.Transactions; @@ -11,11 +10,9 @@ namespace Gnomeshade.Desktop.Views.Transactions; /// -public sealed partial class TransactionUpsertionView : UserControl, IView +public sealed partial class TransactionUpsertionView : UserControl, + IView { /// Initializes a new instance of the class. - public TransactionUpsertionView() - { - AvaloniaXamlLoader.Load(this); - } + public TransactionUpsertionView() => InitializeComponent(); } diff --git a/source/Gnomeshade.Desktop/Views/Transactions/TransactionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/TransactionView.axaml index 1528e0b2a..22f265417 100644 --- a/source/Gnomeshade.Desktop/Views/Transactions/TransactionView.axaml +++ b/source/Gnomeshade.Desktop/Views/Transactions/TransactionView.axaml @@ -6,6 +6,7 @@ xmlns:transactions="clr-namespace:Gnomeshade.Avalonia.Core.Transactions;assembly=Gnomeshade.Avalonia.Core" xmlns:design="clr-namespace:Gnomeshade.Avalonia.Core.DesignTime;assembly=Gnomeshade.Avalonia.Core" xmlns:interactivity="clr-namespace:Gnomeshade.Avalonia.Core.Interactivity;assembly=Gnomeshade.Avalonia.Core" + xmlns:transactions1="clr-namespace:Gnomeshade.Desktop.Views.Transactions" mc:Ignorable="d" d:DesignWidth="1920" d:DesignHeight="600" d:DataContext="{x:Static design:DesignTimeData.TransactionViewModel}" x:Class="Gnomeshade.Desktop.Views.Transactions.TransactionView" @@ -187,7 +188,7 @@ + Text="{Binding OtherCurrencyFormatted, Mode=OneWay}" /> @@ -213,6 +214,16 @@ + + + + + + + + { /// Initializes a new instance of the class. - public TransactionView() - { - AvaloniaXamlLoader.Load(this); - } + public TransactionView() => InitializeComponent(); } diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml new file mode 100644 index 000000000..6aea58492 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml @@ -0,0 +1,96 @@ + + + + + + Specific source account + + + Specific target account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml.cs new file mode 100644 index 000000000..7f3f61476 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferUpsertionView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Transfers; + +namespace Gnomeshade.Desktop.Views.Transactions.Transfers; + +/// +public sealed partial class PlannedTransferUpsertionView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedTransferUpsertionView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml new file mode 100644 index 000000000..b317476d7 --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml.cs b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml.cs new file mode 100644 index 000000000..45eeb078e --- /dev/null +++ b/source/Gnomeshade.Desktop/Views/Transactions/Transfers/PlannedTransferView.axaml.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Avalonia.Controls; + +using Gnomeshade.Avalonia.Core; +using Gnomeshade.Avalonia.Core.Transactions.Transfers; + +namespace Gnomeshade.Desktop.Views.Transactions.Transfers; + +/// +public sealed partial class PlannedTransferView : UserControl, IView +{ + /// Initializes a new instance of the class. + public PlannedTransferView() => InitializeComponent(); +} diff --git a/source/Gnomeshade.WebApi.Client/GnomeshadeClient.cs b/source/Gnomeshade.WebApi.Client/GnomeshadeClient.cs index 874250d70..59edbf34d 100644 --- a/source/Gnomeshade.WebApi.Client/GnomeshadeClient.cs +++ b/source/Gnomeshade.WebApi.Client/GnomeshadeClient.cs @@ -244,6 +244,126 @@ public Task AddRelatedTransactionAsync(Guid id, Guid relatedId) => public Task RemoveRelatedTransactionAsync(Guid id, Guid relatedId) => DeleteAsync(Transactions.RelatedUri(id, relatedId)); + /// + public Task> GetTransactionSchedules(CancellationToken cancellationToken = default) => + GetAsync(TransactionSchedules.Uri, _context.ListTransactionSchedule, cancellationToken); + + /// + public Task GetTransactionSchedule(Guid id, CancellationToken cancellationToken = default) => + GetAsync(TransactionSchedules.IdUri(id), _context.TransactionSchedule, cancellationToken); + + /// + public Task CreateTransactionSchedule(TransactionScheduleCreation schedule) => + PostAsync(TransactionSchedules.Uri, schedule, _context.TransactionScheduleCreation); + + /// + public Task PutTransactionSchedule(Guid id, TransactionScheduleCreation schedule) => + PutAsync(TransactionSchedules.IdUri(id), schedule, _context.TransactionScheduleCreation); + + /// + public Task DeleteTransactionSchedule(Guid id) => + DeleteAsync(TransactionSchedules.IdUri(id)); + + /// + public Task> GetPlannedTransactions(Interval interval, CancellationToken cancellationToken = default) => + GetAsync(PlannedTransactions.DateRangeUri(interval), _context.ListPlannedTransaction, cancellationToken); + + /// + public Task> GetPlannedTransactions(CancellationToken cancellationToken = default) => + GetAsync(PlannedTransactions.Uri, _context.ListPlannedTransaction, cancellationToken); + + /// + public Task> GetPlannedTransactions(Guid scheduleId, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + /// + public Task GetPlannedTransaction(Guid id, CancellationToken cancellationToken = default) => + GetAsync(PlannedTransactions.IdUri(id), _context.PlannedTransaction, cancellationToken); + + /// + public Task CreatePlannedTransaction(PlannedTransactionCreation transaction) => + PostAsync(PlannedTransactions.Uri, transaction, _context.PlannedTransactionCreation); + + /// + public Task PutPlannedTransaction(Guid id, PlannedTransactionCreation transaction) => + PutAsync(PlannedTransactions.IdUri(id), transaction, _context.PlannedTransactionCreation); + + /// + public Task DeletePlannedTransaction(Guid id) => + DeleteAsync(PlannedTransactions.IdUri(id)); + + /// + public Task> GetPlannedTransfers(CancellationToken cancellationToken = default) => + GetAsync(PlannedTransfers.Uri, _context.ListPlannedTransfer, cancellationToken); + + /// + public Task> GetPlannedTransfers(Guid transactionId, CancellationToken cancellationToken = default) => + GetAsync(PlannedTransfers.TransactionUri(transactionId), _context.ListPlannedTransfer, cancellationToken); + + /// + public Task GetPlannedTransfer(Guid id, CancellationToken cancellationToken = default) => + GetAsync(PlannedTransfers.IdUri(id), _context.PlannedTransfer, cancellationToken); + + /// + public Task CreatePlannedTransfer(PlannedTransferCreation transfer) => + PostAsync(PlannedTransfers.Uri, transfer, _context.PlannedTransferCreation); + + /// + public Task PutPlannedTransfer(Guid id, PlannedTransferCreation transfer) => + PutAsync(PlannedTransfers.IdUri(id), transfer, _context.PlannedTransferCreation); + + /// + public Task DeletePlannedTransfer(Guid id) => + DeleteAsync(PlannedTransfers.IdUri(id)); + + /// + public Task> GetPlannedPurchases(CancellationToken cancellationToken = default) => + GetAsync(PlannedPurchases.Uri, _context.ListPlannedPurchase, cancellationToken); + + /// + public Task> GetPlannedPurchases(Guid transactionId, CancellationToken cancellationToken = default) => + GetAsync(PlannedPurchases.TransactionUri(transactionId), _context.ListPlannedPurchase, cancellationToken); + + /// + public Task GetPlannedPurchase(Guid id, CancellationToken cancellationToken = default) => + GetAsync(PlannedPurchases.IdUri(id), _context.PlannedPurchase, cancellationToken); + + /// + public Task CreatePlannedPurchase(PlannedPurchaseCreation purchase) => + PostAsync(PlannedPurchases.Uri, purchase, _context.PlannedPurchaseCreation); + + /// + public Task PutPlannedPurchase(Guid id, PlannedPurchaseCreation purchase) => + PostAsync(PlannedPurchases.IdUri(id), purchase, _context.PlannedPurchaseCreation); + + /// + public Task DeletePlannedPurchase(Guid id) => + DeleteAsync(PlannedPurchases.IdUri(id)); + + /// + public Task> GetPlannedLoanPayments(CancellationToken cancellationToken = default) => + GetAsync(PlannedLoanPayments.Uri, _context.ListPlannedLoanPayment, cancellationToken); + + /// + public Task> GetPlannedLoanPayments(Guid transactionId, CancellationToken cancellationToken = default) => + GetAsync(PlannedLoanPayments.ForTransaction(transactionId), _context.ListPlannedLoanPayment, cancellationToken); + + /// + public Task GetPlannedLoanPayment(Guid id, CancellationToken cancellationToken = default) => + GetAsync(PlannedLoanPayments.IdUri(id), _context.PlannedLoanPayment, cancellationToken); + + /// + public Task CreatePlannedLoanPayment(LoanPaymentCreation loanPayment) => + PostAsync(PlannedLoanPayments.Uri, loanPayment, _context.LoanPaymentCreation); + + /// + public Task PutPlannedLoanPayment(Guid id, LoanPaymentCreation loanPayment) => + PutAsync(PlannedLoanPayments.IdUri(id), loanPayment, _context.LoanPaymentCreation); + + /// + public Task DeletePlannedLoanPayment(Guid id) => + DeleteAsync(PlannedLoanPayments.IdUri(id)); + /// [Obsolete] public Task> GetLegacyLoans(CancellationToken cancellationToken = default) => diff --git a/source/Gnomeshade.WebApi.Client/ITransactionClient.cs b/source/Gnomeshade.WebApi.Client/ITransactionClient.cs index 33cd5f3a8..b5e93d9f9 100644 --- a/source/Gnomeshade.WebApi.Client/ITransactionClient.cs +++ b/source/Gnomeshade.WebApi.Client/ITransactionClient.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Gnomeshade.WebApi.Models; +using Gnomeshade.WebApi.Models.Loans; using Gnomeshade.WebApi.Models.Transactions; using NodaTime; @@ -167,6 +168,177 @@ Task> GetDetailedTransactionsAsync( /// A representing the asynchronous operation. Task RemoveRelatedTransactionAsync(Guid id, Guid relatedId); + /// Gets all transaction schedules. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All transaction schedules. + Task> GetTransactionSchedules(CancellationToken cancellationToken = default); + + /// Gets the specified transaction schedule. + /// The id of the transaction schedule to get. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The transaction schedule with the specified id. + Task GetTransactionSchedule(Guid id, CancellationToken cancellationToken = default); + + /// Creates a new transaction schedule. + /// The transaction schedule to create. + /// The id of the created transaction schedule. + Task CreateTransactionSchedule(TransactionScheduleCreation schedule); + + /// Creates a new transaction schedule or replaces an existing one, if one exists with the specified id. + /// The id of the transaction schedule. + /// The transaction schedule to create or replace. + /// A representing the asynchronous operation. + Task PutTransactionSchedule(Guid id, TransactionScheduleCreation schedule); + + /// Deletes the specified transaction schedule. + /// The id of the transaction schedule to delete. + /// A representing the asynchronous operation. + Task DeleteTransactionSchedule(Guid id); + + /// Gets all planned transactions within the specified time interval. + /// The interval for which to get the planned transactions. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned transactions within the specified time interval. + Task> GetPlannedTransactions( + Interval interval, + CancellationToken cancellationToken = default); + + /// Gets all planned transactions. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned transactions. + Task> GetPlannedTransactions(CancellationToken cancellationToken = default); + + /// Gets all planned transactions with the specified schedule. + /// The id of the schedule for which to get all planned transactions. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned transactions for the specified schedule. + Task> GetPlannedTransactions( + Guid scheduleId, + CancellationToken cancellationToken = default); + + /// Gets the specified planned transaction. + /// The id of the planned transaction to get. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The planned transaction with the specified id. + Task GetPlannedTransaction(Guid id, CancellationToken cancellationToken = default); + + /// Creates a new planned transaction. + /// The planned transaction to create. + /// The id of the created planned transaction. + Task CreatePlannedTransaction(PlannedTransactionCreation transaction); + + /// Creates a new planned transaction or replaces an existing one, if one exists with the specified id. + /// The id of the planned transaction. + /// The planned transaction to create or replace. + /// A representing the asynchronous operation. + Task PutPlannedTransaction(Guid id, PlannedTransactionCreation transaction); + + /// Deletes the specified planned transaction. + /// The id of the planned transaction to delete. + /// A representing the asynchronous operation. + Task DeletePlannedTransaction(Guid id); + + /// Gets all planned transfers. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned transfers. + Task> GetPlannedTransfers(CancellationToken cancellationToken = default); + + /// Gets all planned transfers for the specified transaction. + /// The id of the transaction for which to get all the planned transfers. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned transfers. + Task> GetPlannedTransfers(Guid transactionId, CancellationToken cancellationToken = default); + + /// Gets the specified planned transfer. + /// The id of the planned transfer to get. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The planned transfer with the specified id. + Task GetPlannedTransfer(Guid id, CancellationToken cancellationToken = default); + + /// Creates a new planned transfer. + /// The planned transfer to create. + /// The id of the created planned transfer. + Task CreatePlannedTransfer(PlannedTransferCreation transfer); + + /// Creates a new planned transfer or replaces an existing one, if one exists with the specified id. + /// The id of the planned transfer. + /// The planned transfer to create or replace. + /// A representing the asynchronous operation. + Task PutPlannedTransfer(Guid id, PlannedTransferCreation transfer); + + /// Deletes the specified planned transfer. + /// The id of the planned transfer to delete. + /// A representing the asynchronous operation. + Task DeletePlannedTransfer(Guid id); + + /// Gets all planned purchases. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned purchases. + Task> GetPlannedPurchases(CancellationToken cancellationToken = default); + + /// Gets all planned purchases for the specified transaction. + /// The id of the transaction for which to get all the planned purchases. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned purchases. + Task> GetPlannedPurchases(Guid transactionId, CancellationToken cancellationToken = default); + + /// Gets the specified planned purchase. + /// The id of the planned purchase to get. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The planned purchase with the specified id. + Task GetPlannedPurchase(Guid id, CancellationToken cancellationToken = default); + + /// Creates a new planned purchase. + /// The planned purchase to create. + /// The id of the created planned purchase. + Task CreatePlannedPurchase(PlannedPurchaseCreation purchase); + + /// Creates a new planned purchase or replaces an existing one, if one exists with the specified id. + /// The id of the planned purchase. + /// The planned purchase to create or replace. + /// A representing the asynchronous operation. + Task PutPlannedPurchase(Guid id, PlannedPurchaseCreation purchase); + + /// Deletes the specified planned purchase. + /// The id of the planned purchase to delete. + /// A representing the asynchronous operation. + Task DeletePlannedPurchase(Guid id); + + /// Gets all planned loan payments. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned loan payments. + Task> GetPlannedLoanPayments(CancellationToken cancellationToken = default); + + /// Gets all planned loan payments for the specified transaction. + /// The id of the transaction for which to get all the planned loan payments. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// All planned loan payments. + Task> GetPlannedLoanPayments( + Guid transactionId, + CancellationToken cancellationToken = default); + + /// Gets the specified planned loan payment. + /// The id of the planned loan payment to get. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The planned loan payment with the specified id. + Task GetPlannedLoanPayment(Guid id, CancellationToken cancellationToken = default); + + /// Creates a new planned loan payment. + /// The planned loan payment to create. + /// The id of the created planned loan payment. + Task CreatePlannedLoanPayment(LoanPaymentCreation loanPayment); + + /// Creates a new planned loan payment or replaces an existing one, if one exists with the specified id. + /// The id of the planned loan payment. + /// The planned loan payment to create or replace. + /// A representing the asynchronous operation. + Task PutPlannedLoanPayment(Guid id, LoanPaymentCreation loanPayment); + + /// Deletes the specified planned loan payment. + /// The id of the planned loan payment to delete. + /// A representing the asynchronous operation. + Task DeletePlannedLoanPayment(Guid id); + /// Gets all loans. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// All loans. diff --git a/source/Gnomeshade.WebApi.Client/Routes.cs b/source/Gnomeshade.WebApi.Client/Routes.cs index a8b93ceb9..d9ad3f446 100644 --- a/source/Gnomeshade.WebApi.Client/Routes.cs +++ b/source/Gnomeshade.WebApi.Client/Routes.cs @@ -50,6 +50,29 @@ private static string Parameters(List> parameters) return string.Join("&", parameters.Select(pair => $"{pair.Key}={Format(pair.Value)}")); } + private static string DateRangeUri(string baseUri, Interval interval) + { + var keyValues = new List>(2); + if (interval.HasStart) + { + keyValues.Add(new(interval.Start, "from")); + } + + if (interval.HasEnd) + { + keyValues.Add(new(interval.End, "to")); + } + + if (keyValues.Count is 0) + { + return baseUri; + } + + var parameters = keyValues.Select(pair => $"{pair.Value}={pair.Key}"); + var query = string.Join('&', parameters); + return $"{baseUri}?{query}"; + } + /// Account routes. public static class Accounts { @@ -77,9 +100,9 @@ public static class Transactions /// Gets the relative uri for all transactions within the specified interval. /// The interval for which to get transactions. /// Relative uri for all transaction with a query for the specified interval. - public static string DateRangeUri(Interval interval) => DateRangeUri(Uri, interval); + public static string DateRangeUri(Interval interval) => Routes.DateRangeUri(Uri, interval); - internal static string DetailedDateRangeUri(Interval interval) => DateRangeUri(_detailedUri, interval); + internal static string DetailedDateRangeUri(Interval interval) => Routes.DateRangeUri(_detailedUri, interval); internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; @@ -98,29 +121,53 @@ internal static string MergeUri(Guid targetId, IEnumerable sourceIds) internal static string RelatedUri(Guid id) => $"{IdUri(id)}/Related"; internal static string RelatedUri(Guid id, Guid relatedId) => $"{RelatedUri(id)}/{Format(relatedId)}"; + } - private static string DateRangeUri(string baseUri, Interval interval) - { - var keyValues = new List>(2); - if (interval.HasStart) - { - keyValues.Add(new(interval.Start, "from")); - } - - if (interval.HasEnd) - { - keyValues.Add(new(interval.End, "to")); - } - - if (!keyValues.Any()) - { - return baseUri; - } - - var parameters = keyValues.Select(pair => $"{pair.Value}={pair.Key}"); - var query = string.Join('&', parameters); - return $"{baseUri}?{query}"; - } + internal static class TransactionSchedules + { + internal const string Uri = $"{V1}/TransactionSchedules"; + + internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; + } + + internal static class PlannedTransactions + { + internal const string Uri = $"{V1}/Transactions/Planned"; + + internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; + + internal static string DateRangeUri(Interval interval) => Routes.DateRangeUri(Uri, interval); + } + + internal static class PlannedTransfers + { + internal const string Uri = $"{V1}/Transfers/Planned"; + + internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; + + internal static string TransactionUri(Guid transactionId) => $"{PlannedTransactions.IdUri(transactionId)}/Transfers"; + } + + internal static class PlannedPurchases + { + internal const string Uri = $"{V1}/Purchases/Planned"; + + internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; + + internal static string TransactionUri(Guid transactionId) => $"{PlannedTransactions.IdUri(transactionId)}/Purchases"; + } + + internal static class PlannedLoanPayments + { + internal const string Uri = $"{V2}/{nameof(LoanPayments)}/Planned"; + + internal static string IdUri(Guid id) => $"{Uri}/{Format(id)}"; + + internal static string ForLoan(Guid loanId) => + $"{V2}/{nameof(Loans)}/{Format(loanId)}/{nameof(LoanPayments)}"; + + internal static string ForTransaction(Guid transactionId) => + $"{V2}/{nameof(Transactions)}/{Format(transactionId)}/{nameof(LoanPayments)}"; // todo } internal static class Products diff --git a/source/Gnomeshade.WebApi.Models/GnomeshadeSerializerContext.cs b/source/Gnomeshade.WebApi.Models/GnomeshadeSerializerContext.cs index da6c65ac3..29e83818d 100644 --- a/source/Gnomeshade.WebApi.Models/GnomeshadeSerializerContext.cs +++ b/source/Gnomeshade.WebApi.Models/GnomeshadeSerializerContext.cs @@ -8,6 +8,7 @@ using Gnomeshade.WebApi.Models.Accounts; using Gnomeshade.WebApi.Models.Authentication; using Gnomeshade.WebApi.Models.Importing; +using Gnomeshade.WebApi.Models.Loans; using Gnomeshade.WebApi.Models.Owners; using Gnomeshade.WebApi.Models.Products; using Gnomeshade.WebApi.Models.Projects; @@ -51,13 +52,22 @@ namespace Gnomeshade.WebApi.Models; [JsonSerializable(typeof(LoginResponse))] [JsonSerializable(typeof(RegistrationModel))] [JsonSerializable(typeof(List))] -[JsonSerializable(typeof(List))] -[JsonSerializable(typeof(Loans.LoanCreation))] -[JsonSerializable(typeof(List))] -[JsonSerializable(typeof(Loans.LoanPaymentCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(LoanCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(LoanPaymentCreation))] #pragma warning disable CS0612 // Type or member is obsolete [JsonSerializable(typeof(List))] #pragma warning restore CS0612 // Type or member is obsolete [JsonSerializable(typeof(List))] [JsonSerializable(typeof(ProjectCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(TransactionScheduleCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(PlannedTransactionCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(PlannedTransferCreation))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(PlannedPurchaseCreation))] +[JsonSerializable(typeof(List))] public sealed partial class GnomeshadeSerializerContext : JsonSerializerContext; diff --git a/source/Gnomeshade.WebApi.Models/Loans/LoanPayment.cs b/source/Gnomeshade.WebApi.Models/Loans/LoanPayment.cs index dee24076c..1ef1ff258 100644 --- a/source/Gnomeshade.WebApi.Models/Loans/LoanPayment.cs +++ b/source/Gnomeshade.WebApi.Models/Loans/LoanPayment.cs @@ -2,50 +2,11 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. -using System; - -using Gnomeshade.WebApi.Models.Transactions; - using JetBrains.Annotations; -using NodaTime; - namespace Gnomeshade.WebApi.Models.Loans; /// A payment that was to either issue or pay back a loan. /// [PublicAPI] -public sealed record LoanPayment -{ - /// The id of the loan payment. - public Guid Id { get; set; } - - /// The point in time when the loan was created. - public Instant CreatedAt { get; set; } - - /// The id of the user that created this loan payment. - public Guid CreatedByUserId { get; set; } - - /// The id of the owner of the loan payment. - public Guid OwnerId { get; set; } - - /// The point in time when the loan payment was last modified. - public Instant ModifiedAt { get; set; } - - /// The id of the user that last modified this loan payment. - public Guid ModifiedByUserId { get; set; } - - /// The id of the loan that this payment is a part of. - /// - public Guid LoanId { get; set; } - - /// The id of the transaction that this payment is a part of. - /// - public Guid TransactionId { get; set; } - - /// The amount that was loaned or paid back. - public decimal Amount { get; set; } - - /// The interest amount of this loan payment. - public decimal Interest { get; set; } -} +public sealed record LoanPayment : LoanPaymentBase; diff --git a/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentBase.cs b/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentBase.cs new file mode 100644 index 000000000..1459fdf02 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentBase.cs @@ -0,0 +1,48 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using Gnomeshade.WebApi.Models.Transactions; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Loans; + +/// A payment that was to either issue or pay back a loan. +/// +public abstract record LoanPaymentBase +{ + /// The id of the loan payment. + public Guid Id { get; set; } + + /// The point in time when the loan was created. + public Instant CreatedAt { get; set; } + + /// The id of the user that created this loan payment. + public Guid CreatedByUserId { get; set; } + + /// The id of the owner of the loan payment. + public Guid OwnerId { get; set; } + + /// The point in time when the loan payment was last modified. + public Instant ModifiedAt { get; set; } + + /// The id of the user that last modified this loan payment. + public Guid ModifiedByUserId { get; set; } + + /// The id of the loan that this payment is a part of. + /// + public Guid LoanId { get; set; } + + /// The id of the transaction that this payment is a part of. + /// + public Guid TransactionId { get; set; } + + /// The amount that was loaned or paid back. + public decimal Amount { get; set; } + + /// The interest amount of this loan payment. + public decimal Interest { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentCreation.cs b/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentCreation.cs index 64c263a80..276953802 100644 --- a/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentCreation.cs +++ b/source/Gnomeshade.WebApi.Models/Loans/LoanPaymentCreation.cs @@ -14,19 +14,19 @@ namespace Gnomeshade.WebApi.Models.Loans; [PublicAPI] public sealed record LoanPaymentCreation : Creation { - /// + /// [Required] public Guid? LoanId { get; set; } - /// + /// [Required] public Guid? TransactionId { get; set; } - /// + /// [Required] public decimal? Amount { get; set; } - /// + /// [Required] public decimal? Interest { get; set; } } diff --git a/source/Gnomeshade.WebApi.Models/Loans/PlannedLoanPayment.cs b/source/Gnomeshade.WebApi.Models/Loans/PlannedLoanPayment.cs new file mode 100644 index 000000000..c4a6b1fb1 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Loans/PlannedLoanPayment.cs @@ -0,0 +1,12 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Loans; + +/// A payment that was to either issue or pay back a loan. +/// +[PublicAPI] +public sealed record PlannedLoanPayment : LoanPaymentBase; diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchase.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchase.cs new file mode 100644 index 000000000..3fdeb3da2 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchase.cs @@ -0,0 +1,13 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using Gnomeshade.WebApi.Models.Products; + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// The act or an instance of buying of a as a part of a . +[PublicAPI] +public sealed record PlannedPurchase : PurchaseBase; diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchaseCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchaseCreation.cs new file mode 100644 index 000000000..13be58bbd --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedPurchaseCreation.cs @@ -0,0 +1,12 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create a planned purchase. +/// +[PublicAPI] +public sealed record PlannedPurchaseCreation : PurchaseCreationBase; diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransaction.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransaction.cs new file mode 100644 index 000000000..17a31e608 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransaction.cs @@ -0,0 +1,17 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// A financial transaction during which funds can be transferred between multiple accounts. +[PublicAPI] +public record PlannedTransaction : TransactionBase +{ + /// The id of the schedule of this planned transaction. + public Guid ScheduleId { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransactionCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransactionCreation.cs new file mode 100644 index 000000000..2b872a948 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransactionCreation.cs @@ -0,0 +1,20 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.ComponentModel.DataAnnotations; + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create a planned transaction. +/// +[PublicAPI] +public sealed record PlannedTransactionCreation : Creation +{ + /// + [Required] + public Guid? ScheduleId { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransfer.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransfer.cs new file mode 100644 index 000000000..1f08e46cd --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransfer.cs @@ -0,0 +1,50 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; + +using Gnomeshade.WebApi.Models.Accounts; + +using JetBrains.Annotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// A planned transfer between two accounts. +/// +[PublicAPI] +public sealed record PlannedTransfer : TransferBase +{ + /// The id of the account from which currency is withdrawn from. + /// + public Guid? SourceAccountId { get; set; } + + /// Whether or is specified. + [MemberNotNullWhen(true, nameof(SourceAccountId))] + [MemberNotNullWhen(false, nameof(SourceCounterpartyId))] + [MemberNotNullWhen(false, nameof(SourceCurrencyId))] + public bool IsSourceAccount => SourceAccountId.HasValue; + + /// The id of the counterparty from which currency will be withdrawn from. + public Guid? SourceCounterpartyId { get; set; } + + /// The id of the currency in which funds will be withdrawn from . + public Guid? SourceCurrencyId { get; set; } + + /// The id of the account to which currency is deposited to. + /// + public Guid? TargetAccountId { get; set; } + + /// Whether or is specified. + [MemberNotNullWhen(true, nameof(TargetAccountId))] + [MemberNotNullWhen(false, nameof(TargetCounterpartyId))] + [MemberNotNullWhen(false, nameof(TargetCurrencyId))] + public bool IsTargetAccount => TargetAccountId.HasValue; + + /// The id of the counterparty to which currency will be deposited to. + public Guid? TargetCounterpartyId { get; set; } + + /// The id of the currency in which funds will be deposited to . + public Guid? TargetCurrencyId { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransferCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransferCreation.cs new file mode 100644 index 000000000..ce25a6e24 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PlannedTransferCreation.cs @@ -0,0 +1,50 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.ComponentModel.DataAnnotations; + +using JetBrains.Annotations; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create a planned transfer. +/// +[PublicAPI] +public sealed record PlannedTransferCreation : TransferCreationBase +{ + /// + [Required] + public override Guid? TransactionId { get; set; } + + /// + [RequiredIfNull(nameof(SourceCounterpartyId))] + public override Guid? SourceAccountId { get; set; } + + /// + [RequiredIfNull(nameof(SourceAccountId))] + public Guid? SourceCounterpartyId { get; set; } + + /// + [RequiredIfNotNull(nameof(SourceCounterpartyId))] + public Guid? SourceCurrencyId { get; set; } + + /// + [RequiredIfNull(nameof(TargetCounterpartyId))] + public override Guid? TargetAccountId { get; set; } + + /// + [RequiredIfNull(nameof(TargetAccountId))] + public Guid? TargetCounterpartyId { get; set; } + + /// + [RequiredIfNotNull(nameof(TargetCounterpartyId))] + public Guid? TargetCurrencyId { get; set; } + + /// + [Required] + public override Instant? BookedAt { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/Purchase.cs b/source/Gnomeshade.WebApi.Models/Transactions/Purchase.cs index 3291780d1..b5fa5d820 100644 --- a/source/Gnomeshade.WebApi.Models/Transactions/Purchase.cs +++ b/source/Gnomeshade.WebApi.Models/Transactions/Purchase.cs @@ -2,10 +2,6 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. -using System; -using System.Collections.Generic; - -using Gnomeshade.WebApi.Models.Accounts; using Gnomeshade.WebApi.Models.Products; using JetBrains.Annotations; @@ -16,50 +12,8 @@ namespace Gnomeshade.WebApi.Models.Transactions; /// The act or an instance of buying of a as a part of a . [PublicAPI] -public sealed record Purchase +public sealed record Purchase : PurchaseBase { - /// The id of the purchase. - public Guid Id { get; set; } - - /// The point in time when the purchase was created. - public Instant CreatedAt { get; set; } - - /// The id of the owner of the purchase. - public Guid OwnerId { get; set; } - - /// The id of the user that created this purchase. - public Guid CreatedByUserId { get; set; } - - /// The point in time when the purchase was last modified. - public Instant ModifiedAt { get; set; } - - /// The id of the user that last modified this purchase. - public Guid ModifiedByUserId { get; set; } - - /// The id of transaction this purchase is a part of. - /// - public Guid TransactionId { get; set; } - - /// The amount paid to purchase an of . - public decimal Price { get; set; } - - /// The id of the currency of . - /// - public Guid CurrencyId { get; set; } - - /// The id of the purchased product. - /// - public Guid ProductId { get; set; } - - /// The amount of that was purchased. - public decimal Amount { get; set; } - - /// The date when the was delivered. + /// The date when the was delivered. public Instant? DeliveryDate { get; set; } - - /// The order of the purchase within a transaction. - public uint? Order { get; set; } - - /// The ids of the projects that this purchase is a part of. - public List ProjectIds { get; set; } = null!; } diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PurchaseBase.cs b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseBase.cs new file mode 100644 index 000000000..b728a7cb4 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseBase.cs @@ -0,0 +1,59 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using Gnomeshade.WebApi.Models.Accounts; +using Gnomeshade.WebApi.Models.Products; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Base model for purchases. +public abstract record PurchaseBase +{ + /// The id of the purchase. + public Guid Id { get; set; } + + /// The point in time when the purchase was created. + public Instant CreatedAt { get; set; } + + /// The id of the owner of the purchase. + public Guid OwnerId { get; set; } + + /// The id of the user that created this purchase. + public Guid CreatedByUserId { get; set; } + + /// The point in time when the purchase was last modified. + public Instant ModifiedAt { get; set; } + + /// The id of the user that last modified this purchase. + public Guid ModifiedByUserId { get; set; } + + /// The order of the purchase within a transaction. + public uint? Order { get; set; } + + /// The id of transaction this purchase is a part of. + /// + public Guid TransactionId { get; set; } + + /// The amount paid to purchase an of . + public decimal Price { get; set; } + + /// The id of the currency of . + /// + public Guid CurrencyId { get; set; } + + /// The id of the purchased product. + /// + public Guid ProductId { get; set; } + + /// The amount of that was purchased. + public decimal Amount { get; set; } + + /// The ids of the projects that this purchase is a part of. + public List ProjectIds { get; set; } = null!; +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreation.cs index 7043cd8b5..164d2ca1d 100644 --- a/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreation.cs +++ b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreation.cs @@ -2,9 +2,6 @@ // Licensed under the GNU Affero General Public License v3.0 or later. // See LICENSE.txt file in the project root for full license information. -using System; -using System.ComponentModel.DataAnnotations; - using JetBrains.Annotations; using NodaTime; @@ -14,31 +11,8 @@ namespace Gnomeshade.WebApi.Models.Transactions; /// Information needed to create a purchase. /// [PublicAPI] -public sealed record PurchaseCreation : TransactionItemCreation +public sealed record PurchaseCreation : PurchaseCreationBase { - /// - [Required] - public override Guid? TransactionId { get; set; } - - /// - [Required] - public decimal? Price { get; set; } - - /// - [Required] - public Guid? CurrencyId { get; set; } - - /// - [Required] - public Guid? ProductId { get; set; } - - /// - [Required] - public decimal? Amount { get; set; } - /// public Instant? DeliveryDate { get; set; } - - /// - public uint? Order { get; set; } } diff --git a/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreationBase.cs b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreationBase.cs new file mode 100644 index 000000000..57e6a22e8 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/PurchaseCreationBase.cs @@ -0,0 +1,36 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create a purchase. +/// +public abstract record PurchaseCreationBase : TransactionItemCreation +{ + /// + [Required] + public override Guid? TransactionId { get; set; } + + /// + [Required] + public decimal? Price { get; set; } + + /// + [Required] + public Guid? CurrencyId { get; set; } + + /// + [Required] + public Guid? ProductId { get; set; } + + /// + [Required] + public decimal? Amount { get; set; } + + /// + public uint? Order { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/Transaction.cs b/source/Gnomeshade.WebApi.Models/Transactions/Transaction.cs index 942212d7e..210978254 100644 --- a/source/Gnomeshade.WebApi.Models/Transactions/Transaction.cs +++ b/source/Gnomeshade.WebApi.Models/Transactions/Transaction.cs @@ -12,44 +12,26 @@ namespace Gnomeshade.WebApi.Models.Transactions; /// A financial transaction during which funds can be transferred between multiple accounts. [PublicAPI] -public record Transaction +public record Transaction : TransactionBase { - /// The id of the transaction. - public Guid Id { get; set; } - - /// The id of the owner of the transaction. - public Guid OwnerId { get; set; } - - /// The point in time when the transaction was created. - public Instant CreatedAt { get; set; } - - /// The id of the user that created this transaction. - public Guid CreatedByUserId { get; set; } - - /// The point in the when the transaction was last modified. - public Instant ModifiedAt { get; set; } - - /// The id of the user that last modified this transaction. - public Guid ModifiedByUserId { get; set; } - /// The description of the transaction. public string? Description { get; set; } /// The point in time when this transaction was imported. public Instant? ImportedAt { get; set; } - /// Whether or not this transaction was imported. + /// Whether this transaction was imported. public bool Imported => ImportedAt.HasValue; /// The point in time when this transaction was reconciled. public Instant? ReconciledAt { get; set; } - /// Whether or not this transaction was reconciled. + /// Whether this transaction was reconciled. public bool Reconciled => ReconciledAt.HasValue; /// The id of the transaction that refunds this one. public Guid? RefundedBy { get; set; } - /// Whether or not this transaction was refunded. + /// Whether this transaction was refunded. public bool Refunded => RefundedBy.HasValue; } diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransactionBase.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransactionBase.cs new file mode 100644 index 000000000..9dd6bc1dd --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransactionBase.cs @@ -0,0 +1,34 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Base model for transactions. +public abstract record TransactionBase +{ + /// The id of the transaction. + public Guid Id { get; set; } + + /// The id of the owner of the transaction. + public Guid OwnerId { get; set; } + + /// The point in time when the transaction was created. + public Instant CreatedAt { get; set; } + + /// The id of the user that created this transaction. + public Guid CreatedByUserId { get; set; } + + /// The point in time when the transaction was last modified. + public Instant ModifiedAt { get; set; } + + /// The id of the user that last modified this transaction. + public Guid ModifiedByUserId { get; set; } + + /// Whether this transaction is planned or not. + public bool Planned { get; set; } // todo is this needed? +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransactionSchedule.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransactionSchedule.cs new file mode 100644 index 000000000..48a523fe4 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransactionSchedule.cs @@ -0,0 +1,47 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using JetBrains.Annotations; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// A schedule for planned transactions. +/// +[PublicAPI] +public sealed record TransactionSchedule +{ + /// The id of the transaction. + public Guid Id { get; set; } + + /// The id of the owner of the transaction. + public Guid OwnerId { get; set; } + + /// The point in time when the transaction was created. + public Instant CreatedAt { get; set; } + + /// The id of the user that created this transaction. + public Guid CreatedByUserId { get; set; } + + /// The point in time when the transaction was last modified. + public Instant ModifiedAt { get; set; } + + /// The id of the user that last modified this transaction. + public Guid ModifiedByUserId { get; set; } + + /// The name of the planned transaction schedule. + public string Name { get; set; } = null!; + + /// The point in time when the first planned transaction will be booked. + public Instant StartingAt { get; set; } + + /// The period between each repeated planned transaction. + public Period Period { get; set; } = null!; + + /// The number of planned transactions to repeat. + public int Count { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransactionScheduleCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransactionScheduleCreation.cs new file mode 100644 index 000000000..66a169f9b --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransactionScheduleCreation.cs @@ -0,0 +1,33 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System.ComponentModel.DataAnnotations; + +using JetBrains.Annotations; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create a transaction schedule. +/// +[PublicAPI] +public sealed record TransactionScheduleCreation : Creation +{ + /// + [Required] + public string Name { get; set; } = null!; + + /// + [Required] + public Instant StartingAt { get; set; } + + /// + [Required] + public Period Period { get; set; } = null!; + + /// + [Required] + public int Count { get; set; } +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/Transfer.cs b/source/Gnomeshade.WebApi.Models/Transactions/Transfer.cs index 98e4c83f8..0fb2adcc4 100644 --- a/source/Gnomeshade.WebApi.Models/Transactions/Transfer.cs +++ b/source/Gnomeshade.WebApi.Models/Transactions/Transfer.cs @@ -15,40 +15,12 @@ namespace Gnomeshade.WebApi.Models.Transactions; /// A transfer between two accounts. /// [PublicAPI] -public sealed record Transfer +public sealed record Transfer : TransferBase { - /// The id of the transfer. - public Guid Id { get; set; } - - /// The point in time when the transfer was created. - public Instant CreatedAt { get; set; } - - /// The id of the owner of the transfer. - public Guid OwnerId { get; set; } - - /// The id of the user that created this transfer. - public Guid CreatedByUserId { get; set; } - - /// The point in the when the transfer was last modified. - public Instant ModifiedAt { get; set; } - - /// The id of the user that last modified this transfer. - public Guid ModifiedByUserId { get; set; } - - /// The id of transaction this transfer is a part of. - /// - public Guid TransactionId { get; set; } - - /// The amount withdrawn from the source account. - public decimal SourceAmount { get; set; } - /// The id of the account from which currency is withdrawn from. /// public Guid SourceAccountId { get; set; } - /// The amount deposited in the target account. - public decimal TargetAmount { get; set; } - /// The id of the account to which currency is deposited to. /// public Guid TargetAccountId { get; set; } @@ -62,12 +34,6 @@ public sealed record Transfer /// The reference id issued by the user. public string? InternalReference { get; set; } - /// The order of the transfer within a transaction. - public uint? Order { get; set; } - - /// The point in time when this transfer was posted to an account on the account servicer accounting books. - public Instant? BookedAt { get; set; } - /// The point in time when assets become available in case of deposit, or when assets cease to be available in case of withdrawal. public Instant? ValuedAt { get; set; } } diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransferBase.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransferBase.cs new file mode 100644 index 000000000..5f5353422 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransferBase.cs @@ -0,0 +1,47 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Base model for transfers. +public abstract record TransferBase +{ + /// The id of the transfer. + public Guid Id { get; set; } + + /// The point in time when the transfer was created. + public Instant CreatedAt { get; set; } + + /// The id of the owner of the transfer. + public Guid OwnerId { get; set; } + + /// The id of the user that created this transfer. + public Guid CreatedByUserId { get; set; } + + /// The point in time when the transfer was last modified. + public Instant ModifiedAt { get; set; } + + /// The id of the user that last modified this transfer. + public Guid ModifiedByUserId { get; set; } + + /// The id of transaction this transfer is a part of. + /// + public Guid TransactionId { get; set; } + + /// The amount withdrawn from the source account. + public decimal SourceAmount { get; set; } + + /// The amount deposited in the target account. + public decimal TargetAmount { get; set; } + + /// The order of the transfer within a transaction. + public uint? Order { get; set; } + + /// The point in time when this transfer was posted to an account on the account servicer accounting books. + public Instant? BookedAt { get; set; } // todo LocalTime ? +} diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransferCreation.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransferCreation.cs index ca1dbde22..62f3bd914 100644 --- a/source/Gnomeshade.WebApi.Models/Transactions/TransferCreation.cs +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransferCreation.cs @@ -14,27 +14,15 @@ namespace Gnomeshade.WebApi.Models.Transactions; /// Information needed to create a transfer. /// [PublicAPI] -public sealed record TransferCreation : TransactionItemCreation +public sealed record TransferCreation : TransferCreationBase { - /// - [Required] - public override Guid? TransactionId { get; set; } - - /// - [Required] - public decimal? SourceAmount { get; set; } - /// [Required] - public Guid? SourceAccountId { get; set; } - - /// - [Required] - public decimal? TargetAmount { get; set; } + public override Guid? SourceAccountId { get; set; } /// [Required] - public Guid? TargetAccountId { get; set; } + public override Guid? TargetAccountId { get; set; } /// public string? BankReference { get; set; } @@ -45,12 +33,9 @@ public sealed record TransferCreation : TransactionItemCreation /// public string? InternalReference { get; set; } - /// - public uint? Order { get; set; } - - /// + /// [RequiredIfNull(nameof(ValuedAt))] - public Instant? BookedAt { get; set; } + public override Instant? BookedAt { get; set; } /// [RequiredIfNull(nameof(BookedAt))] diff --git a/source/Gnomeshade.WebApi.Models/Transactions/TransferCreationBase.cs b/source/Gnomeshade.WebApi.Models/Transactions/TransferCreationBase.cs new file mode 100644 index 000000000..22059ef57 --- /dev/null +++ b/source/Gnomeshade.WebApi.Models/Transactions/TransferCreationBase.cs @@ -0,0 +1,41 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.ComponentModel.DataAnnotations; + +using NodaTime; + +namespace Gnomeshade.WebApi.Models.Transactions; + +/// Information needed to create as transfer. +/// +public abstract record TransferCreationBase : TransactionItemCreation +{ + /// + [Required] + public override Guid? TransactionId { get; set; } + + /// + [Required] + public decimal? SourceAmount { get; set; } + + /// + [Required] + public abstract Guid? SourceAccountId { get; set; } + + /// + [Required] + public decimal? TargetAmount { get; set; } + + /// + [Required] + public abstract Guid? TargetAccountId { get; set; } + + /// + public uint? Order { get; set; } + + /// + public abstract Instant? BookedAt { get; set; } +} diff --git a/source/Gnomeshade.WebApi/V1/Controllers/PlannedPurchasesController.cs b/source/Gnomeshade.WebApi/V1/Controllers/PlannedPurchasesController.cs new file mode 100644 index 000000000..2a5d58d31 --- /dev/null +++ b/source/Gnomeshade.WebApi/V1/Controllers/PlannedPurchasesController.cs @@ -0,0 +1,57 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AutoMapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Repositories; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using Microsoft.AspNetCore.Mvc; + +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace Gnomeshade.WebApi.V1.Controllers; + +/// CRUD operations on purchase entity. +[Route("api/v{version:apiVersion}/Purchases/Planned")] +public sealed class PlannedPurchasesController(Mapper mapper, PlannedPurchaseRepository repository, DbConnection dbConnection, TransactionRepository transactionRepository) + : TransactionItemController(mapper, repository, dbConnection, transactionRepository) +{ + /// + /// Successfully got the purchase. + /// Planned purchase with the specified id does not exist. + [ProducesResponseType(Status200OK)] + public override Task> Get(Guid id, CancellationToken cancellationToken) => + base.Get(id, cancellationToken); + + /// + /// Successfully got all purchases. + [ProducesResponseType>(Status200OK)] + public override Task> Get(CancellationToken cancellationToken) => + base.Get(cancellationToken); + + /// + /// A new purchase was created. + /// An existing purchase was replaced. + /// The specified purchase does not exist. + public override Task Put(Guid id, [FromBody] PlannedPurchaseCreation product) => + base.Put(id, product); + + // ReSharper disable once RedundantOverriddenMember + + /// + /// Planned purchase was successfully deleted. + /// Planned purchase with the specified id does not exist. + /// Planned purchase cannot be deleted because some other entity is still referencing it. + public override Task Delete(Guid id) => + base.Delete(id); +} diff --git a/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransactionsController.cs b/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransactionsController.cs new file mode 100644 index 000000000..facf758d6 --- /dev/null +++ b/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransactionsController.cs @@ -0,0 +1,80 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AutoMapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Repositories; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Gnomeshade.WebApi.V1.Controllers; + +/// CRUD operations on planned transaction entity. +[Route("api/v{version:apiVersion}/Transactions/Planned")] +public sealed class PlannedTransactionsController(Mapper mapper, PlannedTransactionRepository repository, DbConnection dbConnection) + : CreatableBase(mapper, repository, dbConnection) +{ + /// + /// Successfully got the planned transaction. + /// Planned transaction with the specified id does not exist. + [ProducesResponseType(StatusCodes.Status200OK)] + public override Task> Get(Guid id, CancellationToken cancellationToken) => + base.Get(id, cancellationToken); + + /// + /// Successfully got all planned transactions. + [ProducesResponseType>(StatusCodes.Status200OK)] + public override Task> Get(CancellationToken cancellationToken) => + base.Get(cancellationToken); + + /// + /// A new planned transaction was created. + /// A planned transaction with the specified name already exists. + public override Task Post(PlannedTransactionCreation transaction) => + base.Post(transaction); + + /// + /// A new planned transaction was created. + /// An existing planned transaction was replaced. + /// A planned transaction with the specified name already exists. + public override Task Put(Guid id, PlannedTransactionCreation transaction) => + base.Put(id, transaction); + + // ReSharper disable once RedundantOverriddenMember + + /// + /// Planned transaction was successfully deleted. + /// Planned transaction with the specified id does not exist. + /// Planned transaction cannot be deleted because some other entity is still referencing it. + public override Task Delete(Guid id) => + base.Delete(id); + + /// + protected override Task UpdateExistingAsync( + Guid id, + PlannedTransactionCreation creation, + UserEntity user) + { + throw new NotImplementedException(); + } + + /// + protected override Task CreateNewAsync( + Guid id, + PlannedTransactionCreation creation, + UserEntity user) + { + throw new NotImplementedException(); + } +} diff --git a/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransfersController.cs b/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransfersController.cs new file mode 100644 index 000000000..2d5d248d3 --- /dev/null +++ b/source/Gnomeshade.WebApi/V1/Controllers/PlannedTransfersController.cs @@ -0,0 +1,57 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AutoMapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Repositories; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Transactions; + +using Microsoft.AspNetCore.Mvc; + +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace Gnomeshade.WebApi.V1.Controllers; + +/// CRUD operations on planned transfer entity. +[Route("api/v{version:apiVersion}/Transfers/Planned")] +public sealed class PlannedTransfersController(Mapper mapper, PlannedTransferRepository repository, DbConnection dbConnection, TransactionRepository transactionRepository) + : TransactionItemController(mapper, repository, dbConnection, transactionRepository) +{ + /// + /// Successfully got the planned transfer. + /// Planned transfer with the specified id does not exist. + [ProducesResponseType(Status200OK)] + public override Task> Get(Guid id, CancellationToken cancellationToken) => + base.Get(id, cancellationToken); + + /// + /// Successfully got all planned transfers. + [ProducesResponseType>(Status200OK)] + public override Task> Get(CancellationToken cancellationToken) => + base.Get(cancellationToken); + + /// + /// A new planned transfer was created. + /// An existing planned transfer was replaced. + /// The specified planned transfer does not exist. + public override Task Put(Guid id, [FromBody] PlannedTransferCreation product) => + base.Put(id, product); + + // ReSharper disable once RedundantOverriddenMember + + /// + /// Planned transfer was successfully deleted. + /// Planned transfer with the specified id does not exist. + /// Planned transfer cannot be deleted because some other entity is still referencing it. + public override Task Delete(Guid id) => + base.Delete(id); +} diff --git a/source/Gnomeshade.WebApi/V1/Controllers/PurchasesController.cs b/source/Gnomeshade.WebApi/V1/Controllers/PurchasesController.cs index 6ba1f2518..b8a76fb1c 100644 --- a/source/Gnomeshade.WebApi/V1/Controllers/PurchasesController.cs +++ b/source/Gnomeshade.WebApi/V1/Controllers/PurchasesController.cs @@ -41,7 +41,7 @@ public override Task> Get(CancellationToken cancellationToken) => /// /// A new purchase was created. /// An existing purchase was replaced. - /// The specified transaction does not exist. + /// The specified purchase does not exist. public override Task Put(Guid id, [FromBody] PurchaseCreation product) => base.Put(id, product); diff --git a/source/Gnomeshade.WebApi/V1/Controllers/TransactionItemController.cs b/source/Gnomeshade.WebApi/V1/Controllers/TransactionItemController.cs index 7381f0da1..a783c16e9 100644 --- a/source/Gnomeshade.WebApi/V1/Controllers/TransactionItemController.cs +++ b/source/Gnomeshade.WebApi/V1/Controllers/TransactionItemController.cs @@ -111,8 +111,8 @@ protected sealed override async Task CreateNewAsync( return null; } - return await _transactionRepository.FindByIdAsync(transactionId, dbTransaction) is null - ? NotFound() - : StatusCode(Status403Forbidden); + return await _transactionRepository.ExistsAsync(transactionId, dbTransaction) + ? StatusCode(Status403Forbidden) + : NotFound(); } } diff --git a/source/Gnomeshade.WebApi/V1/Controllers/TransactionSchedulesController.cs b/source/Gnomeshade.WebApi/V1/Controllers/TransactionSchedulesController.cs new file mode 100644 index 000000000..95883490e --- /dev/null +++ b/source/Gnomeshade.WebApi/V1/Controllers/TransactionSchedulesController.cs @@ -0,0 +1,130 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AutoMapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Repositories; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Projects; +using Gnomeshade.WebApi.Models.Transactions; + +using Microsoft.AspNetCore.Mvc; + +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace Gnomeshade.WebApi.V1.Controllers; + +/// CRUD operations on transaction schedule entity. +[Route("api/v{version:apiVersion}/Transactions/Schedules")] +public sealed class TransactionSchedulesController + : CreatableBase +{ + /// Initializes a new instance of the class. + /// Repository entity and API model mapper. + /// The repository for performing CRUD operations on . + /// Database connection for transaction management. + public TransactionSchedulesController( + Mapper mapper, + TransactionScheduleRepository repository, + DbConnection dbConnection) + : base(mapper, repository, dbConnection) + { + } + + /// + /// Successfully got the transaction schedule. + /// Project with the specified id does not exist. + [ProducesResponseType(Status200OK)] + public override Task> Get(Guid id, CancellationToken cancellationToken) => + base.Get(id, cancellationToken); + + /// + /// Successfully got all transaction schedules. + [ProducesResponseType>(Status200OK)] + public override Task> Get(CancellationToken cancellationToken) => + base.Get(cancellationToken); + + /// + /// A new transaction schedule was created. + /// A transaction schedule with the specified name already exists. + public override Task Post(TransactionScheduleCreation schedule) => + base.Post(schedule); + + /// + /// A new transaction schedule was created. + /// An existing transaction schedule was replaced. + /// A transaction schedule with the specified name already exists. + public override Task Put(Guid id, [FromBody] TransactionScheduleCreation schedule) => + base.Put(id, schedule); + + // ReSharper disable once RedundantOverriddenMember + + /// + /// Transaction schedule was successfully deleted. + /// Transaction schedule with the specified id does not exist. + /// Transaction schedule cannot be deleted because some other entity is still referencing it. + public override Task Delete(Guid id) + => base.Delete(id); + + /// + protected override async Task UpdateExistingAsync( + Guid id, + TransactionScheduleCreation creation, + UserEntity user) + { + var schedule = new TransactionScheduleEntity + { + Id = id, + OwnerId = creation.OwnerId!.Value, + ModifiedByUserId = user.Id, + Name = creation.Name, + StartingAt = creation.StartingAt, + Period = creation.Period, + Count = creation.Count, + }; + + return await Repository.UpdateAsync(schedule) switch + { + 1 => NoContent(), + _ => StatusCode(Status403Forbidden), + }; + + // todo planned transactions + } + + /// + protected override async Task CreateNewAsync(Guid id, TransactionScheduleCreation creation, UserEntity user) + { + var conflictingSchedule = await Repository.FindByNameAsync(creation.Name, user.Id); + if (conflictingSchedule is not null) + { + return Problem( + "Schedule with the specified name already exists", + Url.Action(nameof(Get), new { conflictingSchedule.Id }), + Status409Conflict); + } + + var schedule = new TransactionScheduleEntity + { + Id = id, + OwnerId = creation.OwnerId!.Value, + CreatedByUserId = user.Id, + ModifiedByUserId = user.Id, + Name = creation.Name, + StartingAt = creation.StartingAt, + Period = creation.Period, + Count = creation.Count, + }; + + _ = await Repository.AddAsync(schedule); + return CreatedAtAction(nameof(Get), new { id }, id); + } +} diff --git a/source/Gnomeshade.WebApi/V1/Controllers/TransactionsController.cs b/source/Gnomeshade.WebApi/V1/Controllers/TransactionsController.cs index c84a1f4b1..cd098bb51 100644 --- a/source/Gnomeshade.WebApi/V1/Controllers/TransactionsController.cs +++ b/source/Gnomeshade.WebApi/V1/Controllers/TransactionsController.cs @@ -271,6 +271,32 @@ public async Task Merge(Guid targetId, [FromQuery, MinLength(1)] G await using var dbTransaction = await DbConnection.OpenAndBeginTransaction(); var userId = ApplicationUser.Id; + var tasks = new Task[sourceIds.Length]; + for (var i = 0; i < sourceIds.Length; i++) + { + tasks[i] = Repository.GetByIdAsync(sourceIds[i], userId, dbTransaction); + } + + var sourceTransactions = await Task.WhenAll(tasks); + foreach (var transaction in sourceTransactions) + { + if (transaction.Planned) + { + ModelState.AddModelError(nameof(sourceIds), $"Source transaction {transaction.Id} is planned; only {nameof(targetId)} can be planned"); + } + } + + if (!ModelState.IsValid) + { + return BadRequest(); + } + + var targetTransaction = await Repository.GetByIdAsync(targetId, userId, dbTransaction); + if (targetTransaction.Planned) + { + throw new NotImplementedException("Planned transaction merging is not supported"); + } + foreach (var sourceId in sourceIds) { await Repository.MergeAsync(targetId, sourceId, userId, dbTransaction); diff --git a/source/Gnomeshade.WebApi/V1/Controllers/TransfersController.cs b/source/Gnomeshade.WebApi/V1/Controllers/TransfersController.cs index 7fa73df37..3712b71c2 100644 --- a/source/Gnomeshade.WebApi/V1/Controllers/TransfersController.cs +++ b/source/Gnomeshade.WebApi/V1/Controllers/TransfersController.cs @@ -41,7 +41,7 @@ public override Task> Get(CancellationToken cancellationToken) => /// /// A new transfer was created. /// An existing transfer was replaced. - /// The specified transaction does not exist. + /// The specified transfer does not exist. public override Task Put(Guid id, [FromBody] TransferCreation product) => base.Put(id, product); diff --git a/source/Gnomeshade.WebApi/V1/CreatableBase.cs b/source/Gnomeshade.WebApi/V1/CreatableBase.cs index 7ae2acc01..5099e78d5 100644 --- a/source/Gnomeshade.WebApi/V1/CreatableBase.cs +++ b/source/Gnomeshade.WebApi/V1/CreatableBase.cs @@ -113,8 +113,7 @@ public virtual async Task Put(Guid id, [FromBody] TCreation creati return result; } - var conflictingEntity = await Repository.FindByIdAsync(id, dbTransaction); - if (conflictingEntity is null) + if (!await Repository.ExistsAsync(id, dbTransaction)) { var result = await CreateNewAsync(id, creation, user, dbTransaction); await dbTransaction.CommitAsync(); diff --git a/source/Gnomeshade.WebApi/V2/Controllers/PlannedLoanPaymentsController.cs b/source/Gnomeshade.WebApi/V2/Controllers/PlannedLoanPaymentsController.cs new file mode 100644 index 000000000..4aa62dc20 --- /dev/null +++ b/source/Gnomeshade.WebApi/V2/Controllers/PlannedLoanPaymentsController.cs @@ -0,0 +1,92 @@ +// Copyright 2021 Valters Melnalksnis +// Licensed under the GNU Affero General Public License v3.0 or later. +// See LICENSE.txt file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +using AutoMapper; + +using Gnomeshade.Data.Entities; +using Gnomeshade.Data.Repositories; +using Gnomeshade.WebApi.Client; +using Gnomeshade.WebApi.Models.Loans; +using Gnomeshade.WebApi.V1; + +using Microsoft.AspNetCore.Mvc; + +using static Microsoft.AspNetCore.Http.StatusCodes; + +namespace Gnomeshade.WebApi.V2.Controllers; + +/// CRUD operations on planned loan payment entity. +[Route("api/v{version:apiVersion}/LoanPayments/Planned")] +public sealed class PlannedLoanPaymentsController(Mapper mapper, LoanPaymentRepository repository, DbConnection dbConnection) + : CreatableBase(mapper, repository, dbConnection) +{ + /// + /// Successfully got the planned loan payment. + /// Planned loan payment with the specified id does not exist. + [ProducesResponseType(Status200OK)] + public override Task> Get(Guid id, CancellationToken cancellationToken) => + base.Get(id, cancellationToken); + + /// + /// Successfully got all loans payments. + [ProducesResponseType(Status200OK)] + public override Task> Get(CancellationToken cancellationToken) => + base.Get(cancellationToken); + + /// + /// A new loan payments was created. + public override Task Post(LoanPaymentCreation loanPayment) => + base.Post(loanPayment); + + /// + /// A new planned loan payment was created. + /// An existing planned loan payment was replaced. + public override Task Put(Guid id, LoanPaymentCreation loanPayment) => + base.Put(id, loanPayment); + + // ReSharper disable once RedundantOverriddenMember + + /// + /// Planned loan payment was successfully deleted. + /// Planned loan payment with the specified id does not exist. + /// Planned loan payment cannot be deleted because some other entity is still referencing it. + public override Task Delete(Guid id) => + base.Delete(id); + + /// + protected override async Task UpdateExistingAsync(Guid id, LoanPaymentCreation creation, UserEntity user) + { + var payment = Mapper.Map(creation) with + { + Id = id, + ModifiedByUserId = user.Id, + }; + + return await Repository.UpdateAsync(payment) switch + { + 1 => NoContent(), + _ => StatusCode(Status403Forbidden), + }; + } + + /// + protected override async Task CreateNewAsync(Guid id, LoanPaymentCreation creation, UserEntity user) + { + var loan = Mapper.Map(creation) with + { + Id = id, + CreatedByUserId = user.Id, + ModifiedByUserId = user.Id, + }; + + await Repository.AddAsync(loan); + return CreatedAtAction(nameof(Get), new { id }, id); + } +} diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/Accounts/AccountViewModelTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/Accounts/AccountViewModelTests.cs index aa122a91e..53dca5522 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/Accounts/AccountViewModelTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/Accounts/AccountViewModelTests.cs @@ -17,6 +17,6 @@ public async Task DataGridView_ShouldBeGrouped() var viewModel = new AccountViewModel(new StubbedActivityService(), new DesignTimeGnomeshadeClient()); await viewModel.RefreshAsync(); - viewModel.DataGridView.Groups.Should().ContainSingle(); + viewModel.DataGridView.Groups.Should().HaveCount(2); } } diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/NodaTimeValueConverterTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/NodaTimeValueConverterTests.cs index 5a9c1b5d6..cd3e826da 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/NodaTimeValueConverterTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/NodaTimeValueConverterTests.cs @@ -16,7 +16,7 @@ namespace Gnomeshade.Avalonia.Core.Tests; -[TestOf(typeof(NodaTimeValueConverter<>))] +[TestOf(typeof(NodaTimeValueStructConverter<>))] public sealed class NodaTimeValueConverterTests { private PurchaseUpsertionViewModel _viewModel = null!; diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/Products/ProductViewModelTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/Products/ProductViewModelTests.cs index 74f107a5f..3cca3fb51 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/Products/ProductViewModelTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/Products/ProductViewModelTests.cs @@ -22,13 +22,16 @@ public async Task Product_SaveAsync_ShouldUpdateDataGridView() var viewModel = new ProductViewModel(new StubbedActivityService(), new DesignTimeGnomeshadeClient(), DateTimeZoneProviders.Tzdb); await viewModel.RefreshAsync(); - viewModel.Rows.Should().HaveCount(2); + viewModel.Rows.Should().HaveCount(3); var newProductName = Guid.NewGuid().ToString("N"); viewModel.Details.Name = newProductName; await viewModel.Details.SaveAsync(); - viewModel.Rows.Should().HaveCount(3).And.ContainSingle(product => product.Name == newProductName); + viewModel.Rows + .Should() + .HaveCount(4) + .And.ContainSingle(product => product.Name == newProductName); } [Test] diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/Reports/BalanceReportViewModelTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/Reports/BalanceReportViewModelTests.cs index 1d129240e..f05c1c523 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/Reports/BalanceReportViewModelTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/Reports/BalanceReportViewModelTests.cs @@ -8,6 +8,8 @@ using Gnomeshade.Avalonia.Core.DesignTime; using Gnomeshade.Avalonia.Core.Reports; +using LiveChartsCore.Defaults; + using NodaTime; namespace Gnomeshade.Avalonia.Core.Tests.Reports; @@ -40,19 +42,20 @@ public void ShouldDisplayCorrectData() _viewModel.Currencies.Should().HaveCount(2); _viewModel.SelectedCurrency.Should().BeNull(); - var point = _viewModel.Series.Should().ContainSingle().Which.Values.Should().ContainSingle().Subject; - - point.Open.Should().Be(0); - point.Low.Should().Be(0); - point.High.Should().Be(0); - point.Close.Should().Be(0); + var series = _viewModel.Series.Should().ContainSingle().Subject; + series + .Values.Should() + .HaveCount(14) + .And.Subject.First().Should() + .BeEquivalentTo(new FinancialPointI(0, 0, 0, 0)); } - _viewModel.SelectedAccounts.Add(_viewModel.UserAccounts.First()); + _viewModel.SelectedAccounts.Add(_viewModel.UserAccounts.Single(account => account.Name is "Cash")); using (new AssertionScope()) { - var point = _viewModel.Series.Should().ContainSingle().Which.Values.Should().ContainSingle().Subject; + var series = _viewModel.Series.Should().ContainSingle().Subject; + var point = series.Values.Should().ContainSingle().Subject; point.Open.Should().Be(0); point.Low.Should().Be(0); @@ -61,16 +64,16 @@ public void ShouldDisplayCorrectData() } _viewModel.SelectedAccounts.Clear(); - _viewModel.SelectedAccounts.Add(_viewModel.UserAccounts.Last()); + _viewModel.SelectedAccounts.Add(_viewModel.UserAccounts.Single(account => account.Name is "Spending")); using (new AssertionScope()) { - var point = _viewModel.Series.Should().ContainSingle().Which.Values.Should().ContainSingle().Subject; - - point.Open.Should().Be(0); - point.Low.Should().Be(-127.3); - point.High.Should().Be(0); - point.Close.Should().Be(-127.3); + var series = _viewModel.Series.Should().ContainSingle().Subject; + series + .Values.Should() + .HaveCount(14) + .And.Subject.First().Should() + .BeEquivalentTo(new FinancialPointI(0, 0, -127.3, -127.3)); } } } diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/Controls/TransactionFilterTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/Controls/TransactionFilterTests.cs index 9579a33e4..11ddfea3d 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/Controls/TransactionFilterTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/Controls/TransactionFilterTests.cs @@ -6,6 +6,7 @@ using Gnomeshade.Avalonia.Core.Transactions; using Gnomeshade.Avalonia.Core.Transactions.Controls; +using Gnomeshade.WebApi.Models.Loans; using NodaTime; @@ -41,7 +42,7 @@ public void Filter_ShouldBeFalseIfTransferSumIsZero() public void Filter_Loan() { var loanId = Guid.NewGuid(); - var overview = new TransactionOverview(Guid.Empty, null, null, null, [], [], [new() { LoanId = loanId }]); + var overview = new TransactionOverview(Guid.Empty, null, null, null, [], [], [new LoanPayment { LoanId = loanId }]); _filter.Filter(overview).Should().BeTrue("should display all rows without any filters"); diff --git a/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/TransactionViewModelTests.cs b/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/TransactionViewModelTests.cs index e24cbe14c..2352b3480 100644 --- a/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/TransactionViewModelTests.cs +++ b/tests/Gnomeshade.Avalonia.Core.Tests/Transactions/TransactionViewModelTests.cs @@ -14,7 +14,7 @@ namespace Gnomeshade.Avalonia.Core.Tests.Transactions; [TestOf(typeof(TransactionViewModel))] -public class TransactionViewModelTests +public sealed class TransactionViewModelTests { private TransactionViewModel _viewModel = null!; @@ -37,18 +37,22 @@ public void Details_ShouldBeUpdatedBySelected() using (new AssertionScope()) { _viewModel.Selected.Should().BeNull(); - _viewModel.Details.Properties.Description.Should().BeNull(); + _viewModel.Details.Should().BeOfType().Which.Properties.Description.Should().BeNull(); } - _viewModel.UpdateSelectedItems.Execute(new ArrayList { _viewModel.Rows.First() }); + _viewModel.UpdateSelectedItems.Execute(new ArrayList { _viewModel.Rows.First(transaction => !transaction.Projection) }); - _viewModel.Details.Should().NotBeNull(); - _viewModel.Details.Properties.Description.Should().NotBeNull(); + _viewModel.Details.Should().BeOfType().Which.Properties.Description.Should().NotBeNull(); + + _viewModel.Selected = _viewModel.Rows.First(transaction => transaction.Projection); + _viewModel.UpdateSelectedItems.Execute(new ArrayList { _viewModel.Rows.First(transaction => transaction.Projection) }); + + _viewModel.Details.Should().BeOfType(); _viewModel.Selected = null; _viewModel.UpdateSelectedItems.Execute(new ArrayList()); - _viewModel.Details.Properties.Description.Should().BeNull(); + _viewModel.Details.Should().BeOfType().Which.Properties.Description.Should().BeNull(); } [Test] diff --git a/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v1_swagger.json.verified.txt b/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v1_swagger.json.verified.txt index b5cc49546..53c3f3494 100644 --- a/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v1_swagger.json.verified.txt +++ b/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v1_swagger.json.verified.txt @@ -4621,17 +4621,17 @@ } } }, - /api/v1/Products/{id}: { + /api/v1/Purchases/Planned/{id}: { get: { tags: [ - Products + PlannedPurchases ], - summary: Gets the specified product., + summary: Gets the specified planned purchase., parameters: [ { name: id, in: path, - description: The id of the product to get., + description: The id of the planned purchase to get., required: true, schema: { type: string, @@ -4645,17 +4645,17 @@ content: { text/plain: { schema: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } }, application/json: { schema: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } }, text/json: { schema: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } } } @@ -4733,14 +4733,14 @@ }, put: { tags: [ - Products + PlannedPurchases ], - summary: Creates a new product or replaces an existing one if one exists with the specified id., + summary: Creates a new planned purchase or replaces an existing one, if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the product., + description: The id of the planned purchase., required: true, schema: { type: string, @@ -4749,21 +4749,20 @@ } ], requestBody: { - description: The product to create or replace., content: { application/json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } }, text/json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } } } @@ -4888,14 +4887,14 @@ }, delete: { tags: [ - Products + PlannedPurchases ], - summary: Deletes the entity., + summary: Deletes the specified planned purchase., parameters: [ { name: id, in: path, - description: The id of the entity to delete., + description: The id of the planned purchase to delete., required: true, schema: { type: string, @@ -4999,12 +4998,12 @@ } } }, - /api/v1/Products: { + /api/v1/Purchases/Planned: { get: { tags: [ - Products + PlannedPurchases ], - summary: Gets all products., + summary: Gets all planned purchases., responses: { 200: { description: OK, @@ -5013,7 +5012,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } } }, @@ -5021,7 +5020,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } } }, @@ -5029,7 +5028,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Product + $ref: #/components/schemas/PlannedPurchase } } } @@ -5045,7 +5044,7 @@ }, post: { tags: [ - Products + PlannedPurchases ], summary: Creates a new entity., requestBody: { @@ -5053,17 +5052,17 @@ content: { application/json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } }, text/json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/ProductCreation + $ref: #/components/schemas/PlannedPurchaseCreation } } } @@ -5121,137 +5120,17 @@ } } }, - /api/v1/Products/{id}/Purchases: { - get: { - tags: [ - Products - ], - summary: Gets all purchases of the specified product., - parameters: [ - { - name: id, - in: path, - description: The id of the product for which to get all the purchases., - required: true, - schema: { - type: string, - format: uuid - } - } - ], - responses: { - 200: { - description: OK, - content: { - text/plain: { - schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } - } - }, - application/json: { - schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } - } - }, - text/json: { - schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } - } - } - } - }, - 404: { - description: Not Found, - content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - } - } - }, - 400: { - description: Bad request, - content: { - application/problem+json: { - schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } - } - } - } - } - }, - 500: { - description: Internal Server Error - }, - 401: { - description: Unauthorized - } - } - } - }, - /api/v1/Projects/{id}: { + /api/v1/Transactions/Planned/{id}: { get: { tags: [ - Projects + PlannedTransactions ], - summary: Gets the project with the specified id., + summary: Gets the specified planned transaction., parameters: [ { name: id, in: path, - description: The id by which to search for the project., + description: The id of the planned transaction to get., required: true, schema: { type: string, @@ -5265,17 +5144,17 @@ content: { text/plain: { schema: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } }, application/json: { schema: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } }, text/json: { schema: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } } } @@ -5353,14 +5232,14 @@ }, put: { tags: [ - Projects + PlannedTransactions ], - summary: Creates a new project, or replaces and existing one if one exists with the specified id., + summary: Creates a new planned transaction or replaces an existing one, if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the project., + description: The id of the planned transaction., required: true, schema: { type: string, @@ -5369,20 +5248,21 @@ } ], requestBody: { + description: The planned transaction to create or replace., content: { application/json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } }, text/json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } } } @@ -5507,14 +5387,14 @@ }, delete: { tags: [ - Projects + PlannedTransactions ], - summary: Deletes the specified project., + summary: Deletes the specified planned transaction., parameters: [ { name: id, in: path, - description: The id of the project to delete., + description: The id of the planned transaction to delete., required: true, schema: { type: string, @@ -5618,12 +5498,12 @@ } } }, - /api/v1/Projects: { + /api/v1/Transactions/Planned: { get: { tags: [ - Projects + PlannedTransactions ], - summary: Gets all projects., + summary: Gets all planned transactions., responses: { 200: { description: OK, @@ -5632,7 +5512,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } } }, @@ -5640,7 +5520,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } } }, @@ -5648,7 +5528,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Project + $ref: #/components/schemas/PlannedTransaction } } } @@ -5664,25 +5544,25 @@ }, post: { tags: [ - Projects + PlannedTransactions ], - summary: Creates a new project., + summary: Creates a new planned transaction., requestBody: { - description: The project to create., + description: The planned transaction to create., content: { application/json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } }, text/json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/ProjectCreation + $ref: #/components/schemas/PlannedTransactionCreation } } } @@ -5740,17 +5620,17 @@ } } }, - /api/v1/Projects/{id}/Purchases: { + /api/v1/Transfers/Planned/{id}: { get: { tags: [ - Projects + PlannedTransfers ], - summary: Gets all purchases that are a part of the specified project., + summary: Gets the specified planned transfer., parameters: [ { name: id, in: path, - description: The id of the project for which to get all the purchases., + description: The id of the planned transfer to get., required: true, schema: { type: string, @@ -5764,26 +5644,17 @@ content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + $ref: #/components/schemas/PlannedTransfer } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + $ref: #/components/schemas/PlannedTransfer } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + $ref: #/components/schemas/PlannedTransfer } } } @@ -5858,29 +5729,17 @@ description: Unauthorized } } - } - }, - /api/v1/Projects/{id}/Purchases/{purchaseId}: { + }, put: { tags: [ - Projects + PlannedTransfers ], - summary: Adds the specified purchase to the specified project., + summary: Creates a new planned transfer or replaces an existing one, if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the project to which to add the purchase., - required: true, - schema: { - type: string, - format: uuid - } - }, - { - name: purchaseId, - in: path, - description: The id of the purchase which to add to the project., + description: The id of the planned transfer., required: true, schema: { type: string, @@ -5888,9 +5747,54 @@ } } ], + requestBody: { + content: { + application/json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + } + } + }, responses: { - 404: { - description: Not Found, + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 204: { + description: No Content + }, + 403: { + description: Forbidden, content: { text/plain: { schema: { @@ -5909,8 +5813,28 @@ } } }, - 400: { - description: Bad request, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, content: { application/problem+json: { schema: { @@ -5962,24 +5886,14 @@ }, delete: { tags: [ - Projects + PlannedTransfers ], - summary: Removes the specified purchase to the specified project., + summary: Deletes the specified planned transfer., parameters: [ { name: id, in: path, - description: The id of the project from which to remove the purchase., - required: true, - schema: { - type: string, - format: uuid - } - }, - { - name: purchaseId, - in: path, - description: The id of the purchase which to remove from the project., + description: The id of the planned transfer to delete., required: true, schema: { type: string, @@ -5988,6 +5902,9 @@ } ], responses: { + 204: { + description: No Content + }, 404: { description: Not Found, content: { @@ -6008,6 +5925,26 @@ } } }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, 400: { description: Bad request, content: { @@ -6060,17 +5997,139 @@ } } }, - /api/v1/Purchases/{id}: { + /api/v1/Transfers/Planned: { get: { tags: [ - Purchases + PlannedTransfers ], - summary: Gets the specified purchase., + summary: Gets all planned transfers., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/PlannedTransfer + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/PlannedTransfer + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/PlannedTransfer + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + PlannedTransfers + ], + summary: Creates a new entity., + requestBody: { + description: Information for creating the entity., + content: { + application/json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/PlannedTransferCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Products/{id}: { + get: { + tags: [ + Products + ], + summary: Gets the specified product., parameters: [ { name: id, in: path, - description: The id of the purchase to get., + description: The id of the product to get., required: true, schema: { type: string, @@ -6084,17 +6143,17 @@ content: { text/plain: { schema: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } }, application/json: { schema: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } }, text/json: { schema: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } } } @@ -6172,14 +6231,14 @@ }, put: { tags: [ - Purchases + Products ], - summary: Creates a new purchase or replaces an existing one, if one exists with the specified id., + summary: Creates a new product or replaces an existing one if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the purchase., + description: The id of the product., required: true, schema: { type: string, @@ -6188,20 +6247,21 @@ } ], requestBody: { + description: The product to create or replace., content: { application/json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } }, text/json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } } } @@ -6326,14 +6386,14 @@ }, delete: { tags: [ - Purchases + Products ], - summary: Deletes the specified purchase., + summary: Deletes the entity., parameters: [ { name: id, in: path, - description: The id of the purchase to delete., + description: The id of the entity to delete., required: true, schema: { type: string, @@ -6437,12 +6497,12 @@ } } }, - /api/v1/Purchases: { + /api/v1/Products: { get: { tags: [ - Purchases + Products ], - summary: Gets all purchases., + summary: Gets all products., responses: { 200: { description: OK, @@ -6451,7 +6511,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } } }, @@ -6459,7 +6519,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } } }, @@ -6467,7 +6527,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Purchase + $ref: #/components/schemas/Product } } } @@ -6483,7 +6543,7 @@ }, post: { tags: [ - Purchases + Products ], summary: Creates a new entity., requestBody: { @@ -6491,17 +6551,17 @@ content: { application/json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } }, text/json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/PurchaseCreation + $ref: #/components/schemas/ProductCreation } } } @@ -6559,21 +6619,33 @@ } } }, - /api/v1/Transactions: { + /api/v1/Products/{id}/Purchases: { get: { tags: [ - Transactions + Products + ], + summary: Gets all purchases of the specified product., + parameters: [ + { + name: id, + in: path, + description: The id of the product for which to get all the purchases., + required: true, + schema: { + type: string, + format: uuid + } + } ], - summary: Gets all transactions., responses: { 200: { - description: Successfully got all transactions., + description: OK, content: { text/plain: { schema: { type: array, items: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Purchase } } }, @@ -6581,7 +6653,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Purchase } } }, @@ -6589,85 +6661,71 @@ schema: { type: array, items: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Purchase } } } } }, - 500: { - description: Internal Server Error - }, - 401: { - description: Unauthorized - } - } - }, - post: { - tags: [ - Transactions - ], - summary: Creates a new transaction., - requestBody: { - description: Information for creating the transaction., - content: { - application/json: { - schema: { - $ref: #/components/schemas/TransactionCreation - } - }, - text/json: { - schema: { - $ref: #/components/schemas/TransactionCreation - } - }, - application/*+json: { - schema: { - $ref: #/components/schemas/TransactionCreation - } - } - } - }, - responses: { - 201: { - description: Created, + 404: { + description: Not Found, content: { text/plain: { schema: { - type: string, - format: uuid + $ref: #/components/schemas/ProblemDetails } }, application/json: { schema: { - type: string, - format: uuid + $ref: #/components/schemas/ProblemDetails } }, text/json: { schema: { - type: string, - format: uuid + $ref: #/components/schemas/ProblemDetails } } } }, - 409: { - description: Conflict, + 400: { + description: Bad request, content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { + application/problem+json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } } } } @@ -6681,17 +6739,17 @@ } } }, - /api/v1/Transactions/{id}: { + /api/v1/Projects/{id}: { get: { tags: [ - Transactions + Projects ], - summary: Gets the specified transaction., + summary: Gets the project with the specified id., parameters: [ { name: id, in: path, - description: The id of the transaction to get., + description: The id by which to search for the project., required: true, schema: { type: string, @@ -6705,17 +6763,17 @@ content: { text/plain: { schema: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Project } }, application/json: { schema: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Project } }, text/json: { schema: { - $ref: #/components/schemas/Transaction + $ref: #/components/schemas/Project } } } @@ -6793,14 +6851,14 @@ }, put: { tags: [ - Transactions + Projects ], - summary: Creates a new transaction or replaces an existing one, if one exists with the specified id., + summary: Creates a new project, or replaces and existing one if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the transaction., + description: The id of the project., required: true, schema: { type: string, @@ -6809,21 +6867,20 @@ } ], requestBody: { - description: The transaction to create or replace., content: { application/json: { schema: { - $ref: #/components/schemas/TransactionCreation + $ref: #/components/schemas/ProjectCreation } }, text/json: { schema: { - $ref: #/components/schemas/TransactionCreation + $ref: #/components/schemas/ProjectCreation } }, application/*+json: { schema: { - $ref: #/components/schemas/TransactionCreation + $ref: #/components/schemas/ProjectCreation } } } @@ -6948,14 +7005,14 @@ }, delete: { tags: [ - Transactions + Projects ], - summary: Deletes the specified transaction., + summary: Deletes the specified project., parameters: [ { name: id, in: path, - description: The id of the transaction to delete., + description: The id of the project to delete., required: true, schema: { type: string, @@ -7059,104 +7116,115 @@ } } }, - /api/v1/Transactions/{id}/Details: { + /api/v1/Projects: { get: { tags: [ - Transactions - ], - summary: Gets the specified transaction with all details., - parameters: [ - { - name: id, - in: path, - description: The id of the transaction to get., - required: true, - schema: { - type: string, - format: uuid - } - } + Projects ], + summary: Gets all projects., responses: { 200: { description: OK, content: { text/plain: { schema: { - $ref: #/components/schemas/DetailedTransaction + type: array, + items: { + $ref: #/components/schemas/Project + } } }, application/json: { schema: { - $ref: #/components/schemas/DetailedTransaction + type: array, + items: { + $ref: #/components/schemas/Project + } } }, text/json: { schema: { - $ref: #/components/schemas/DetailedTransaction + type: array, + items: { + $ref: #/components/schemas/Project + } } } } }, - 404: { - description: Not Found, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + Projects + ], + summary: Creates a new project., + requestBody: { + description: The project to create., + content: { + application/json: { + schema: { + $ref: #/components/schemas/ProjectCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProjectCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/ProjectCreation + } + } + } + }, + responses: { + 201: { + description: Created, content: { text/plain: { schema: { - $ref: #/components/schemas/ProblemDetails + type: string, + format: uuid } }, application/json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: string, + format: uuid } }, text/json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: string, + format: uuid } } } }, - 400: { - description: Bad request, + 409: { + description: Conflict, content: { - application/problem+json: { + text/plain: { schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } - } + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails } } } @@ -7167,43 +7235,36 @@ 401: { description: Unauthorized } - }, - deprecated: true + } } }, - /api/v1/Transactions/Details: { + /api/v1/Projects/{id}/Purchases: { get: { tags: [ - Transactions + Projects ], - summary: Gets all transactions., + summary: Gets all purchases that are a part of the specified project., parameters: [ { - name: From, - in: query, - description: The start of the date range., - schema: { - $ref: #/components/schemas/Instant - } - }, - { - name: To, - in: query, - description: The end of the date range., + name: id, + in: path, + description: The id of the project for which to get all the purchases., + required: true, schema: { - $ref: #/components/schemas/Instant + type: string, + format: uuid } } ], responses: { 200: { - description: Successfully got all transactions., + description: OK, content: { text/plain: { schema: { type: array, items: { - $ref: #/components/schemas/DetailedTransaction + $ref: #/components/schemas/Purchase } } }, @@ -7211,7 +7272,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/DetailedTransaction + $ref: #/components/schemas/Purchase } } }, @@ -7219,109 +7280,28 @@ schema: { type: array, items: { - $ref: #/components/schemas/DetailedTransaction - } - } - } - } - }, - 400: { - description: Bad request, - content: { - application/problem+json: { - schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } + $ref: #/components/schemas/Purchase } } } } }, - 500: { - description: Internal Server Error - }, - 401: { - description: Unauthorized - } - }, - deprecated: true - } - }, - /api/v1/Transactions/{transactionId}/Links: { - get: { - tags: [ - Transactions - ], - summary: Gets all links for the specified transaction., - parameters: [ - { - name: transactionId, - in: path, - description: The id of the transaction for which to get the links., - required: true, - schema: { - type: string, - format: uuid - } - } - ], - responses: { - 200: { - description: OK, + 404: { + description: Not Found, content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Link - } + $ref: #/components/schemas/ProblemDetails } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Link - } + $ref: #/components/schemas/ProblemDetails } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Link - } + $ref: #/components/schemas/ProblemDetails } } } @@ -7378,17 +7358,17 @@ } } }, - /api/v1/Transactions/{transactionId}/Links/{id}: { + /api/v1/Projects/{id}/Purchases/{purchaseId}: { put: { tags: [ - Transactions + Projects ], - summary: Adds the specified link to a transaction., + summary: Adds the specified purchase to the specified project., parameters: [ { - name: transactionId, + name: id, in: path, - description: The id of the transaction to which to add the link., + description: The id of the project to which to add the purchase., required: true, schema: { type: string, @@ -7396,8 +7376,9 @@ } }, { - name: id, + name: purchaseId, in: path, + description: The id of the purchase which to add to the project., required: true, schema: { type: string, @@ -7406,9 +7387,6 @@ } ], responses: { - 204: { - description: No Content - }, 404: { description: Not Found, content: { @@ -7482,14 +7460,14 @@ }, delete: { tags: [ - Transactions + Projects ], - summary: Removes the specified link from a transaction., + summary: Removes the specified purchase to the specified project., parameters: [ { - name: transactionId, + name: id, in: path, - description: The id of the transaction from which to remove the link., + description: The id of the project from which to remove the purchase., required: true, schema: { type: string, @@ -7497,8 +7475,9 @@ } }, { - name: id, + name: purchaseId, in: path, + description: The id of the purchase which to remove from the project., required: true, schema: { type: string, @@ -7507,9 +7486,6 @@ } ], responses: { - 204: { - description: No Content - }, 404: { description: Not Found, content: { @@ -7582,17 +7558,17 @@ } } }, - /api/v1/Transactions/{transactionId}/Transfers: { + /api/v1/Purchases/{id}: { get: { tags: [ - Transactions + Purchases ], - summary: Gets all transfers for the specified transaction., + summary: Gets the specified purchase., parameters: [ { - name: transactionId, + name: id, in: path, - description: The id of the transaction for which to get transfers., + description: The id of the purchase to get., required: true, schema: { type: string, @@ -7606,26 +7582,37 @@ content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transfer - } + $ref: #/components/schemas/Purchase } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transfer - } + $ref: #/components/schemas/Purchase } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transfer - } + $ref: #/components/schemas/Purchase + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails } } } @@ -7680,19 +7667,17 @@ description: Unauthorized } } - } - }, - /api/v1/Transactions/{transactionId}/Purchases: { - get: { + }, + put: { tags: [ - Transactions + Purchases ], - summary: Gets all purchases for the specified transaction., + summary: Creates a new purchase or replaces an existing one, if one exists with the specified id., parameters: [ { - name: transactionId, + name: id, in: path, - description: The id of the transaction for which to get all the purchases., + description: The id of the purchase., required: true, schema: { type: string, @@ -7700,37 +7685,93 @@ } } ], + requestBody: { + content: { + application/json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + } + } + }, responses: { - 200: { - description: OK, + 201: { + description: Created, content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + type: string, + format: uuid } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + type: string, + format: uuid } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Purchase - } + type: string, + format: uuid } } } }, - 400: { + 204: { + description: No Content + }, + 403: { + description: Forbidden, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { description: Bad request, content: { application/problem+json: { @@ -7780,19 +7821,17 @@ description: Unauthorized } } - } - }, - /api/v1/Transactions/{transactionId}/Loans: { - get: { + }, + delete: { tags: [ - Transactions + Purchases ], - summary: Gets all loans for the specified transaction., + summary: Deletes the specified purchase., parameters: [ { - name: transactionId, + name: id, in: path, - description: The id of the transaction for which to get all the loans., + description: The id of the purchase to delete., required: true, schema: { type: string, @@ -7801,31 +7840,45 @@ } ], responses: { - 200: { - description: OK, + 204: { + description: No Content + }, + 404: { + description: Not Found, content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Loan - } + $ref: #/components/schemas/ProblemDetails } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Loan - } + $ref: #/components/schemas/ProblemDetails } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Loan - } + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails } } } @@ -7879,36 +7932,24 @@ 401: { description: Unauthorized } - }, - deprecated: true + } } }, - /api/v1/Transactions/Loans: { + /api/v1/Purchases: { get: { tags: [ - Transactions - ], - summary: Gets all loans issued or received by the specified counterparty., - parameters: [ - { - name: counterpartyId, - in: query, - description: The id of the counterparty for which to get all the loans for., - schema: { - type: string, - format: uuid - } - } + Purchases ], + summary: Gets all purchases., responses: { 200: { - description: Successfully got all loans., + description: OK, content: { text/plain: { schema: { type: array, items: { - $ref: #/components/schemas/Loan + $ref: #/components/schemas/Purchase } } }, @@ -7916,7 +7957,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Loan + $ref: #/components/schemas/Purchase } } }, @@ -7924,51 +7965,85 @@ schema: { type: array, items: { - $ref: #/components/schemas/Loan + $ref: #/components/schemas/Purchase } } } } }, - 400: { - description: Bad request, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + Purchases + ], + summary: Creates a new entity., + requestBody: { + description: Information for creating the entity., + content: { + application/json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/PurchaseCreation + } + } + } + }, + responses: { + 201: { + description: Created, content: { - application/problem+json: { + text/plain: { schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } - } + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails } } } @@ -7979,83 +8054,40 @@ 401: { description: Unauthorized } - }, - deprecated: true + } } }, - /api/v1/Transactions/{targetId}/Merge: { - post: { + /api/v1/Transactions: { + get: { tags: [ Transactions ], - summary: Merges one transaction into another., - parameters: [ - { - name: targetId, - in: path, - description: The id of the transaction in to which to merge., - required: true, - schema: { - type: string, - format: uuid - } - }, - { - name: sourceIds, - in: query, - description: The ids of the transactions which to merge into the target transactions., - schema: { - minItems: 1, - type: array, - items: { - type: string, - format: uuid - } - } - } - ], + summary: Gets all transactions., responses: { - 204: { - description: No Content - }, - 400: { - description: Bad request, + 200: { + description: Successfully got all transactions., content: { - application/problem+json: { + text/plain: { schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } + type: array, + items: { + $ref: #/components/schemas/Transaction + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transaction + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transaction } } } @@ -8068,95 +8100,72 @@ description: Unauthorized } } - } - }, - /api/v1/Transactions/{id}/Related: { - get: { + }, + post: { tags: [ Transactions ], - summary: Gets all related transactions for the specified transaction., - parameters: [ - { - name: id, - in: path, - description: The id of the transaction for which to get related transactions., - required: true, - schema: { - type: string, - format: uuid + summary: Creates a new transaction., + requestBody: { + description: Information for creating the transaction., + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } } } - ], + }, responses: { - 200: { - description: OK, + 201: { + description: Created, content: { text/plain: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transaction - } + type: string, + format: uuid } }, application/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transaction - } + type: string, + format: uuid } }, text/json: { schema: { - type: array, - items: { - $ref: #/components/schemas/Transaction - } + type: string, + format: uuid } } } }, - 400: { - description: Bad request, + 409: { + description: Conflict, content: { - application/problem+json: { + text/plain: { schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } - } + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails } } } @@ -8170,27 +8179,17 @@ } } }, - /api/v1/Transactions/{id}/Related/{relatedId}: { - post: { + /api/v1/Transactions/{id}: { + get: { tags: [ Transactions ], - summary: Adds a related transaction., + summary: Gets the specified transaction., parameters: [ { name: id, in: path, - description: The id of the transaction to which to add the relation., - required: true, - schema: { - type: string, - format: uuid - } - }, - { - name: relatedId, - in: path, - description: The id of the related transaction., + description: The id of the transaction to get., required: true, schema: { type: string, @@ -8199,8 +8198,45 @@ } ], responses: { - 204: { - description: No Content + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/Transaction + } + }, + application/json: { + schema: { + $ref: #/components/schemas/Transaction + } + }, + text/json: { + schema: { + $ref: #/components/schemas/Transaction + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } }, 400: { description: Bad request, @@ -8253,26 +8289,16 @@ } } }, - delete: { + put: { tags: [ Transactions ], - summary: Removes a related transaction., + summary: Creates a new transaction or replaces an existing one, if one exists with the specified id., parameters: [ { name: id, in: path, - description: The id of the transaction from which to remove the relation., - required: true, - schema: { - type: string, - format: uuid - } - }, - { - name: relatedId, - in: path, - description: The id of the related transaction., + description: The id of the transaction., required: true, schema: { type: string, @@ -8280,103 +8306,75 @@ } } ], + requestBody: { + description: The transaction to create or replace., + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransactionCreation + } + } + } + }, responses: { - 204: { - description: No Content - }, - 400: { - description: Bad request, + 201: { + description: Created, content: { - application/problem+json: { + text/plain: { schema: { - type: object, - properties: { - type: { - type: string, - nullable: true - }, - title: { - type: string, - nullable: true - }, - status: { - type: integer, - format: int32, - default: 400, - nullable: true - }, - detail: { - type: string, - nullable: true - }, - instance: { - type: string, - nullable: true - }, - errors: { - type: object, - additionalProperties: { - type: array, - items: { - type: string - } - } - } - } + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid } } } }, - 500: { - description: Internal Server Error + 204: { + description: No Content }, - 401: { - description: Unauthorized - } - } - } - }, - /api/v1/Transfers/{id}: { - get: { - tags: [ - Transfers - ], - summary: Gets the specified transfer., - parameters: [ - { - name: id, - in: path, - description: The id of the transfer to get., - required: true, - schema: { - type: string, - format: uuid - } - } - ], - responses: { - 200: { - description: OK, + 403: { + description: Forbidden, content: { text/plain: { schema: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/ProblemDetails } }, application/json: { schema: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/ProblemDetails } }, text/json: { schema: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/ProblemDetails } } } }, - 404: { - description: Not Found, + 409: { + description: Conflict, content: { text/plain: { schema: { @@ -8446,16 +8444,16 @@ } } }, - put: { + delete: { tags: [ - Transfers + Transactions ], - summary: Creates a new transfer or replaces an existing one, if one exists with the specified id., + summary: Deletes the specified transaction., parameters: [ { name: id, in: path, - description: The id of the transfer., + description: The id of the transaction to delete., required: true, schema: { type: string, @@ -8463,54 +8461,12 @@ } } ], - requestBody: { - content: { - application/json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - }, - text/json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - }, - application/*+json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - } - } - }, responses: { - 201: { - description: Created, - content: { - text/plain: { - schema: { - type: string, - format: uuid - } - }, - application/json: { - schema: { - type: string, - format: uuid - } - }, - text/json: { - schema: { - type: string, - format: uuid - } - } - } - }, 204: { description: No Content }, - 403: { - description: Forbidden, + 404: { + description: Not Found, content: { text/plain: { schema: { @@ -8599,17 +8555,19 @@ description: Unauthorized } } - }, - delete: { + } + }, + /api/v1/Transactions/{id}/Details: { + get: { tags: [ - Transfers + Transactions ], - summary: Deletes the specified transfer., + summary: Gets the specified transaction with all details., parameters: [ { name: id, in: path, - description: The id of the transfer to delete., + description: The id of the transaction to get., required: true, schema: { type: string, @@ -8618,31 +8576,28 @@ } ], responses: { - 204: { - description: No Content - }, - 404: { - description: Not Found, + 200: { + description: OK, content: { text/plain: { schema: { - $ref: #/components/schemas/ProblemDetails + $ref: #/components/schemas/DetailedTransaction } }, application/json: { schema: { - $ref: #/components/schemas/ProblemDetails + $ref: #/components/schemas/DetailedTransaction } }, text/json: { schema: { - $ref: #/components/schemas/ProblemDetails + $ref: #/components/schemas/DetailedTransaction } } } }, - 409: { - description: Conflict, + 404: { + description: Not Found, content: { text/plain: { schema: { @@ -8710,24 +8665,43 @@ 401: { description: Unauthorized } - } + }, + deprecated: true } }, - /api/v1/Transfers: { + /api/v1/Transactions/Details: { get: { tags: [ - Transfers + Transactions + ], + summary: Gets all transactions., + parameters: [ + { + name: From, + in: query, + description: The start of the date range., + schema: { + $ref: #/components/schemas/Instant + } + }, + { + name: To, + in: query, + description: The end of the date range., + schema: { + $ref: #/components/schemas/Instant + } + } ], - summary: Gets all transfers., responses: { 200: { - description: OK, + description: Successfully got all transactions., content: { text/plain: { schema: { type: array, items: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/DetailedTransaction } } }, @@ -8735,7 +8709,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/DetailedTransaction } } }, @@ -8743,85 +8717,51 @@ schema: { type: array, items: { - $ref: #/components/schemas/Transfer + $ref: #/components/schemas/DetailedTransaction } } } } }, - 500: { - description: Internal Server Error - }, - 401: { - description: Unauthorized - } - } - }, - post: { - tags: [ - Transfers - ], - summary: Creates a new entity., - requestBody: { - description: Information for creating the entity., - content: { - application/json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - }, - text/json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - }, - application/*+json: { - schema: { - $ref: #/components/schemas/TransferCreation - } - } - } - }, - responses: { - 201: { - description: Created, - content: { - text/plain: { - schema: { - type: string, - format: uuid - } - }, - application/json: { - schema: { - type: string, - format: uuid - } - }, - text/json: { - schema: { - type: string, - format: uuid - } - } - } - }, - 409: { - description: Conflict, + 400: { + description: Bad request, content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { + application/problem+json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } } } } @@ -8832,20 +8772,21 @@ 401: { description: Unauthorized } - } + }, + deprecated: true } }, - /api/v1/Units/{id}: { + /api/v1/Transactions/{transactionId}/Links: { get: { tags: [ - Units + Transactions ], - summary: Gets the specified unit., + summary: Gets all links for the specified transaction., parameters: [ { - name: id, + name: transactionId, in: path, - description: The id of the unit to get., + description: The id of the transaction for which to get the links., required: true, schema: { type: string, @@ -8859,37 +8800,26 @@ content: { text/plain: { schema: { - $ref: #/components/schemas/Unit - } - }, - application/json: { - schema: { - $ref: #/components/schemas/Unit - } - }, - text/json: { - schema: { - $ref: #/components/schemas/Unit - } - } - } - }, - 404: { - description: Not Found, - content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails + type: array, + items: { + $ref: #/components/schemas/Link + } } }, application/json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: array, + items: { + $ref: #/components/schemas/Link + } } }, text/json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: array, + items: { + $ref: #/components/schemas/Link + } } } } @@ -8944,17 +8874,28 @@ description: Unauthorized } } - }, + } + }, + /api/v1/Transactions/{transactionId}/Links/{id}: { put: { tags: [ - Units + Transactions ], - summary: Creates a new unit or replaces an existing one if one exists with the specified id., + summary: Adds the specified link to a transaction., parameters: [ + { + name: transactionId, + in: path, + description: The id of the transaction to which to add the link., + required: true, + schema: { + type: string, + format: uuid + } + }, { name: id, in: path, - description: The id of the unit., required: true, schema: { type: string, @@ -8962,76 +8903,12 @@ } } ], - requestBody: { - description: The unit to create or replace., - content: { - application/json: { - schema: { - $ref: #/components/schemas/UnitCreation - } - }, - text/json: { - schema: { - $ref: #/components/schemas/UnitCreation - } - }, - application/*+json: { - schema: { - $ref: #/components/schemas/UnitCreation - } - } - }, - required: true - }, responses: { - 201: { - description: Created, - content: { - text/plain: { - schema: { - type: string, - format: uuid - } - }, - application/json: { - schema: { - type: string, - format: uuid - } - }, - text/json: { - schema: { - type: string, - format: uuid - } - } - } - }, 204: { description: No Content }, - 403: { - description: Forbidden, - content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - } - } - }, - 409: { - description: Conflict, + 404: { + description: Not Found, content: { text/plain: { schema: { @@ -9103,14 +8980,23 @@ }, delete: { tags: [ - Units + Transactions ], - summary: Deletes the entity., + summary: Removes the specified link from a transaction., parameters: [ + { + name: transactionId, + in: path, + description: The id of the transaction from which to remove the link., + required: true, + schema: { + type: string, + format: uuid + } + }, { name: id, in: path, - description: The id of the entity to delete., required: true, schema: { type: string, @@ -9142,30 +9028,10 @@ } } }, - 409: { - description: Conflict, + 400: { + description: Bad request, content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - } - } - }, - 400: { - description: Bad request, - content: { - application/problem+json: { + application/problem+json: { schema: { type: object, properties: { @@ -9214,12 +9080,24 @@ } } }, - /api/v1/Units: { + /api/v1/Transactions/{transactionId}/Transfers: { get: { tags: [ - Units + Transactions + ], + summary: Gets all transfers for the specified transaction., + parameters: [ + { + name: transactionId, + in: path, + description: The id of the transaction for which to get transfers., + required: true, + schema: { + type: string, + format: uuid + } + } ], - summary: Gets all units., responses: { 200: { description: OK, @@ -9228,7 +9106,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Unit + $ref: #/components/schemas/Transfer } } }, @@ -9236,7 +9114,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/Unit + $ref: #/components/schemas/Transfer } } }, @@ -9244,7 +9122,50 @@ schema: { type: array, items: { - $ref: #/components/schemas/Unit + $ref: #/components/schemas/Transfer + } + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } } } } @@ -9257,72 +9178,95 @@ description: Unauthorized } } - }, - post: { + } + }, + /api/v1/Transactions/{transactionId}/Purchases: { + get: { tags: [ - Units + Transactions ], - summary: Creates a new entity., - requestBody: { - description: Information for creating the entity., - content: { - application/json: { - schema: { - $ref: #/components/schemas/UnitCreation - } - }, - text/json: { - schema: { - $ref: #/components/schemas/UnitCreation - } - }, - application/*+json: { - schema: { - $ref: #/components/schemas/UnitCreation - } + summary: Gets all purchases for the specified transaction., + parameters: [ + { + name: transactionId, + in: path, + description: The id of the transaction for which to get all the purchases., + required: true, + schema: { + type: string, + format: uuid } } - }, + ], responses: { - 201: { - description: Created, + 200: { + description: OK, content: { text/plain: { schema: { - type: string, - format: uuid + type: array, + items: { + $ref: #/components/schemas/Purchase + } } }, application/json: { schema: { - type: string, - format: uuid + type: array, + items: { + $ref: #/components/schemas/Purchase + } } }, text/json: { schema: { - type: string, - format: uuid + type: array, + items: { + $ref: #/components/schemas/Purchase + } } } } }, - 409: { - description: Conflict, + 400: { + description: Bad request, content: { - text/plain: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - application/json: { - schema: { - $ref: #/components/schemas/ProblemDetails - } - }, - text/json: { + application/problem+json: { schema: { - $ref: #/components/schemas/ProblemDetails + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } } } } @@ -9336,12 +9280,24 @@ } } }, - /api/v1/Users: { + /api/v1/Transactions/{transactionId}/Loans: { get: { tags: [ - Users + Transactions + ], + summary: Gets all loans for the specified transaction., + parameters: [ + { + name: transactionId, + in: path, + description: The id of the transaction for which to get all the loans., + required: true, + schema: { + type: string, + format: uuid + } + } ], - summary: Gets all users., responses: { 200: { description: OK, @@ -9350,7 +9306,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/UserModel + $ref: #/components/schemas/Loan } } }, @@ -9358,7 +9314,7 @@ schema: { type: array, items: { - $ref: #/components/schemas/UserModel + $ref: #/components/schemas/Loan } } }, @@ -9366,59 +9322,2357 @@ schema: { type: array, items: { - $ref: #/components/schemas/UserModel + $ref: #/components/schemas/Loan } } } } }, - 500: { - description: Internal Server Error - }, - 401: { - description: Unauthorized - } - } - } - } - }, - components: { - schemas: { - Access: { - type: object, - properties: { - id: { - type: string, - description: The id of the access., - format: uuid - }, - name: { - type: string, - description: The name of the access. - } - }, - additionalProperties: false, - description: The level of access a user can have to a group of resources. - }, - Account: { - type: object, - properties: { - id: { - type: string, - description: The id of the account., - format: uuid - }, - createdAt: { - $ref: #/components/schemas/Instant - }, - ownerId: { - type: string, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + }, + deprecated: true + } + }, + /api/v1/Transactions/Loans: { + get: { + tags: [ + Transactions + ], + summary: Gets all loans issued or received by the specified counterparty., + parameters: [ + { + name: counterpartyId, + in: query, + description: The id of the counterparty for which to get all the loans for., + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: Successfully got all loans., + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Loan + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Loan + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Loan + } + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + }, + deprecated: true + } + }, + /api/v1/Transactions/{targetId}/Merge: { + post: { + tags: [ + Transactions + ], + summary: Merges one transaction into another., + parameters: [ + { + name: targetId, + in: path, + description: The id of the transaction in to which to merge., + required: true, + schema: { + type: string, + format: uuid + } + }, + { + name: sourceIds, + in: query, + description: The ids of the transactions which to merge into the target transactions., + schema: { + minItems: 1, + type: array, + items: { + type: string, + format: uuid + } + } + } + ], + responses: { + 204: { + description: No Content + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transactions/{id}/Related: { + get: { + tags: [ + Transactions + ], + summary: Gets all related transactions for the specified transaction., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction for which to get related transactions., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transaction + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transaction + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transaction + } + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transactions/{id}/Related/{relatedId}: { + post: { + tags: [ + Transactions + ], + summary: Adds a related transaction., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction to which to add the relation., + required: true, + schema: { + type: string, + format: uuid + } + }, + { + name: relatedId, + in: path, + description: The id of the related transaction., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + delete: { + tags: [ + Transactions + ], + summary: Removes a related transaction., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction from which to remove the relation., + required: true, + schema: { + type: string, + format: uuid + } + }, + { + name: relatedId, + in: path, + description: The id of the related transaction., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transactions/Schedules/{id}: { + get: { + tags: [ + TransactionSchedules + ], + summary: Gets the specified transaction schedule., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction schedule to get., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/Project + } + }, + application/json: { + schema: { + $ref: #/components/schemas/Project + } + }, + text/json: { + schema: { + $ref: #/components/schemas/Project + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + put: { + tags: [ + TransactionSchedules + ], + summary: Creates a new transaction schedule or replaces an existing one, if one exists with the specified id., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction schedule., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + requestBody: { + description: The transaction schedule to create or replace., + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 204: { + description: No Content + }, + 403: { + description: Forbidden, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + delete: { + tags: [ + TransactionSchedules + ], + summary: Deletes the specified transaction schedule., + parameters: [ + { + name: id, + in: path, + description: The id of the transaction schedule to delete., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transactions/Schedules: { + get: { + tags: [ + TransactionSchedules + ], + summary: Gets all transaction schedules., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/TransactionSchedule + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/TransactionSchedule + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/TransactionSchedule + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + TransactionSchedules + ], + summary: Creates a new transaction schedule., + requestBody: { + description: The transaction schedule to create., + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransactionScheduleCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transfers/{id}: { + get: { + tags: [ + Transfers + ], + summary: Gets the specified transfer., + parameters: [ + { + name: id, + in: path, + description: The id of the transfer to get., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/Transfer + } + }, + application/json: { + schema: { + $ref: #/components/schemas/Transfer + } + }, + text/json: { + schema: { + $ref: #/components/schemas/Transfer + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + put: { + tags: [ + Transfers + ], + summary: Creates a new transfer or replaces an existing one, if one exists with the specified id., + parameters: [ + { + name: id, + in: path, + description: The id of the transfer., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + requestBody: { + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 204: { + description: No Content + }, + 403: { + description: Forbidden, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + delete: { + tags: [ + Transfers + ], + summary: Deletes the specified transfer., + parameters: [ + { + name: id, + in: path, + description: The id of the transfer to delete., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Transfers: { + get: { + tags: [ + Transfers + ], + summary: Gets all transfers., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transfer + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transfer + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Transfer + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + Transfers + ], + summary: Creates a new entity., + requestBody: { + description: Information for creating the entity., + content: { + application/json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/TransferCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Units/{id}: { + get: { + tags: [ + Units + ], + summary: Gets the specified unit., + parameters: [ + { + name: id, + in: path, + description: The id of the unit to get., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/Unit + } + }, + application/json: { + schema: { + $ref: #/components/schemas/Unit + } + }, + text/json: { + schema: { + $ref: #/components/schemas/Unit + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + put: { + tags: [ + Units + ], + summary: Creates a new unit or replaces an existing one if one exists with the specified id., + parameters: [ + { + name: id, + in: path, + description: The id of the unit., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + requestBody: { + description: The unit to create or replace., + content: { + application/json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + } + }, + required: true + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 204: { + description: No Content + }, + 403: { + description: Forbidden, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + delete: { + tags: [ + Units + ], + summary: Deletes the entity., + parameters: [ + { + name: id, + in: path, + description: The id of the entity to delete., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Units: { + get: { + tags: [ + Units + ], + summary: Gets all units., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Unit + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Unit + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/Unit + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + Units + ], + summary: Creates a new entity., + requestBody: { + description: Information for creating the entity., + content: { + application/json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/UnitCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v1/Users: { + get: { + tags: [ + Users + ], + summary: Gets all users., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + type: array, + items: { + $ref: #/components/schemas/UserModel + } + } + }, + application/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/UserModel + } + } + }, + text/json: { + schema: { + type: array, + items: { + $ref: #/components/schemas/UserModel + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + } + }, + components: { + schemas: { + Access: { + type: object, + properties: { + id: { + type: string, + description: The id of the access., + format: uuid + }, + name: { + type: string, + description: The name of the access. + } + }, + additionalProperties: false, + description: The level of access a user can have to a group of resources. + }, + Account: { + type: object, + properties: { + id: { + type: string, + description: The id of the account., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + ownerId: { + type: string, description: The id of the owner of this account., format: uuid }, createdByUserId: { type: string, - description: The id of the user which created this account., + description: The id of the user which created this account., + format: uuid + }, + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { + type: string, + description: The id of the user which last modified this account., + format: uuid + }, + name: { + type: string, + description: The name of the account. + }, + counterpartyId: { + type: string, + description: The id of the counterparty to which this account belongs to., + format: uuid + }, + preferredCurrencyId: { + type: string, + description: The id of the preferred currency of the account., + format: uuid + }, + bic: { + type: string, + description: The BIC (Business Identifier Code) of the account., + nullable: true + }, + iban: { + type: string, + description: The IBAN (International Bank Account Number) of the account., + nullable: true + }, + accountNumber: { + type: string, + description: The account number of the account., + nullable: true + }, + currencies: { + type: array, + items: { + $ref: #/components/schemas/AccountInCurrency + }, + description: A collection of currencies available for the account. + } + }, + additionalProperties: false, + description: An account in one or multiple currencies, which belongs to a counterparty. + }, + AccountCreation: { + required: [ + counterpartyId, + currencies, + name, + preferredCurrencyId + ], + type: object, + properties: { + ownerId: { + type: string, + description: The id of the owner of the resource., + format: uuid, + nullable: true + }, + name: { + minLength: 1, + type: string, + description: The name of the account. + }, + counterpartyId: { + type: string, + description: The id of the counterparty to which this account belongs to., + format: uuid + }, + preferredCurrencyId: { + type: string, + description: The id of the preferred currency of the account., + format: uuid + }, + bic: { + type: string, + description: The BIC (Business Identifier Code) of the account., + nullable: true + }, + iban: { + type: string, + description: The IBAN (International Bank Account Number) of the account., + nullable: true + }, + accountNumber: { + type: string, + description: The account number of the account., + nullable: true + }, + currencies: { + minItems: 1, + type: array, + items: { + $ref: #/components/schemas/AccountInCurrencyCreation + }, + description: A collection of currencies available for the account. + } + }, + additionalProperties: false, + description: The information needed to create a new account. + }, + AccountInCurrency: { + type: object, + properties: { + id: { + type: string, + description: The id of the account in currency., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + ownerId: { + type: string, + description: The id of the owner of this account in currency., + format: uuid + }, + createdByUserId: { + type: string, + description: The id of the user which created this account in currency., + format: uuid + }, + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { + type: string, + description: The id of the user which last modified this account in currency., + format: uuid + }, + currencyId: { + type: string, + description: The id of the currency of the account in currency., + format: uuid + }, + currencyAlphabeticCode: { + type: string, + description: The alphabetic code of the currency of the account in currency. + } + }, + additionalProperties: false, + description: A single currency for a specific account. + }, + AccountInCurrencyCreation: { + required: [ + currencyId + ], + type: object, + properties: { + ownerId: { + type: string, + description: The id of the owner of the resource., + format: uuid, + nullable: true + }, + currencyId: { + type: string, + description: The id of the currency to add to an account., + format: uuid + } + }, + additionalProperties: false, + description: The information needed to add a currency to an account. + }, + AccountReference: { + type: object, + properties: { + created: { + type: boolean, + description: Whether or not the account was created during import. + }, + account: { + $ref: #/components/schemas/Account + } + }, + additionalProperties: false, + description: A reference to an account that was used during import. + }, + AccountReportResult: { + type: object, + properties: { + userAccount: { + $ref: #/components/schemas/Account + }, + accountReferences: { + type: array, + items: { + $ref: #/components/schemas/AccountReference + }, + description: The accounts created or referenced during the import. + }, + transferReferences: { + type: array, + items: { + $ref: #/components/schemas/TransferReference + }, + description: The transfers created or referenced during the import. + }, + transactionReferences: { + type: array, + items: { + $ref: #/components/schemas/TransactionReference + }, + description: The transactions created or referenced during the import. + } + }, + additionalProperties: false, + description: Summary of the report import. + }, + Balance: { + type: object, + properties: { + accountInCurrencyId: { + type: string, + description: The id of the Gnomeshade.WebApi.Models.Accounts.AccountInCurrency for which the balance was calculated for., + format: uuid + }, + sourceAmount: { + type: number, + description: The total amount withdrawn from the account., + format: double + }, + targetAmount: { + type: number, + description: The total amount deposited to the account., + format: double + } + }, + additionalProperties: false, + description: Account balance in a single currency. + }, + Category: { + type: object, + properties: { + id: { + type: string, + description: The id of the category., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + ownerId: { + type: string, + description: The id of the owner of the category., + format: uuid + }, + createdByUserId: { + type: string, + description: The id of the user that created this category., format: uuid }, modifiedAt: { @@ -9426,112 +11680,308 @@ }, modifiedByUserId: { type: string, - description: The id of the user which last modified this account., + description: The id of the user that last modified this category., format: uuid }, name: { type: string, - description: The name of the account. + description: The name of the category. }, - counterpartyId: { + description: { type: string, - description: The id of the counterparty to which this account belongs to., - format: uuid + description: The description of the category., + nullable: true }, - preferredCurrencyId: { + categoryId: { type: string, - description: The id of the preferred currency of the account., - format: uuid + description: The id of the category to which the category belongs to., + format: uuid, + nullable: true }, - bic: { + linkedProductId: { type: string, - description: The BIC (Business Identifier Code) of the account., + description: The id of the linked product which represents this category in purchases., + format: uuid, + nullable: true + } + }, + additionalProperties: false, + description: A keyword that can be assigned to other data, for example, transaction items. + }, + CategoryCreation: { + required: [ + name + ], + type: object, + properties: { + ownerId: { + type: string, + description: The id of the owner of the resource., + format: uuid, nullable: true }, - iban: { + name: { + minLength: 1, type: string, - description: The IBAN (International Bank Account Number) of the account., + description: The name of the category. + }, + description: { + type: string, + description: The description of the category., nullable: true }, - accountNumber: { + categoryId: { type: string, - description: The account number of the account., + description: The id of the category to which the category belongs to., + format: uuid, + nullable: true + }, + linkProduct: { + type: boolean, + description: Whether to create a linked product for using this category in purchases. + } + }, + additionalProperties: false, + description: Information needed to create a category. + }, + Counterparty: { + type: object, + properties: { + id: { + type: string, + description: The id of the counterparty., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + ownerId: { + type: string, + description: The id of the owner of this counterparty., + format: uuid + }, + createdByUserId: { + type: string, + description: The id of the user which created this counterparty., + format: uuid + }, + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { + type: string, + description: The id of the user which last modified this counterparty., + format: uuid + }, + name: { + type: string, + description: The name of the counterparty. + } + }, + additionalProperties: false, + description: A party that participates in a financial transaction. + }, + CounterpartyCreation: { + required: [ + name + ], + type: object, + properties: { + ownerId: { + type: string, + description: The id of the owner of the resource., + format: uuid, nullable: true }, - currencies: { - type: array, - items: { - $ref: #/components/schemas/AccountInCurrency - }, - description: A collection of currencies available for the account. + name: { + minLength: 1, + type: string, + description: The name of the counterparty. + } + }, + additionalProperties: false, + description: The information needed to create a new counterparty. + }, + Currency: { + type: object, + properties: { + id: { + type: string, + description: The id of the currency., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + name: { + type: string, + description: The name of the currency. + }, + numericCode: { + type: integer, + description: The ISO 4217 three digit numeric code., + format: int32 + }, + alphabeticCode: { + type: string, + description: The ISO 4217 three letter alphabetic code. + }, + minorUnit: { + type: integer, + description: The number of minor unit decimal places., + format: int32 + }, + official: { + type: boolean, + description: A value indicating whether this currency is listed in ISO 4217. + }, + crypto: { + type: boolean, + description: A value indicating whether this currency is a cryptocurrency. + }, + historical: { + type: boolean, + description: A value indicating whether this currency is no longer being used. + }, + activeFrom: { + $ref: #/components/schemas/Instant + }, + activeUntil: { + $ref: #/components/schemas/Instant } }, additionalProperties: false, - description: An account in one or multiple currencies, which belongs to a counterparty. + description: A currency used in transactions. }, - AccountCreation: { - required: [ - counterpartyId, - currencies, - name, - preferredCurrencyId - ], + DetailedTransaction: { type: object, properties: { - ownerId: { + id: { type: string, - description: The id of the owner of the resource., - format: uuid, - nullable: true + description: The id of the transaction., + format: uuid }, - name: { - minLength: 1, + ownerId: { type: string, - description: The name of the account. + description: The id of the owner of the transaction., + format: uuid }, - counterpartyId: { + createdAt: { + $ref: #/components/schemas/Instant + }, + createdByUserId: { type: string, - description: The id of the counterparty to which this account belongs to., + description: The id of the user that created this transaction., format: uuid }, - preferredCurrencyId: { + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { type: string, - description: The id of the preferred currency of the account., + description: The id of the user that last modified this transaction., format: uuid }, - bic: { - type: string, - description: The BIC (Business Identifier Code) of the account., - nullable: true + planned: { + type: boolean, + description: Whether this transaction is planned or not. }, - iban: { + description: { type: string, - description: The IBAN (International Bank Account Number) of the account., + description: The description of the transaction., nullable: true }, - accountNumber: { + importedAt: { + $ref: #/components/schemas/Instant + }, + imported: { + type: boolean, + description: Whether this transaction was imported., + readOnly: true + }, + reconciledAt: { + $ref: #/components/schemas/Instant + }, + reconciled: { + type: boolean, + description: Whether this transaction was reconciled., + readOnly: true + }, + refundedBy: { type: string, - description: The account number of the account., + description: The id of the transaction that refunds this one., + format: uuid, nullable: true }, - currencies: { - minItems: 1, + refunded: { + type: boolean, + description: Whether this transaction was refunded., + readOnly: true + }, + bookedAt: { + $ref: #/components/schemas/Instant + }, + valuedAt: { + $ref: #/components/schemas/Instant + }, + transfers: { type: array, items: { - $ref: #/components/schemas/AccountInCurrencyCreation + $ref: #/components/schemas/Transfer }, - description: A collection of currencies available for the account. + description: All transfers in the transaction. + }, + transferBalance: { + type: number, + description: The total balance of the transfers for the user., + format: double + }, + purchases: { + type: array, + items: { + $ref: #/components/schemas/Purchase + }, + description: All the purchases in the transaction. + }, + purchaseTotal: { + type: number, + description: The sum of all the prices from Gnomeshade.WebApi.V1.Transactions.DetailedTransaction.Purchases., + format: double + }, + loans: { + type: array, + items: { + $ref: #/components/schemas/Loan + }, + description: All the loans in the transaction. + }, + loanTotal: { + type: number, + description: The sum of all the amounts from Gnomeshade.WebApi.V1.Transactions.DetailedTransaction.Loans., + format: double + }, + links: { + type: array, + items: { + $ref: #/components/schemas/Link + }, + description: All the links attached to the transaction. } }, additionalProperties: false, - description: The information needed to create a new account. + description: A Gnomeshade.WebApi.Models.Transactions.Transaction with all sub-resources and additional details. }, - AccountInCurrency: { + Instant: { + type: string, + additionalProperties: false, + format: date-time + }, + Link: { type: object, properties: { id: { type: string, - description: The id of the account in currency., + description: The id of the link., format: uuid }, createdAt: { @@ -9539,12 +11989,12 @@ }, ownerId: { type: string, - description: The id of the owner of this account in currency., + description: The id of the owner of the link., format: uuid }, createdByUserId: { type: string, - description: The id of the user which created this account in currency., + description: The id of the user that created this link., format: uuid }, modifiedAt: { @@ -9552,26 +12002,18 @@ }, modifiedByUserId: { type: string, - description: The id of the user which last modified this account in currency., - format: uuid - }, - currencyId: { - type: string, - description: The id of the currency of the account in currency., + description: The id of the user that last modified this link., format: uuid }, - currencyAlphabeticCode: { + uri: { type: string, - description: The alphabetic code of the currency of the account in currency. + description: The unescaped canonical representation of the uniform resource identifier of the linked data. } }, additionalProperties: false, - description: A single currency for a specific account. + description: A link to an external resource. }, - AccountInCurrencyCreation: { - required: [ - currencyId - ], + LinkCreation: { type: object, properties: { ownerId: { @@ -9580,101 +12022,35 @@ format: uuid, nullable: true }, - currencyId: { + uri: { type: string, - description: The id of the currency to add to an account., - format: uuid - } - }, - additionalProperties: false, - description: The information needed to add a currency to an account. - }, - AccountReference: { - type: object, - properties: { - created: { - type: boolean, - description: Whether or not the account was created during import. - }, - account: { - $ref: #/components/schemas/Account - } - }, - additionalProperties: false, - description: A reference to an account that was used during import. - }, - AccountReportResult: { - type: object, - properties: { - userAccount: { - $ref: #/components/schemas/Account - }, - accountReferences: { - type: array, - items: { - $ref: #/components/schemas/AccountReference - }, - description: The accounts created or referenced during the import. - }, - transferReferences: { - type: array, - items: { - $ref: #/components/schemas/TransferReference - }, - description: The transfers created or referenced during the import. - }, - transactionReferences: { - type: array, - items: { - $ref: #/components/schemas/TransactionReference - }, - description: The transactions created or referenced during the import. + description: The unescaped canonical representation of the uniform resource identifier of the linked data., + format: uri, + nullable: true } }, additionalProperties: false, - description: Summary of the report import. + description: Information needed to create a link. }, - Balance: { + Loan: { type: object, properties: { - accountInCurrencyId: { + id: { type: string, - description: The id of the Gnomeshade.WebApi.Models.Accounts.AccountInCurrency for which the balance was calculated for., + description: The id of the loan., format: uuid }, - sourceAmount: { - type: number, - description: The total amount withdrawn from the account., - format: double - }, - targetAmount: { - type: number, - description: The total amount deposited to the account., - format: double - } - }, - additionalProperties: false, - description: Account balance in a single currency. - }, - Category: { - type: object, - properties: { - id: { + ownerId: { type: string, - description: The id of the category., + description: The id of the owner of the loan., format: uuid }, createdAt: { $ref: #/components/schemas/Instant }, - ownerId: { - type: string, - description: The id of the owner of the category., - format: uuid - }, createdByUserId: { type: string, - description: The id of the user that created this category., + description: The id of the user that created this loan., format: uuid }, modifiedAt: { @@ -9682,37 +12058,45 @@ }, modifiedByUserId: { type: string, - description: The id of the user that last modified this category., + description: The id of the user that last modified this loan., format: uuid }, - name: { + transactionId: { type: string, - description: The name of the category. + description: The id of the the transaction this loan is a part of., + format: uuid }, - description: { + issuingCounterpartyId: { type: string, - description: The description of the category., - nullable: true + description: The id of the counterparty the gave (issued) the loan to Gnomeshade.WebApi.V1.Transactions.Loan.ReceivingCounterpartyId., + format: uuid }, - categoryId: { + receivingCounterpartyId: { type: string, - description: The id of the category to which the category belongs to., - format: uuid, - nullable: true + description: The id of the counterparty the received the loan from Gnomeshade.WebApi.V1.Transactions.Loan.IssuingCounterpartyId., + format: uuid }, - linkedProductId: { + amount: { + type: number, + description: The amount that was loaned or payed back., + format: double + }, + currencyId: { type: string, - description: The id of the linked product which represents this category in purchases., - format: uuid, - nullable: true + description: The id of the currency of the Gnomeshade.WebApi.V1.Transactions.Loan.Amount., + format: uuid } }, additionalProperties: false, - description: A keyword that can be assigned to other data, for example, transaction items. + description: An amount that was loaned or payed back as a part of a transaction. }, - CategoryCreation: { + LoanCreation: { required: [ - name + amount, + currencyId, + issuingCounterpartyId, + receivingCounterpartyId, + transactionId ], type: object, properties: { @@ -9722,68 +12106,87 @@ format: uuid, nullable: true }, - name: { - minLength: 1, + transactionId: { type: string, - description: The name of the category. + description: The id of the the transaction this loan is a part of., + format: uuid }, - description: { + issuingCounterpartyId: { type: string, - description: The description of the category., - nullable: true + description: The id of the counterparty the gave (issued) the loan to Gnomeshade.WebApi.V1.Transactions.Loan.ReceivingCounterpartyId., + format: uuid }, - categoryId: { + receivingCounterpartyId: { type: string, - description: The id of the category to which the category belongs to., - format: uuid, - nullable: true + description: The id of the counterparty the received the loan from Gnomeshade.WebApi.V1.Transactions.Loan.IssuingCounterpartyId., + format: uuid }, - linkProduct: { - type: boolean, - description: Whether to create a linked product for using this category in purchases. + amount: { + type: number, + description: The amount that was loaned or payed back., + format: double + }, + currencyId: { + type: string, + description: The id of the currency of the Gnomeshade.WebApi.V1.Transactions.Loan.Amount., + format: uuid } }, additionalProperties: false, - description: Information needed to create a category. + description: Information needed to create a loan. }, - Counterparty: { + Login: { + required: [ + password, + username + ], type: object, properties: { - id: { + username: { + minLength: 1, type: string, - description: The id of the counterparty., - format: uuid - }, - createdAt: { - $ref: #/components/schemas/Instant + description: The username to log in with. Required. }, - ownerId: { + password: { + minLength: 1, type: string, - description: The id of the owner of this counterparty., - format: uuid - }, - createdByUserId: { + description: The password to log in with. Required. + } + }, + additionalProperties: false, + description: The information needed to log in. + }, + LoginResponse: { + type: object, + properties: { + token: { type: string, - description: The id of the user which created this counterparty., - format: uuid + description: A JWT for authenticating the session. }, - modifiedAt: { + validTo: { $ref: #/components/schemas/Instant - }, - modifiedByUserId: { + } + }, + additionalProperties: false, + description: Information about the started session. + }, + Owner: { + type: object, + properties: { + id: { type: string, - description: The id of the user which last modified this counterparty., + description: The id of the owner., format: uuid }, name: { type: string, - description: The name of the counterparty. + description: The name of the owner. } }, additionalProperties: false, - description: A party that participates in a financial transaction. + description: A group of resources. }, - CounterpartyCreation: { + OwnerCreation: { required: [ name ], @@ -9798,188 +12201,132 @@ name: { minLength: 1, type: string, - description: The name of the counterparty. + description: The name of the owner. } }, additionalProperties: false, - description: The information needed to create a new counterparty. + description: Information needed to create an owner. }, - Currency: { + Ownership: { type: object, properties: { id: { type: string, - description: The id of the currency., + description: The id of the ownership., format: uuid }, - createdAt: { - $ref: #/components/schemas/Instant - }, - name: { + ownerId: { type: string, - description: The name of the currency. - }, - numericCode: { - type: integer, - description: The ISO 4217 three digit numeric code., - format: int32 + description: The id of the owner., + format: uuid }, - alphabeticCode: { + userId: { type: string, - description: The ISO 4217 three letter alphabetic code. - }, - minorUnit: { - type: integer, - description: The number of minor unit decimal places., - format: int32 - }, - official: { - type: boolean, - description: A value indicating whether this currency is listed in ISO 4217. - }, - crypto: { - type: boolean, - description: A value indicating whether this currency is a cryptocurrency. - }, - historical: { - type: boolean, - description: A value indicating whether this currency is no longer being used. - }, - activeFrom: { - $ref: #/components/schemas/Instant + description: The id of the user that has the access., + format: uuid }, - activeUntil: { - $ref: #/components/schemas/Instant + accessId: { + type: string, + description: The id of the access level., + format: uuid } }, additionalProperties: false, - description: A currency used in transactions. + description: Access rights for a user to a group of resources. }, - DetailedTransaction: { + OwnershipCreation: { type: object, properties: { - id: { - type: string, - description: The id of the transaction., - format: uuid - }, ownerId: { type: string, - description: The id of the owner of the transaction., - format: uuid - }, - createdAt: { - $ref: #/components/schemas/Instant + description: The id of the owner of the resource., + format: uuid, + nullable: true }, - createdByUserId: { + userId: { type: string, - description: The id of the user that created this transaction., + description: The id of the user that has the access., format: uuid }, - modifiedAt: { - $ref: #/components/schemas/Instant - }, - modifiedByUserId: { + accessId: { type: string, - description: The id of the user that last modified this transaction., + description: The id of the access level., format: uuid - }, - description: { - type: string, - description: The description of the transaction., - nullable: true - }, - importedAt: { - $ref: #/components/schemas/Instant - }, - imported: { - type: boolean, - description: Whether or not this transaction was imported., + } + }, + additionalProperties: false, + description: Information needed to create an ownership. + }, + Period: { + type: object, + properties: { + nanoseconds: { + type: integer, + format: int64, readOnly: true }, - reconciledAt: { - $ref: #/components/schemas/Instant - }, - reconciled: { - type: boolean, - description: Whether or not this transaction was reconciled., + ticks: { + type: integer, + format: int64, readOnly: true }, - refundedBy: { - type: string, - description: The id of the transaction that refunds this one., - format: uuid, - nullable: true - }, - refunded: { - type: boolean, - description: Whether or not this transaction was refunded., + milliseconds: { + type: integer, + format: int64, readOnly: true }, - bookedAt: { - $ref: #/components/schemas/Instant - }, - valuedAt: { - $ref: #/components/schemas/Instant + seconds: { + type: integer, + format: int64, + readOnly: true }, - transfers: { - type: array, - items: { - $ref: #/components/schemas/Transfer - }, - description: All transfers in the transaction. + minutes: { + type: integer, + format: int64, + readOnly: true }, - transferBalance: { - type: number, - description: The total balance of the transfers for the user., - format: double + hours: { + type: integer, + format: int64, + readOnly: true }, - purchases: { - type: array, - items: { - $ref: #/components/schemas/Purchase - }, - description: All the purchases in the transaction. + days: { + type: integer, + format: int32, + readOnly: true }, - purchaseTotal: { - type: number, - description: The sum of all the prices from Gnomeshade.WebApi.V1.Transactions.DetailedTransaction.Purchases., - format: double + weeks: { + type: integer, + format: int32, + readOnly: true }, - loans: { - type: array, - items: { - $ref: #/components/schemas/Loan - }, - description: All the loans in the transaction. + months: { + type: integer, + format: int32, + readOnly: true }, - loanTotal: { - type: number, - description: The sum of all the amounts from Gnomeshade.WebApi.V1.Transactions.DetailedTransaction.Loans., - format: double + years: { + type: integer, + format: int32, + readOnly: true }, - links: { - type: array, - items: { - $ref: #/components/schemas/Link - }, - description: All the links attached to the transaction. + hasTimeComponent: { + type: boolean, + readOnly: true + }, + hasDateComponent: { + type: boolean, + readOnly: true } }, - additionalProperties: false, - description: A Gnomeshade.WebApi.Models.Transactions.Transaction with all sub-resources and additional details. - }, - Instant: { - type: string, - additionalProperties: false, - format: date-time + additionalProperties: false }, - Link: { + PlannedPurchase: { type: object, properties: { id: { type: string, - description: The id of the link., + description: The id of the purchase., format: uuid }, createdAt: { @@ -9987,12 +12334,12 @@ }, ownerId: { type: string, - description: The id of the owner of the link., + description: The id of the owner of the purchase., format: uuid }, createdByUserId: { type: string, - description: The id of the user that created this link., + description: The id of the user that created this purchase., format: uuid }, modifiedAt: { @@ -10000,18 +12347,60 @@ }, modifiedByUserId: { type: string, - description: The id of the user that last modified this link., + description: The id of the user that last modified this purchase., format: uuid }, - uri: { + order: { + type: integer, + description: The order of the purchase within a transaction., + format: int32, + nullable: true + }, + transactionId: { type: string, - description: The unescaped canonical representation of the uniform resource identifier of the linked data. + description: The id of transaction this purchase is a part of., + format: uuid + }, + price: { + type: number, + description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId., + format: double + }, + currencyId: { + type: string, + description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Price., + format: uuid + }, + productId: { + type: string, + description: The id of the purchased product., + format: uuid + }, + amount: { + type: number, + description: The amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId that was purchased., + format: double + }, + projectIds: { + type: array, + items: { + type: string, + format: uuid + }, + description: The ids of the projects that this purchase is a part of. } }, additionalProperties: false, - description: A link to an external resource. + description: The act or an instance of buying of a Gnomeshade.WebApi.Models.Products.Product as a part of a Gnomeshade.WebApi.Models.Transactions.Transaction. }, - LinkCreation: { + PlannedPurchaseCreation: { + required: [ + amount, + currencyId, + price, + productId, + transactionId + ], type: object, properties: { ownerId: { @@ -10020,27 +12409,52 @@ format: uuid, nullable: true }, - uri: { + transactionId: { type: string, - description: The unescaped canonical representation of the uniform resource identifier of the linked data., - format: uri, + description: The id of transaction this purchase is a part of., + format: uuid + }, + price: { + type: number, + description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId., + format: double + }, + currencyId: { + type: string, + description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Price., + format: uuid + }, + productId: { + type: string, + description: The id of the purchased product., + format: uuid + }, + amount: { + type: number, + description: The amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId that was purchased., + format: double + }, + order: { + type: integer, + description: The order of the purchase within a transaction., + format: int32, nullable: true } }, additionalProperties: false, - description: Information needed to create a link. + description: Information needed to create a planned purchase. }, - Loan: { + PlannedTransaction: { type: object, properties: { id: { type: string, - description: The id of the loan., + description: The id of the transaction., format: uuid }, ownerId: { type: string, - description: The id of the owner of the loan., + description: The id of the owner of the transaction., format: uuid }, createdAt: { @@ -10048,7 +12462,7 @@ }, createdByUserId: { type: string, - description: The id of the user that created this loan., + description: The id of the user that created this transaction., format: uuid }, modifiedAt: { @@ -10056,45 +12470,25 @@ }, modifiedByUserId: { type: string, - description: The id of the user that last modified this loan., - format: uuid - }, - transactionId: { - type: string, - description: The id of the the transaction this loan is a part of., - format: uuid - }, - issuingCounterpartyId: { - type: string, - description: The id of the counterparty the gave (issued) the loan to Gnomeshade.WebApi.V1.Transactions.Loan.ReceivingCounterpartyId., - format: uuid - }, - receivingCounterpartyId: { - type: string, - description: The id of the counterparty the received the loan from Gnomeshade.WebApi.V1.Transactions.Loan.IssuingCounterpartyId., + description: The id of the user that last modified this transaction., format: uuid }, - amount: { - type: number, - description: The amount that was loaned or payed back., - format: double + planned: { + type: boolean, + description: Whether this transaction is planned or not. }, - currencyId: { + scheduleId: { type: string, - description: The id of the currency of the Gnomeshade.WebApi.V1.Transactions.Loan.Amount., + description: The id of the schedule of this planned transaction., format: uuid } }, additionalProperties: false, - description: An amount that was loaned or payed back as a part of a transaction. + description: A financial transaction during which funds can be transferred between multiple accounts. }, - LoanCreation: { + PlannedTransactionCreation: { required: [ - amount, - currencyId, - issuingCounterpartyId, - receivingCounterpartyId, - transactionId + scheduleId ], type: object, properties: { @@ -10104,89 +12498,130 @@ format: uuid, nullable: true }, - transactionId: { + scheduleId: { type: string, - description: The id of the the transaction this loan is a part of., + description: The id of the schedule of this planned transaction., + format: uuid + } + }, + additionalProperties: false, + description: Information needed to create a planned transaction. + }, + PlannedTransfer: { + type: object, + properties: { + id: { + type: string, + description: The id of the transfer., format: uuid }, - issuingCounterpartyId: { + createdAt: { + $ref: #/components/schemas/Instant + }, + ownerId: { type: string, - description: The id of the counterparty the gave (issued) the loan to Gnomeshade.WebApi.V1.Transactions.Loan.ReceivingCounterpartyId., + description: The id of the owner of the transfer., format: uuid }, - receivingCounterpartyId: { + createdByUserId: { type: string, - description: The id of the counterparty the received the loan from Gnomeshade.WebApi.V1.Transactions.Loan.IssuingCounterpartyId., + description: The id of the user that created this transfer., format: uuid }, - amount: { + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { + type: string, + description: The id of the user that last modified this transfer., + format: uuid + }, + transactionId: { + type: string, + description: The id of transaction this transfer is a part of., + format: uuid + }, + sourceAmount: { type: number, - description: The amount that was loaned or payed back., + description: The amount withdrawn from the source account., format: double }, - currencyId: { + targetAmount: { + type: number, + description: The amount deposited in the target account., + format: double + }, + order: { + type: integer, + description: The order of the transfer within a transaction., + format: int32, + nullable: true + }, + bookedAt: { + $ref: #/components/schemas/Instant + }, + sourceAccountId: { type: string, - description: The id of the currency of the Gnomeshade.WebApi.V1.Transactions.Loan.Amount., - format: uuid - } - }, - additionalProperties: false, - description: Information needed to create a loan. - }, - Login: { - required: [ - password, - username - ], - type: object, - properties: { - username: { - minLength: 1, + description: The id of the account from which currency is withdrawn from., + format: uuid, + nullable: true + }, + isSourceAccount: { + type: boolean, + description: Whether Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.SourceAccountId or Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.SourceCounterpartyId is specified., + readOnly: true + }, + sourceCounterpartyId: { type: string, - description: The username to log in with. Required. + description: The id of the counterparty from which currency will be withdrawn from., + format: uuid, + nullable: true }, - password: { - minLength: 1, + sourceCurrencyId: { type: string, - description: The password to log in with. Required. - } - }, - additionalProperties: false, - description: The information needed to log in. - }, - LoginResponse: { - type: object, - properties: { - token: { + description: The id of the currency in which funds will be withdrawn from Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.SourceCounterpartyId., + format: uuid, + nullable: true + }, + targetAccountId: { type: string, - description: A JWT for authenticating the session. + description: The id of the account to which currency is deposited to., + format: uuid, + nullable: true }, - validTo: { - $ref: #/components/schemas/Instant - } - }, - additionalProperties: false, - description: Information about the started session. - }, - Owner: { - type: object, - properties: { - id: { + isTargetAccount: { + type: boolean, + description: Whether Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.TargetAccountId or Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.TargetCounterpartyId is specified., + readOnly: true + }, + targetCounterpartyId: { type: string, - description: The id of the owner., - format: uuid + description: The id of the counterparty to which currency will be deposited to., + format: uuid, + nullable: true }, - name: { + targetCurrencyId: { type: string, - description: The name of the owner. + description: The id of the currency in which funds will be deposited to Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.TargetCounterpartyId., + format: uuid, + nullable: true } }, additionalProperties: false, - description: A group of resources. + description: A planned transfer between two accounts. }, - OwnerCreation: { + PlannedTransferCreation: { required: [ - name + bookedAt, + sourceAccountId, + sourceAmount, + sourceCounterpartyId, + sourceCurrencyId, + targetAccountId, + targetAmount, + targetCounterpartyId, + targetCurrencyId, + transactionId ], type: object, properties: { @@ -10196,64 +12631,63 @@ format: uuid, nullable: true }, - name: { - minLength: 1, - type: string, - description: The name of the owner. - } - }, - additionalProperties: false, - description: Information needed to create an owner. - }, - Ownership: { - type: object, - properties: { - id: { + sourceAmount: { + type: number, + description: The amount withdrawn from the source account., + format: double + }, + targetAmount: { + type: number, + description: The amount deposited in the target account., + format: double + }, + order: { + type: integer, + description: The order of the transfer within a transaction., + format: int32, + nullable: true + }, + transactionId: { type: string, - description: The id of the ownership., + description: The id of transaction this transfer is a part of., format: uuid }, - ownerId: { + sourceAccountId: { type: string, - description: The id of the owner., + description: The id of the account from which currency is withdrawn from., format: uuid }, - userId: { + sourceCounterpartyId: { type: string, - description: The id of the user that has the access., + description: The id of the counterparty from which currency will be withdrawn from., format: uuid }, - accessId: { + sourceCurrencyId: { type: string, - description: The id of the access level., + description: The id of the currency in which funds will be withdrawn from Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.SourceCounterpartyId., format: uuid - } - }, - additionalProperties: false, - description: Access rights for a user to a group of resources. - }, - OwnershipCreation: { - type: object, - properties: { - ownerId: { + }, + targetAccountId: { type: string, - description: The id of the owner of the resource., - format: uuid, - nullable: true + description: The id of the account to which currency is deposited to., + format: uuid }, - userId: { + targetCounterpartyId: { type: string, - description: The id of the user that has the access., + description: The id of the counterparty to which currency will be deposited to., format: uuid }, - accessId: { + targetCurrencyId: { type: string, - description: The id of the access level., + description: The id of the currency in which funds will be deposited to Gnomeshade.WebApi.Models.Transactions.PlannedTransfer.TargetCounterpartyId., format: uuid + }, + bookedAt: { + $ref: #/components/schemas/Instant } }, additionalProperties: false, - description: Information needed to create an ownership. + description: Information needed to create a planned transfer. }, ProblemDetails: { type: object, @@ -10482,6 +12916,12 @@ description: The id of the user that last modified this purchase., format: uuid }, + order: { + type: integer, + description: The order of the purchase within a transaction., + format: int32, + nullable: true + }, transactionId: { type: string, description: The id of transaction this purchase is a part of., @@ -10489,12 +12929,12 @@ }, price: { type: number, - description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.Purchase.Amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId., + description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId., format: double }, currencyId: { type: string, - description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.Purchase.Price., + description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Price., format: uuid }, productId: { @@ -10504,18 +12944,9 @@ }, amount: { type: number, - description: The amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId that was purchased., + description: The amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId that was purchased., format: double }, - deliveryDate: { - $ref: #/components/schemas/Instant - }, - order: { - type: integer, - description: The order of the purchase within a transaction., - format: int32, - nullable: true - }, projectIds: { type: array, items: { @@ -10523,6 +12954,9 @@ format: uuid }, description: The ids of the projects that this purchase is a part of. + }, + deliveryDate: { + $ref: #/components/schemas/Instant } }, additionalProperties: false, @@ -10551,12 +12985,12 @@ }, price: { type: number, - description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.Purchase.Amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId., + description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId., format: double }, currencyId: { type: string, - description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.Purchase.Price., + description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Price., format: uuid }, productId: { @@ -10566,17 +13000,17 @@ }, amount: { type: number, - description: The amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId that was purchased., + description: The amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId that was purchased., format: double }, - deliveryDate: { - $ref: #/components/schemas/Instant - }, order: { type: integer, description: The order of the purchase within a transaction., format: int32, nullable: true + }, + deliveryDate: { + $ref: #/components/schemas/Instant } }, additionalProperties: false, @@ -10611,6 +13045,10 @@ description: The id of the user that last modified this transaction., format: uuid }, + planned: { + type: boolean, + description: Whether this transaction is planned or not. + }, description: { type: string, description: The description of the transaction., @@ -10621,7 +13059,7 @@ }, imported: { type: boolean, - description: Whether or not this transaction was imported., + description: Whether this transaction was imported., readOnly: true }, reconciledAt: { @@ -10629,7 +13067,7 @@ }, reconciled: { type: boolean, - description: Whether or not this transaction was reconciled., + description: Whether this transaction was reconciled., readOnly: true }, refundedBy: { @@ -10640,7 +13078,7 @@ }, refunded: { type: boolean, - description: Whether or not this transaction was refunded., + description: Whether this transaction was refunded., readOnly: true } }, @@ -10699,6 +13137,89 @@ additionalProperties: false, description: A reference to an transaction that was used during import. }, + TransactionSchedule: { + type: object, + properties: { + id: { + type: string, + description: The id of the transaction., + format: uuid + }, + ownerId: { + type: string, + description: The id of the owner of the transaction., + format: uuid + }, + createdAt: { + $ref: #/components/schemas/Instant + }, + createdByUserId: { + type: string, + description: The id of the user that created this transaction., + format: uuid + }, + modifiedAt: { + $ref: #/components/schemas/Instant + }, + modifiedByUserId: { + type: string, + description: The id of the user that last modified this transaction., + format: uuid + }, + name: { + type: string, + description: The name of the planned transaction schedule. + }, + startingAt: { + $ref: #/components/schemas/Instant + }, + period: { + $ref: #/components/schemas/Period + }, + count: { + type: integer, + description: The number of planned transactions to repeat., + format: int32 + } + }, + additionalProperties: false, + description: A schedule for planned transactions. + }, + TransactionScheduleCreation: { + required: [ + count, + name, + period, + startingAt + ], + type: object, + properties: { + ownerId: { + type: string, + description: The id of the owner of the resource., + format: uuid, + nullable: true + }, + name: { + minLength: 1, + type: string, + description: The name of the planned transaction schedule. + }, + startingAt: { + $ref: #/components/schemas/Instant + }, + period: { + $ref: #/components/schemas/Period + }, + count: { + type: integer, + description: The number of planned transactions to repeat., + format: int32 + } + }, + additionalProperties: false, + description: Information needed to create a transaction schedule. + }, Transfer: { type: object, properties: { @@ -10738,16 +13259,25 @@ description: The amount withdrawn from the source account., format: double }, - sourceAccountId: { - type: string, - description: The id of the account from which currency is withdrawn from., - format: uuid - }, targetAmount: { type: number, description: The amount deposited in the target account., format: double }, + order: { + type: integer, + description: The order of the transfer within a transaction., + format: int32, + nullable: true + }, + bookedAt: { + $ref: #/components/schemas/Instant + }, + sourceAccountId: { + type: string, + description: The id of the account from which currency is withdrawn from., + format: uuid + }, targetAccountId: { type: string, description: The id of the account to which currency is deposited to., @@ -10768,15 +13298,6 @@ description: The reference id issued by the user., nullable: true }, - order: { - type: integer, - description: The order of the transfer within a transaction., - format: int32, - nullable: true - }, - bookedAt: { - $ref: #/components/schemas/Instant - }, valuedAt: { $ref: #/components/schemas/Instant } @@ -10812,16 +13333,22 @@ description: The amount withdrawn from the source account., format: double }, - sourceAccountId: { - type: string, - description: The id of the account from which currency is withdrawn from., - format: uuid - }, targetAmount: { type: number, description: The amount deposited in the target account., format: double }, + order: { + type: integer, + description: The order of the transfer within a transaction., + format: int32, + nullable: true + }, + sourceAccountId: { + type: string, + description: The id of the account from which currency is withdrawn from., + format: uuid + }, targetAccountId: { type: string, description: The id of the account to which currency is deposited to., @@ -10842,12 +13369,6 @@ description: The reference id issued by the user., nullable: true }, - order: { - type: integer, - description: The order of the transfer within a transaction., - format: int32, - nullable: true - }, bookedAt: { $ref: #/components/schemas/Instant }, diff --git a/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v2_swagger.json.verified.txt b/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v2_swagger.json.verified.txt index fcd3e5fe6..8492b0752 100644 --- a/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v2_swagger.json.verified.txt +++ b/tests/Gnomeshade.WebApi.Tests.Integration/OpenApiTests.ApiDefinition_ShouldBeExpected._swagger_v2_swagger.json.verified.txt @@ -1101,6 +1101,497 @@ } } }, + /api/v2/LoanPayments/Planned/{id}: { + get: { + tags: [ + PlannedLoanPayments + ], + summary: Gets the specified loan payment., + parameters: [ + { + name: id, + in: path, + description: The id of the loan payment to get., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/LoanPayment + } + }, + application/json: { + schema: { + $ref: #/components/schemas/LoanPayment + } + }, + text/json: { + schema: { + $ref: #/components/schemas/LoanPayment + } + } + } + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + put: { + tags: [ + PlannedLoanPayments + ], + summary: Creates a new loan payment or replaces an existing one, if one exists with the specified id., + parameters: [ + { + name: id, + in: path, + description: The id of the loan payment., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + requestBody: { + description: The loan payment to create or replace., + content: { + application/json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 204: { + description: No Content + }, + 403: { + description: Forbidden, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + delete: { + tags: [ + PlannedLoanPayments + ], + summary: Deletes the loan payment., + parameters: [ + { + name: id, + in: path, + description: The id of the loan payment. to delete., + required: true, + schema: { + type: string, + format: uuid + } + } + ], + responses: { + 204: { + description: No Content + }, + 404: { + description: Not Found, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 400: { + description: Bad request, + content: { + application/problem+json: { + schema: { + type: object, + properties: { + type: { + type: string, + nullable: true + }, + title: { + type: string, + nullable: true + }, + status: { + type: integer, + format: int32, + default: 400, + nullable: true + }, + detail: { + type: string, + nullable: true + }, + instance: { + type: string, + nullable: true + }, + errors: { + type: object, + additionalProperties: { + type: array, + items: { + type: string + } + } + } + } + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, + /api/v2/LoanPayments/Planned: { + get: { + tags: [ + PlannedLoanPayments + ], + summary: Gets all loan payments., + responses: { + 200: { + description: OK, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/LoanPayment + } + }, + application/json: { + schema: { + $ref: #/components/schemas/LoanPayment + } + }, + text/json: { + schema: { + $ref: #/components/schemas/LoanPayment + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + }, + post: { + tags: [ + PlannedLoanPayments + ], + summary: Creates a new loan payment., + requestBody: { + description: The loan payment to create., + content: { + application/json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + }, + text/json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + }, + application/*+json: { + schema: { + $ref: #/components/schemas/LoanPaymentCreation + } + } + } + }, + responses: { + 201: { + description: Created, + content: { + text/plain: { + schema: { + type: string, + format: uuid + } + }, + application/json: { + schema: { + type: string, + format: uuid + } + }, + text/json: { + schema: { + type: string, + format: uuid + } + } + } + }, + 409: { + description: Conflict, + content: { + text/plain: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + application/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + }, + text/json: { + schema: { + $ref: #/components/schemas/ProblemDetails + } + } + } + }, + 500: { + description: Internal Server Error + }, + 401: { + description: Unauthorized + } + } + } + }, /api/v2/Transactions/{id}/Details: { get: { tags: [ @@ -1450,6 +1941,10 @@ description: The id of the user that last modified this transaction., format: uuid }, + planned: { + type: boolean, + description: Whether this transaction is planned or not. + }, description: { type: string, description: The description of the transaction., @@ -1460,7 +1955,7 @@ }, imported: { type: boolean, - description: Whether or not this transaction was imported., + description: Whether this transaction was imported., readOnly: true }, reconciledAt: { @@ -1468,7 +1963,7 @@ }, reconciled: { type: boolean, - description: Whether or not this transaction was reconciled., + description: Whether this transaction was reconciled., readOnly: true }, refundedBy: { @@ -1479,7 +1974,7 @@ }, refunded: { type: boolean, - description: Whether or not this transaction was refunded., + description: Whether this transaction was refunded., readOnly: true }, bookedAt: { @@ -1826,6 +2321,12 @@ description: The id of the user that last modified this purchase., format: uuid }, + order: { + type: integer, + description: The order of the purchase within a transaction., + format: int32, + nullable: true + }, transactionId: { type: string, description: The id of transaction this purchase is a part of., @@ -1833,12 +2334,12 @@ }, price: { type: number, - description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.Purchase.Amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId., + description: The amount paid to purchase an Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId., format: double }, currencyId: { type: string, - description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.Purchase.Price., + description: The id of the currency of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.Price., format: uuid }, productId: { @@ -1848,18 +2349,9 @@ }, amount: { type: number, - description: The amount of Gnomeshade.WebApi.Models.Transactions.Purchase.ProductId that was purchased., + description: The amount of Gnomeshade.WebApi.Models.Transactions.PurchaseBase.ProductId that was purchased., format: double }, - deliveryDate: { - $ref: #/components/schemas/Instant - }, - order: { - type: integer, - description: The order of the purchase within a transaction., - format: int32, - nullable: true - }, projectIds: { type: array, items: { @@ -1867,6 +2359,9 @@ format: uuid }, description: The ids of the projects that this purchase is a part of. + }, + deliveryDate: { + $ref: #/components/schemas/Instant } }, additionalProperties: false, @@ -1911,16 +2406,25 @@ description: The amount withdrawn from the source account., format: double }, - sourceAccountId: { - type: string, - description: The id of the account from which currency is withdrawn from., - format: uuid - }, targetAmount: { type: number, description: The amount deposited in the target account., format: double }, + order: { + type: integer, + description: The order of the transfer within a transaction., + format: int32, + nullable: true + }, + bookedAt: { + $ref: #/components/schemas/Instant + }, + sourceAccountId: { + type: string, + description: The id of the account from which currency is withdrawn from., + format: uuid + }, targetAccountId: { type: string, description: The id of the account to which currency is deposited to., @@ -1941,15 +2445,6 @@ description: The reference id issued by the user., nullable: true }, - order: { - type: integer, - description: The order of the transfer within a transaction., - format: int32, - nullable: true - }, - bookedAt: { - $ref: #/components/schemas/Instant - }, valuedAt: { $ref: #/components/schemas/Instant } diff --git a/tests/Gnomeshade.WebApi.Tests.Integration/Scenarios/TransactionPlanningTests.cs b/tests/Gnomeshade.WebApi.Tests.Integration/Scenarios/TransactionPlanningTests.cs new file mode 100644 index 000000000..bd6b923a0 --- /dev/null +++ b/tests/Gnomeshade.WebApi.Tests.Integration/Scenarios/TransactionPlanningTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Gnomeshade.TestingHelpers.Models; +using Gnomeshade.WebApi.Tests.Integration.Fixtures; + +using NodaTime; + +namespace Gnomeshade.WebApi.Tests.Integration.Scenarios; + +public sealed class TransactionPlanningTests(WebserverFixture fixture) : WebserverTests(fixture) +{ + [Test] + public async Task Test() + { + var client = await Fixture.CreateAuthorizedClientAsync(); + + var bankCounterparty = await client.CreateCounterpartyAsync(); + var bankAccount = await client.CreateAccountAsync(bankCounterparty.Id); + + var counterparty = await client.GetMyCounterpartyAsync(); + var account = await client.CreateAccountAsync(counterparty.Id); + + var productId = await client.CreateProductAsync(new() { Name = Guid.NewGuid().ToString("N") }); + var loan = await client.CreateLoanAsync(bankCounterparty.Id, counterparty.Id); + + var scheduleId = await client.CreateTransactionSchedule(new() + { + Name = Guid.NewGuid().ToString("N"), + StartingAt = Instant.FromUtc(2024, 10, 15, 08, 00, 00), + Period = Period.FromMonths(1), + Count = 120, + }); + + var transactionId = await client.CreatePlannedTransaction(new() { ScheduleId = scheduleId }); + + var principalTransferId = await client.CreatePlannedTransfer(new() + { + SourceAmount = 500, + TargetAmount = 500, + TransactionId = transactionId, + SourceAccountId = account.Currencies.Single().Id, + TargetCounterpartyId = bankCounterparty.Id, + TargetCurrencyId = bankAccount.Currencies.Single().CurrencyId, + BookedAt = Instant.FromUtc(2024, 10, 15, 08, 00, 00), + }); + + var interestTransferId = await client.CreatePlannedTransfer(new() + { + SourceAmount = 100, + TargetAmount = 100, + TransactionId = transactionId, + SourceAccountId = account.Currencies.Single().Id, + TargetCounterpartyId = bankCounterparty.Id, + TargetCurrencyId = bankAccount.Currencies.Single().CurrencyId, + BookedAt = Instant.FromUtc(2024, 10, 15, 08, 00, 00), + }); + + var purchaseId = await client.CreatePlannedPurchase(new() + { + TransactionId = transactionId, + Price = 600, + CurrencyId = bankAccount.Currencies.Single().CurrencyId, + ProductId = productId, + Amount = 1, + }); + + var loanPaymentId = await client.CreatePlannedLoanPayment(new() + { + LoanId = loan.Id, + TransactionId = transactionId, + Amount = 500, + Interest = 100, + }); + } +}