From 82eb073a969ad7765b2784e3ac45501199ff1f98 Mon Sep 17 00:00:00 2001 From: damien-git Date: Mon, 22 Apr 2024 09:09:17 -0400 Subject: [PATCH] [MODFISTO-460] Removed the old transaction API (#408) --- descriptors/ModuleDescriptor-template.json | 153 +- ramls/transaction-summary.raml | 121 -- ramls/transaction.raml | 18 +- .../error/NameCodeConstraintErrorBuilder.java | 7 +- .../org/folio/config/DAOConfiguration.java | 57 +- .../folio/config/ServicesConfiguration.java | 161 +- .../folio/dao/budget/BudgetPostgresDAO.java | 2 +- .../dao/fiscalyear/FiscalYearPostgresDAO.java | 2 +- .../org/folio/dao/fund/FundPostgresDAO.java | 2 +- .../folio/dao/ledger/LedgerPostgresDAO.java | 2 +- .../summary/BaseTransactionSummaryDAO.java | 75 - .../summary/InvoiceTransactionSummaryDAO.java | 11 - .../summary/OrderTransactionSummaryDAO.java | 12 - .../dao/summary/TransactionSummaryDao.java | 17 - .../BaseTemporaryTransactionsDAO.java | 83 -- .../dao/transactions/BaseTransactionDAO.java | 138 -- .../dao/transactions/BatchTransactionDAO.java | 2 + .../BatchTransactionPostgresDAO.java | 1 - .../transactions/DefaultTransactionDAO.java | 17 - .../dao/transactions/EncumbranceDAO.java | 41 - .../dao/transactions/PaymentCreditDAO.java | 31 - .../dao/transactions/PendingPaymentDAO.java | 30 - .../transactions/TemporaryEncumbranceDAO.java | 14 + ...a => TemporaryEncumbrancePostgresDAO.java} | 19 +- .../TemporaryInvoiceTransactionDAO.java | 32 - .../TemporaryOrderTransactionDAO.java | 32 - .../transactions/TemporaryTransactionDAO.java | 17 - .../dao/transactions/TransactionDAO.java | 30 - .../folio/rest/exception/HttpException.java | 32 +- .../java/org/folio/rest/impl/FundAPI.java | 2 +- .../java/org/folio/rest/impl/LedgerAPI.java | 2 +- .../folio/rest/impl/LedgerRolloverAPI.java | 4 +- .../org/folio/rest/impl/TransactionAPI.java | 57 +- .../rest/impl/TransactionSummaryAPI.java | 197 --- .../java/org/folio/rest/persist/DBConn.java | 2 +- .../org/folio/rest/persist/HelperUtils.java | 25 +- .../java/org/folio/rest/util/ErrorCodes.java | 11 +- .../org/folio/rest/util/ResponseUtils.java | 25 +- .../folio/service/budget/BudgetService.java | 121 +- ...lloverBudgetExpenseClassTotalsService.java | 10 +- .../service/expence/ExpenseClassService.java | 4 +- .../org/folio/service/group/GroupService.java | 4 +- .../rollover/LedgerRolloverService.java | 4 +- .../rollover/RolloverProgressService.java | 2 +- .../rollover/RolloverValidationService.java | 2 +- .../AbstractTransactionSummaryService.java | 70 - .../EncumbranceTransactionSummaryService.java | 38 - ...aymentCreditTransactionSummaryService.java | 33 - ...ndingPaymentTransactionSummaryService.java | 33 - .../summary/TransactionSummaryService.java | 22 - .../AbstractTransactionService.java | 36 - .../AllOrNothingTransactionService.java | 146 -- .../transactions/AllocationService.java | 113 -- .../DefaultTransactionService.java | 11 - .../transactions/EncumbranceService.java | 277 ---- .../transactions/PaymentCreditService.java | 319 ---- .../transactions/PendingPaymentService.java | 353 ----- ....java => TemporaryEncumbranceService.java} | 12 +- .../TransactionManagingStrategy.java | 7 - .../TransactionManagingStrategyFactory.java | 31 - .../transactions/TransactionService.java | 14 - .../service/transactions/TransferService.java | 97 -- .../batch/BatchTransactionChecks.java | 20 +- .../batch/BatchTransactionHolder.java | 13 + .../cancel/CancelPaymentCreditService.java | 71 - .../cancel/CancelPendingPaymentService.java | 59 - .../cancel/CancelTransactionService.java | 107 -- .../BaseTransactionRestrictionService.java | 85 -- .../EncumbranceRestrictionService.java | 79 - .../PaymentCreditRestrictionService.java | 113 -- .../PendingPaymentRestrictionService.java | 99 -- .../TransactionRestrictionService.java | 11 - .../invoice-transaction-summary.json | 5 - .../order-306857_transaction-summary.json | 4 - ...te_processed_order_transaction_summary.sql | 5 - .../templates/db_scripts/schema.json | 19 +- src/test/java/org/folio/StorageTestSuite.java | 73 +- .../dao/rollover/RolloverBudgetDAOTest.java | 2 +- .../dao/rollover/RolloverProgressDAOTest.java | 4 +- .../transactions/BaseTransactionDAOTest.java | 125 -- .../transactions/PendingPaymentDAOTest.java | 312 ---- .../java/org/folio/rest/impl/BudgetTest.java | 39 +- .../org/folio/rest/impl/EncumbrancesTest.java | 898 ----------- .../org/folio/rest/impl/EntitiesCrudTest.java | 29 +- .../folio/rest/impl/PaymentsCreditsTest.java | 510 ------- .../folio/rest/impl/TenantSampleDataTest.java | 14 +- .../java/org/folio/rest/impl/TestBase.java | 17 - .../org/folio/rest/impl/TransactionTest.java | 365 +---- .../rest/impl/TransactionsSummariesTest.java | 95 -- .../org/folio/rest/utils/DBClientTest.java | 6 +- .../org/folio/rest/utils/TestEntities.java | 6 - .../rollover/RolloverProgressServiceTest.java | 5 +- .../RolloverValidationServiceTest.java | 6 +- ...gPaymentTransactionSummaryServiceTest.java | 67 - .../transactions/AllocationServiceTest.java | 124 -- .../transactions/AllocationTransferTest.java | 425 ++++++ .../BatchTransactionServiceTest.java | 1324 ----------------- .../BatchTransactionServiceTestBase.java | 273 ++++ .../transactions/EncumbranceServiceTest.java | 146 -- .../service/transactions/EncumbranceTest.java | 430 ++++++ .../PaymentCreditServiceTest.java | 94 -- .../transactions/PaymentCreditTest.java | 452 ++++++ .../PendingPaymentServiceTest.java | 832 ----------- .../transactions/PendingPaymentTest.java | 410 +++++ .../CancelPaymentCreditServiceTest.java | 79 - .../cancel/CancelTransactionServiceTest.java | 237 --- .../EncumbranceRestrictionServiceTest.java | 62 - .../PaymentCreditRestrictionServiceTest.java | 74 - .../PendingPaymentRestrictionServiceTest.java | 110 -- ...location_AFRICAHIST-FY24_ANZHIST-FY24.json | 12 - 110 files changed, 2237 insertions(+), 9353 deletions(-) delete mode 100644 ramls/transaction-summary.raml delete mode 100644 src/main/java/org/folio/dao/summary/BaseTransactionSummaryDAO.java delete mode 100644 src/main/java/org/folio/dao/summary/InvoiceTransactionSummaryDAO.java delete mode 100644 src/main/java/org/folio/dao/summary/OrderTransactionSummaryDAO.java delete mode 100644 src/main/java/org/folio/dao/summary/TransactionSummaryDao.java delete mode 100644 src/main/java/org/folio/dao/transactions/BaseTemporaryTransactionsDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/BaseTransactionDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/DefaultTransactionDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/EncumbranceDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/PaymentCreditDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/PendingPaymentDAO.java create mode 100644 src/main/java/org/folio/dao/transactions/TemporaryEncumbranceDAO.java rename src/main/java/org/folio/dao/transactions/{TemporaryEncumbranceTransactionDAO.java => TemporaryEncumbrancePostgresDAO.java} (74%) delete mode 100644 src/main/java/org/folio/dao/transactions/TemporaryInvoiceTransactionDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/TemporaryOrderTransactionDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/TemporaryTransactionDAO.java delete mode 100644 src/main/java/org/folio/dao/transactions/TransactionDAO.java delete mode 100644 src/main/java/org/folio/rest/impl/TransactionSummaryAPI.java delete mode 100644 src/main/java/org/folio/service/summary/AbstractTransactionSummaryService.java delete mode 100644 src/main/java/org/folio/service/summary/EncumbranceTransactionSummaryService.java delete mode 100644 src/main/java/org/folio/service/summary/PaymentCreditTransactionSummaryService.java delete mode 100644 src/main/java/org/folio/service/summary/PendingPaymentTransactionSummaryService.java delete mode 100644 src/main/java/org/folio/service/summary/TransactionSummaryService.java delete mode 100644 src/main/java/org/folio/service/transactions/AbstractTransactionService.java delete mode 100644 src/main/java/org/folio/service/transactions/AllOrNothingTransactionService.java delete mode 100644 src/main/java/org/folio/service/transactions/AllocationService.java delete mode 100644 src/main/java/org/folio/service/transactions/DefaultTransactionService.java delete mode 100644 src/main/java/org/folio/service/transactions/EncumbranceService.java delete mode 100644 src/main/java/org/folio/service/transactions/PaymentCreditService.java delete mode 100644 src/main/java/org/folio/service/transactions/PendingPaymentService.java rename src/main/java/org/folio/service/transactions/{TemporaryTransactionService.java => TemporaryEncumbranceService.java} (68%) delete mode 100644 src/main/java/org/folio/service/transactions/TransactionManagingStrategy.java delete mode 100644 src/main/java/org/folio/service/transactions/TransactionManagingStrategyFactory.java delete mode 100644 src/main/java/org/folio/service/transactions/TransactionService.java delete mode 100644 src/main/java/org/folio/service/transactions/TransferService.java delete mode 100644 src/main/java/org/folio/service/transactions/cancel/CancelPaymentCreditService.java delete mode 100644 src/main/java/org/folio/service/transactions/cancel/CancelPendingPaymentService.java delete mode 100644 src/main/java/org/folio/service/transactions/cancel/CancelTransactionService.java delete mode 100644 src/main/java/org/folio/service/transactions/restriction/BaseTransactionRestrictionService.java delete mode 100644 src/main/java/org/folio/service/transactions/restriction/EncumbranceRestrictionService.java delete mode 100644 src/main/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionService.java delete mode 100644 src/main/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionService.java delete mode 100644 src/main/java/org/folio/service/transactions/restriction/TransactionRestrictionService.java delete mode 100644 src/main/resources/data/invoice-transaction-summaries/invoice-transaction-summary.json delete mode 100644 src/main/resources/data/order-transaction-summaries/order-306857_transaction-summary.json delete mode 100644 src/main/resources/templates/db_scripts/migration/update_processed_order_transaction_summary.sql delete mode 100644 src/test/java/org/folio/dao/transactions/BaseTransactionDAOTest.java delete mode 100644 src/test/java/org/folio/dao/transactions/PendingPaymentDAOTest.java delete mode 100644 src/test/java/org/folio/rest/impl/EncumbrancesTest.java delete mode 100644 src/test/java/org/folio/rest/impl/PaymentsCreditsTest.java delete mode 100644 src/test/java/org/folio/rest/impl/TransactionsSummariesTest.java delete mode 100644 src/test/java/org/folio/service/summary/PendingPaymentTransactionSummaryServiceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/AllocationServiceTest.java create mode 100644 src/test/java/org/folio/service/transactions/AllocationTransferTest.java delete mode 100644 src/test/java/org/folio/service/transactions/BatchTransactionServiceTest.java create mode 100644 src/test/java/org/folio/service/transactions/BatchTransactionServiceTestBase.java delete mode 100644 src/test/java/org/folio/service/transactions/EncumbranceServiceTest.java create mode 100644 src/test/java/org/folio/service/transactions/EncumbranceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/PaymentCreditServiceTest.java create mode 100644 src/test/java/org/folio/service/transactions/PaymentCreditTest.java delete mode 100644 src/test/java/org/folio/service/transactions/PendingPaymentServiceTest.java create mode 100644 src/test/java/org/folio/service/transactions/PendingPaymentTest.java delete mode 100644 src/test/java/org/folio/service/transactions/cancel/CancelPaymentCreditServiceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/cancel/CancelTransactionServiceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/restriction/EncumbranceRestrictionServiceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionServiceTest.java delete mode 100644 src/test/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionServiceTest.java delete mode 100644 src/test/resources/data/transactions/zallocation_AFRICAHIST-FY24_ANZHIST-FY24.json diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 10854bf5..a604987b 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -412,33 +412,18 @@ }, { "id": "finance-storage.transactions", - "version": "4.1", + "version": "5.0", "handlers": [ { "methods": ["GET"], "pathPattern": "/finance-storage/transactions", "permissionsRequired": ["finance-storage.transactions.collection.get"] }, - { - "methods": ["POST"], - "pathPattern": "/finance-storage/transactions", - "permissionsRequired": ["finance-storage.transactions.item.post"] - }, { "methods": ["GET"], "pathPattern": "/finance-storage/transactions/{id}", "permissionsRequired": ["finance-storage.transactions.item.get"] }, - { - "methods": ["PUT"], - "pathPattern": "/finance-storage/transactions/{id}", - "permissionsRequired": ["finance-storage.transactions.item.put"] - }, - { - "methods": ["DELETE"], - "pathPattern": "/finance-storage/transactions/{id}", - "permissionsRequired": ["finance-storage.transactions.item.delete"] - }, { "methods": ["POST"], "pathPattern": "/finance-storage/transactions/batch-all-or-nothing", @@ -446,58 +431,6 @@ } ] }, - { - "id": "finance-storage.order-transaction-summaries", - "version": "1.1", - "handlers": [ - { - "methods": ["POST"], - "pathPattern": "/finance-storage/order-transaction-summaries", - "permissionsRequired": ["finance-storage.order-transaction-summaries.item.post"] - }, - { - "methods": ["GET"], - "pathPattern": "/finance-storage/order-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.order-transaction-summaries.item.get"] - }, - { - "methods": ["PUT"], - "pathPattern": "/finance-storage/order-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.order-transaction-summaries.item.put"] - }, - { - "methods": ["DELETE"], - "pathPattern": "/finance-storage/order-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.order-transaction-summaries.item.delete"] - } - ] - }, - { - "id": "finance-storage.invoice-transaction-summaries", - "version": "2.1", - "handlers": [ - { - "methods": ["POST"], - "pathPattern": "/finance-storage/invoice-transaction-summaries", - "permissionsRequired": ["finance-storage.invoice-transaction-summaries.item.post"] - }, - { - "methods": ["GET"], - "pathPattern": "/finance-storage/invoice-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.invoice-transaction-summaries.item.get"] - }, - { - "methods": ["PUT"], - "pathPattern": "/finance-storage/invoice-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.invoice-transaction-summaries.item.put"] - }, - { - "methods": ["DELETE"], - "pathPattern": "/finance-storage/invoice-transaction-summaries/{id}", - "permissionsRequired": ["finance-storage.invoice-transaction-summaries.item.delete"] - } - ] - }, { "id" : "_tenant", "version" : "2.0", @@ -963,26 +896,11 @@ "displayName" : "finance-storage.transactions.-collection get", "description" : "Get collection of transactions" }, - { - "permissionName" : "finance-storage.transactions.item.post", - "displayName" : "finance-storage.transactions.-item post", - "description" : "Create a new transaction" - }, { "permissionName" : "finance-storage.transactions.item.get", "displayName" : "finance-storage.transactions.-item get", "description" : "Fetch a transaction" }, - { - "permissionName" : "finance-storage.transactions.item.put", - "displayName" : "finance-storage.transactions.-item put", - "description" : "Update a transaction" - }, - { - "permissionName" : "finance-storage.transactions.item.delete", - "displayName" : "finance-storage.transactions.-item delete", - "description" : "Delete a transaction" - }, { "permissionName" : "finance-storage.transactions.batch", "displayName" : "process transactions in batch", @@ -994,75 +912,10 @@ "description" : "All permissions for the transaction", "subPermissions" : [ "finance-storage.transactions.collection.get", - "finance-storage.transactions.item.post", "finance-storage.transactions.item.get", - "finance-storage.transactions.item.put", - "finance-storage.transactions.item.delete", "finance-storage.transactions.batch" ] }, - { - "permissionName" : "finance-storage.order-transaction-summaries.item.get", - "displayName" : "Retrieve a new order transaction summary record", - "description" : "Retrieve a new order transaction summary record" - }, - { - "permissionName" : "finance-storage.order-transaction-summaries.item.post", - "displayName" : "Create a new order transaction summary record", - "description" : "Create a new order transaction summary record" - }, - { - "permissionName" : "finance-storage.order-transaction-summaries.item.put", - "displayName" : "Update order transaction summary record", - "description" : "Update order transaction summary record" - }, - { - "permissionName" : "finance-storage.order-transaction-summaries.item.delete", - "displayName" : "Delete a new order transaction summary record", - "description" : "Delete a new order transaction summary record" - }, - { - "permissionName" : "finance-storage.order-transaction-summaries.all", - "displayName" : "All order transaction summary perms", - "description" : "All permissions for the order transaction summary", - "subPermissions" : [ - "finance-storage.order-transaction-summaries.item.get", - "finance-storage.order-transaction-summaries.item.post", - "finance-storage.order-transaction-summaries.item.put", - "finance-storage.order-transaction-summaries.item.delete" - ] - }, - { - "permissionName" : "finance-storage.invoice-transaction-summaries.item.get", - "displayName" : "Retrieve an invoice transaction summary record", - "description" : "Retrieve an invoice transaction summary record" - }, - { - "permissionName" : "finance-storage.invoice-transaction-summaries.item.post", - "displayName" : "Create a new invoice transaction summary record", - "description" : "Create a new invoice transaction summary record" - }, - { - "permissionName" : "finance-storage.invoice-transaction-summaries.item.put", - "displayName" : "Update invoice transaction summary record", - "description" : "Update invoice transaction summary record" - }, - { - "permissionName" : "finance-storage.invoice-transaction-summaries.item.delete", - "displayName" : "Delete an invoice transaction summary record", - "description" : "Delete an invoice transaction summary record" - }, - { - "permissionName" : "finance-storage.invoice-transaction-summaries.all", - "displayName" : "All invoice transaction summary perms", - "description" : "All permissions for the invoice transaction summary", - "subPermissions" : [ - "finance-storage.invoice-transaction-summaries.item.get", - "finance-storage.invoice-transaction-summaries.item.post", - "finance-storage.invoice-transaction-summaries.item.put", - "finance-storage.invoice-transaction-summaries.item.delete" - ] - }, { "permissionName" : "finance.module.all", "displayName" : "All finance-module perms", @@ -1076,9 +929,7 @@ "finance-storage.groups.all", "finance-storage.ledgers.all", "finance-storage.transactions.all", - "finance-storage.fund-types.all", - "finance-storage.order-transaction-summaries.all", - "finance-storage.invoice-transaction-summaries.all" + "finance-storage.fund-types.all" ] } ], diff --git a/ramls/transaction-summary.raml b/ramls/transaction-summary.raml deleted file mode 100644 index 1bc5f2a9..00000000 --- a/ramls/transaction-summary.raml +++ /dev/null @@ -1,121 +0,0 @@ -#%RAML 1.0 -title: "mod-finance-storage" -baseUri: https://github.com/folio-org/mod-finance-storage -version: v1.2 - -documentation: - - title: mod-finance-storage transaction summaries - DEPRECATED - content: CRUD APIs used to manage transaction summaries. Use transactions/batch-all-or-nothing instead. - -types: - errors: !include raml-util/schemas/errors.schema - order-transaction-summary: !include acq-models/mod-finance/schemas/order_transaction_summary.json - invoice-transaction-summary: !include acq-models/mod-finance/schemas/invoice_transaction_summary.json - UUID: - type: string - pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ - -traits: - pageable: !include raml-util/traits/pageable.raml - searchable: !include raml-util/traits/searchable.raml - validate: !include raml-util/traits/validation.raml - -resourceTypes: - get-delete: !include raml-util/rtypes/get-delete.raml - item-collection: !include raml-util/rtypes/item-collection.raml - - -/finance-storage: - /order-transaction-summaries: - post: - description: Create a new order transaction summary item. - body: - application/json: - type: order-transaction-summary - example: - strict: false - value: !include acq-models/mod-finance/examples/order_transaction_summary.sample - responses: - 201: - description: "Returns a newly created item, with server-controlled fields like 'id' populated" - headers: - Location: - description: URI to the created order transaction summary item - body: - application/json: - example: !include acq-models/mod-finance/examples/order_transaction_summary.sample - 400: - description: "Bad request, e.g. malformed request body or query parameter. Details of the error (e.g. name of the parameter or line/character number with malformed data) provided in the response." - body: - text/plain: - example: | - "unable to add order transaction summary -- malformed JSON at 13:3" - 401: - description: "Not authorized to perform requested action" - body: - text/plain: - example: "unable to create order transaction summary record -- unauthorized" - 500: - description: "Internal server error, e.g. due to misconfiguration" - body: - text/plain: - example: "Internal server error, contact administrator" - is: [validate] - - /{id}: - uriParameters: - id: - description: The UUID of an order transaction summary - type: UUID - type: - item-collection: - exampleItem: !include acq-models/mod-finance/examples/order_transaction_summary.sample - schema: order-transaction-summary - is: [validate] - - /invoice-transaction-summaries: - post: - description: Create a new invoice transaction summary item. - body: - application/json: - type: invoice-transaction-summary - example: - strict: false - value: !include acq-models/mod-finance/examples/invoice_transaction_summary.sample - responses: - 201: - description: "Returns a newly created item, with server-controlled fields like 'id' populated" - headers: - Location: - description: URI to the created invoice transaction summary item - body: - application/json: - example: !include acq-models/mod-finance/examples/invoice_transaction_summary.sample - 400: - description: "Bad request, e.g. malformed request body or query parameter. Details of the error (e.g. name of the parameter or line/character number with malformed data) provided in the response." - body: - text/plain: - example: | - "unable to add invoice transaction summary -- malformed JSON at 13:3" - 401: - description: "Not authorized to perform requested action" - body: - text/plain: - example: "unable to create invoice transaction summary record -- unauthorized" - 500: - description: "Internal server error, e.g. due to misconfiguration" - body: - text/plain: - example: "Internal server error, contact administrator" - is: [validate] - - /{id}: - uriParameters: - id: - description: The UUID of an invoice transaction summary - type: UUID - type: - item-collection: - exampleItem: !include acq-models/mod-finance/examples/invoice_transaction_summary.sample - schema: invoice-transaction-summary - is: [validate] diff --git a/ramls/transaction.raml b/ramls/transaction.raml index 0da0fe62..c46db18a 100644 --- a/ramls/transaction.raml +++ b/ramls/transaction.raml @@ -1,7 +1,7 @@ #%RAML 1.0 title: "mod-finance-storage" baseUri: https://github.com/folio-org/mod-finance-storage -version: v3 +version: v4 documentation: - title: mod-finance-storage (Transactions) @@ -22,20 +22,15 @@ traits: validate: !include raml-util/traits/validation.raml resourceTypes: - collection: !include raml-util/rtypes/collection.raml - collection-item: !include raml-util/rtypes/item-collection.raml + collection-get: !include raml-util/rtypes/collection-get.raml + collection-item-get: !include raml-util/rtypes/item-collection-get-with-json-response.raml /finance-storage/transactions: type: - collection: + collection-get: exampleCollection: !include acq-models/mod-finance/examples/transaction_collection.sample - exampleItem: !include acq-models/mod-finance/examples/transaction.sample schemaCollection: transaction-collection - schemaItem: transaction - post: - description: Deprecated for encumbrances, pending payments, payments and credits - use batch-all-or-nothing instead. - is: [validate] get: description: Get list of transactions is: [ @@ -48,12 +43,9 @@ resourceTypes: description: The UUID of a transaction type: UUID type: - collection-item: + collection-item-get: exampleItem: !include acq-models/mod-finance/examples/transaction.sample schema: transaction - put: - description: Deprecated for encumbrances, pending payments, payments and credits - use batch-all-or-nothing instead. - is: [validate] /batch-all-or-nothing: displayName: Batch processing of transactions post: diff --git a/src/main/java/org/folio/builders/error/NameCodeConstraintErrorBuilder.java b/src/main/java/org/folio/builders/error/NameCodeConstraintErrorBuilder.java index 1fc8aa89..9bd50a2b 100644 --- a/src/main/java/org/folio/builders/error/NameCodeConstraintErrorBuilder.java +++ b/src/main/java/org/folio/builders/error/NameCodeConstraintErrorBuilder.java @@ -7,7 +7,7 @@ import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.pgclient.PgException; import org.folio.rest.persist.HelperUtils; import org.folio.rest.persist.PgExceptionUtil; @@ -20,9 +20,8 @@ public class NameCodeConstraintErrorBuilder { public HttpException buildException(AsyncResult reply, Class clazz ) { Throwable cause = reply.cause(); - if (cause instanceof PgException && "23F09".equals(((PgException)cause).getCode())) { - String message = MessageFormat.format(ErrorCodes.CONFLICT.getDescription(), ((PgException)cause).getTable(), - ((PgException)cause).getErrorMessage()); + if (cause instanceof PgException pgEx && PgExceptionUtil.isVersionConflict(pgEx)) { + String message = MessageFormat.format(ErrorCodes.CONFLICT.getDescription(), pgEx.getTable(), pgEx.getErrorMessage()); return new HttpException(Response.Status.CONFLICT.getStatusCode(), message); } String error = convertExceptionToStringError(reply, clazz); diff --git a/src/main/java/org/folio/config/DAOConfiguration.java b/src/main/java/org/folio/config/DAOConfiguration.java index 96b35c9b..e862bf1e 100644 --- a/src/main/java/org/folio/config/DAOConfiguration.java +++ b/src/main/java/org/folio/config/DAOConfiguration.java @@ -16,19 +16,10 @@ import org.folio.dao.rollover.RolloverBudgetDAO; import org.folio.dao.rollover.RolloverErrorDAO; import org.folio.dao.rollover.RolloverProgressDAO; -import org.folio.dao.summary.InvoiceTransactionSummaryDAO; -import org.folio.dao.summary.OrderTransactionSummaryDAO; -import org.folio.dao.summary.TransactionSummaryDao; import org.folio.dao.transactions.BatchTransactionDAO; import org.folio.dao.transactions.BatchTransactionPostgresDAO; -import org.folio.dao.transactions.DefaultTransactionDAO; -import org.folio.dao.transactions.EncumbranceDAO; -import org.folio.dao.transactions.PaymentCreditDAO; -import org.folio.dao.transactions.PendingPaymentDAO; -import org.folio.dao.transactions.TemporaryEncumbranceTransactionDAO; -import org.folio.dao.transactions.TemporaryInvoiceTransactionDAO; -import org.folio.dao.transactions.TemporaryOrderTransactionDAO; -import org.folio.dao.transactions.TransactionDAO; +import org.folio.dao.transactions.TemporaryEncumbranceDAO; +import org.folio.dao.transactions.TemporaryEncumbrancePostgresDAO; import org.springframework.context.annotation.Bean; public class DAOConfiguration { @@ -54,48 +45,8 @@ public LedgerDAO ledgerDAO() { } @Bean - public TransactionSummaryDao invoiceTransactionSummaryDao() { - return new InvoiceTransactionSummaryDAO(); - } - - @Bean - public TransactionSummaryDao orderTransactionSummaryDao() { - return new OrderTransactionSummaryDAO(); - } - - @Bean - public TemporaryInvoiceTransactionDAO temporaryInvoiceTransactionDAO() { - return new TemporaryInvoiceTransactionDAO(); - } - - @Bean - public TemporaryOrderTransactionDAO temporaryOrderTransactionDAO() { - return new TemporaryOrderTransactionDAO(); - } - - @Bean - public TemporaryEncumbranceTransactionDAO temporaryEncumbranceTransactionDAO() { - return new TemporaryEncumbranceTransactionDAO(); - } - - @Bean - public TransactionDAO encumbranceDAO() { - return new EncumbranceDAO(); - } - - @Bean - public TransactionDAO paymentCreditDAO() { - return new PaymentCreditDAO(); - } - - @Bean - public TransactionDAO pendingPaymentDAO() { - return new PendingPaymentDAO(); - } - - @Bean - public TransactionDAO defaultTransactionDAO() { - return new DefaultTransactionDAO(); + public TemporaryEncumbranceDAO temporaryEncumbranceDAO() { + return new TemporaryEncumbrancePostgresDAO(); } @Bean diff --git a/src/main/java/org/folio/config/ServicesConfiguration.java b/src/main/java/org/folio/config/ServicesConfiguration.java index 0582d1dc..a6943119 100644 --- a/src/main/java/org/folio/config/ServicesConfiguration.java +++ b/src/main/java/org/folio/config/ServicesConfiguration.java @@ -12,12 +12,8 @@ import org.folio.dao.rollover.RolloverBudgetDAO; import org.folio.dao.rollover.RolloverErrorDAO; import org.folio.dao.rollover.RolloverProgressDAO; -import org.folio.dao.summary.TransactionSummaryDao; import org.folio.dao.transactions.BatchTransactionDAO; -import org.folio.dao.transactions.TemporaryInvoiceTransactionDAO; -import org.folio.dao.transactions.TemporaryOrderTransactionDAO; -import org.folio.dao.transactions.TemporaryEncumbranceTransactionDAO; -import org.folio.dao.transactions.TransactionDAO; +import org.folio.dao.transactions.TemporaryEncumbranceDAO; import org.folio.rest.core.RestClient; import org.folio.rest.persist.DBClientFactory; import org.folio.service.PostgresFunctionExecutionService; @@ -35,21 +31,7 @@ import org.folio.service.rollover.RolloverErrorService; import org.folio.service.rollover.RolloverProgressService; import org.folio.service.rollover.RolloverValidationService; -import org.folio.service.summary.EncumbranceTransactionSummaryService; -import org.folio.service.summary.PaymentCreditTransactionSummaryService; -import org.folio.service.summary.PendingPaymentTransactionSummaryService; -import org.folio.service.summary.TransactionSummaryService; -import org.folio.service.transactions.AllOrNothingTransactionService; -import org.folio.service.transactions.AllocationService; -import org.folio.service.transactions.DefaultTransactionService; -import org.folio.service.transactions.EncumbranceService; -import org.folio.service.transactions.PaymentCreditService; -import org.folio.service.transactions.PendingPaymentService; -import org.folio.service.transactions.TransactionManagingStrategy; -import org.folio.service.transactions.TransactionManagingStrategyFactory; -import org.folio.service.transactions.TransactionService; -import org.folio.service.transactions.TemporaryTransactionService; -import org.folio.service.transactions.TransferService; +import org.folio.service.transactions.TemporaryEncumbranceService; import org.folio.service.transactions.batch.BatchAllocationService; import org.folio.service.transactions.batch.BatchEncumbranceService; import org.folio.service.transactions.batch.BatchPaymentCreditService; @@ -57,13 +39,6 @@ import org.folio.service.transactions.batch.BatchTransactionService; import org.folio.service.transactions.batch.BatchTransactionServiceInterface; import org.folio.service.transactions.batch.BatchTransferService; -import org.folio.service.transactions.cancel.CancelPaymentCreditService; -import org.folio.service.transactions.cancel.CancelPendingPaymentService; -import org.folio.service.transactions.cancel.CancelTransactionService; -import org.folio.service.transactions.restriction.EncumbranceRestrictionService; -import org.folio.service.transactions.restriction.PaymentCreditRestrictionService; -import org.folio.service.transactions.restriction.PendingPaymentRestrictionService; -import org.folio.service.transactions.restriction.TransactionRestrictionService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -89,134 +64,11 @@ public LedgerService ledgerService(LedgerDAO ledgerDAO, FundService fundService) return new StorageLedgerService(ledgerDAO, fundService); } - @Bean - public TransactionSummaryService encumbranceSummaryService(@Qualifier("orderTransactionSummaryDao") TransactionSummaryDao orderTransactionSummaryDao) { - return new EncumbranceTransactionSummaryService(orderTransactionSummaryDao); - } - - @Bean - public TransactionSummaryService paymentCreditSummaryService(@Qualifier("invoiceTransactionSummaryDao") TransactionSummaryDao invoiceTransactionSummaryDao) { - return new PaymentCreditTransactionSummaryService(invoiceTransactionSummaryDao); - } - - @Bean - public TransactionSummaryService pendingPaymentSummaryService(@Qualifier("invoiceTransactionSummaryDao") TransactionSummaryDao invoiceTransactionSummaryDao) { - return new PendingPaymentTransactionSummaryService(invoiceTransactionSummaryDao); - } - - @Bean - public TransactionRestrictionService encumbranceRestrictionService(BudgetService budgetService, LedgerService ledgerService) { - return new EncumbranceRestrictionService(budgetService, ledgerService); - } - - @Bean - public TransactionRestrictionService pendingPaymentRestrictionService(BudgetService budgetService, LedgerService ledgerService, - @Qualifier("pendingPaymentDAO") TransactionDAO pendingPaymentDAO) { - return new PendingPaymentRestrictionService(budgetService, ledgerService, pendingPaymentDAO); - } - - @Bean - public TransactionRestrictionService paymentCreditRestrictionService(BudgetService budgetService, LedgerService ledgerService, - @Qualifier("paymentCreditDAO") TransactionDAO paymentCreditDAO) { - return new PaymentCreditRestrictionService(budgetService, ledgerService, paymentCreditDAO); - } - @Bean public DBClientFactory dbClientFactory() { return new DBClientFactory(); } - @Bean - public AllOrNothingTransactionService allOrNothingEncumbranceService(@Qualifier("encumbranceDAO") TransactionDAO encumbranceDAO, - TemporaryOrderTransactionDAO orderTransactionSummaryDao, - EncumbranceTransactionSummaryService encumbranceSummaryService, - EncumbranceRestrictionService encumbranceRestrictionService) { - return new AllOrNothingTransactionService(encumbranceDAO, orderTransactionSummaryDao, encumbranceSummaryService, - encumbranceRestrictionService); - } - - @Bean - public AllOrNothingTransactionService allOrNothingPaymentCreditService(@Qualifier("paymentCreditDAO") TransactionDAO paymentCreditDAO, - TemporaryInvoiceTransactionDAO temporaryInvoiceTransactionDAO, - PaymentCreditTransactionSummaryService paymentCreditSummaryService, - PaymentCreditRestrictionService paymentCreditRestrictionService) { - return new AllOrNothingTransactionService(paymentCreditDAO, temporaryInvoiceTransactionDAO, paymentCreditSummaryService, - paymentCreditRestrictionService); - } - - @Bean - public AllOrNothingTransactionService allOrNothingPendingPaymentService(@Qualifier("pendingPaymentDAO") TransactionDAO pendingPaymentDAO, - TemporaryInvoiceTransactionDAO temporaryInvoiceTransactionDAO, - PendingPaymentTransactionSummaryService pendingPaymentSummaryService, - PendingPaymentRestrictionService pendingPaymentRestrictionService) { - return new AllOrNothingTransactionService(pendingPaymentDAO, temporaryInvoiceTransactionDAO, pendingPaymentSummaryService, - pendingPaymentRestrictionService); - } - - @Bean - public TransactionService pendingPaymentService(@Qualifier("allOrNothingPendingPaymentService") AllOrNothingTransactionService allOrNothingPendingPaymentService, - @Qualifier("pendingPaymentDAO") TransactionDAO pendingPaymentDAO, - BudgetService budgetService, - @Qualifier("cancelPendingPaymentService") CancelTransactionService cancelPendingPaymentService) { - - return new PendingPaymentService(allOrNothingPendingPaymentService, pendingPaymentDAO, budgetService, cancelPendingPaymentService); - } - - @Bean - public CancelTransactionService cancelPendingPaymentService(BudgetService budgetService, - @Qualifier("paymentCreditDAO") TransactionDAO paymentCreditDAO, - @Qualifier("encumbranceDAO") TransactionDAO encumbranceDAO) { - - return new CancelPendingPaymentService(budgetService, paymentCreditDAO, encumbranceDAO); - } - - @Bean - public TransactionService paymentCreditService(@Qualifier("allOrNothingPaymentCreditService") AllOrNothingTransactionService allOrNothingPaymentCreditService, - BudgetService budgetService, - @Qualifier("paymentCreditDAO") TransactionDAO paymentCreditDAO, - @Qualifier("cancelPaymentCreditService") CancelTransactionService cancelPaymentCreditService) { - - return new PaymentCreditService(allOrNothingPaymentCreditService, paymentCreditDAO, budgetService, cancelPaymentCreditService); - } - - @Bean - public CancelTransactionService cancelPaymentCreditService(BudgetService budgetService, - @Qualifier("paymentCreditDAO") TransactionDAO paymentCreditDAO, - @Qualifier("encumbranceDAO") TransactionDAO encumbranceDAO) { - - return new CancelPaymentCreditService(budgetService, paymentCreditDAO, encumbranceDAO); - } - - @Bean - public TransactionService encumbranceService(@Qualifier("allOrNothingEncumbranceService") AllOrNothingTransactionService allOrNothingEncumbranceService, - @Qualifier("encumbranceDAO") TransactionDAO encumbranceDAO, - BudgetService budgetService) { - - return new EncumbranceService(allOrNothingEncumbranceService, encumbranceDAO, budgetService); - } - - @Bean - public TransactionService allocationService(BudgetService budgetService, - @Qualifier("defaultTransactionDAO") TransactionDAO defaultTransactionDAO) { - return new AllocationService(budgetService, defaultTransactionDAO); - } - - @Bean - public TransactionService transferService(BudgetService budgetService, - @Qualifier("defaultTransactionDAO") TransactionDAO defaultTransactionDAO) { - return new TransferService(budgetService, defaultTransactionDAO); - } - - @Bean - public TransactionManagingStrategyFactory transactionManagingStrategyFactory(Set transactionServices) { - return new TransactionManagingStrategyFactory(transactionServices); - } - - @Bean - public DefaultTransactionService defaultTransactionService(@Qualifier("defaultTransactionDAO") TransactionDAO defaultTransactionDAO) { - return new DefaultTransactionService(defaultTransactionDAO); - } - @Bean public BatchAllocationService batchAllocationService() { return new BatchAllocationService(); @@ -303,12 +155,13 @@ BudgetExpenseClassService budgetExpenseClassService(BudgetExpenseClassDAO budget } @Bean - TemporaryTransactionService temporaryTransactionService(TemporaryEncumbranceTransactionDAO temporaryEncumbranceTransactionDAO) { - return new TemporaryTransactionService(temporaryEncumbranceTransactionDAO); + TemporaryEncumbranceService temporaryEncumbranceService(TemporaryEncumbranceDAO temporaryEncumbranceDAO) { + return new TemporaryEncumbranceService(temporaryEncumbranceDAO); } @Bean - RolloverBudgetExpenseClassTotalsService rolloverBudgetExpenseClassTotalsService(BudgetExpenseClassService budgetExpenseClassService, TemporaryTransactionService temporaryTransactionService) { - return new RolloverBudgetExpenseClassTotalsService(budgetExpenseClassService, temporaryTransactionService); + RolloverBudgetExpenseClassTotalsService rolloverBudgetExpenseClassTotalsService(BudgetExpenseClassService budgetExpenseClassService, + TemporaryEncumbranceService temporaryEncumbranceService) { + return new RolloverBudgetExpenseClassTotalsService(budgetExpenseClassService, temporaryEncumbranceService); } } diff --git a/src/main/java/org/folio/dao/budget/BudgetPostgresDAO.java b/src/main/java/org/folio/dao/budget/BudgetPostgresDAO.java index c2ea9928..8fd8dc93 100644 --- a/src/main/java/org/folio/dao/budget/BudgetPostgresDAO.java +++ b/src/main/java/org/folio/dao/budget/BudgetPostgresDAO.java @@ -20,7 +20,7 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.sqlclient.Tuple; public class BudgetPostgresDAO implements BudgetDAO { diff --git a/src/main/java/org/folio/dao/fiscalyear/FiscalYearPostgresDAO.java b/src/main/java/org/folio/dao/fiscalyear/FiscalYearPostgresDAO.java index 05481d8b..ac542d3f 100644 --- a/src/main/java/org/folio/dao/fiscalyear/FiscalYearPostgresDAO.java +++ b/src/main/java/org/folio/dao/fiscalyear/FiscalYearPostgresDAO.java @@ -5,7 +5,7 @@ import io.vertx.core.Future; import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.FiscalYear; diff --git a/src/main/java/org/folio/dao/fund/FundPostgresDAO.java b/src/main/java/org/folio/dao/fund/FundPostgresDAO.java index 22ab207e..b12390d6 100644 --- a/src/main/java/org/folio/dao/fund/FundPostgresDAO.java +++ b/src/main/java/org/folio/dao/fund/FundPostgresDAO.java @@ -5,7 +5,7 @@ import java.util.Collections; import java.util.List; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.Fund; diff --git a/src/main/java/org/folio/dao/ledger/LedgerPostgresDAO.java b/src/main/java/org/folio/dao/ledger/LedgerPostgresDAO.java index a8e86435..d0acdf68 100644 --- a/src/main/java/org/folio/dao/ledger/LedgerPostgresDAO.java +++ b/src/main/java/org/folio/dao/ledger/LedgerPostgresDAO.java @@ -2,7 +2,7 @@ import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.rest.jaxrs.model.Ledger; diff --git a/src/main/java/org/folio/dao/summary/BaseTransactionSummaryDAO.java b/src/main/java/org/folio/dao/summary/BaseTransactionSummaryDAO.java deleted file mode 100644 index 201a7b7a..00000000 --- a/src/main/java/org/folio/dao/summary/BaseTransactionSummaryDAO.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.folio.dao.summary; - -import io.vertx.core.AsyncResult; -import static org.folio.rest.persist.HelperUtils.ID_FIELD_NAME; -import static org.folio.rest.util.ResponseUtils.handleFailure; -import static org.folio.service.transactions.AllOrNothingTransactionService.TRANSACTION_SUMMARY_NOT_FOUND_FOR_TRANSACTION; - -import javax.ws.rs.core.Response; - -import io.vertx.ext.web.handler.HttpException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.rest.persist.DBConn; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; - -public abstract class BaseTransactionSummaryDAO implements TransactionSummaryDao { - - private static final Logger logger = LogManager.getLogger(BaseTransactionSummaryDAO.class); - - @Override - public Future getSummaryById(String summaryId, DBConn conn) { - logger.debug("Trying to get summary by id {}", summaryId); - return conn.getById(getTableName(), summaryId) - .transform(reply -> processGetResult(summaryId, reply)); - } - - @Override - public Future getSummaryByIdWithLocking(String summaryId, DBConn conn) { - logger.debug("Trying to get summary with locking by id {}", summaryId); - return conn.getByIdForUpdate(getTableName(), summaryId) - .transform(reply -> processGetResult(summaryId, reply)); - } - - private Future processGetResult(String summaryId, AsyncResult reply) { - if (reply.failed()) { - logger.error("Summary retrieval with id={} failed", summaryId, reply.cause()); - return Future.future(promise -> handleFailure(promise, reply)); - } else { - final JsonObject summary = reply.result(); - - if (summary == null) { - logger.warn("Transaction summary with id {} not found for transaction", summaryId, reply.cause()); - return Future.failedFuture(new HttpException(Response.Status.NOT_FOUND.getStatusCode(), TRANSACTION_SUMMARY_NOT_FOUND_FOR_TRANSACTION)); - } else { - logger.info("Summary with id {} successfully extracted", summaryId); - return Future.succeededFuture(summary); - } - } - } - - @Override - public Future createSummary(JsonObject summary, DBConn conn) { - String id = summary.getString(ID_FIELD_NAME); - logger.debug("Trying to create summary in transaction by id {}", id); - return conn.save(getTableName(), id, summary) - .onSuccess(v -> logger.info("Summary with id {} successfully created", id)) - .onFailure(e -> logger.error("Summary creation with id {} failed", id, e)) - .mapEmpty(); - } - - @Override - public Future updateSummary(JsonObject summary, DBConn conn) { - String id = summary.getString(ID_FIELD_NAME); - logger.debug("Trying to update summary in transaction by id {}", id); - return conn.update(getTableName(), summary, id) - .onSuccess(v -> logger.info("Summary with id {} successfully updated", id)) - .onFailure(e -> logger.error("Summary update with id {} failed", id, e)) - .mapEmpty(); - } - - protected abstract String getTableName(); - -} diff --git a/src/main/java/org/folio/dao/summary/InvoiceTransactionSummaryDAO.java b/src/main/java/org/folio/dao/summary/InvoiceTransactionSummaryDAO.java deleted file mode 100644 index 58f9bbb1..00000000 --- a/src/main/java/org/folio/dao/summary/InvoiceTransactionSummaryDAO.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.folio.dao.summary; - -public class InvoiceTransactionSummaryDAO extends BaseTransactionSummaryDAO implements TransactionSummaryDao { - - public static final String INVOICE_TRANSACTION_SUMMARIES = "invoice_transaction_summaries"; - - @Override - protected String getTableName() { - return INVOICE_TRANSACTION_SUMMARIES; - } -} diff --git a/src/main/java/org/folio/dao/summary/OrderTransactionSummaryDAO.java b/src/main/java/org/folio/dao/summary/OrderTransactionSummaryDAO.java deleted file mode 100644 index f0b209ad..00000000 --- a/src/main/java/org/folio/dao/summary/OrderTransactionSummaryDAO.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.folio.dao.summary; - -public class OrderTransactionSummaryDAO extends BaseTransactionSummaryDAO implements TransactionSummaryDao { - - public static final String ORDER_TRANSACTION_SUMMARIES = "order_transaction_summaries"; - - @Override - protected String getTableName() { - return ORDER_TRANSACTION_SUMMARIES; - } - -} diff --git a/src/main/java/org/folio/dao/summary/TransactionSummaryDao.java b/src/main/java/org/folio/dao/summary/TransactionSummaryDao.java deleted file mode 100644 index 65e2edcf..00000000 --- a/src/main/java/org/folio/dao/summary/TransactionSummaryDao.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.folio.dao.summary; - -import io.vertx.core.json.JsonObject; - -import io.vertx.core.Future; -import org.folio.rest.persist.DBConn; - -public interface TransactionSummaryDao { - - Future getSummaryById(String summaryId, DBConn conn); - - Future getSummaryByIdWithLocking(String summaryId, DBConn conn); - - Future createSummary(JsonObject summary, DBConn conn); - - Future updateSummary(JsonObject summary, DBConn conn); -} diff --git a/src/main/java/org/folio/dao/transactions/BaseTemporaryTransactionsDAO.java b/src/main/java/org/folio/dao/transactions/BaseTemporaryTransactionsDAO.java deleted file mode 100644 index 65e63c21..00000000 --- a/src/main/java/org/folio/dao/transactions/BaseTemporaryTransactionsDAO.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.rest.persist.PostgresClient.pojo2JsonObject; - -import io.vertx.sqlclient.SqlResult; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.interfaces.Results; - -import java.util.List; -import java.util.UUID; - -import javax.ws.rs.core.Response; - -import io.vertx.ext.web.handler.HttpException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.Criteria.Criterion; - -import io.vertx.core.Future; -import io.vertx.sqlclient.Tuple; - -public abstract class BaseTemporaryTransactionsDAO implements TemporaryTransactionDAO { - - private static final Logger logger = LogManager.getLogger(BaseTemporaryTransactionsDAO.class); - - private final String tableName; - - protected BaseTemporaryTransactionsDAO(String tableName) { - this.tableName = tableName; - } - - @Override - public Future createTempTransaction(Transaction transaction, String summaryId, String tenantId, DBConn conn) { - logger.debug("Trying to create temp transaction"); - if (transaction.getId() == null) { - transaction.setId(UUID.randomUUID().toString()); - } - try { - return conn.execute(createTempTransactionQuery(tenantId), - Tuple.of(UUID.fromString(transaction.getId()), pojo2JsonObject(transaction))) - .map(transaction) - .onSuccess(x -> logger.info("New transaction with id {} successfully created", transaction.getId())) - .onFailure(e -> logger.error("Transaction creation with id {} failed", transaction.getId(), e)); - } catch (Exception e) { - logger.error("Creating new temp transaction with id {} failed", transaction.getId(), e); - return Future.failedFuture(new HttpException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e.getMessage())); - } - } - - @Override - public Future> getTempTransactions(Criterion criterion, DBConn conn) { - logger.debug("Trying to get temp transactions by query: {}", criterion); - return conn.get(tableName, Transaction.class, criterion, false) - .map(Results::getResults) - .onFailure(e -> logger.error("Failed to extract temporary transaction by criteria = {}", criterion, e)); - } - - @Override - public Future> getTempTransactionsBySummaryId(String summaryId, DBConn conn) { - logger.debug("Trying to temp transactions by summaryid {}", summaryId); - return getTempTransactions(getSummaryIdCriteria(summaryId), conn); - } - - public Future deleteTempTransactions(String summaryId, DBConn conn) { - logger.debug("Trying to delete temp transactions by summaryid {}", summaryId); - Criterion criterion = getSummaryIdCriteria(summaryId); - - return conn.delete(getTableName(), criterion) - .map(SqlResult::rowCount) - .onSuccess(rowCount -> logger.info("Successfully deleted {} temp transactions by summaryid {}", rowCount, summaryId)) - .onFailure(e -> logger.error("Deleting temp transactions by summaryid {} failed", summaryId, e)); - } - - public String getTableName() { - return tableName; - } - - protected abstract String createTempTransactionQuery(String tenantId); - - protected abstract Criterion getSummaryIdCriteria(String summaryId); - -} diff --git a/src/main/java/org/folio/dao/transactions/BaseTransactionDAO.java b/src/main/java/org/folio/dao/transactions/BaseTransactionDAO.java deleted file mode 100644 index 36d6958f..00000000 --- a/src/main/java/org/folio/dao/transactions/BaseTransactionDAO.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.dao.transactions.EncumbranceDAO.TRANSACTIONS_TABLE; -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import io.vertx.sqlclient.SqlResult; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.interfaces.Results; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.sqlclient.Tuple; - -public abstract class BaseTransactionDAO implements TransactionDAO { - - private static final Logger logger = LogManager.getLogger(BaseTransactionDAO.class); - - public static final String INSERT_PERMANENT_TRANSACTIONS_BY_IDS = "INSERT INTO %s (id, jsonb) (SELECT id, jsonb FROM %s WHERE id in (%s)) " - + "ON CONFLICT DO NOTHING;"; - - @Override - public Future> getTransactions(Criterion criterion, DBConn conn) { - logger.debug("Trying to get transactions by query: {}", criterion); - return conn.get(TRANSACTIONS_TABLE, Transaction.class, criterion) - .map(Results::getResults) - .onSuccess(transactions -> logger.info("Successfully retrieved {} transactions", transactions.size())) - .onFailure(e -> logger.error("Getting transactions failed", e)); - } - - @Override - public Future> getTransactions(List ids, DBConn conn) { - logger.debug("Trying to get transactions by ids = {}", ids); - if (ids.isEmpty()) { - return Future.succeededFuture(Collections.emptyList()); - } - CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); - ids.forEach(id -> criterionBuilder.with("id", id)); - return getTransactions(criterionBuilder.build(), conn); - } - - @Override - public Future saveTransactionsToPermanentTable(String summaryId, DBConn conn) { - logger.debug("Trying to save transactions to permanent table with summaryid {}", summaryId); - return conn.execute(createPermanentTransactionsQuery(conn.getTenantId()), Tuple.of(UUID.fromString(summaryId))) - .map(SqlResult::rowCount) - .onSuccess(rowCount -> logger.info("Successfully saved {} transactions to permanent table with summaryid {}", - rowCount, summaryId)) - .onFailure(e -> logger.error("Saving transactions to permanent table with summaryid {} failed", summaryId, e)); - } - - @Override - public Future saveTransactionsToPermanentTable(List ids, DBConn conn) { - logger.debug("Trying to save transactions to permanent table by ids = {}", ids); - return conn.execute(createPermanentTransactionsQuery(conn.getTenantId(), ids)) - .map(SqlResult::rowCount) - .onSuccess(rowCount -> logger.info("Successfully saved {} transactions to permanent table with ids = {}", - rowCount, ids)) - .onFailure(e -> logger.error("Save transactions to permanent table by ids = {} failed", ids, e)); - } - - protected abstract String createPermanentTransactionsQuery(String tenantId); - - protected String createPermanentTransactionsQuery(String tenantId, List ids) { - String idsAsString = ids.stream() - .map(id -> StringUtils.wrap(id, "'")) - .collect(Collectors.joining(",")); - return String.format(INSERT_PERMANENT_TRANSACTIONS_BY_IDS, getFullTableName(tenantId, TRANSACTIONS_TABLE), getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS), idsAsString); - } - - @Override - public Future updatePermanentTransactions(List transactions, DBConn conn) { - logger.debug("Trying to update permanent transactions"); - if (transactions.isEmpty()) { - return Future.succeededFuture(); - } - List ids = transactions.stream().map(Transaction::getId).toList(); - List jsonTransactions = transactions.stream().map(JsonObject::mapFrom).collect(Collectors.toList()); - String sql = buildUpdatePermanentTransactionQuery(jsonTransactions, conn.getTenantId()); - return conn.execute(sql) - .onSuccess(rowSet -> logger.info("updatePermanentTransactions:: success updating permanent transactions, ids = {}", - ids)) - .onFailure(t -> logger.error("updatePermanentTransactions:: failed updating permanent transactions, ids = {}", - ids, t)) - .mapEmpty(); - } - - @Override - public Future deleteTransactions(Criterion criterion, DBConn conn) { - logger.debug("Trying to delete transactions by query: {}", criterion); - return conn.delete(TRANSACTIONS_TABLE, criterion) - .onSuccess(rowSet -> logger.info("deleteTransactions:: success deleting {} transactions", rowSet.rowCount())) - .onFailure(t -> logger.error("deleteTransactions:: failed deleting transactions, criterion = {}", criterion, t)) - .mapEmpty(); - } - - @Override - public Future createTransaction(Transaction transaction, DBConn conn) { - logger.debug("createTransaction:: Trying to create transaction"); - if (StringUtils.isEmpty(transaction.getId())) { - transaction.setId(UUID.randomUUID().toString()); - } - return conn.saveAndReturnUpdatedEntity(TRANSACTIONS_TABLE, transaction.getId(), transaction) - .onSuccess(id -> logger.info("createTransaction:: Transaction with id {} successfully created", id)) - .onFailure(t -> logger.error("createTransaction:: Creating transaction with id {} failed", transaction.getId(), t)); - } - - @Override - public Future deleteTransactionById(String id, DBConn conn) { - logger.debug("Trying to delete transaction by id {}", id); - return conn.delete(TRANSACTIONS_TABLE, id) - .onSuccess(s -> logger.info("Successfully deleted a transaction with id {}", id)) - .onFailure(t -> logger.error("Deleting transaction by id {} failed", id, t)) - .mapEmpty(); - } - - @Override - public Future updateTransaction(Transaction transaction, DBConn conn) { - logger.debug("updateTransaction:: Trying to update transaction with id {}", transaction.getId()); - return conn.update(TRANSACTIONS_TABLE, transaction, transaction.getId()) - .onSuccess(rowSet -> logger.info("updateTransaction:: Transaction with id {} successfully updated", transaction.getId())) - .onFailure(t -> logger.error("updateTransaction:: Updating transaction with id {} failed", transaction.getId(), t)) - .mapEmpty(); - } - - protected abstract String buildUpdatePermanentTransactionQuery(List jsonTransactions, String tenantId); -} diff --git a/src/main/java/org/folio/dao/transactions/BatchTransactionDAO.java b/src/main/java/org/folio/dao/transactions/BatchTransactionDAO.java index e8ece39e..25596dbe 100644 --- a/src/main/java/org/folio/dao/transactions/BatchTransactionDAO.java +++ b/src/main/java/org/folio/dao/transactions/BatchTransactionDAO.java @@ -8,6 +8,8 @@ import java.util.List; public interface BatchTransactionDAO { + String TRANSACTIONS_TABLE = "transaction"; + Future> getTransactionsByCriterion(Criterion criterion, DBConn conn); Future> getTransactionsByIds(List ids, DBConn conn); Future createTransactions(List transactions, DBConn conn); diff --git a/src/main/java/org/folio/dao/transactions/BatchTransactionPostgresDAO.java b/src/main/java/org/folio/dao/transactions/BatchTransactionPostgresDAO.java index ee0dec52..206b183b 100644 --- a/src/main/java/org/folio/dao/transactions/BatchTransactionPostgresDAO.java +++ b/src/main/java/org/folio/dao/transactions/BatchTransactionPostgresDAO.java @@ -14,7 +14,6 @@ public class BatchTransactionPostgresDAO implements BatchTransactionDAO { private static final Logger logger = LogManager.getLogger(); - public static final String TRANSACTIONS_TABLE = "transaction"; @Override public Future> getTransactionsByCriterion(Criterion criterion, DBConn conn) { diff --git a/src/main/java/org/folio/dao/transactions/DefaultTransactionDAO.java b/src/main/java/org/folio/dao/transactions/DefaultTransactionDAO.java deleted file mode 100644 index 3c7938e0..00000000 --- a/src/main/java/org/folio/dao/transactions/DefaultTransactionDAO.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.folio.dao.transactions; - -import io.vertx.core.json.JsonObject; - -import java.util.List; - -public class DefaultTransactionDAO extends BaseTransactionDAO { - @Override - protected String createPermanentTransactionsQuery(String tenantId) { - return null; - } - - @Override - protected String buildUpdatePermanentTransactionQuery(List transactions, String tenantId) { - return null; - } -} diff --git a/src/main/java/org/folio/dao/transactions/EncumbranceDAO.java b/src/main/java/org/folio/dao/transactions/EncumbranceDAO.java deleted file mode 100644 index 065a3afe..00000000 --- a/src/main/java/org/folio/dao/transactions/EncumbranceDAO.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.folio.rest.persist.HelperUtils; - -import io.vertx.core.json.JsonObject; - -public class EncumbranceDAO extends BaseTransactionDAO implements TransactionDAO { - - private static final String TEMPORARY_ORDER_TRANSACTIONS = "temporary_order_transactions"; - public static final String TRANSACTIONS_TABLE = "transaction"; - - public static final String INSERT_PERMANENT_ENCUMBRANCES = "INSERT INTO %s (id, jsonb) (SELECT id, jsonb FROM %s WHERE encumbrance_sourcePurchaseOrderId = $1) " - + "ON CONFLICT DO NOTHING;"; - - @Override - protected String buildUpdatePermanentTransactionQuery(List transactions, String tenantId) { - return String.format("UPDATE %s AS transactions " + - "SET jsonb = t.jsonb FROM (VALUES %s) AS t (id, jsonb) " + - "WHERE t.id::uuid = transactions.id;", getFullTableName(tenantId, TRANSACTIONS_TABLE), HelperUtils.getQueryValues(transactions)); - } - - @Override - protected String createPermanentTransactionsQuery(String tenantId) { - return String.format(INSERT_PERMANENT_ENCUMBRANCES, getFullTableName(tenantId, TRANSACTIONS_TABLE), getFullTableName(tenantId, TEMPORARY_ORDER_TRANSACTIONS)); - } - - @Override - protected String createPermanentTransactionsQuery(String tenantId, List ids) { - String idsAsString = ids.stream() - .map(id -> StringUtils.wrap(id, "'")) - .collect(Collectors.joining(",")); - return String.format(INSERT_PERMANENT_TRANSACTIONS_BY_IDS, getFullTableName(tenantId, TRANSACTIONS_TABLE), getFullTableName(tenantId, TEMPORARY_ORDER_TRANSACTIONS), idsAsString); - } - -} diff --git a/src/main/java/org/folio/dao/transactions/PaymentCreditDAO.java b/src/main/java/org/folio/dao/transactions/PaymentCreditDAO.java deleted file mode 100644 index 55b92eb3..00000000 --- a/src/main/java/org/folio/dao/transactions/PaymentCreditDAO.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.dao.transactions.EncumbranceDAO.TRANSACTIONS_TABLE; -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import java.util.List; - -import org.folio.rest.persist.HelperUtils; - -import io.vertx.core.json.JsonObject; - -public class PaymentCreditDAO extends BaseTransactionDAO implements TransactionDAO { - - public static final String INSERT_PERMANENT_PAYMENTS_CREDITS = "INSERT INTO %s (id, jsonb) (SELECT id, jsonb FROM %s AS transactions WHERE sourceInvoiceId = $1 " + - "AND (transactions.jsonb ->> 'transactionType' = 'Payment' OR transactions.jsonb ->> 'transactionType' = 'Credit')) ON CONFLICT DO NOTHING;"; - - private static final String TEMPORARY_INVOICE_TRANSACTIONS = "temporary_invoice_transactions"; - - @Override - protected String createPermanentTransactionsQuery(String tenantId) { - return String.format(INSERT_PERMANENT_PAYMENTS_CREDITS, getFullTableName(tenantId, TRANSACTIONS_TABLE), getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS)); - } - - @Override - protected String buildUpdatePermanentTransactionQuery(List transactions, String tenantId) { - return String.format("UPDATE %s AS transactions " + - "SET jsonb = t.jsonb FROM (VALUES %s) AS t (id, jsonb) " + - "WHERE t.id::uuid = transactions.id;", getFullTableName(tenantId, TRANSACTIONS_TABLE), HelperUtils.getQueryValues(transactions)); - } - -} diff --git a/src/main/java/org/folio/dao/transactions/PendingPaymentDAO.java b/src/main/java/org/folio/dao/transactions/PendingPaymentDAO.java deleted file mode 100644 index 8b917eae..00000000 --- a/src/main/java/org/folio/dao/transactions/PendingPaymentDAO.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.dao.transactions.EncumbranceDAO.TRANSACTIONS_TABLE; -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import java.util.List; - -import org.folio.rest.persist.HelperUtils; - -import io.vertx.core.json.JsonObject; - -public class PendingPaymentDAO extends BaseTransactionDAO { - - public static final String INSERT_PERMANENT_PAYMENTS_CREDITS = "INSERT INTO %s (id, jsonb) (SELECT id, jsonb FROM %s AS transactions WHERE sourceInvoiceId = $1 " + - "AND transactions.jsonb ->> 'transactionType' = 'Pending payment') ON CONFLICT DO NOTHING;"; - - private static final String TEMPORARY_INVOICE_TRANSACTIONS = "temporary_invoice_transactions"; - - @Override - protected String createPermanentTransactionsQuery(String tenantId) { - return String.format(INSERT_PERMANENT_PAYMENTS_CREDITS, getFullTableName(tenantId, TRANSACTIONS_TABLE), getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS)); - } - - @Override - protected String buildUpdatePermanentTransactionQuery(List transactions, String tenantId) { - return String.format("UPDATE %s AS transactions " + - "SET jsonb = t.jsonb FROM (VALUES %s) AS t (id, jsonb) " + - "WHERE t.id::uuid = transactions.id;", getFullTableName(tenantId, TRANSACTIONS_TABLE), HelperUtils.getQueryValues(transactions)); - } -} diff --git a/src/main/java/org/folio/dao/transactions/TemporaryEncumbranceDAO.java b/src/main/java/org/folio/dao/transactions/TemporaryEncumbranceDAO.java new file mode 100644 index 00000000..df8d65cc --- /dev/null +++ b/src/main/java/org/folio/dao/transactions/TemporaryEncumbranceDAO.java @@ -0,0 +1,14 @@ +package org.folio.dao.transactions; + +import java.util.List; + +import org.folio.rest.jaxrs.model.Transaction; +import org.folio.rest.persist.Criteria.Criterion; + +import io.vertx.core.Future; +import org.folio.rest.persist.DBConn; + +public interface TemporaryEncumbranceDAO { + + Future> getTempTransactions(Criterion criterion, DBConn conn); +} diff --git a/src/main/java/org/folio/dao/transactions/TemporaryEncumbranceTransactionDAO.java b/src/main/java/org/folio/dao/transactions/TemporaryEncumbrancePostgresDAO.java similarity index 74% rename from src/main/java/org/folio/dao/transactions/TemporaryEncumbranceTransactionDAO.java rename to src/main/java/org/folio/dao/transactions/TemporaryEncumbrancePostgresDAO.java index 9fccaf08..1831890b 100644 --- a/src/main/java/org/folio/dao/transactions/TemporaryEncumbranceTransactionDAO.java +++ b/src/main/java/org/folio/dao/transactions/TemporaryEncumbrancePostgresDAO.java @@ -13,29 +13,14 @@ import java.util.List; import static java.lang.String.format; -import static org.apache.commons.lang3.StringUtils.EMPTY; -public class TemporaryEncumbranceTransactionDAO extends BaseTemporaryTransactionsDAO{ +public class TemporaryEncumbrancePostgresDAO implements TemporaryEncumbranceDAO { - private static final Logger logger = LogManager.getLogger(TemporaryEncumbranceTransactionDAO.class); + private static final Logger logger = LogManager.getLogger(TemporaryEncumbrancePostgresDAO.class); public static final String TEMPORARY_ENCUMBRANCE_TRANSACTIONS_TABLE = "tmp_encumbered_transactions"; private static final String TEMPORARY_ENCUMBRANCE_TRANSACTIONS_QUERY = "SELECT jsonb FROM tmp_encumbered_transactions WHERE %s "; - public TemporaryEncumbranceTransactionDAO() { - super(TEMPORARY_ENCUMBRANCE_TRANSACTIONS_TABLE); - } - - @Override - protected String createTempTransactionQuery(String tenantId) { - return EMPTY; - } - - @Override - protected Criterion getSummaryIdCriteria(String summaryId) { - return new Criterion(); - } - @Override public Future> getTempTransactions(Criterion criterion, DBConn conn) { logger.debug("Trying to get temp transactions by query: {}", criterion); diff --git a/src/main/java/org/folio/dao/transactions/TemporaryInvoiceTransactionDAO.java b/src/main/java/org/folio/dao/transactions/TemporaryInvoiceTransactionDAO.java deleted file mode 100644 index 97776a30..00000000 --- a/src/main/java/org/folio/dao/transactions/TemporaryInvoiceTransactionDAO.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.Criteria.Criterion; - -public class TemporaryInvoiceTransactionDAO extends BaseTemporaryTransactionsDAO implements TemporaryTransactionDAO { - - public static final String TEMPORARY_INVOICE_TRANSACTIONS = "temporary_invoice_transactions"; - - public static final String INSERT_TEMPORARY_TRANSACTIONS = "INSERT INTO %s (id, jsonb) VALUES ($1, $2) " + - "ON CONFLICT (concat_space_sql(lower(f_unaccent(jsonb->>'amount')), lower(f_unaccent(jsonb->>'fromFundId')), lower(f_unaccent(jsonb->>'sourceInvoiceId'))," + - " lower(f_unaccent(jsonb->>'sourceInvoiceLineId')), lower(f_unaccent(jsonb->>'toFundId')), lower(f_unaccent(jsonb->>'transactionType')), lower(f_unaccent(jsonb->>'expenseClassId'))))" + - " DO UPDATE SET id = excluded.id RETURNING id;"; - - - public TemporaryInvoiceTransactionDAO() { - super(TEMPORARY_INVOICE_TRANSACTIONS); - } - - - protected String createTempTransactionQuery(String tenantId) { - return String.format(INSERT_TEMPORARY_TRANSACTIONS, getFullTableName(tenantId, getTableName())); - } - - @Override - protected Criterion getSummaryIdCriteria(String summaryId) { - return new CriterionBuilder().with("sourceInvoiceId", summaryId).build(); - } - -} diff --git a/src/main/java/org/folio/dao/transactions/TemporaryOrderTransactionDAO.java b/src/main/java/org/folio/dao/transactions/TemporaryOrderTransactionDAO.java deleted file mode 100644 index 630d006e..00000000 --- a/src/main/java/org/folio/dao/transactions/TemporaryOrderTransactionDAO.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.Criteria.Criterion; - -public class TemporaryOrderTransactionDAO extends BaseTemporaryTransactionsDAO implements TemporaryTransactionDAO { - - private static final String TEMPORARY_ORDER_TRANSACTIONS = "temporary_order_transactions"; - - public static final String INSERT_TEMPORARY_ENCUMBRANCES = "INSERT INTO %s (id, jsonb) VALUES ($1, $2) " + - "ON CONFLICT (lower(f_unaccent(concat_space_sql(jsonb->>'amount', jsonb->>'fromFundId', " + - "jsonb->'encumbrance'->>'sourcePurchaseOrderId', jsonb->'encumbrance'->>'sourcePoLineId' , " + - "jsonb->'encumbrance'->>'initialAmountEncumbered', jsonb->'encumbrance'->>'status', jsonb->>'expenseClassId')))) " + - "DO UPDATE SET id = excluded.id RETURNING id;"; - - public TemporaryOrderTransactionDAO() { - super(TEMPORARY_ORDER_TRANSACTIONS); - } - - @Override - protected String createTempTransactionQuery(String tenantId) { - return String.format(INSERT_TEMPORARY_ENCUMBRANCES, getFullTableName(tenantId, getTableName())); - } - - @Override - public Criterion getSummaryIdCriteria(String summaryId) { - return new CriterionBuilder().with("encumbrance_sourcePurchaseOrderId", summaryId).build(); - } - -} diff --git a/src/main/java/org/folio/dao/transactions/TemporaryTransactionDAO.java b/src/main/java/org/folio/dao/transactions/TemporaryTransactionDAO.java deleted file mode 100644 index 6a87243a..00000000 --- a/src/main/java/org/folio/dao/transactions/TemporaryTransactionDAO.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.folio.dao.transactions; - -import java.util.List; - -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.Criteria.Criterion; - -import io.vertx.core.Future; -import org.folio.rest.persist.DBConn; - -public interface TemporaryTransactionDAO { - - Future createTempTransaction(Transaction transaction, String summaryId, String tenantId, DBConn conn); - Future> getTempTransactions(Criterion criterion, DBConn conn); - Future> getTempTransactionsBySummaryId(String summaryId, DBConn conn); - Future deleteTempTransactions(String summaryId, DBConn conn); -} diff --git a/src/main/java/org/folio/dao/transactions/TransactionDAO.java b/src/main/java/org/folio/dao/transactions/TransactionDAO.java deleted file mode 100644 index a51d343f..00000000 --- a/src/main/java/org/folio/dao/transactions/TransactionDAO.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.folio.dao.transactions; - -import java.util.List; - -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.Criteria.Criterion; - -import io.vertx.core.Future; -import org.folio.rest.persist.DBConn; - -public interface TransactionDAO { - - Future> getTransactions(Criterion criterion, DBConn conn); - - Future> getTransactions(List ids, DBConn conn); - - Future saveTransactionsToPermanentTable(String summaryId, DBConn conn); - - Future saveTransactionsToPermanentTable(List ids, DBConn conn); - - Future updatePermanentTransactions(List transactions, DBConn conn); - - Future deleteTransactions(Criterion build, DBConn conn); - - Future createTransaction(Transaction transaction, DBConn conn); - - Future deleteTransactionById(String id, DBConn conn); - - Future updateTransaction(Transaction transaction, DBConn conn); -} diff --git a/src/main/java/org/folio/rest/exception/HttpException.java b/src/main/java/org/folio/rest/exception/HttpException.java index 47cb9d1a..3d3ca912 100644 --- a/src/main/java/org/folio/rest/exception/HttpException.java +++ b/src/main/java/org/folio/rest/exception/HttpException.java @@ -1,12 +1,13 @@ package org.folio.rest.exception; -import java.util.Collections; - import org.apache.commons.lang3.StringUtils; import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Errors; +import org.folio.rest.jaxrs.model.Parameter; import org.folio.rest.util.ErrorCodes; +import java.util.List; + public class HttpException extends RuntimeException { private static final long serialVersionUID = 8109197948434861504L; @@ -17,14 +18,35 @@ public HttpException(int code, String message) { super(StringUtils.isNotEmpty(message) ? message : ErrorCodes.GENERIC_ERROR_CODE.getDescription()); this.code = code; this.errors = new Errors() - .withErrors(Collections.singletonList(new Error().withCode(ErrorCodes.GENERIC_ERROR_CODE.getCode()).withMessage(message))) + .withErrors(List.of(new Error().withCode(ErrorCodes.GENERIC_ERROR_CODE.getCode()).withMessage(message))) + .withTotalRecords(1); + } + + public HttpException(int code, String message, Throwable cause) { + super(message, cause); + this.code = code; + Parameter causeParam = new Parameter().withKey("cause").withValue(cause.getMessage()); + Error error = new Error() + .withCode(ErrorCodes.GENERIC_ERROR_CODE.getCode()) + .withMessage(message) + .withParameters(List.of(causeParam)); + this.errors = new Errors() + .withErrors(List.of(error)) + .withTotalRecords(1); + } + + public HttpException(int code, Throwable cause) { + super(cause.getMessage(), cause); + this.code = code; + this.errors = new Errors() + .withErrors(List.of(new Error().withCode(ErrorCodes.GENERIC_ERROR_CODE.getCode()).withMessage(cause.getMessage()))) .withTotalRecords(1); } public HttpException(int code, ErrorCodes errCodes) { super(errCodes.getDescription()); this.errors = new Errors() - .withErrors(Collections.singletonList(new Error().withCode(errCodes.getCode()).withMessage(errCodes.getDescription()))) + .withErrors(List.of(new Error().withCode(errCodes.getCode()).withMessage(errCodes.getDescription()))) .withTotalRecords(1); this.code = code; } @@ -33,7 +55,7 @@ public HttpException(int code, Error error) { super(error.getMessage()); this.code = code; this.errors = new Errors() - .withErrors(Collections.singletonList(error)) + .withErrors(List.of(error)) .withTotalRecords(1); } diff --git a/src/main/java/org/folio/rest/impl/FundAPI.java b/src/main/java/org/folio/rest/impl/FundAPI.java index e6326800..1ec16596 100644 --- a/src/main/java/org/folio/rest/impl/FundAPI.java +++ b/src/main/java/org/folio/rest/impl/FundAPI.java @@ -12,7 +12,7 @@ import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.logging.log4j.LogManager; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.Fund; diff --git a/src/main/java/org/folio/rest/impl/LedgerAPI.java b/src/main/java/org/folio/rest/impl/LedgerAPI.java index bc648fde..a5461377 100644 --- a/src/main/java/org/folio/rest/impl/LedgerAPI.java +++ b/src/main/java/org/folio/rest/impl/LedgerAPI.java @@ -18,7 +18,7 @@ import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/org/folio/rest/impl/LedgerRolloverAPI.java b/src/main/java/org/folio/rest/impl/LedgerRolloverAPI.java index 812327d4..9d88ffcb 100644 --- a/src/main/java/org/folio/rest/impl/LedgerRolloverAPI.java +++ b/src/main/java/org/folio/rest/impl/LedgerRolloverAPI.java @@ -10,7 +10,7 @@ import javax.ws.rs.core.Response; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; @@ -61,7 +61,7 @@ public void postFinanceStorageLedgerRollovers(LedgerFiscalYearRollover entity, M t = new HttpException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), t); } HttpException cause = (HttpException)t; - if (Response.Status.CONFLICT.getStatusCode() == cause.getStatusCode()) { + if (Response.Status.CONFLICT.getStatusCode() == cause.getCode()) { logger.error("Rollover already exists with id {}", entity.getId(), cause); } else { logger.error("Creating rollover with id {} failed", entity.getId(), cause); diff --git a/src/main/java/org/folio/rest/impl/TransactionAPI.java b/src/main/java/org/folio/rest/impl/TransactionAPI.java index 952b75b1..e930e1e1 100644 --- a/src/main/java/org/folio/rest/impl/TransactionAPI.java +++ b/src/main/java/org/folio/rest/impl/TransactionAPI.java @@ -1,10 +1,8 @@ package org.folio.rest.impl; -import static io.vertx.core.Future.succeededFuture; import static org.folio.rest.util.ResponseUtils.buildErrorResponse; import static org.folio.rest.util.ResponseUtils.buildNoContentResponse; -import static org.folio.rest.util.ResponseUtils.buildResponseWithLocation; -import static org.folio.service.transactions.AbstractTransactionService.TRANSACTION_TABLE; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; import java.util.Map; @@ -16,12 +14,8 @@ import org.folio.rest.jaxrs.model.Transaction; import org.folio.rest.jaxrs.model.TransactionCollection; import org.folio.rest.jaxrs.resource.FinanceStorageTransactions; -import org.folio.rest.persist.DBClient; -import org.folio.rest.persist.DBClientFactory; import org.folio.rest.persist.PgUtil; import org.folio.service.transactions.batch.BatchTransactionService; -import org.folio.service.transactions.TransactionManagingStrategyFactory; -import org.folio.service.transactions.TransactionService; import org.folio.spring.SpringContextUtil; import org.springframework.beans.factory.annotation.Autowired; @@ -32,14 +26,8 @@ public class TransactionAPI implements FinanceStorageTransactions { - public static final String OKAPI_URL = "X-Okapi-Url"; - - @Autowired - private TransactionManagingStrategyFactory managingServiceFactory; @Autowired private BatchTransactionService batchTransactionService; - @Autowired - private DBClientFactory dbClientFactory; public TransactionAPI() { @@ -50,49 +38,14 @@ public TransactionAPI() { @Validate public void getFinanceStorageTransactions(String query, String totalRecords, int offset, int limit, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - PgUtil.get(TRANSACTION_TABLE, Transaction.class, TransactionCollection.class, query, offset, limit, okapiHeaders, vertxContext, + PgUtil.get(TRANSACTIONS_TABLE, Transaction.class, TransactionCollection.class, query, offset, limit, okapiHeaders, vertxContext, GetFinanceStorageTransactionsResponse.class, asyncResultHandler); } - @Override - @Validate - public void postFinanceStorageTransactions(Transaction transaction, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - DBClient client = dbClientFactory.getDbClient(new RequestContext(vertxContext, okapiHeaders)); - client.withTrans(conn -> getTransactionService(transaction).createTransaction(transaction, conn)) - .onComplete(event -> { - if (event.succeeded()) { - asyncResultHandler.handle(succeededFuture(buildResponseWithLocation(okapiHeaders.get(OKAPI_URL), "/finance-storage/transactions", event.result()))); - } else { - asyncResultHandler.handle(buildErrorResponse(event.cause())); - } - }); - } - @Override @Validate public void getFinanceStorageTransactionsById(String id, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - PgUtil.getById(TRANSACTION_TABLE, Transaction.class, id, okapiHeaders, vertxContext, GetFinanceStorageTransactionsByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void deleteFinanceStorageTransactionsById(String id, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - PgUtil.deleteById(TRANSACTION_TABLE, id, okapiHeaders, vertxContext, DeleteFinanceStorageTransactionsByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void putFinanceStorageTransactionsById(String id, Transaction transaction, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - transaction.setId(id); - DBClient client = dbClientFactory.getDbClient(new RequestContext(vertxContext, okapiHeaders)); - client.withTrans(conn -> getTransactionService(transaction).updateTransaction(transaction, conn)) - .onComplete(event -> { - if (event.succeeded()) { - asyncResultHandler.handle(buildNoContentResponse()); - } else { - asyncResultHandler.handle(buildErrorResponse(event.cause())); - } - }); + PgUtil.getById(TRANSACTIONS_TABLE, Transaction.class, id, okapiHeaders, vertxContext, GetFinanceStorageTransactionsByIdResponse.class, asyncResultHandler); } @Override @@ -109,8 +62,4 @@ public void postFinanceStorageTransactionsBatchAllOrNothing(Batch batch, Map Handler {@link Handler} which must be called as follows - Note the - * 'GetPatronsResponse' should be replaced with '[nameOfYourFunction]Response': (example only) - * asyncResultHandler.handle(io.vertx.core.Future.succeededFuture(GetPatronsResponse.withJsonOK( new ObjectMapper().readValue(reply.result().body().toString(), Patron.class)))); - * in the final callback (most internal callback) of the function. - * @param vertxContext The Vertx Context Object io.vertx.core.Context - */ - @Override - @Validate - public void postFinanceStorageOrderTransactionSummaries(OrderTransactionSummary summary, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - logger.debug("Trying to create finance storage order transaction summaries with id {}", summary.getId()); - if (summary.getNumTransactions() < 1) { - logger.error("Summary with id {} transactions less than 1", summary.getId()); - handleValidationError(summary.getNumTransactions(), asyncResultHandler); - } else { - String sql = "INSERT INTO " + getFullTableName(tenantId, ORDER_TRANSACTION_SUMMARIES) - + " (id, jsonb) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING"; - try { - - pgClient.execute(sql, Tuple.of(UUID.fromString(summary.getId()), pojo2JsonObject(summary)), result -> { - if (result.failed()) { - logger.error("Create finance storage order transaction summaries with id {} failed", summary.getId(), result.cause()); - String badRequestMessage = PgExceptionUtil.badRequestMessage(result.cause()); - if (badRequestMessage != null) { - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageOrderTransactionSummariesResponse - .respond400WithTextPlain(Response.Status.BAD_REQUEST.getReasonPhrase()))); - } else { - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageOrderTransactionSummariesResponse - .respond500WithTextPlain(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase()))); - } - } else { - logger.info("Successfully created {} finance storage order transaction summaries with summaryId {}", summary.getNumTransactions(), summary.getId()); - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageOrderTransactionSummariesResponse - .respond201WithApplicationJson(summary, PostFinanceStorageOrderTransactionSummariesResponse.headersFor201() - .withLocation(ORDER_TRANSACTION_SUMMARIES_LOCATION_PREFIX + summary.getId())))); - } - }); - } catch (Exception e) { - logger.error("Creating finance storage order transaction summaries with id {} failed", summary.getId(), e); - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageOrderTransactionSummariesResponse - .respond500WithTextPlain(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase()))); - } - } - - } - - private void handleValidationError(int numOfTransactions, Handler> asyncResultHandler) { - Parameter parameter = new Parameter().withKey("numOfTransactions") - .withValue(String.valueOf(numOfTransactions)); - Error error = new Error().withCode("-1") - .withMessage("must be greater than or equal to 1") - .withParameters(Collections.singletonList(parameter)); - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageOrderTransactionSummariesResponse - .respond422WithApplicationJson(new Errors().withErrors(Collections.singletonList(error))))); - } - - @Override - @Validate - public void getFinanceStorageOrderTransactionSummariesById(String id, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - PgUtil.getById(ORDER_TRANSACTION_SUMMARIES, OrderTransactionSummary.class, id, okapiHeaders, vertxContext, - GetFinanceStorageOrderTransactionSummariesByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void putFinanceStorageOrderTransactionSummariesById(String id, OrderTransactionSummary entity, - Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - PgUtil.put(ORDER_TRANSACTION_SUMMARIES, entity, id, okapiHeaders, vertxContext, - PutFinanceStorageOrderTransactionSummariesByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void deleteFinanceStorageOrderTransactionSummariesById(String id, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - PgUtil.deleteById(ORDER_TRANSACTION_SUMMARIES, id, okapiHeaders, vertxContext, - DeleteFinanceStorageOrderTransactionSummariesByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void postFinanceStorageInvoiceTransactionSummaries(InvoiceTransactionSummary summary, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - logger.debug("Trying to create finance storage invoice transaction summaries with id {}", summary.getId()); - if (summary.getNumPaymentsCredits() < 1) { - logger.error("Summary with id {} transactions less than 1", summary.getId()); - handleValidationError(summary.getNumPaymentsCredits(), asyncResultHandler); - } else { - String sql = "INSERT INTO " + getFullTableName(tenantId, INVOICE_TRANSACTION_SUMMARIES) - + " (id, jsonb) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING"; - try { - pgClient.execute(sql, Tuple.of(UUID.fromString(summary.getId()), pojo2JsonObject(summary)), result -> { - if (result.failed()) { - logger.error("Create finance storage invoice transaction summaries with id {} failed", summary.getId(), result.cause()); - String badRequestMessage = PgExceptionUtil.badRequestMessage(result.cause()); - if (badRequestMessage != null) { - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageInvoiceTransactionSummariesResponse - .respond400WithTextPlain(Response.Status.BAD_REQUEST.getReasonPhrase()))); - } else { - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageInvoiceTransactionSummariesResponse - .respond500WithTextPlain(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase()))); - } - } else { - logger.info("Successfully created {} finance storage invoice transaction summaries with summaryId {}", summary.getId(), summary.getNumPaymentsCredits()); - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageInvoiceTransactionSummariesResponse - .respond201WithApplicationJson(summary, PostFinanceStorageInvoiceTransactionSummariesResponse.headersFor201() - .withLocation(INVOICE_TRANSACTION_SUMMARIES_LOCATION_PREFIX + summary.getId())))); - } - }); - } catch (Exception e) { - logger.error("Creating finance storage invoice transaction summaries with id {} failed", summary.getId(), e); - asyncResultHandler.handle(Future.succeededFuture(PostFinanceStorageInvoiceTransactionSummariesResponse - .respond500WithTextPlain(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase()))); - } - } - } - - @Override - @Validate - public void getFinanceStorageInvoiceTransactionSummariesById(String id, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - PgUtil.getById(INVOICE_TRANSACTION_SUMMARIES, InvoiceTransactionSummary.class, id, okapiHeaders, vertxContext, - GetFinanceStorageInvoiceTransactionSummariesByIdResponse.class, asyncResultHandler); - } - - @Override - @Validate - public void deleteFinanceStorageInvoiceTransactionSummariesById(String id, Map okapiHeaders, - Handler> asyncResultHandler, Context vertxContext) { - PgUtil.deleteById(INVOICE_TRANSACTION_SUMMARIES, id, okapiHeaders, vertxContext, - DeleteFinanceStorageInvoiceTransactionSummariesByIdResponse.class, asyncResultHandler); - } - - @Override - public void putFinanceStorageInvoiceTransactionSummariesById(String id, InvoiceTransactionSummary entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - PgUtil.put(INVOICE_TRANSACTION_SUMMARIES, entity, id, okapiHeaders, vertxContext, - PutFinanceStorageInvoiceTransactionSummariesByIdResponse.class, asyncResultHandler); - } - -} diff --git a/src/main/java/org/folio/rest/persist/DBConn.java b/src/main/java/org/folio/rest/persist/DBConn.java index bd7e86a8..908338f7 100644 --- a/src/main/java/org/folio/rest/persist/DBConn.java +++ b/src/main/java/org/folio/rest/persist/DBConn.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.Tuple; diff --git a/src/main/java/org/folio/rest/persist/HelperUtils.java b/src/main/java/org/folio/rest/persist/HelperUtils.java index dc000fc9..878d3bae 100644 --- a/src/main/java/org/folio/rest/persist/HelperUtils.java +++ b/src/main/java/org/folio/rest/persist/HelperUtils.java @@ -7,14 +7,12 @@ import java.lang.reflect.Method; import java.text.MessageFormat; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; @@ -30,8 +28,7 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; public final class HelperUtils { private HelperUtils() { } @@ -43,8 +40,8 @@ public static String getEndpoint(Class clazz) { } public static void replyWithErrorResponse(Handler> asyncResultHandler, HttpException cause) { - asyncResultHandler.handle(succeededFuture(Response.status(cause.getStatusCode()) - .entity(Optional.of(cause).map(HttpException::getPayload).orElse(cause.getMessage())) + asyncResultHandler.handle(succeededFuture(Response.status(cause.getCode()) + .entity(Optional.of(cause).map(HttpException::getMessage).orElse(cause.getMessage())) .header(CONTENT_TYPE, MediaType.TEXT_PLAIN) .build())); } @@ -132,22 +129,6 @@ public static Error buildFieldConstraintError(String entityName, String fieldNam return error; } - public static String getQueryValues(List entities) { - return entities.stream().map(entity -> "('" + entity.getString("id") + "', '" + entity.encode() + "'::json)").collect(Collectors.joining(",")); - } - - public static List buildNullValidationError(String value, String key) { - if (value == null) { - Parameter parameter = new Parameter().withKey(key) - .withValue("null"); - Error error = new Error().withCode("-1") - .withMessage("may not be null") - .withParameters(Collections.singletonList(parameter)); - return Collections.singletonList(error); - } - return Collections.emptyList(); - } - /** * The method allows to compose any elements with the same action in sequence. * diff --git a/src/main/java/org/folio/rest/util/ErrorCodes.java b/src/main/java/org/folio/rest/util/ErrorCodes.java index 9463060e..3c86d5c0 100644 --- a/src/main/java/org/folio/rest/util/ErrorCodes.java +++ b/src/main/java/org/folio/rest/util/ErrorCodes.java @@ -5,18 +5,17 @@ public enum ErrorCodes { GENERIC_ERROR_CODE("genericError", "Generic error"), UNIQUE_FIELD_CONSTRAINT_ERROR("uniqueField{0}{1}Error", "Field {0} must be unique"), - NOT_ENOUGH_MONEY_FOR_ALLOCATION("notEnoughMoneyForAllocationError", "Allocation was not successful. There is not enough money Available in the budget to complete this Allocation."), BUDGET_EXPENSE_CLASS_REFERENCE_ERROR("budgetExpenseClassReferenceError", "Can't delete budget that referenced with expense class"), MISSING_FUND_ID("missingFundId", "One of the fields toFundId or fromFundId must be specified"), ALLOCATION_MUST_BE_POSITIVE("allocationMustBePositive", "Allocation amount must be greater than zero"), CONFLICT("conflict", "Conflict when updating a record in table {0}: {1}"), - BUDGET_NOT_FOUND_FOR_TRANSACTION("budgetNotFoundForTransaction", "Budget not found for pair fiscalYear-fundId"), - OUTDATED_FUND_ID_IN_ENCUMBRANCE("outdatedFundIdInEncumbrance", - "Could not find the budget for the encumbrance. The encumbrance fund id is probably not matching the fund id in the invoice line."), - BUDGET_IS_INACTIVE("budgetIsInactive", "Cannot create transaction from the not active budget {0}"), BUDGET_RESTRICTED_EXPENDITURES_ERROR("budgetRestrictedExpendituresError", "Expenditure restriction does not allow this operation"), BUDGET_RESTRICTED_ENCUMBRANCE_ERROR("budgetRestrictedEncumbranceError", "Encumbrance restriction does not allow this operation"), - PAYMENT_OR_CREDIT_HAS_NEGATIVE_AMOUNT("paymentOrCreditHasNegativeAmount", "A payment or credit has a negative amount"); + PAYMENT_OR_CREDIT_HAS_NEGATIVE_AMOUNT("paymentOrCreditHasNegativeAmount", "A payment or credit has a negative amount"), + TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR("transactionIsPresentBudgetDeleteError", "Budget related transactions found. Deletion of the budget is forbidden."), + BUDGET_IS_NOT_ACTIVE_OR_PLANNED("budgetIsNotActiveOrPlanned", "Cannot process transactions because a budget is not active or planned"), + ID_IS_REQUIRED_IN_TRANSACTIONS("idIsRequiredInTransactions", "Id is required in transactions to {0}."), + LINKED_ENCUMBRANCES_NOT_FOUND("linkedEncumbrancesNotFound","Could not find some linked encumbrances in the database"); private final String code; private final String description; diff --git a/src/main/java/org/folio/rest/util/ResponseUtils.java b/src/main/java/org/folio/rest/util/ResponseUtils.java index 3192f735..12ffa626 100644 --- a/src/main/java/org/folio/rest/util/ResponseUtils.java +++ b/src/main/java/org/folio/rest/util/ResponseUtils.java @@ -21,7 +21,7 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; public class ResponseUtils { @@ -32,9 +32,8 @@ private ResponseUtils() { public static Future handleNoContentResponse(AsyncResult result, String id, String logMessage) { if (result.failed()) { - HttpException cause = (HttpException) result.cause(); - logger.error(logMessage, cause, id, "or associated data failed to be"); - return buildErrorResponse(cause); + logger.error(logMessage, result.cause(), id, "or associated data failed to be"); + return buildErrorResponse(result.cause()); } else { logger.info(logMessage, id, "and associated data were successfully"); return buildNoContentResponse(); @@ -54,9 +53,8 @@ public static Future handleFailure(Throwable cause) { public static void handleFailure(Promise promise, AsyncResult reply) { Throwable cause = reply.cause(); logger.error(cause.getMessage()); - if (cause instanceof PgException && "23F09".equals(((PgException)cause).getCode())) { - String message = MessageFormat.format(ErrorCodes.CONFLICT.getDescription(), ((PgException)cause).getTable(), - ((PgException)cause).getErrorMessage()); + if (cause instanceof PgException pgEx && PgExceptionUtil.isVersionConflict(pgEx)) { + String message = MessageFormat.format(ErrorCodes.CONFLICT.getDescription(), pgEx.getTable(), pgEx.getErrorMessage()); promise.fail(new HttpException(Response.Status.CONFLICT.getStatusCode(), message, cause)); return; } @@ -76,12 +74,11 @@ public static Future buildErrorResponse(Throwable throwable) { final String message; final int code; - if (throwable instanceof HttpException) { - code = ((HttpException) throwable).getStatusCode(); - message = ((HttpException) throwable).getPayload(); - } else if (throwable instanceof org.folio.rest.exception.HttpException) { - org.folio.rest.exception.HttpException exception = (org.folio.rest.exception.HttpException) throwable; - return Future.succeededFuture(buildErrorResponse(exception)); + if (throwable instanceof io.vertx.ext.web.handler.HttpException exception) { + code = exception.getStatusCode(); + message = exception.getPayload(); + } else if (throwable instanceof org.folio.rest.exception.HttpException exception) { + return Future.succeededFuture(buildErrorResponseFromHttpException(exception)); } else { code = INTERNAL_SERVER_ERROR.getStatusCode(); message = throwable.getMessage(); @@ -97,7 +94,7 @@ private static Response buildErrorResponse(int code, String message) { .build(); } - private static Response buildErrorResponse(org.folio.rest.exception.HttpException exception) { + private static Response buildErrorResponseFromHttpException(HttpException exception) { return Response.status(exception.getCode()) .header(CONTENT_TYPE, APPLICATION_JSON) .entity(exception.getErrors()) diff --git a/src/main/java/org/folio/service/budget/BudgetService.java b/src/main/java/org/folio/service/budget/BudgetService.java index 5fe089a0..d556c103 100644 --- a/src/main/java/org/folio/service/budget/BudgetService.java +++ b/src/main/java/org/folio/service/budget/BudgetService.java @@ -1,37 +1,30 @@ package org.folio.service.budget; -import static java.util.Collections.singletonList; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; import static org.folio.rest.impl.FundAPI.FUND_TABLE; import static org.folio.rest.persist.HelperUtils.getFullTableName; -import static org.folio.rest.util.ErrorCodes.*; +import static org.folio.rest.util.ErrorCodes.TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR; import static org.folio.rest.util.ResponseUtils.handleNoContentResponse; import java.util.*; import javax.ws.rs.core.Response; -import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.dao.budget.BudgetDAO; import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Errors; import org.folio.rest.jaxrs.model.LedgerFiscalYearRollover; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; import org.folio.rest.persist.DBClient; import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.rest.util.ErrorCodes; import org.folio.utils.CalculationUtils; import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.sqlclient.Tuple; public class BudgetService { @@ -39,11 +32,6 @@ public class BudgetService { private static final Logger logger = LogManager.getLogger(BudgetService.class); private static final String GROUP_FUND_FY_TABLE = "group_fund_fiscal_year"; - private static final String TRANSACTIONS_TABLE = "transaction"; - public static final String TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR = "transactionIsPresentBudgetDeleteError"; - - public static final String SELECT_BUDGETS_BY_FY_AND_FUND_FOR_UPDATE = "SELECT jsonb FROM %s " - + "WHERE fiscalYearId = $1 AND fundId = $2 FOR UPDATE"; private final BudgetDAO budgetDAO; @@ -67,48 +55,6 @@ public void deleteById(String id, Context vertxContext, Map head }); } - public Future getBudgetByFundIdAndFiscalYearId(String fiscalYearId, String fundId, DBConn conn) { - logger.debug("getBudgetByFundIdAndFiscalYearId:: Trying to get budget by fund id {} and fiscal year id {}", fundId, fiscalYearId); - - Criterion criterion = new CriterionBuilder().with("fundId", fundId) - .with("fiscalYearId", fiscalYearId) - .build(); - return budgetDAO.getBudgets(criterion, conn) - .map(budgets -> { - if (budgets.isEmpty()) { - logger.error("getBudgetByFundIdAndFiscalYearId:: Budget not found for fundId {} and fiscalYearId {}", fundId, fiscalYearId); - throw new HttpException(Response.Status.NOT_FOUND.getStatusCode(), - JsonObject.mapFrom(BUDGET_NOT_FOUND_FOR_TRANSACTION.toError()).encodePrettily()); - } else { - logger.info("getBudgetByFundIdAndFiscalYearId:: Successfully retrieved budget by fund id {} and fiscal year id {}", fundId, fiscalYearId); - return budgets.get(0); - } - }) - .onFailure(e -> logger.error("getBudgetByFundIdAndFiscalYearId:: Getting budget by fund id {} and fiscal year id {} failed", fundId, fiscalYearId, e)); - } - - /** - * Get budget by fiscal year id and fund id for update. - * This should only be called in a transactional context because it is using SELECT FOR UPDATE. - */ - public Future getBudgetByFiscalYearIdAndFundIdForUpdate(String fiscalYearId, String fundId, DBConn conn) { - logger.debug("getBudgetByFiscalYearIdAndFundIdForUpdate:: Trying to get budget by fund id {} and fiscal year id {} for update", fundId, fiscalYearId); - - String sql = getSelectBudgetQueryByFyAndFundForUpdate(conn.getTenantId()); - return budgetDAO.getBudgetsBySql(sql, Tuple.of(fiscalYearId, fundId), conn) - .map(budgets -> { - if (budgets.isEmpty()) { - logger.error("getBudgetByFiscalYearIdAndFundIdForUpdate:: Budget for update not found for fundId {} and fiscalYearId {}", fundId, fiscalYearId); - throw new HttpException(Response.Status.NOT_FOUND.getStatusCode(), - JsonObject.mapFrom(BUDGET_NOT_FOUND_FOR_TRANSACTION.toError()).encodePrettily()); - } else { - return budgets.get(0); - } - }) - .onFailure(e -> logger.error("getBudgetByFiscalYearIdAndFundIdForUpdate:: Getting budget by fund id {} and fiscal year id {} failed", - fundId, fiscalYearId, e)); - } - public Future updateBatchBudgets(List budgets, DBConn conn) { budgets.forEach(this::clearReadOnlyFields); return budgetDAO.updateBatchBudgets(budgets, conn); @@ -118,14 +64,6 @@ public Future> getBudgets(String sql, Tuple params, DBConn conn) { return budgetDAO.getBudgetsBySql(sql, params, conn); } - public void updateBudgetMetadata(Budget budget, Transaction transaction) { - budget.getMetadata() - .setUpdatedDate(transaction.getMetadata() - .getUpdatedDate()); - budget.getMetadata() - .setUpdatedByUserId(transaction.getMetadata() - .getUpdatedByUserId()); - } public void clearReadOnlyFields(Budget budgetFromNew) { budgetFromNew.setAllocated(null); budgetFromNew.setAvailable(null); @@ -136,33 +74,6 @@ public void clearReadOnlyFields(Budget budgetFromNew) { budgetFromNew.setTotalFunding(null); } - /** - * Checks if there is enough budget money available for a transaction. - * This method is used exclusively for "Move allocation" action and is skipped for other types. - * - * @param transaction The transaction to be checked. - * @param conn database connection - * @return A {@link Future} that completes successfully if there is enough money available, - * or fails with a {@link HttpException} if not enough money is available. - */ - public Future checkBudgetHaveMoneyForTransaction(Transaction transaction, DBConn conn) { - if (isNotMoveAllocation(transaction)) { - return Future.succeededFuture(); - } - - return getBudgetByFundIdAndFiscalYearId(transaction.getFiscalYearId(), transaction.getFromFundId(), conn) - .map(budget -> { - if (budget.getAvailable() < transaction.getAmount()) { - ErrorCodes errorCode = transaction.getTransactionType() == Transaction.TransactionType.ALLOCATION ? - NOT_ENOUGH_MONEY_FOR_ALLOCATION : GENERIC_ERROR_CODE; - logger.error(errorCode.getDescription()); - throw new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), - JsonObject.mapFrom(new Errors().withErrors(singletonList(errorCode.toError())).withTotalRecords(1)).encode()); - } - return null; - }); - } - public void updateBudgetsWithCalculatedFields(List budgets){ budgets.forEach(CalculationUtils::calculateBudgetSummaryFields); } @@ -208,7 +119,19 @@ public Future> getBudgetsByFiscalYearIdsAndFundIdsForUpdate( } sb.append(" FOR UPDATE"); String sql = sb.toString(); - return budgetDAO.getBudgetsBySql(sql, Tuple.tuple(), conn); + return budgetDAO.getBudgetsBySql(sql, Tuple.tuple(), conn) + .map(budgets -> { + int expectedNumberOfBudgets = fiscalYearIdToFundIds.values().stream().map(Set::size).mapToInt(Integer::intValue).sum(); + if (budgets.size() != expectedNumberOfBudgets) { + List idsOfFundsWithNoBudget = fiscalYearIdToFundIds.values().stream() + .flatMap(Collection::stream) + .distinct() + .filter(fundId -> budgets.stream().noneMatch(b -> fundId.equals(b.getFundId()))) + .toList(); + throw new HttpException(500, "Could not find some budgets in the database, fund ids=" + idsOfFundsWithNoBudget); + } + return budgets; + }); } private Future deleteAllocationTransactions(Budget budget, DBConn conn) { @@ -239,7 +162,7 @@ private Future checkTransactions(Budget budget, DBConn conn) { .map(rowSet -> { if (rowSet.size() > 0) { logger.error("checkTransactions:: Transaction is present"); - throw new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR); + throw new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR.toError()); } return null; }) @@ -262,14 +185,4 @@ private Future unlinkGroupFundFiscalYears(String id, DBConn conn) { .mapEmpty(); } - private String getSelectBudgetQueryByFyAndFundForUpdate(String tenantId){ - String budgetTableName = getFullTableName(tenantId, BUDGET_TABLE); - return String.format(SELECT_BUDGETS_BY_FY_AND_FUND_FOR_UPDATE, budgetTableName); - } - - private boolean isNotMoveAllocation(Transaction transaction) { - return ObjectUtils.anyNull(transaction.getFromFundId(), transaction.getToFundId()) - && transaction.getTransactionType() == Transaction.TransactionType.ALLOCATION; - } - } diff --git a/src/main/java/org/folio/service/budget/RolloverBudgetExpenseClassTotalsService.java b/src/main/java/org/folio/service/budget/RolloverBudgetExpenseClassTotalsService.java index 921c622a..eeca40d8 100644 --- a/src/main/java/org/folio/service/budget/RolloverBudgetExpenseClassTotalsService.java +++ b/src/main/java/org/folio/service/budget/RolloverBudgetExpenseClassTotalsService.java @@ -7,7 +7,7 @@ import org.folio.rest.jaxrs.model.BudgetExpenseClassTotal; import org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverBudget; import org.folio.rest.persist.DBConn; -import org.folio.service.transactions.TemporaryTransactionService; +import org.folio.service.transactions.TemporaryEncumbranceService; import org.folio.utils.MoneyUtils; import javax.money.CurrencyUnit; @@ -27,17 +27,17 @@ public class RolloverBudgetExpenseClassTotalsService { private final BudgetExpenseClassService budgetExpenseClassService; - private final TemporaryTransactionService temporaryTransactionService; + private final TemporaryEncumbranceService temporaryEncumbranceService; - public RolloverBudgetExpenseClassTotalsService(BudgetExpenseClassService budgetExpenseClassService, TemporaryTransactionService temporaryTransactionService) { + public RolloverBudgetExpenseClassTotalsService(BudgetExpenseClassService budgetExpenseClassService, TemporaryEncumbranceService temporaryEncumbranceService) { this.budgetExpenseClassService = budgetExpenseClassService; - this.temporaryTransactionService = temporaryTransactionService; + this.temporaryEncumbranceService = temporaryEncumbranceService; } public Future getBudgetWithUpdatedExpenseClassTotals(LedgerFiscalYearRolloverBudget budget, DBConn conn) { return budgetExpenseClassService.getExpenseClassesByTemporaryBudgetId(budget.getBudgetId(), conn) - .compose(expenseClasses -> temporaryTransactionService.getTransactions(budget, conn) + .compose(expenseClasses -> temporaryEncumbranceService.getTransactions(budget, conn) .map(transactions -> buildBudgetExpenseClassesTotals(expenseClasses, transactions, budget))) .compose(budgetExpenseClassTotals -> budgetExpenseClassService.getTempBudgetExpenseClasses(budget.getBudgetId(), conn) .map(budgetExpenseClasses -> updateExpenseClassStatus(budgetExpenseClassTotals, budgetExpenseClasses))) diff --git a/src/main/java/org/folio/service/expence/ExpenseClassService.java b/src/main/java/org/folio/service/expence/ExpenseClassService.java index f536b1a2..8f79a660 100644 --- a/src/main/java/org/folio/service/expence/ExpenseClassService.java +++ b/src/main/java/org/folio/service/expence/ExpenseClassService.java @@ -21,7 +21,7 @@ import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; public class ExpenseClassService { @@ -89,7 +89,7 @@ private Future updateExpenseClass(String id, ExpenseClass group) { promise.fail(nameCodeConstraintErrorBuilder.buildException(reply, ExpenseClass.class)); } else if(reply.result().rowCount() == 0) { - promise.fail(new HttpException(Response.Status.NOT_FOUND.getStatusCode())); + promise.fail(new HttpException(Response.Status.NOT_FOUND.getStatusCode(), "Expense class not found for update, id=" + id)); } else { promise.complete(group); diff --git a/src/main/java/org/folio/service/group/GroupService.java b/src/main/java/org/folio/service/group/GroupService.java index be16294b..71e10955 100644 --- a/src/main/java/org/folio/service/group/GroupService.java +++ b/src/main/java/org/folio/service/group/GroupService.java @@ -21,7 +21,7 @@ import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; public class GroupService { @@ -89,7 +89,7 @@ private Future updateGroup(Group group, String id) { promise.fail(nameCodeConstraintErrorBuilder.buildException(reply, Group.class)); } else if(reply.result().rowCount() == 0) { - promise.fail(new HttpException(Response.Status.NOT_FOUND.getStatusCode())); + promise.fail(new HttpException(Response.Status.NOT_FOUND.getStatusCode(), "Group not found for update, id=" + id)); } else { promise.complete(group); diff --git a/src/main/java/org/folio/service/rollover/LedgerRolloverService.java b/src/main/java/org/folio/service/rollover/LedgerRolloverService.java index bd60e172..90fc939c 100644 --- a/src/main/java/org/folio/service/rollover/LedgerRolloverService.java +++ b/src/main/java/org/folio/service/rollover/LedgerRolloverService.java @@ -1,7 +1,7 @@ package org.folio.service.rollover; import static org.folio.dao.budget.BudgetExpenseClassDAOImpl.TEMPORARY_BUDGET_EXPENSE_CLASS_TABLE; -import static org.folio.dao.transactions.TemporaryEncumbranceTransactionDAO.TEMPORARY_ENCUMBRANCE_TRANSACTIONS_TABLE; +import static org.folio.dao.transactions.TemporaryEncumbrancePostgresDAO.TEMPORARY_ENCUMBRANCE_TRANSACTIONS_TABLE; import static org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverError.ErrorType.FINANCIAL_ROLLOVER; import static org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverError.ErrorType.ORDER_ROLLOVER; import static org.folio.rest.jaxrs.model.RolloverStatus.ERROR; @@ -32,7 +32,7 @@ import io.vertx.core.Future; import io.vertx.core.Promise; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.folio.utils.CalculationUtils; public class LedgerRolloverService { diff --git a/src/main/java/org/folio/service/rollover/RolloverProgressService.java b/src/main/java/org/folio/service/rollover/RolloverProgressService.java index 05339ea2..0158d08c 100644 --- a/src/main/java/org/folio/service/rollover/RolloverProgressService.java +++ b/src/main/java/org/folio/service/rollover/RolloverProgressService.java @@ -1,7 +1,7 @@ package org.folio.service.rollover; import io.vertx.core.Future; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import org.folio.dao.rollover.RolloverProgressDAO; import org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverProgress; import org.folio.rest.jaxrs.model.RolloverStatus; diff --git a/src/main/java/org/folio/service/rollover/RolloverValidationService.java b/src/main/java/org/folio/service/rollover/RolloverValidationService.java index b2eaac33..923f669c 100644 --- a/src/main/java/org/folio/service/rollover/RolloverValidationService.java +++ b/src/main/java/org/folio/service/rollover/RolloverValidationService.java @@ -11,7 +11,7 @@ import io.vertx.core.Future; import io.vertx.core.Promise; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.impl.ArrayTuple; import org.folio.rest.persist.DBConn; diff --git a/src/main/java/org/folio/service/summary/AbstractTransactionSummaryService.java b/src/main/java/org/folio/service/summary/AbstractTransactionSummaryService.java deleted file mode 100644 index bdef8931..00000000 --- a/src/main/java/org/folio/service/summary/AbstractTransactionSummaryService.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.folio.service.summary; - -import static org.folio.service.transactions.AllOrNothingTransactionService.ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED; - -import javax.ws.rs.core.Response; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.dao.summary.TransactionSummaryDao; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.HelperUtils; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; - -public abstract class AbstractTransactionSummaryService implements TransactionSummaryService { - - private static final Logger logger = LogManager.getLogger(AbstractTransactionSummaryService.class); - - private final TransactionSummaryDao transactionSummaryDao; - - AbstractTransactionSummaryService(TransactionSummaryDao transactionSummaryDao) { - this.transactionSummaryDao = transactionSummaryDao; - } - - @Override - public Future getAndCheckTransactionSummary(Transaction transaction, DBConn conn) { - String summaryId = getSummaryId(transaction); - return this.getTransactionSummary(summaryId, conn) - .map(summary -> { - if ((isProcessed(summary))) { - logger.error("Expected number of transactions for summary with id={} already processed", summary.getString(HelperUtils.ID_FIELD_NAME)); - throw new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED); - } else { - logger.info("Successfully retrieved a transaction summary with id {}", summary.getString(HelperUtils.ID_FIELD_NAME)); - return JsonObject.mapFrom(summary); - } - }); - } - - @Override - public Future getTransactionSummary(String summaryId, DBConn conn) { - logger.debug("Get summary by id {}", summaryId); - return transactionSummaryDao.getSummaryById(summaryId, conn); - } - - @Override - public Future getTransactionSummaryWithLocking(String summaryId, DBConn conn) { - logger.debug("Get summary by id {} with locking", summaryId); - return transactionSummaryDao.getSummaryByIdWithLocking(summaryId, conn); - } - - @Override - public Future setTransactionsSummariesProcessed(JsonObject summary, DBConn conn) { - setTransactionsSummariesProcessed(summary); - return transactionSummaryDao.updateSummary(summary, conn); - } - - protected abstract boolean isProcessed(JsonObject summary); - - /** - * Updates summary with negative numbers to highlight that associated transaction list was successfully processed - * - * @param summary processed transaction - */ - abstract void setTransactionsSummariesProcessed(JsonObject summary); - -} diff --git a/src/main/java/org/folio/service/summary/EncumbranceTransactionSummaryService.java b/src/main/java/org/folio/service/summary/EncumbranceTransactionSummaryService.java deleted file mode 100644 index ffa92983..00000000 --- a/src/main/java/org/folio/service/summary/EncumbranceTransactionSummaryService.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.folio.service.summary; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.summary.TransactionSummaryDao; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; - -import java.util.Optional; - -public class EncumbranceTransactionSummaryService extends AbstractTransactionSummaryService { - - public EncumbranceTransactionSummaryService(TransactionSummaryDao transactionSummaryDao) { - super(transactionSummaryDao); - } - - @Override - public String getSummaryId(Transaction transaction) { - return Optional.ofNullable(transaction.getEncumbrance()) - .map(Encumbrance::getSourcePurchaseOrderId) - .orElse(null); - } - - @Override - protected boolean isProcessed(JsonObject summary) { - return getNumTransactions(summary) < 0; - } - - @Override - protected void setTransactionsSummariesProcessed(JsonObject summary) { - summary.put("numTransactions", -Math.abs(getNumTransactions(summary))); - } - - @Override - public Integer getNumTransactions(JsonObject summary) { - return summary.getInteger("numTransactions"); - } - -} diff --git a/src/main/java/org/folio/service/summary/PaymentCreditTransactionSummaryService.java b/src/main/java/org/folio/service/summary/PaymentCreditTransactionSummaryService.java deleted file mode 100644 index 7b808aec..00000000 --- a/src/main/java/org/folio/service/summary/PaymentCreditTransactionSummaryService.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.folio.service.summary; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.summary.TransactionSummaryDao; -import org.folio.rest.jaxrs.model.Transaction; - -public class PaymentCreditTransactionSummaryService extends AbstractTransactionSummaryService { - - public PaymentCreditTransactionSummaryService(TransactionSummaryDao transactionSummaryDao) { - super(transactionSummaryDao); - } - - @Override - public String getSummaryId(Transaction transaction) { - return transaction.getSourceInvoiceId(); - } - - @Override - protected boolean isProcessed(JsonObject summary) { - return getNumTransactions(summary) < 0; - } - - @Override - protected void setTransactionsSummariesProcessed(JsonObject summary) { - summary.put("numPaymentsCredits", -getNumTransactions(summary)); - } - - @Override - public Integer getNumTransactions(JsonObject summary) { - return summary.getInteger("numPaymentsCredits"); - } - -} diff --git a/src/main/java/org/folio/service/summary/PendingPaymentTransactionSummaryService.java b/src/main/java/org/folio/service/summary/PendingPaymentTransactionSummaryService.java deleted file mode 100644 index 73ea37fc..00000000 --- a/src/main/java/org/folio/service/summary/PendingPaymentTransactionSummaryService.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.folio.service.summary; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.summary.TransactionSummaryDao; -import org.folio.rest.jaxrs.model.Transaction; - -public class PendingPaymentTransactionSummaryService extends AbstractTransactionSummaryService { - - public PendingPaymentTransactionSummaryService(TransactionSummaryDao transactionSummaryDao) { - super(transactionSummaryDao); - } - - @Override - public String getSummaryId(Transaction transaction) { - return transaction.getSourceInvoiceId(); - } - - @Override - protected boolean isProcessed(JsonObject summary) { - return getNumTransactions(summary) < 0; - } - - @Override - protected void setTransactionsSummariesProcessed(JsonObject summary) { - summary.put("numPendingPayments", -getNumTransactions(summary)); - } - - @Override - public Integer getNumTransactions(JsonObject summary) { - return summary.getInteger("numPendingPayments"); - } - -} diff --git a/src/main/java/org/folio/service/summary/TransactionSummaryService.java b/src/main/java/org/folio/service/summary/TransactionSummaryService.java deleted file mode 100644 index a76ea154..00000000 --- a/src/main/java/org/folio/service/summary/TransactionSummaryService.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.folio.service.summary; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; - -public interface TransactionSummaryService { - - Future getTransactionSummary(String summaryId, DBConn conn); - - Future getTransactionSummaryWithLocking(String summaryId, DBConn conn); - - Future setTransactionsSummariesProcessed(JsonObject summary, DBConn conn); - - Future getAndCheckTransactionSummary(Transaction transaction, DBConn conn); - - Integer getNumTransactions(JsonObject summary); - - String getSummaryId(Transaction transaction); - -} diff --git a/src/main/java/org/folio/service/transactions/AbstractTransactionService.java b/src/main/java/org/folio/service/transactions/AbstractTransactionService.java deleted file mode 100644 index ba6a8777..00000000 --- a/src/main/java/org/folio/service/transactions/AbstractTransactionService.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.folio.service.transactions; - -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; - -import io.vertx.core.Future; - -public abstract class AbstractTransactionService implements TransactionService { - - protected final TransactionDAO transactionDAO; - - public static final String TRANSACTION_TABLE = "transaction"; - public static final String FROM_FUND_ID = "fromFundId"; - public static final String TO_FUND_ID = "toFundId"; - - AbstractTransactionService(TransactionDAO transactionDAO) { - this.transactionDAO = transactionDAO; - } - - @Override - public Future createTransaction(Transaction transaction, DBConn conn) { - return transactionDAO.createTransaction(transaction, conn); - } - - @Override - public Future updateTransaction(Transaction transaction, DBConn conn) { - return transactionDAO.updateTransaction(transaction, conn); - } - - @Override - public Future deleteTransactionById(String id, DBConn conn) { - return transactionDAO.deleteTransactionById(id, conn); - } - -} diff --git a/src/main/java/org/folio/service/transactions/AllOrNothingTransactionService.java b/src/main/java/org/folio/service/transactions/AllOrNothingTransactionService.java deleted file mode 100644 index f3065c33..00000000 --- a/src/main/java/org/folio/service/transactions/AllOrNothingTransactionService.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.folio.service.transactions; - -import static org.folio.rest.persist.HelperUtils.ID_FIELD_NAME; - -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - -import javax.ws.rs.core.Response; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.dao.transactions.TemporaryTransactionDAO; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBConn; -import org.folio.service.summary.TransactionSummaryService; -import org.folio.service.transactions.restriction.TransactionRestrictionService; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; - -public class AllOrNothingTransactionService { - - private static final Logger logger = LogManager.getLogger(AllOrNothingTransactionService.class); - - public static final String TRANSACTION_SUMMARY_NOT_FOUND_FOR_TRANSACTION = "Transaction summary not found for transaction"; - public static final String ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED = "All expected transactions already processed"; - - private final TransactionDAO transactionDAO; - private final TemporaryTransactionDAO temporaryTransactionDAO; - private final TransactionSummaryService transactionSummaryService; - private final TransactionRestrictionService transactionRestrictionService; - - public AllOrNothingTransactionService(TransactionDAO transactionDAO, - TemporaryTransactionDAO temporaryTransactionDAO, - TransactionSummaryService transactionSummaryService, - TransactionRestrictionService transactionRestrictionService) { - this.transactionDAO = transactionDAO; - this.temporaryTransactionDAO = temporaryTransactionDAO; - this.transactionSummaryService = transactionSummaryService; - this.transactionRestrictionService = transactionRestrictionService; - } - - public Future createTransaction(Transaction transaction, DBConn conn, - BiFunction, DBConn, Future> operation) { - return validateTransactionAsFuture(transaction) - .compose(v -> transactionRestrictionService.verifyBudgetHasEnoughMoney(transaction, conn)) - .compose(v -> processAllOrNothing(transaction, conn, operation)) - .map(transaction); - } - - public Future updateTransaction(Transaction transaction, DBConn conn, - BiFunction, DBConn, Future> operation) { - return validateTransactionAsFuture(transaction) - .compose(v -> verifyTransactionExistence(transaction.getId(), conn)) - .compose(v -> processAllOrNothing(transaction, conn, operation)); - } - - private Future validateTransactionAsFuture(Transaction transaction) { - return Future.succeededFuture() - .map(v -> { - transactionRestrictionService.handleValidationError(transaction); - return null; - }); - } - - /** - * Accumulate transactions in a temporary table until expected number of transactions are present, - * then apply them all at once. - * Updating all the required tables together is occurred in a database transaction. - * - * @param transaction processed transaction - * - * @return future with void - */ - private Future processAllOrNothing(Transaction transaction, DBConn conn, BiFunction, DBConn, Future> operation) { - return transactionSummaryService.getAndCheckTransactionSummary(transaction, conn) - .recover(t -> { - if (t instanceof HttpException he && he.getStatusCode() == 404) { - logger.error("Summary was not found when processing transaction with id {}", transaction.getId()); - return Future.failedFuture(new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), - "Cannot process the transaction because the summary was not found.", t)); - } - return Future.failedFuture(t); - }) - .compose(summary -> addTempTransactionSequentially(transaction, conn) - .compose(transactions -> { - if (transactions.size() != transactionSummaryService.getNumTransactions(summary)) { - return Future.succeededFuture(); - } - // handle create or update - return operation.apply(transactions, conn) - .compose(ok -> finishAllOrNothing(summary, conn)) - .onComplete(result -> { - if (result.failed()) { - logger.error("processAllOrNothing:: Transactions with id {} or associated data failed to be processed", - transaction.getId(), result.cause()); - } else { - logger.info("processAllOrNothing:: Transactions with id {} and associated data were successfully processed", transaction.getId()); - } - }); - }) - ); - } - - private Future verifyTransactionExistence(String transactionId, DBConn conn) { - CriterionBuilder criterionBuilder = new CriterionBuilder(); - criterionBuilder.with("id", transactionId); - return transactionDAO.getTransactions(criterionBuilder.build(), conn) - .map(transactions -> { - if (transactions.isEmpty()) { - logger.warn("verifyTransactionExistence:: Transaction with id {} not found", transactionId); - throw new HttpException(Response.Status.NOT_FOUND.getStatusCode(), "Transaction not found"); - } - return null; - }); - } - - private Future finishAllOrNothing(JsonObject summary, DBConn conn) { - return temporaryTransactionDAO.deleteTempTransactions(summary.getString(ID_FIELD_NAME), conn) - .compose(tr -> transactionSummaryService.setTransactionsSummariesProcessed(summary, conn)) ; - } - - /** - * This method uses SELECT FOR UPDATE locking on summary table by summaryId. - * So in this case requests to create temp transaction and get temp transaction count for the same summaryId - * will be executed only by a single thread. - * The other thread will wait until DB Lock is released when the database transaction ends. - * Method {@link org.folio.rest.persist.PostgresClient#withTrans(Function)} ends the database transaction after executing. - * - * @param transaction temp transaction to create - * @param conn the db connection - * @return future with list of temp transactions - */ - private Future> addTempTransactionSequentially(Transaction transaction, DBConn conn) { - final String tenantId = conn.getTenantId(); - final String summaryId = transactionSummaryService.getSummaryId(transaction); - return transactionSummaryService.getTransactionSummaryWithLocking(summaryId, conn) - .compose(ar -> temporaryTransactionDAO.createTempTransaction(transaction, summaryId, tenantId, conn)) - .compose(tr -> temporaryTransactionDAO.getTempTransactionsBySummaryId(summaryId, conn)); - } - -} diff --git a/src/main/java/org/folio/service/transactions/AllocationService.java b/src/main/java/org/folio/service/transactions/AllocationService.java deleted file mode 100644 index 895dd782..00000000 --- a/src/main/java/org/folio/service/transactions/AllocationService.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.folio.service.transactions; - -import static java.util.Collections.singletonList; -import static org.folio.rest.util.ErrorCodes.ALLOCATION_MUST_BE_POSITIVE; -import static org.folio.rest.util.ErrorCodes.MISSING_FUND_ID; - -import java.util.List; - -import io.vertx.ext.web.handler.HttpException; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Parameter; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.utils.CalculationUtils; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; - -public class AllocationService extends AbstractTransactionService implements TransactionManagingStrategy { - - private static final Logger logger = LogManager.getLogger(AllocationService.class); - - private final BudgetService budgetService; - - public AllocationService(BudgetService budgetService, TransactionDAO transactionDAO) { - super(transactionDAO); - this.budgetService = budgetService; - } - - @Override - public Transaction.TransactionType getStrategyName() { - return Transaction.TransactionType.ALLOCATION; - } - - @Override - public Future createTransaction(Transaction allocation, DBConn conn) { - try { - handleValidationError(allocation); - } catch (HttpException e) { - return Future.failedFuture(e); - } - return budgetService.checkBudgetHaveMoneyForTransaction(allocation, conn) - .compose(v -> super.createTransaction(allocation, conn)) - .compose(v -> updateFromBudget(allocation, conn)) - .compose(v -> updateBudgetTo(allocation, conn)) - .map(v -> allocation) - .onSuccess(v -> logger.info("createTransaction:: Allocation with id {} and associated data were successfully processed", - allocation.getId())) - .onFailure(e -> logger.error("createTransaction:: Allocation with id {} or associated data failed to be processed", - allocation.getId(), e)); - } - - private Future updateBudgetTo(Transaction allocation, DBConn conn) { - if (StringUtils.isEmpty(allocation.getToFundId())) { - return Future.succeededFuture(); - } - return budgetService.getBudgetByFiscalYearIdAndFundIdForUpdate(allocation.getFiscalYearId(), allocation.getToFundId(), conn) - .map(budgetTo -> { - Budget budgetToNew = JsonObject.mapFrom(budgetTo) - .mapTo(Budget.class); - CalculationUtils.recalculateBudgetAllocationTo(budgetToNew, allocation); - budgetService.updateBudgetMetadata(budgetToNew, allocation); - return budgetToNew; - }) - .compose(budgetFrom -> budgetService.updateBatchBudgets(singletonList(budgetFrom), conn)); - } - - private Future updateFromBudget(Transaction allocation, DBConn conn) { - if (StringUtils.isEmpty(allocation.getFromFundId())) { - return Future.succeededFuture(); - } - return budgetService.getBudgetByFiscalYearIdAndFundIdForUpdate(allocation.getFiscalYearId(), allocation.getFromFundId(), conn) - .map(budgetFrom -> { - Budget budgetFromNew = JsonObject.mapFrom(budgetFrom).mapTo(Budget.class); - CalculationUtils.recalculateBudgetAllocationFrom(budgetFromNew, allocation); - budgetService.updateBudgetMetadata(budgetFromNew, allocation); - return budgetFromNew; - }) - .compose(budgetFrom -> budgetService.updateBatchBudgets(singletonList(budgetFrom), conn)); - } - - private void handleValidationError(Transaction transaction) { - checkRequiredFields(transaction); - checkAmount(transaction); - } - - private void checkAmount(Transaction transaction) { - if (transaction.getAmount() <= 0) { - List parameters = singletonList(new Parameter().withKey("fieldName") - .withValue("amount")); - Errors errors = new Errors().withErrors(singletonList(ALLOCATION_MUST_BE_POSITIVE.toError() - .withParameters(parameters))) - .withTotalRecords(1); - throw new HttpException(422, JsonObject.mapFrom(errors) - .encode()); - } - } - - private void checkRequiredFields(Transaction transaction) { - if (transaction.getToFundId() == null && transaction.getFromFundId() == null) { - throw new HttpException(422, JsonObject.mapFrom(new Errors().withErrors(singletonList(MISSING_FUND_ID.toError())) - .withTotalRecords(1)) - .encode()); - } - } - -} diff --git a/src/main/java/org/folio/service/transactions/DefaultTransactionService.java b/src/main/java/org/folio/service/transactions/DefaultTransactionService.java deleted file mode 100644 index 5796ff60..00000000 --- a/src/main/java/org/folio/service/transactions/DefaultTransactionService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.folio.service.transactions; - -import org.folio.dao.transactions.TransactionDAO; - -public class DefaultTransactionService extends AbstractTransactionService { - - public DefaultTransactionService(TransactionDAO transactionDAO) { - super(transactionDAO); - } - -} diff --git a/src/main/java/org/folio/service/transactions/EncumbranceService.java b/src/main/java/org/folio/service/transactions/EncumbranceService.java deleted file mode 100644 index b1078962..00000000 --- a/src/main/java/org/folio/service/transactions/EncumbranceService.java +++ /dev/null @@ -1,277 +0,0 @@ -package org.folio.service.transactions; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.sqlclient.Tuple; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.rest.persist.HelperUtils.getFullTableName; -import static org.folio.utils.MoneyUtils.subtractMoney; -import static org.folio.utils.MoneyUtils.sumMoney; - -public class EncumbranceService extends AbstractTransactionService implements TransactionManagingStrategy { - - private static final String TEMPORARY_ORDER_TRANSACTIONS = "temporary_order_transactions"; - - public static final String SELECT_BUDGETS_BY_ORDER_ID_FOR_UPDATE = - "SELECT b.jsonb FROM %s b INNER JOIN (SELECT DISTINCT budgets.id FROM %s budgets INNER JOIN %s transactions " - + "ON transactions.fromFundId = budgets.fundId AND transactions.fiscalYearId = budgets.fiscalYearId " - + "WHERE transactions.jsonb -> 'encumbrance' ->> 'sourcePurchaseOrderId' = $1) sub ON sub.id = b.id " - + "FOR UPDATE OF b"; - public static final String FOR_UPDATE = "FOR_UPDATE"; - public static final String FOR_CREATE = "FOR_CREATE"; - public static final String EXISTING = "EXISTING"; - - private final AllOrNothingTransactionService allOrNothingEncumbranceService; - private final BudgetService budgetService; - - public EncumbranceService(AllOrNothingTransactionService allOrNothingEncumbranceService, - TransactionDAO transactionDAO, - BudgetService budgetService) { - super(transactionDAO); - this.allOrNothingEncumbranceService = allOrNothingEncumbranceService; - this.budgetService = budgetService; - } - - @Override - public Future createTransaction(Transaction transaction, DBConn conn) { - return allOrNothingEncumbranceService.createTransaction(transaction, conn, this::processEncumbrances); - } - - @Override - public Future updateTransaction(Transaction transaction, DBConn conn) { - return allOrNothingEncumbranceService.updateTransaction(transaction, conn, this::processEncumbrances); - } - - private Map> groupTransactionsByBudget(List existingTransactions, List budgets) { - Map> groupedTransactions = existingTransactions.stream().collect(groupingBy(Transaction::getFromFundId)); - return budgets.stream().collect(toMap(identity(), budget -> groupedTransactions.getOrDefault(budget.getFundId(), Collections.emptyList()))); - } - - private String getSelectBudgetsQueryForUpdate(String tenantId) { - String budgetTableName = getFullTableName(tenantId, BUDGET_TABLE); - String transactionTableName = getFullTableName(tenantId, TEMPORARY_ORDER_TRANSACTIONS); - return String.format(SELECT_BUDGETS_BY_ORDER_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - } - - public Transaction.TransactionType getStrategyName() { - return Transaction.TransactionType.ENCUMBRANCE; - } - - /** - * To prevent partial encumbrance transactions for an order, all the encumbrances must be created following All or nothing - */ - public Future processEncumbrances(List tmpTransactions, DBConn conn) { - return getTransactionsForCreateAndUpdate(tmpTransactions, conn) - .compose(transactionsForCreateAndUpdate -> updateBudgetsTotals(transactionsForCreateAndUpdate, tmpTransactions, conn) - .map(v -> findTransactionForUpdate(transactionsForCreateAndUpdate.get(FOR_UPDATE), - transactionsForCreateAndUpdate.get(EXISTING))) - .compose(transactions -> transactionDAO.updatePermanentTransactions(transactions, conn)) - .compose(ok -> { - if (!transactionsForCreateAndUpdate.get(FOR_CREATE).isEmpty()) { - List ids = transactionsForCreateAndUpdate.get(FOR_CREATE) - .stream() - .map(Transaction::getId) - .collect(toList()); - return transactionDAO.saveTransactionsToPermanentTable(ids, conn); - } else { - return Future.succeededFuture(); - } - })) - .mapEmpty(); - } - - private List updateBudgetsTotalsForCreatingTransactions(List tempTransactions, List budgets) { - // create tr - Map> tempGrouped = groupTransactionsByBudget(tempTransactions, budgets); - return tempGrouped.entrySet() - .stream() - .map(this::updateBudgetTotals) - .collect(toList()); - } - - private Budget updateBudgetTotals(Map.Entry> entry) { - Budget budget = JsonObject.mapFrom(entry.getKey()).mapTo(Budget.class); - if (isNotEmpty(entry.getValue())) { - CurrencyUnit currency = Monetary.getCurrency(entry.getValue().get(0).getCurrency()); - entry.getValue() - .forEach(tmpTransaction -> { - - double newEncumbered = sumMoney(budget.getEncumbered(), tmpTransaction.getAmount(), currency); - budget.setEncumbered(newEncumbered); - budgetService.updateBudgetMetadata(budget, tmpTransaction); - budgetService.clearReadOnlyFields(budget); - }); - } - return budget; - } - - private Future>> getTransactionsForCreateAndUpdate(List tmpTransactions, DBConn conn) { - CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); - - tmpTransactions.stream() - .map(Transaction::getId) - .forEach(id -> criterionBuilder.with("id", id)); - - return transactionDAO.getTransactions(criterionBuilder.build(), conn) - .map(trs -> groupTransactionForCreateAndUpdate(tmpTransactions, trs)); - } - - private Map> groupTransactionForCreateAndUpdate(List tmpTransactions, List permanentTransactions) { - Map> groupedTransactions = new HashMap<>(); - Set ids = permanentTransactions.stream() - .map(Transaction::getId) - .collect(Collectors.toSet()); - - groupedTransactions.put(FOR_UPDATE, tmpTransactions.stream().filter(tr -> ids.contains(tr.getId())).collect(Collectors.toList())); - groupedTransactions.put(FOR_CREATE, tmpTransactions.stream().filter(tr -> !ids.contains(tr.getId())).collect(Collectors.toList())); - groupedTransactions.put(EXISTING, permanentTransactions); - return groupedTransactions; - } - - private List findTransactionForUpdate(List incomingTransactions, List existingTransactions) { - // if nothing to update - if (existingTransactions.isEmpty()) { - return new ArrayList<>(); - } - Map groupedExistingTransactions = existingTransactions.stream().collect(toMap(Transaction::getId, identity())); - - return incomingTransactions.stream() - // filter transactions for update - .filter(incomingTransaction -> groupedExistingTransactions.containsKey(incomingTransaction.getId())) - .filter(incomingTransaction -> { - Transaction existingTransaction = groupedExistingTransactions.get(incomingTransaction.getId()); - return isNotFromReleasedExceptToUnreleased(incomingTransaction, existingTransaction) - || isEncumbranceOrderStatusUpdated(incomingTransaction, existingTransaction); - }) - .collect(Collectors.toList()); - } - - private boolean isEncumbranceOrderStatusUpdated(Transaction incomingTransaction, Transaction existingTransaction) { - return Encumbrance.OrderStatus.CLOSED == incomingTransaction.getEncumbrance().getOrderStatus() - && Encumbrance.OrderStatus.OPEN == existingTransaction.getEncumbrance().getOrderStatus(); - } - - private Future updateBudgetsTotals(Map> groupedTransactions, - List newTransactions, DBConn conn) { - - return budgetService.getBudgets(getSelectBudgetsQueryForUpdate(conn.getTenantId()), - Tuple.of(newTransactions.get(0).getEncumbrance().getSourcePurchaseOrderId()), conn) - .compose(oldBudgets -> { - - List updatedBudgets = updateBudgetsTotalsForCreatingTransactions(groupedTransactions.get(FOR_CREATE), oldBudgets); - List finalNewBudgets = updateBudgetsTotalsForUpdatingTransactions(groupedTransactions.get(EXISTING), - groupedTransactions.get(FOR_UPDATE), updatedBudgets); - - return budgetService.updateBatchBudgets(finalNewBudgets, conn); - }); - } - - - private List updateBudgetsTotalsForUpdatingTransactions(List existingTransactions, List tempTransactions, List budgets) { - // if nothing to update - if (existingTransactions.isEmpty()) { - return budgets; - } - Map existingGrouped = existingTransactions.stream().collect(toMap(Transaction::getId, identity())); - Map> tempGrouped = groupTransactionsByBudget(tempTransactions, budgets); - return tempGrouped.entrySet().stream() - .map(listEntry -> updateBudgetTotals(listEntry, existingGrouped)) - .collect(Collectors.toList()); - } - - private Budget updateBudgetTotals(Map.Entry> entry, Map existingGrouped) { - Budget budget = entry.getKey(); - - if (isNotEmpty(entry.getValue())) { - CurrencyUnit currency = Monetary.getCurrency(entry.getValue().get(0).getCurrency()); - entry.getValue() - .forEach(tmpTransaction -> { - Transaction existingTransaction = existingGrouped.get(tmpTransaction.getId()); - if (isNotFromReleasedExceptToUnreleased(tmpTransaction, existingTransaction)) { - processBudget(budget, currency, tmpTransaction, existingTransaction); - } - }); - } - return budget; - } - - private void processBudget(Budget budget, CurrencyUnit currency, Transaction tmpTransaction, Transaction existingTransaction) { - - double newEncumbered = budget.getEncumbered(); - if (isEncumbranceReleased(tmpTransaction)) { - newEncumbered = subtractMoney(newEncumbered, tmpTransaction.getAmount(), currency); - tmpTransaction.setAmount(0d); - } else if (isTransitionFromUnreleasedToPending(tmpTransaction, existingTransaction)) { - tmpTransaction.setAmount(0d); - tmpTransaction.getEncumbrance().setInitialAmountEncumbered(0d); - newEncumbered = subtractMoney(newEncumbered, existingTransaction.getAmount(), currency); - } else if (isTransitionFromPendingToUnreleased(tmpTransaction, existingTransaction)) { - double newAmount = subtractMoney(tmpTransaction.getEncumbrance().getInitialAmountEncumbered(), existingTransaction.getEncumbrance().getAmountAwaitingPayment(), currency); - newAmount = subtractMoney(newAmount, existingTransaction.getEncumbrance().getAmountExpended(), currency); - tmpTransaction.setAmount(newAmount); - newEncumbered = sumMoney(currency, newEncumbered, newAmount); - } else if (isTransitionFromReleasedToUnreleased(tmpTransaction, existingTransaction)) { - double newAmount = subtractMoney(tmpTransaction.getEncumbrance().getInitialAmountEncumbered(), - tmpTransaction.getEncumbrance().getAmountAwaitingPayment(), currency); - newAmount = subtractMoney(newAmount, tmpTransaction.getEncumbrance().getAmountExpended(), currency); - tmpTransaction.setAmount(newAmount); - newEncumbered = sumMoney(newEncumbered, newAmount, currency); - } else { - newEncumbered = sumMoney(newEncumbered, tmpTransaction.getAmount(), currency); - newEncumbered = subtractMoney(newEncumbered, existingTransaction.getAmount(), currency); - } - - budget.setEncumbered(newEncumbered); - budgetService.updateBudgetMetadata(budget, tmpTransaction); - budgetService.clearReadOnlyFields(budget); - } - - - private boolean isEncumbranceReleased(Transaction transaction) { - return transaction.getEncumbrance() - .getStatus() == Encumbrance.Status.RELEASED; - } - - private boolean isNotFromReleasedExceptToUnreleased(Transaction newTransaction, Transaction existingTransaction) { - return existingTransaction.getEncumbrance().getStatus() != Encumbrance.Status.RELEASED || - newTransaction.getEncumbrance().getStatus() == Encumbrance.Status.UNRELEASED; - } - - private boolean isTransitionFromUnreleasedToPending(Transaction newTransaction, Transaction existingTransaction) { - return existingTransaction.getEncumbrance().getStatus() == Encumbrance.Status.UNRELEASED - && newTransaction.getEncumbrance().getStatus() == Encumbrance.Status.PENDING; - } - - private boolean isTransitionFromPendingToUnreleased(Transaction newTransaction, Transaction existingTransaction) { - return existingTransaction.getEncumbrance().getStatus() == Encumbrance.Status.PENDING - && newTransaction.getEncumbrance().getStatus() == Encumbrance.Status.UNRELEASED; - } - - private boolean isTransitionFromReleasedToUnreleased(Transaction newTransaction, Transaction existingTransaction) { - return existingTransaction.getEncumbrance().getStatus() == Encumbrance.Status.RELEASED - && newTransaction.getEncumbrance().getStatus() == Encumbrance.Status.UNRELEASED; - } -} diff --git a/src/main/java/org/folio/service/transactions/PaymentCreditService.java b/src/main/java/org/folio/service/transactions/PaymentCreditService.java deleted file mode 100644 index c21ac680..00000000 --- a/src/main/java/org/folio/service/transactions/PaymentCreditService.java +++ /dev/null @@ -1,319 +0,0 @@ -package org.folio.service.transactions; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.sqlclient.Tuple; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBConn; -import org.folio.service.transactions.cancel.CancelTransactionService; -import org.folio.utils.MoneyUtils; -import org.folio.service.budget.BudgetService; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.Function; -import java.util.function.Predicate; - -import static java.lang.Boolean.TRUE; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PAYMENT; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -public class PaymentCreditService extends AbstractTransactionService implements TransactionManagingStrategy { - - private static final Logger logger = LogManager.getLogger(PaymentCreditService.class); - - private static final String TRANSACTIONS_TABLE = "transaction"; - - public static final String SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE = - "SELECT b.jsonb FROM %s b INNER JOIN (SELECT DISTINCT budgets.id FROM %s budgets INNER JOIN %s transactions " - + "ON ((budgets.fundId = transactions.fromFundId OR budgets.fundId = transactions.toFundId) AND " - + "transactions.fiscalYearId = budgets.fiscalYearId) " - + "WHERE transactions.sourceInvoiceId = $1 AND " - + "(transactions.jsonb ->> 'transactionType' = 'Payment' OR transactions.jsonb ->> 'transactionType' = 'Credit')) " - + "sub ON sub.id = b.id " - + "FOR UPDATE OF b"; - - public static final String SELECT_PERMANENT_TRANSACTIONS = "SELECT DISTINCT ON (permtransactions.id) permtransactions.jsonb FROM %s AS permtransactions INNER JOIN %s AS transactions " - + "ON transactions.paymentEncumbranceId = permtransactions.id WHERE transactions.sourceInvoiceId = $1 AND (transactions.jsonb ->> 'transactionType' = 'Payment' OR transactions.jsonb ->> 'transactionType' = 'Credit')"; - public static final String TRANSACTION_TYPE = "transactionType"; - public static final String SOURCE_INVOICE_ID = "sourceInvoiceId"; - - Predicate hasEncumbrance = txn -> txn.getPaymentEncumbranceId() != null; - Predicate isPaymentTransaction = txn -> txn.getTransactionType().equals(Transaction.TransactionType.PAYMENT); - Predicate isCreditTransaction = txn -> txn.getTransactionType().equals(Transaction.TransactionType.CREDIT); - - private final AllOrNothingTransactionService allOrNothingTransactionService; - private final BudgetService budgetService; - private final CancelTransactionService cancelTransactionService; - - public PaymentCreditService(AllOrNothingTransactionService allOrNothingTransactionService, - TransactionDAO transactionDAO, - BudgetService budgetService, - CancelTransactionService cancelTransactionService) { - super(transactionDAO); - this.allOrNothingTransactionService = allOrNothingTransactionService; - this.budgetService = budgetService; - this.cancelTransactionService = cancelTransactionService; - } - - @Override - public Future createTransaction(Transaction transaction, DBConn conn) { - return allOrNothingTransactionService.createTransaction(transaction, conn, this::createTransactions); - } - - @Override - public Future updateTransaction(Transaction transaction, DBConn conn) { - return allOrNothingTransactionService.updateTransaction(transaction, conn, this::updateTransactions); - } - - /** - * Before the temporary transaction can be moved to permanent transaction table, the corresponding fields in encumbrances and - * budgets for payments/credits must be recalculated. To avoid partial transactions, all payments and credits will be done in a - * database transaction - * - * @param transactions to be processed - * @param conn - database connection - */ - - public Future createTransactions(List transactions, DBConn conn) { - String summaryId = getSummaryId(transactions.get(0)); - return updateEncumbranceTotals(transactions, conn) - .compose(dbc -> updateBudgetsTotals(transactions, conn)) - .compose(dbc -> transactionDAO.saveTransactionsToPermanentTable(summaryId, conn)) - .compose(integer -> deletePendingPayments(summaryId, conn)); - } - - public Future updateTransactions(List tmpTransactions, DBConn conn) { - return getTransactions(tmpTransactions, conn) - .map(existingTransactions -> tmpTransactions.stream() - .filter(tr -> TRUE.equals(tr.getInvoiceCancelled())) - .filter(tr -> existingTransactions.stream().anyMatch(tr2 -> tr2.getId().equals(tr.getId()) && - !TRUE.equals(tr2.getInvoiceCancelled()))) - .collect(toList())) - .compose(transactionsToCancel -> cancelTransactionService.cancelTransactions(transactionsToCancel, conn)); - } - - private Future> getTransactions(List tmpTransactions, DBConn conn) { - List ids = tmpTransactions.stream() - .map(Transaction::getId) - .collect(toList()); - return transactionDAO.getTransactions(ids, conn); - } - - private Future deletePendingPayments(String summaryId, DBConn conn) { - CriterionBuilder criterionBuilder = new CriterionBuilder(); - criterionBuilder.withJson(SOURCE_INVOICE_ID, "=", summaryId) - .withJson(TRANSACTION_TYPE, "=", PENDING_PAYMENT.value()); - return transactionDAO.deleteTransactions(criterionBuilder.build(), conn); - } - - /** - * Update the Encumbrance transaction attached to the payment/Credit(from paymentEncumbranceID) - * in a transaction - * - * @param transactions to be processed - * @param conn - database connection - */ - private Future updateEncumbranceTotals(List transactions, DBConn conn) { - boolean noEncumbrances = transactions - .stream() - .allMatch(transaction -> StringUtils.isBlank(transaction.getPaymentEncumbranceId())); - - if (noEncumbrances) { - return Future.succeededFuture(conn); - } - String summaryId = getSummaryId(transactions.get(0)); - return getAllEncumbrances(summaryId, conn).map(encumbrances -> encumbrances.stream() - .collect(toMap(Transaction::getId, identity()))) - .map(encumbrancesMap -> applyPayments(transactions, encumbrancesMap)) - .map(encumbrancesMap -> applyCredits(transactions, encumbrancesMap)) - //update all the re-calculated encumbrances into the Transaction table - .map(map -> new ArrayList<>(map.values())) - .compose(trns -> transactionDAO.updatePermanentTransactions(trns, conn)) - .compose(ok -> Future.succeededFuture(conn)); - } - - private Future updateBudgetsTotals(List transactions, DBConn conn) { - String summaryId = getSummaryId(transactions.get(0)); - String sql = getSelectBudgetsQueryForUpdate(conn.getTenantId()); - return budgetService.getBudgets(sql, Tuple.of(UUID.fromString(summaryId)), conn) - .map(budgets -> budgets.stream().collect(toMap(Budget::getFundId, Function.identity()))) - .map(groupedBudgets -> calculatePaymentBudgetsTotals(transactions, groupedBudgets)) - .map(grpBudgets -> calculateCreditBudgetsTotals(transactions, grpBudgets)) - .compose(grpBudgets -> budgetService.updateBatchBudgets(grpBudgets.values().stream().toList(), conn)); - } - - private Map calculatePaymentBudgetsTotals(List tempTransactions, - Map groupedBudgets) { - Map> paymentBudgetsGrouped = tempTransactions.stream() - .filter(isPaymentTransaction) - .collect(groupingBy(transaction -> groupedBudgets.get(transaction.getFromFundId()))); - - if (!paymentBudgetsGrouped.isEmpty()) { - logger.debug("calculatePaymentBudgetsTotals:: Calculating budget totals for payment transactions"); - paymentBudgetsGrouped.entrySet() - .forEach(this::updateBudgetPaymentTotals); - } - - return groupedBudgets; - } - - private Map calculateCreditBudgetsTotals(List tempTransactions, Map groupedBudgets) { - Map> creditBudgetsGrouped = tempTransactions.stream() - .filter(isCreditTransaction) - .collect(groupingBy(transaction -> groupedBudgets.get(transaction.getToFundId()))); - - if (!creditBudgetsGrouped.isEmpty()) { - logger.debug("calculateCreditBudgetsTotals:: Calculating budget totals for credit transactions"); - creditBudgetsGrouped.entrySet() - .forEach(this::updateBudgetCreditTotals); - } - - return groupedBudgets; - } - - private void updateBudgetPaymentTotals(Map.Entry> entry) { - Budget budget = entry.getKey(); - CurrencyUnit currency = Monetary.getCurrency(entry.getValue() - .get(0) - .getCurrency()); - entry.getValue() - .forEach(txn -> { - budget.setExpenditures(MoneyUtils.sumMoney(budget.getExpenditures(), txn.getAmount(), currency)); - double newAwaitingPayment = MoneyUtils.subtractMoney(budget.getAwaitingPayment(), txn.getAmount(), currency); - budget.setAwaitingPayment(newAwaitingPayment); - budgetService.updateBudgetMetadata(budget, txn); - budgetService.clearReadOnlyFields(budget); - }); - } - - private void updateBudgetCreditTotals(Map.Entry> entry) { - Budget budget = entry.getKey(); - CurrencyUnit currency = Monetary.getCurrency(entry.getValue() - .get(0) - .getCurrency()); - entry.getValue() - .forEach(txn -> { - budget.setExpenditures(MoneyUtils.subtractMoney(budget.getExpenditures(), txn.getAmount(), currency)); - budget.setAwaitingPayment(MoneyUtils.sumMoney(budget.getAwaitingPayment(), txn.getAmount(), currency)); - budgetService.updateBudgetMetadata(budget, txn); - budgetService.clearReadOnlyFields(budget); - }); - } - - /** - *
-   * Encumbrances are recalculated with the credit transaction as below:
-   * - expended decreases by credit transaction amount (min 0)
-   * 
- */ - private Map applyCredits(List tempTxns, Map encumbrancesMap) { - - List tempCredits = tempTxns.stream() - .filter(isCreditTransaction.and(hasEncumbrance)) - .toList(); - - if (tempCredits.isEmpty()) { - return encumbrancesMap; - } - CurrencyUnit currency = Monetary.getCurrency(tempCredits.get(0) - .getCurrency()); - tempCredits.forEach(creditTxn -> { - Transaction encumbranceTxn = encumbrancesMap.get(creditTxn.getPaymentEncumbranceId()); - - double newExpended = MoneyUtils.subtractMoneyNonNegative(encumbranceTxn.getEncumbrance() - .getAmountExpended(), creditTxn.getAmount(), currency); - double newAwaitingPayment = MoneyUtils.sumMoney(encumbranceTxn.getEncumbrance() - .getAmountAwaitingPayment(), creditTxn.getAmount(), currency); - encumbranceTxn.getEncumbrance() - .withAmountExpended(newExpended) - .withAmountAwaitingPayment(newAwaitingPayment); - - }); - - return encumbrancesMap; - } - - /** - *
-   * Encumbrances are recalculated with the payment transaction as below:
-   * - expended increases by payment transaction amount
-   * 
- */ - private Map applyPayments(List tempTxns, Map encumbrancesMap) { - List tempPayments = tempTxns.stream() - .filter(isPaymentTransaction.and(hasEncumbrance)) - .toList(); - if (tempPayments.isEmpty()) { - return encumbrancesMap; - } - - CurrencyUnit currency = Monetary.getCurrency(tempPayments.get(0) - .getCurrency()); - tempPayments.forEach(pymtTxn -> { - Transaction encumbranceTxn = encumbrancesMap.get(pymtTxn.getPaymentEncumbranceId()); - double newExpended = MoneyUtils.sumMoney(encumbranceTxn.getEncumbrance() - .getAmountExpended(), pymtTxn.getAmount(), currency); - double newAwaitingPayment = MoneyUtils.subtractMoney(encumbranceTxn.getEncumbrance() - .getAmountAwaitingPayment(), pymtTxn.getAmount(), currency); - - encumbranceTxn.getEncumbrance() - .withAmountExpended(newExpended) - .withAmountAwaitingPayment(newAwaitingPayment); - - }); - - return encumbrancesMap; - } - - private Future> getAllEncumbrances(String summaryId, DBConn conn) { - logger.debug("getAllEncumbrances:: Trying to get all encumbrances by summary id {}", summaryId); - String sql = buildGetPermanentEncumbrancesQuery(conn.getTenantId()); - return conn.execute(sql, Tuple.of(UUID.fromString(summaryId))) - .map(rowSet -> { - List encumbrances = new ArrayList<>(); - rowSet.spliterator().forEachRemaining(row -> encumbrances.add(row.get(JsonObject.class, 0).mapTo(Transaction.class))); - return encumbrances; - }) - .onSuccess(encumbrances -> logger.info("Successfully retrieved {} encumbrances by summary id {}", - encumbrances.size(), summaryId)) - .onFailure(e -> logger.error("getAllEncumbrances:: Getting all encumbrances by summary id {} failed", summaryId, e)); - } - - private String buildGetPermanentEncumbrancesQuery(String tenantId) { - return String.format(SELECT_PERMANENT_TRANSACTIONS, getFullTableName(tenantId, TRANSACTIONS_TABLE), - getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS)); - } - - private String getSelectBudgetsQueryForUpdate(String tenantId){ - String budgetTableName = getFullTableName(tenantId, BUDGET_TABLE); - String transactionTableName = getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS); - return String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - } - - public Transaction.TransactionType getStrategyName() { - return PAYMENT; - } - - - public String getSummaryId(Transaction transaction) { - return transaction.getSourceInvoiceId(); - } -} diff --git a/src/main/java/org/folio/service/transactions/PendingPaymentService.java b/src/main/java/org/folio/service/transactions/PendingPaymentService.java deleted file mode 100644 index da1eea60..00000000 --- a/src/main/java/org/folio/service/transactions/PendingPaymentService.java +++ /dev/null @@ -1,353 +0,0 @@ -package org.folio.service.transactions; - -import static io.vertx.core.Future.succeededFuture; -import static java.lang.Boolean.TRUE; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; -import static javax.money.Monetary.getDefaultRounding; -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.persist.HelperUtils.getFullTableName; -import static org.folio.rest.util.ErrorCodes.OUTDATED_FUND_ID_IN_ENCUMBRANCE; -import static org.folio.utils.MoneyUtils.subtractMoney; -import static org.folio.utils.MoneyUtils.sumMoney; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.stream.Collectors; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import javax.money.MonetaryAmount; -import javax.ws.rs.core.Response; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.rest.exception.HttpException; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Parameter; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.service.transactions.cancel.CancelTransactionService; -import org.javamoney.moneta.Money; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.sqlclient.Tuple; -import org.javamoney.moneta.function.MonetaryFunctions; - -public class PendingPaymentService extends AbstractTransactionService implements TransactionManagingStrategy { - - private static final Logger logger = LogManager.getLogger(PendingPaymentService.class); - - public static final String SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE = - "SELECT b.jsonb FROM %s b INNER JOIN (SELECT DISTINCT budgets.id FROM %s budgets INNER JOIN %s transactions " - + "ON (budgets.fundId = transactions.fromFundId AND transactions.fiscalYearId = budgets.fiscalYearId) " - + "WHERE transactions.sourceInvoiceId = $1 AND transactions.jsonb ->> 'transactionType' = 'Pending payment') " - + "sub ON sub.id = b.id " - + "FOR UPDATE OF b"; - - private final AllOrNothingTransactionService allOrNothingTransactionService; - private final BudgetService budgetService; - private final CancelTransactionService cancelTransactionService; - - public PendingPaymentService(AllOrNothingTransactionService allOrNothingTransactionService, - TransactionDAO transactionDAO, - BudgetService budgetService, - CancelTransactionService cancelTransactionService) { - super(transactionDAO); - this.allOrNothingTransactionService = allOrNothingTransactionService; - this.budgetService = budgetService; - this.cancelTransactionService = cancelTransactionService; - } - - @Override - public Future createTransaction(Transaction transaction, DBConn conn) { - return allOrNothingTransactionService.createTransaction(transaction, conn, this::createTransactions); - } - - @Override - public Future updateTransaction(Transaction transaction, DBConn conn) { - return allOrNothingTransactionService.updateTransaction(transaction, conn, this::cancelAndUpdateTransactions); - } - - @Override - public Transaction.TransactionType getStrategyName() { - return PENDING_PAYMENT; - } - - public Future createTransactions(List transactions, DBConn conn) { - return processPendingPayments(transactions, conn) - .compose(aVoid -> transactionDAO.saveTransactionsToPermanentTable(transactions.get(0).getSourceInvoiceId(), conn)) - .mapEmpty(); - } - - public Future cancelAndUpdateTransactions(List tmpTransactions, DBConn conn) { - return getTransactions(tmpTransactions, conn) - .compose(existingTransactions -> { - List idsToCancel = tmpTransactions.stream() - .filter(tr -> TRUE.equals(tr.getInvoiceCancelled())) - .map(Transaction::getId) - .filter(id -> existingTransactions.stream().anyMatch( - tr -> tr.getId().equals(id) && !TRUE.equals(tr.getInvoiceCancelled()))) - .toList(); - List tmpTransactionsToCancel = tmpTransactions.stream() - .filter(tr -> idsToCancel.contains(tr.getId())) - .collect(toList()); - List tmpTransactionsToUpdate = tmpTransactions.stream() - .filter(tr -> !idsToCancel.contains(tr.getId())) - .collect(toList()); - List existingTransactionsToUpdate = existingTransactions.stream() - .filter(tr -> !idsToCancel.contains(tr.getId())) - .collect(toList()); - return cancelTransactions(tmpTransactionsToCancel, conn) - .compose(v -> updateTransactions(tmpTransactionsToUpdate, existingTransactionsToUpdate, conn)); - }); - } - - private Future cancelTransactions(List tmpTransactions, DBConn conn) { - if (tmpTransactions.isEmpty()) - return succeededFuture(); - return cancelTransactionService.cancelTransactions(tmpTransactions, conn) - .map(voidedTransactions -> null); - } - - private Future updateTransactions(List tmpTransactions, List existingTransactions, - DBConn conn) { - if (tmpTransactions.isEmpty()) - return succeededFuture(); - List transactions = createDifferenceTransactions(tmpTransactions, existingTransactions); - return processPendingPayments(transactions, conn) - .map(processedTransactions -> null) - .compose(v -> transactionDAO.updatePermanentTransactions(tmpTransactions, conn)); - } - - private Future> processPendingPayments(List transactions, DBConn conn) { - List linkedToEncumbrance = transactions.stream() - .filter(transaction -> Objects.nonNull(transaction.getAwaitingPayment()) && Objects.nonNull(transaction.getAwaitingPayment().getEncumbranceId())) - .collect(Collectors.toList()); - List notLinkedToEncumbrance = transactions.stream() - .filter(transaction -> Objects.isNull(transaction.getAwaitingPayment()) || Objects.isNull(transaction.getAwaitingPayment().getEncumbranceId())) - .collect(Collectors.toList()); - - String summaryId = getSummaryId(transactions.get(0)); - - return budgetService.getBudgets(getSelectBudgetsQueryForUpdate(conn.getTenantId()), Tuple.of(UUID.fromString(summaryId)), conn) - .compose(oldBudgets -> processLinkedPendingPayments(linkedToEncumbrance, oldBudgets, conn) - .map(budgets -> processNotLinkedPendingPayments(notLinkedToEncumbrance, budgets)) - .compose(newBudgets -> budgetService.updateBatchBudgets(newBudgets, conn))) - .map(transactions); - } - - private List createDifferenceTransactions(List tmpTransactions, List transactionsFromDB) { - Map amountIdMap = tmpTransactions.stream().collect(toMap(Transaction::getId, Transaction::getAmount)); - return transactionsFromDB.stream() - .map(transaction -> createDifferenceTransaction(transaction, amountIdMap.getOrDefault(transaction.getId(), 0d))) - .collect(Collectors.toList()); - } - - private Transaction createDifferenceTransaction(Transaction transaction, double newAmount) { - Transaction transactionDifference = JsonObject.mapFrom(transaction).mapTo(Transaction.class); - CurrencyUnit currency = Monetary.getCurrency(transaction.getCurrency()); - double amountDifference = subtractMoney(newAmount, transaction.getAmount(), currency); - return transactionDifference.withAmount(amountDifference); - } - - - private Future> getTransactions(List tmpTransactions, DBConn conn) { - - List ids = tmpTransactions.stream() - .map(Transaction::getId) - .collect(toList()); - - return transactionDAO.getTransactions(ids, conn); - } - - private List processNotLinkedPendingPayments(List pendingPayments, List oldBudgets) { - if (isNotEmpty(pendingPayments)) { - return updateBudgetsTotalsWithNotLinkedPendingPayments(pendingPayments, oldBudgets); - } - return oldBudgets; - } - - private List updateBudgetsTotalsWithNotLinkedPendingPayments(List tempTransactions, List budgets) { - Map> tempGrouped = groupTransactionsByBudget(tempTransactions, budgets); - return tempGrouped.entrySet().stream() - .map(this::updateBudgetTotalsWithNotLinkedPendingPayments) - .collect(toList()); - } - - private Map> groupTransactionsByBudget(List existingTransactions, List budgets) { - Map> groupedTransactions = existingTransactions.stream().collect(groupingBy(Transaction::getFromFundId)); - - return budgets.stream() - .collect(toMap(identity(), budget -> groupedTransactions.getOrDefault(budget.getFundId(), Collections.emptyList()))); - - } - - private Budget updateBudgetTotalsWithNotLinkedPendingPayments(Map.Entry> entry) { - Budget budget = JsonObject.mapFrom(entry.getKey()).mapTo(Budget.class); - if (isNotEmpty(entry.getValue())) { - CurrencyUnit currency = Monetary.getCurrency(entry.getValue().get(0).getCurrency()); - entry.getValue() - .forEach(tmpTransaction -> { - double newAwaitingPayment = sumMoney(budget.getAwaitingPayment(), tmpTransaction.getAmount(), currency); - budget.setAwaitingPayment(newAwaitingPayment); - budgetService.updateBudgetMetadata(budget, tmpTransaction); - budgetService.clearReadOnlyFields(budget); - }); - } - return budget; - } - - private Future> processLinkedPendingPayments(List pendingPayments, List oldBudgets, DBConn conn) { - if (isNotEmpty(pendingPayments)) { - List ids = pendingPayments.stream() - .map(transaction -> transaction.getAwaitingPayment().getEncumbranceId()) - .collect(toList()); - - return transactionDAO.getTransactions(ids, conn) - .map(encumbrances -> updateEncumbrancesTotals(encumbrances, pendingPayments)) - .compose(encumbrances -> { - List newBudgets = updateBudgetsTotalsWithLinkedPendingPayments(pendingPayments, encumbrances, oldBudgets); - return transactionDAO.updatePermanentTransactions(encumbrances, conn) - .map(newBudgets); - }); - } - return succeededFuture(oldBudgets); - } - - private List updateEncumbrancesTotals(List encumbrances, List pendingPayments) { - Map> pendingPaymentsByEncumbranceId = pendingPayments.stream() - .collect(groupingBy(transaction -> transaction.getAwaitingPayment().getEncumbranceId())); - encumbrances.forEach(encumbrance -> updateEncumbranceTotals(encumbrance, pendingPaymentsByEncumbranceId.get(encumbrance.getId()))); - return encumbrances; - } - - private void updateEncumbranceTotals(Transaction encumbrance, List transactions) { - MonetaryAmount ppAmountTotal = transactions.stream() - .map(transaction -> Money.of(transaction.getAmount(), transaction.getCurrency())) - .reduce(Money::add) - .orElse(Money.zero(Monetary.getCurrency(encumbrance.getCurrency()))); - - - if (transactions.stream().anyMatch(transaction -> transaction.getAwaitingPayment().getReleaseEncumbrance())) { - encumbrance.getEncumbrance().setStatus(Encumbrance.Status.RELEASED); - } - - // set awaiting payment value - MonetaryAmount awaitingPayment = Money.of(encumbrance.getEncumbrance().getAmountAwaitingPayment(), encumbrance.getCurrency()).add(ppAmountTotal); - encumbrance.getEncumbrance().setAmountAwaitingPayment(awaitingPayment.with(getDefaultRounding()).getNumber().doubleValue()); - - MonetaryAmount amount = Money.of(encumbrance.getAmount(), encumbrance.getCurrency()).subtract(ppAmountTotal); - encumbrance.setAmount(amount.with(getDefaultRounding()).getNumber().doubleValue()); - } - - private List updateBudgetsTotalsWithLinkedPendingPayments(List pendingPayments, List encumbrances, List budgets) { - - Map fundIdBudgetMap = budgets.stream().collect(toMap(Budget::getFundId, budget -> JsonObject.mapFrom(budget).mapTo(Budget.class))); - - List releasedEncumbrances = encumbrances.stream() - .filter(transaction -> transaction.getEncumbrance().getStatus() == Encumbrance.Status.RELEASED) - .collect(Collectors.toList()); - - List negativeEncumbrances = encumbrances.stream() - .filter(transaction -> transaction.getAmount() < 0) - .collect(Collectors.toList()); - - applyNegativeEncumbrances(negativeEncumbrances, fundIdBudgetMap); - applyPendingPayments(pendingPayments, fundIdBudgetMap); - applyEncumbrances(releasedEncumbrances, fundIdBudgetMap); - - - return new ArrayList<>(fundIdBudgetMap.values()); - } - - private void applyNegativeEncumbrances(List negativeEncumbrances, Map fundIdBudgetMap) { - if (isNotEmpty(negativeEncumbrances)) { - CurrencyUnit currency = Monetary.getCurrency(negativeEncumbrances.get(0).getCurrency()); - negativeEncumbrances.forEach(transaction -> { - Budget budget = fundIdBudgetMap.get(transaction.getFromFundId()); - if (budget == null) { - List parameters = new ArrayList<>(); - parameters.add(new Parameter().withKey("encumbranceId").withValue(transaction.getId())); - parameters.add(new Parameter().withKey("fundId").withValue(transaction.getFromFundId())); - parameters.add(new Parameter().withKey("poLineId").withValue(transaction.getEncumbrance().getSourcePoLineId())); - Error error = OUTDATED_FUND_ID_IN_ENCUMBRANCE.toError().withParameters(parameters); - logger.error("applyNegativeEncumbrances:: Applying negative encumbrances failed {}", JsonObject.mapFrom(error).encodePrettily()); - throw new HttpException(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), error); - } - MonetaryAmount amount = Money.of(transaction.getAmount(), currency).negate(); - - MonetaryAmount encumbered = Money.of(budget.getEncumbered(), currency); - - budget.setEncumbered(encumbered.add(amount).getNumber().doubleValue()); - budgetService.updateBudgetMetadata(budget, transaction); - budgetService.clearReadOnlyFields(budget); - transaction.setAmount(0.00); - }); - } - } - - private void applyPendingPayments(List pendingPayments, Map fundIdBudgetMap) { - CurrencyUnit currency = Monetary.getCurrency(pendingPayments.get(0).getCurrency()); - - // sort pending payments by amount to apply negative amounts first - List sortedPendingPayments = pendingPayments.stream() - .sorted(Comparator.comparing(Transaction::getAmount)) - .toList(); - sortedPendingPayments.forEach(transaction -> { - Budget budget = fundIdBudgetMap.get(transaction.getFromFundId()); - MonetaryAmount amount = Money.of(transaction.getAmount(), currency); - MonetaryAmount encumbered = Money.of(budget.getEncumbered(), currency); - MonetaryAmount awaitingPayment = Money.of(budget.getAwaitingPayment(), currency); - double newEncumbered = MonetaryFunctions.max().apply(encumbered.subtract(amount), Money.zero(currency)).getNumber().doubleValue(); - double newAwaitingPayment = awaitingPayment.add(amount).getNumber().doubleValue(); - budget.setEncumbered(newEncumbered); - budget.setAwaitingPayment(newAwaitingPayment); - budgetService.updateBudgetMetadata(budget, transaction); - budgetService.clearReadOnlyFields(budget); - }); - } - - private void applyEncumbrances(List releasedEncumbrances, Map fundIdBudgetMap) { - if (isNotEmpty(releasedEncumbrances)) { - CurrencyUnit currency = Monetary.getCurrency(releasedEncumbrances.get(0).getCurrency()); - releasedEncumbrances.forEach(transaction -> { - Budget budget = fundIdBudgetMap.get(transaction.getFromFundId()); - MonetaryAmount amount = Money.of(transaction.getAmount(), currency); - MonetaryAmount encumbered = Money.of(budget.getEncumbered(), currency); - double newEncumbered = encumbered.subtract(amount).getNumber().doubleValue(); - budget.setEncumbered(newEncumbered); - transaction.setAmount(0.00); - budgetService.updateBudgetMetadata(budget, transaction); - budgetService.clearReadOnlyFields(budget); - }); - } - - } - - public String getSummaryId(Transaction transaction) { - return transaction.getSourceInvoiceId(); - } - - private String getSelectBudgetsQueryForUpdate(String tenantId) { - String budgetTableName = getFullTableName(tenantId, BUDGET_TABLE); - String transactionTableName = getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS); - return String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - } - -} diff --git a/src/main/java/org/folio/service/transactions/TemporaryTransactionService.java b/src/main/java/org/folio/service/transactions/TemporaryEncumbranceService.java similarity index 68% rename from src/main/java/org/folio/service/transactions/TemporaryTransactionService.java rename to src/main/java/org/folio/service/transactions/TemporaryEncumbranceService.java index c529ada7..0dc5602f 100644 --- a/src/main/java/org/folio/service/transactions/TemporaryTransactionService.java +++ b/src/main/java/org/folio/service/transactions/TemporaryEncumbranceService.java @@ -1,7 +1,7 @@ package org.folio.service.transactions; import io.vertx.core.Future; -import org.folio.dao.transactions.TemporaryTransactionDAO; +import org.folio.dao.transactions.TemporaryEncumbranceDAO; import org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverBudget; import org.folio.rest.jaxrs.model.Transaction; import org.folio.rest.persist.CriterionBuilder; @@ -9,12 +9,12 @@ import java.util.List; -public class TemporaryTransactionService { +public class TemporaryEncumbranceService { - private final TemporaryTransactionDAO temporaryTransactionDAO; + private final TemporaryEncumbranceDAO temporaryEncumbranceDAO; - public TemporaryTransactionService(TemporaryTransactionDAO temporaryTransactionDAO) { - this.temporaryTransactionDAO = temporaryTransactionDAO; + public TemporaryEncumbranceService(TemporaryEncumbranceDAO temporaryEncumbranceDAO) { + this.temporaryEncumbranceDAO = temporaryEncumbranceDAO; } public Future> getTransactions(LedgerFiscalYearRolloverBudget budget, DBConn conn) { @@ -26,7 +26,7 @@ public Future> getTransactions(LedgerFiscalYearRolloverBudget .withOperation("AND") .withJson("fromFundId", "=", budget.getFundId()); - return temporaryTransactionDAO.getTempTransactions(criterionBuilder.build(), conn); + return temporaryEncumbranceDAO.getTempTransactions(criterionBuilder.build(), conn); } } diff --git a/src/main/java/org/folio/service/transactions/TransactionManagingStrategy.java b/src/main/java/org/folio/service/transactions/TransactionManagingStrategy.java deleted file mode 100644 index 53b3e50c..00000000 --- a/src/main/java/org/folio/service/transactions/TransactionManagingStrategy.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.folio.service.transactions; - -import org.folio.rest.jaxrs.model.Transaction; - -public interface TransactionManagingStrategy extends TransactionService { - Transaction.TransactionType getStrategyName(); -} diff --git a/src/main/java/org/folio/service/transactions/TransactionManagingStrategyFactory.java b/src/main/java/org/folio/service/transactions/TransactionManagingStrategyFactory.java deleted file mode 100644 index 4f9566a3..00000000 --- a/src/main/java/org/folio/service/transactions/TransactionManagingStrategyFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.folio.service.transactions; - -import org.folio.rest.jaxrs.model.Transaction.TransactionType; - -import java.util.EnumMap; -import java.util.Map; -import java.util.Set; - -public class TransactionManagingStrategyFactory { - - private Map strategies; - - public TransactionManagingStrategyFactory(Set strategySet) { - createStrategy(strategySet); - } - - public static TransactionType transactionTypeWithoutCredit(TransactionType transactionType) { - return transactionType == TransactionType.CREDIT ? TransactionType.PAYMENT : transactionType; - } - - public TransactionManagingStrategy findStrategy(TransactionType transactionType) { - return strategies.get(transactionTypeWithoutCredit(transactionType)); - } - - private void createStrategy(Set strategySet) { - strategies = new EnumMap<>(TransactionType.class); - strategySet.forEach( - strategy -> strategies.put(strategy.getStrategyName(), strategy)); - } - -} diff --git a/src/main/java/org/folio/service/transactions/TransactionService.java b/src/main/java/org/folio/service/transactions/TransactionService.java deleted file mode 100644 index f60470b2..00000000 --- a/src/main/java/org/folio/service/transactions/TransactionService.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.folio.service.transactions; - -import io.vertx.core.Future; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; - -public interface TransactionService { - - Future createTransaction(Transaction transaction, DBConn conn); - - Future updateTransaction(Transaction transaction, DBConn conn); - - Future deleteTransactionById(String id, DBConn conn); -} diff --git a/src/main/java/org/folio/service/transactions/TransferService.java b/src/main/java/org/folio/service/transactions/TransferService.java deleted file mode 100644 index 593a49ff..00000000 --- a/src/main/java/org/folio/service/transactions/TransferService.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.folio.service.transactions; - -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.rest.persist.HelperUtils.buildNullValidationError; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.utils.CalculationUtils; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; - -public class TransferService extends AbstractTransactionService implements TransactionManagingStrategy { - - private static final Logger logger = LogManager.getLogger(TransferService.class); - - private final BudgetService budgetService; - - public TransferService(BudgetService budgetService, TransactionDAO transactionDAO) { - super(transactionDAO); - this.budgetService = budgetService; - } - - @Override - public Future createTransaction(Transaction transfer, DBConn conn) { - try { - handleValidationError(transfer); - } catch (HttpException e) { - return Future.failedFuture(e); - } - return super.createTransaction(transfer, conn) - .compose(createdTransfer -> { - if (createdTransfer.getFromFundId() != null) { - return updateBudgetsTransferFrom(createdTransfer, conn); - } - return Future.succeededFuture(createdTransfer); - }) - .compose(createdTransfer -> updateBudgetsTransferTo(createdTransfer, conn)) - .onSuccess(v -> logger.info("createTransaction:: Transfer with id {} and associated data were successfully processed", - transfer.getId())) - .onFailure(e -> logger.error("createTransaction:: Transfer with id {} or associated data failed to be processed", - transfer.getId(), e)); - } - - @Override - public Transaction.TransactionType getStrategyName() { - return Transaction.TransactionType.TRANSFER; - } - - private Future updateBudgetsTransferFrom(Transaction transfer, DBConn conn) { - return budgetService.getBudgetByFiscalYearIdAndFundIdForUpdate(transfer.getFiscalYearId(), transfer.getFromFundId(), conn) - .map(budgetFromOld -> { - Budget budgetFromNew = JsonObject.mapFrom(budgetFromOld).mapTo(Budget.class); - CalculationUtils.recalculateBudgetTransfer(budgetFromNew, transfer, transfer.getAmount()); - budgetService.updateBudgetMetadata(budgetFromNew, transfer); - return budgetFromNew; - }) - .compose(budgetFrom -> budgetService.updateBatchBudgets(Collections.singletonList(budgetFrom), conn)) - .map(v -> transfer); - } - - private Future updateBudgetsTransferTo(Transaction transfer, DBConn conn) { - return budgetService.getBudgetByFiscalYearIdAndFundIdForUpdate(transfer.getFiscalYearId(), transfer.getToFundId(), conn) - .map(budgetTo -> { - Budget budgetToNew = JsonObject.mapFrom(budgetTo).mapTo(Budget.class); - CalculationUtils.recalculateBudgetTransfer(budgetToNew, transfer, -transfer.getAmount()); - budgetService.updateBudgetMetadata(budgetToNew, transfer); - return budgetToNew; - }) - .compose(budgetFrom -> budgetService.updateBatchBudgets(Collections.singletonList(budgetFrom), conn)) - .map(v -> transfer); - } - - private void handleValidationError(Transaction transfer) { - - List errors = new ArrayList<>(buildNullValidationError(transfer.getToFundId(), TO_FUND_ID)); - - if (isNotEmpty(errors)) { - logger.error("handleValidationError: Validation error for transfer with toFundId {}", transfer.getToFundId()); - throw new HttpException(422, JsonObject.mapFrom(new Errors().withErrors(errors) - .withTotalRecords(errors.size())) - .encode()); - } - } -} diff --git a/src/main/java/org/folio/service/transactions/batch/BatchTransactionChecks.java b/src/main/java/org/folio/service/transactions/batch/BatchTransactionChecks.java index 8f6c7d25..e0c36e94 100644 --- a/src/main/java/org/folio/service/transactions/batch/BatchTransactionChecks.java +++ b/src/main/java/org/folio/service/transactions/batch/BatchTransactionChecks.java @@ -16,6 +16,7 @@ import org.javamoney.moneta.Money; import javax.money.MonetaryAmount; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,7 +26,6 @@ import static io.vertx.core.Future.succeededFuture; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.ACTIVE; import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.PLANNED; import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ALLOCATION; @@ -35,8 +35,10 @@ import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; import static org.folio.rest.jaxrs.model.Transaction.TransactionType.TRANSFER; import static org.folio.rest.util.ErrorCodes.ALLOCATION_MUST_BE_POSITIVE; +import static org.folio.rest.util.ErrorCodes.BUDGET_IS_NOT_ACTIVE_OR_PLANNED; import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_ENCUMBRANCE_ERROR; import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_EXPENDITURES_ERROR; +import static org.folio.rest.util.ErrorCodes.ID_IS_REQUIRED_IN_TRANSACTIONS; import static org.folio.rest.util.ErrorCodes.MISSING_FUND_ID; import static org.folio.rest.util.ErrorCodes.PAYMENT_OR_CREDIT_HAS_NEGATIVE_AMOUNT; @@ -73,8 +75,10 @@ public static void sanityChecks(Batch batch) { public static void checkBudgetsAreActive(BatchTransactionHolder holder) { holder.getBudgets().forEach(budget -> { if (!List.of(ACTIVE, PLANNED).contains(budget.getBudgetStatus())) { - throw new HttpException(400, String.format("Cannot process transactions because a budget is not active or planned, fundId=%s", - holder.getFundCodeForBudget(budget))); + Error error = BUDGET_IS_NOT_ACTIVE_OR_PLANNED.toError(); + Parameter fundCodeParam = new Parameter().withKey("fundCode").withValue(holder.getFundCodeForBudget(budget)); + error.setParameters(List.of(fundCodeParam)); + throw new HttpException(400, error); } }); } @@ -156,8 +160,9 @@ public static void checkRestrictedBudgets(BatchTransactionHolder holder) { private static void checkIdIsPresent(List transactions, String operation) { for (Transaction transaction : transactions) { if (transaction.getId() == null) { - throw new HttpException(400, - String.format("Id is required in transactions to %s.", operation)); + Error error = ID_IS_REQUIRED_IN_TRANSACTIONS.toError(); + error.setMessage(MessageFormat.format(error.getMessage(), operation)); + throw new HttpException(400, error); } if (ENCUMBRANCE == transaction.getTransactionType() && (transaction.getEncumbrance() == null || transaction.getEncumbrance().getSourcePurchaseOrderId() == null)) { @@ -190,8 +195,7 @@ private static void checkTransactionsToUpdateHaveMetadata(List tran private static void checkAllocation(Transaction allocation) { if (allocation.getAmount() <= 0) { - List parameters = singletonList(new Parameter().withKey("fieldName") - .withValue("amount")); + List parameters = List.of(new Parameter().withKey("fieldName").withValue("amount")); Error error = ALLOCATION_MUST_BE_POSITIVE.toError().withParameters(parameters); throw new HttpException(400, error); } @@ -273,7 +277,7 @@ private static void checkPaymentsAndCreditsAmounts(Batch batch) { .toList(); for (Transaction tr : newPaymentsAndCredits) { if (tr.getAmount() < 0) { - List parameters = singletonList(new Parameter().withKey("id").withValue(tr.getId())); + List parameters = List.of(new Parameter().withKey("id").withValue(tr.getId())); Error error = PAYMENT_OR_CREDIT_HAS_NEGATIVE_AMOUNT.toError().withParameters(parameters); throw new HttpException(422, error); } diff --git a/src/main/java/org/folio/service/transactions/batch/BatchTransactionHolder.java b/src/main/java/org/folio/service/transactions/batch/BatchTransactionHolder.java index 0456513c..65a1f2d8 100644 --- a/src/main/java/org/folio/service/transactions/batch/BatchTransactionHolder.java +++ b/src/main/java/org/folio/service/transactions/batch/BatchTransactionHolder.java @@ -2,10 +2,13 @@ import io.vertx.core.Future; import org.folio.dao.transactions.BatchTransactionDAO; +import org.folio.rest.exception.HttpException; import org.folio.rest.jaxrs.model.Batch; import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Error; import org.folio.rest.jaxrs.model.Fund; import org.folio.rest.jaxrs.model.Ledger; +import org.folio.rest.jaxrs.model.Parameter; import org.folio.rest.jaxrs.model.Transaction; import org.folio.rest.jaxrs.model.TransactionPatch; import org.folio.rest.persist.Criteria.Criteria; @@ -39,6 +42,7 @@ import static org.folio.rest.jaxrs.model.Transaction.TransactionType.CREDIT; import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PAYMENT; import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; +import static org.folio.rest.util.ErrorCodes.LINKED_ENCUMBRANCES_NOT_FOUND; public class BatchTransactionHolder { private static final String TRANSACTION_TYPE = "transactionType"; @@ -189,6 +193,15 @@ private Future loadLinkedEncumbrances(DBConn conn) { .toList(); return transactionDAO.getTransactionsByIds(ids, conn) .map(transactions -> { + if (transactions.size() != ids.size()) { + List missingIds = ids.stream() + .filter(id -> transactions.stream().noneMatch(tr -> id.equals(tr.getId()))) + .toList(); + Error error = LINKED_ENCUMBRANCES_NOT_FOUND.toError(); + Parameter idsParam = new Parameter().withKey("ids").withValue(missingIds.toString()); + error.setParameters(List.of(idsParam)); + throw new HttpException(400, error); + } linkedEncumbrances = transactions; return null; }); diff --git a/src/main/java/org/folio/service/transactions/cancel/CancelPaymentCreditService.java b/src/main/java/org/folio/service/transactions/cancel/CancelPaymentCreditService.java deleted file mode 100644 index 03be0e00..00000000 --- a/src/main/java/org/folio/service/transactions/cancel/CancelPaymentCreditService.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.folio.service.transactions.cancel; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.jaxrs.model.Transaction.TransactionType; -import org.folio.service.budget.BudgetService; -import org.folio.utils.MoneyUtils; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import java.util.List; -import java.util.Optional; - -import static org.folio.utils.MoneyUtils.subtractMoney; -import static org.folio.utils.MoneyUtils.sumMoney; - -public class CancelPaymentCreditService extends CancelTransactionService { - - public CancelPaymentCreditService(BudgetService budgetService, TransactionDAO paymentCreditDAO, TransactionDAO encumbranceDAO) { - super(budgetService, paymentCreditDAO, encumbranceDAO); - } - - @Override - protected Budget budgetMoneyBack(Budget budget, List transactions) { - Budget newBudget = JsonObject.mapFrom(budget).mapTo(Budget.class); - CurrencyUnit currency = Monetary.getCurrency(transactions.get(0).getCurrency()); - transactions.forEach(tmpTransaction -> { - double newExpenditures; - if (tmpTransaction.getTransactionType().equals(TransactionType.CREDIT)) { - newExpenditures = MoneyUtils.sumMoney(newBudget.getExpenditures(), - tmpTransaction.getAmount(), currency); - } else { - newExpenditures = MoneyUtils.subtractMoney(newBudget.getExpenditures(), - tmpTransaction.getAmount(), currency); - } - double newVoidedAmount = tmpTransaction.getAmount(); - newBudget.setExpenditures(newExpenditures); - tmpTransaction.setVoidedAmount(newVoidedAmount); - tmpTransaction.setAmount(0.0); - }); - return newBudget; - } - - @Override - protected Optional getEncumbranceId(Transaction pendingPayment) { - return Optional.ofNullable(pendingPayment.getPaymentEncumbranceId()); - } - - @Override - protected void cancelEncumbrance(Transaction encumbrance, List paymentsAndCredits) { - CurrencyUnit currency = Monetary.getCurrency(encumbrance.getCurrency()); - double newEncumbranceAmount = encumbrance.getAmount(); - double newAmountExpended = encumbrance.getEncumbrance().getAmountExpended(); - for (Transaction paymentOrCredit : paymentsAndCredits) { - if (paymentOrCredit.getTransactionType().equals(TransactionType.CREDIT)) { - newAmountExpended = sumMoney(newAmountExpended, paymentOrCredit.getAmount(), currency); - if (Encumbrance.Status.RELEASED != encumbrance.getEncumbrance().getStatus()) { - newEncumbranceAmount = subtractMoney(newEncumbranceAmount, paymentOrCredit.getAmount(), currency); - } - } else { - newAmountExpended = subtractMoney(newAmountExpended, paymentOrCredit.getAmount(), currency); - newEncumbranceAmount = sumMoney(newEncumbranceAmount, paymentOrCredit.getAmount(), currency); - } - } - encumbrance.setAmount(newEncumbranceAmount); - encumbrance.getEncumbrance().setAmountExpended(newAmountExpended); - } -} diff --git a/src/main/java/org/folio/service/transactions/cancel/CancelPendingPaymentService.java b/src/main/java/org/folio/service/transactions/cancel/CancelPendingPaymentService.java deleted file mode 100644 index 87f0cd86..00000000 --- a/src/main/java/org/folio/service/transactions/cancel/CancelPendingPaymentService.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.folio.service.transactions.cancel; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.service.budget.BudgetService; -import org.folio.utils.MoneyUtils; - -import javax.money.CurrencyUnit; -import javax.money.Monetary; -import java.util.List; -import java.util.Optional; - -public class CancelPendingPaymentService extends CancelTransactionService { - - public CancelPendingPaymentService(BudgetService budgetService, TransactionDAO paymentCreditDAO, TransactionDAO encumbranceDAO) { - super(budgetService, paymentCreditDAO, encumbranceDAO); - } - - @Override - protected Budget budgetMoneyBack(Budget budget, List transactions) { - Budget newBudget = JsonObject.mapFrom(budget).mapTo(Budget.class); - CurrencyUnit currency = Monetary.getCurrency(transactions.get(0).getCurrency()); - transactions.forEach(tmpTransaction -> { - double newAwaitingPayment = MoneyUtils.subtractMoney(newBudget.getAwaitingPayment(), - tmpTransaction.getAmount(), currency); - double newVoidedAmount = tmpTransaction.getAmount(); - - newBudget.setAwaitingPayment(newAwaitingPayment); - tmpTransaction.setVoidedAmount(newVoidedAmount); - tmpTransaction.setAmount(0.0); - }); - return newBudget; - } - - @Override - protected Optional getEncumbranceId(Transaction pendingPayment) { - if (pendingPayment.getAwaitingPayment() == null) - return Optional.empty(); - return Optional.ofNullable(pendingPayment.getAwaitingPayment().getEncumbranceId()); - } - - @Override - protected void cancelEncumbrance(Transaction encumbrance, List pendingPayments) { - CurrencyUnit currency = Monetary.getCurrency(encumbrance.getCurrency()); - double newEncumbranceAmount = encumbrance.getAmount(); - double newAmountAwaitingPayment = encumbrance.getEncumbrance().getAmountAwaitingPayment(); - for (Transaction pendingPayment : pendingPayments) { - if (Encumbrance.Status.RELEASED != encumbrance.getEncumbrance().getStatus()) { - newEncumbranceAmount = MoneyUtils.sumMoney(newEncumbranceAmount, pendingPayment.getAmount(), currency); - } - newAmountAwaitingPayment = MoneyUtils.subtractMoney(newAmountAwaitingPayment, pendingPayment.getAmount(), currency); - } - encumbrance.setAmount(newEncumbranceAmount); - encumbrance.getEncumbrance().setAmountAwaitingPayment(newAmountAwaitingPayment); - } -} diff --git a/src/main/java/org/folio/service/transactions/cancel/CancelTransactionService.java b/src/main/java/org/folio/service/transactions/cancel/CancelTransactionService.java deleted file mode 100644 index 8bb46a63..00000000 --- a/src/main/java/org/folio/service/transactions/cancel/CancelTransactionService.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.folio.service.transactions.cancel; - -import io.vertx.core.Future; -import io.vertx.sqlclient.Tuple; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -import static java.util.Objects.requireNonNullElse; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.*; -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.rest.persist.HelperUtils.getFullTableName; - -public abstract class CancelTransactionService { - - private static final String SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE = - "SELECT b.jsonb FROM %s b INNER JOIN (SELECT DISTINCT budgets.id FROM %s budgets INNER JOIN %s transactions " - + "ON ((budgets.fundId = transactions.fromFundId OR budgets.fundId = transactions.toFundId) AND " - + "transactions.fiscalYearId = budgets.fiscalYearId) " - + "WHERE transactions.sourceInvoiceId = $1 AND " - + "transactions.jsonb ->> 'transactionType' IN ('Payment', 'Credit', 'Pending payment')) " - + "sub ON sub.id = b.id " - + "FOR UPDATE OF b"; - - private final BudgetService budgetService; - private final TransactionDAO paymentCreditDAO; - private final TransactionDAO encumbranceDAO; - - public CancelTransactionService(BudgetService budgetService, TransactionDAO paymentCreditDAO, TransactionDAO encumbranceDAO) { - this.budgetService = budgetService; - this.paymentCreditDAO = paymentCreditDAO; - this.encumbranceDAO = encumbranceDAO; - } - - /** - * Updates given transactions, related encumbrances and related budgets to cancel the transactions. - * All transactions are supposed to be either pending payment or payment/credit. - */ - public Future cancelTransactions(List transactions, DBConn conn) { - String summaryId = getSummaryId(transactions.get(0)); - return updateRelatedEncumbrances(transactions, conn) - .compose(v -> budgetService.getBudgets(getSelectBudgetsQuery(conn.getTenantId()), - Tuple.of(UUID.fromString(summaryId)), conn)) - .map(budgets -> budgetsMoneyBack(transactions, budgets)) - .compose(newBudgets -> budgetService.updateBatchBudgets(newBudgets, conn)) - .compose(v -> paymentCreditDAO.updatePermanentTransactions(transactions, conn)); - } - - private List budgetsMoneyBack(List transactions, List budgets) { - Map> budgetToTransactions = groupTransactionsByBudget(transactions, budgets); - return budgetToTransactions.entrySet().stream() - .map(entry -> budgetMoneyBack(entry.getKey(), entry.getValue())) - .collect(toList()); - } - - private Map> groupTransactionsByBudget(List transactions, List budgets) { - Map> fundIdToTransactions = transactions.stream() - .collect(groupingBy(tr -> requireNonNullElse(tr.getFromFundId(), tr.getToFundId()))); - return budgets.stream() - .filter(budget -> fundIdToTransactions.get(budget.getFundId()) != null) - .collect(toMap(identity(), budget -> fundIdToTransactions.get(budget.getFundId()))); - } - - private String getSummaryId(Transaction transaction) { - return transaction.getSourceInvoiceId(); - } - - private String getSelectBudgetsQuery(String tenantId) { - String budgetTableName = getFullTableName(tenantId, BUDGET_TABLE); - String transactionTableName = getFullTableName(tenantId, TEMPORARY_INVOICE_TRANSACTIONS); - return String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - } - - private Future updateRelatedEncumbrances(List transactions, DBConn conn) { - Map> encumbranceIdToTransactions = transactions.stream() - .filter(tr -> getEncumbranceId(tr).isPresent()) - .collect(groupingBy(tr -> getEncumbranceId(tr).orElse(null))); - if (encumbranceIdToTransactions.isEmpty()) - return Future.succeededFuture(); - List encumbranceIds = new ArrayList<>(encumbranceIdToTransactions.keySet()); - return encumbranceDAO.getTransactions(encumbranceIds, conn) - .map(tr -> updateEncumbrances(tr, encumbranceIdToTransactions)) - .compose(encumbrances -> encumbranceDAO.updatePermanentTransactions(encumbrances, conn)); - } - - private List updateEncumbrances(List encumbrances, - Map> encumbranceIdToTransactions) { - encumbrances.forEach(enc -> cancelEncumbrance(enc, encumbranceIdToTransactions.get(enc.getId()))); - return encumbrances; - } - - protected abstract Budget budgetMoneyBack(Budget budget, List transactions); - - protected abstract Optional getEncumbranceId(Transaction pendingPayment); - - protected abstract void cancelEncumbrance(Transaction encumbrance, List transactions); -} diff --git a/src/main/java/org/folio/service/transactions/restriction/BaseTransactionRestrictionService.java b/src/main/java/org/folio/service/transactions/restriction/BaseTransactionRestrictionService.java deleted file mode 100644 index 4ab3296c..00000000 --- a/src/main/java/org/folio/service/transactions/restriction/BaseTransactionRestrictionService.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.folio.service.transactions.restriction; - -import io.vertx.core.Future; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.folio.rest.exception.HttpException; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Parameter; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.service.ledger.LedgerService; -import org.javamoney.moneta.Money; - -import javax.ws.rs.core.Response; -import java.text.MessageFormat; - -import static org.folio.rest.util.ErrorCodes.BUDGET_IS_INACTIVE; - -public abstract class BaseTransactionRestrictionService implements TransactionRestrictionService { - - private static final Logger logger = LogManager.getLogger(BaseTransactionRestrictionService.class); - - private static final String BUDGET_ID = "budgetId"; - public static final String FUND_CANNOT_BE_PAID = "Fund cannot be paid due to restrictions"; - - private final BudgetService budgetService; - private final LedgerService ledgerService; - - public BaseTransactionRestrictionService(BudgetService budgetService, LedgerService ledgerService) { - this.budgetService = budgetService; - this.ledgerService = ledgerService; - } - - - @Override - public Future verifyBudgetHasEnoughMoney(Transaction transaction, DBConn conn) { - String fundId = transaction.getTransactionType() == Transaction.TransactionType.CREDIT ? transaction.getToFundId() : transaction.getFromFundId(); - - return budgetService.getBudgetByFundIdAndFiscalYearId(transaction.getFiscalYearId(), fundId, conn) - .compose(budget -> { - if (budget.getBudgetStatus() != Budget.BudgetStatus.ACTIVE) { - Error error = buildBudgetIsInactiveError(budget); - logger.error("verifyBudgetHasEnoughMoney:: The verification failed, the budget with id {} has the status {} ", budget.getId(), budget.getBudgetStatus().value()); - return Future.failedFuture(new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), error)); - } - if (transaction.getTransactionType() == Transaction.TransactionType.CREDIT || transaction.getAmount() <= 0) { - return Future.succeededFuture(); - } - return getRelatedTransaction(transaction, conn) - .compose(relatedTransaction -> ledgerService.getLedgerByTransaction(transaction, conn) - .map(ledger -> checkTransactionAllowed(transaction, relatedTransaction, budget, ledger))); - }); - } - - private Error buildBudgetIsInactiveError(Budget budget) { - String description = MessageFormat.format(BUDGET_IS_INACTIVE.getDescription(), budget.getId()); - Error error = new Error().withCode(BUDGET_IS_INACTIVE.getCode()).withMessage(description); - error.getParameters().add(new Parameter().withKey(BUDGET_ID).withValue(budget.getId())); - return error; - } - - /** - * @param transaction Used in overrides - * @param conn Used in overrides - */ - protected Future getRelatedTransaction(Transaction transaction, DBConn conn) { - return Future.succeededFuture(null); - } - - private Void checkTransactionAllowed(Transaction transaction, Transaction relatedTransaction, Budget budget, Ledger ledger) { - if (isTransactionOverspendRestricted(ledger, budget)) { - Money budgetRemainingAmount = getBudgetRemainingAmount(budget, transaction.getCurrency(), relatedTransaction); - if (Money.of(transaction.getAmount(), transaction.getCurrency()).isGreaterThan(budgetRemainingAmount)) { - throw new HttpException(Response.Status.BAD_REQUEST.getStatusCode(), FUND_CANNOT_BE_PAID); - } - } - return null; - } - - abstract Money getBudgetRemainingAmount(Budget budget, String currency, Transaction relatedTransaction); - abstract boolean isTransactionOverspendRestricted(Ledger ledger, Budget budget); -} diff --git a/src/main/java/org/folio/service/transactions/restriction/EncumbranceRestrictionService.java b/src/main/java/org/folio/service/transactions/restriction/EncumbranceRestrictionService.java deleted file mode 100644 index 6935276a..00000000 --- a/src/main/java/org/folio/service/transactions/restriction/EncumbranceRestrictionService.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.folio.service.transactions.restriction; - -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.service.budget.BudgetService; -import org.folio.service.ledger.LedgerService; -import org.javamoney.moneta.Money; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.rest.persist.HelperUtils.buildNullValidationError; - -public class EncumbranceRestrictionService extends BaseTransactionRestrictionService { - - public EncumbranceRestrictionService(BudgetService budgetService, LedgerService ledgerService) { - super(budgetService, ledgerService); - } - - /** - * Calculates remaining amount for encumbrance - * [remaining amount] = (allocated + netTransfers) * allowableEncumbered - (encumbered + awaitingPayment + expended) - * - * @param budget processed budget - * @param currency - * @param relatedTransaction - * @return remaining amount for encumbrance - */ - @Override - Money getBudgetRemainingAmount(Budget budget, String currency, Transaction relatedTransaction) { - Money allocated = Money.of(budget.getAllocated(), currency); - // get allowableEncumbered converted from percentage value - double allowableEncumbered = Money.of(budget.getAllowableEncumbrance(), currency).divide(100d).getNumber().doubleValue(); - Money encumbered = Money.of(budget.getEncumbered(), currency); - Money awaitingPayment = Money.of(budget.getAwaitingPayment(), currency); - Money expended = Money.of(budget.getExpenditures(), currency); - Money netTransfers = Money.of(budget.getNetTransfers(), currency); - - Money totalFunding = allocated.add(netTransfers); - Money unavailable = encumbered.add(awaitingPayment).add(expended); - - return totalFunding.multiply(allowableEncumbered).subtract(unavailable); - } - - @Override - boolean isTransactionOverspendRestricted(Ledger ledger, Budget budget) { - return ledger.getRestrictEncumbrance() - && budget.getAllowableEncumbrance() != null; - } - - @Override - public void handleValidationError(Transaction transaction) { - List errors = new ArrayList<>(); - - errors.addAll(buildNullValidationError(getSummaryId(transaction), "encumbrance")); - errors.addAll(buildNullValidationError(transaction.getFromFundId(), "fromFundId")); - - if (isNotEmpty(errors)) { - throw new HttpException(422, JsonObject.mapFrom(new Errors().withErrors(errors) - .withTotalRecords(errors.size())) - .encode()); - } - } - - private String getSummaryId(Transaction transaction) { - return Optional.ofNullable(transaction.getEncumbrance()) - .map(Encumbrance::getSourcePurchaseOrderId) - .orElse(null); - } - -} diff --git a/src/main/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionService.java b/src/main/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionService.java deleted file mode 100644 index eca48619..00000000 --- a/src/main/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionService.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.folio.service.transactions.restriction; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.service.ledger.LedgerService; -import org.javamoney.moneta.Money; - -import java.util.ArrayList; -import java.util.List; - -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.persist.HelperUtils.buildNullValidationError; -import static org.folio.service.transactions.AbstractTransactionService.FROM_FUND_ID; -import static org.folio.service.transactions.AbstractTransactionService.TO_FUND_ID; - -public class PaymentCreditRestrictionService extends BaseTransactionRestrictionService { - - private final TransactionDAO transactionDAO; - - public PaymentCreditRestrictionService(BudgetService budgetService, LedgerService ledgerService, TransactionDAO transactionDAO) { - super(budgetService, ledgerService); - this.transactionDAO = transactionDAO; - } - - /** - * Calculates remaining amount for payment - * [remaining amount] = (allocated + netTransfers) * allowableExpenditure - (encumbered + awaitingPayment + expended) + relatedAwaitingPayment - * - * @param budget processed budget - * @param currency - * @param relatedTransaction - * @return remaining amount for payment - */ - @Override - protected Money getBudgetRemainingAmount(Budget budget, String currency, Transaction relatedTransaction) { - Money allocated = Money.of(budget.getAllocated(), currency); - // get allowableExpenditure from percentage value - double allowableExpenditure = Money.of(budget.getAllowableExpenditure(), currency).divide(100d).getNumber().doubleValue(); - - Money expended = Money.of(budget.getExpenditures(), currency); - Money relatedAwaitingPayment = relatedTransaction == null ? Money.of(0d, currency) : Money.of(relatedTransaction.getAmount(), currency); - Money awaitingPayment = Money.of(budget.getAwaitingPayment(), currency); - Money encumbered = Money.of(budget.getEncumbered(), currency); - Money netTransfers = Money.of(budget.getNetTransfers(), currency); - - Money totalFunding = allocated.add(netTransfers); - Money unavailable = encumbered.add(awaitingPayment).add(expended); - - if (unavailable.isGreaterThan(totalFunding) && relatedAwaitingPayment.isLessThan(unavailable)) { - return totalFunding.multiply(allowableExpenditure).subtract(unavailable.subtract(relatedAwaitingPayment)).add(relatedAwaitingPayment); - } - - return totalFunding.multiply(allowableExpenditure).subtract(unavailable).add(relatedAwaitingPayment); - } - - @Override - boolean isTransactionOverspendRestricted(Ledger ledger, Budget budget) { - return ledger.getRestrictExpenditures() - && budget.getAllowableExpenditure() != null; - } - - @Override - public void handleValidationError(Transaction transaction) { - List errors = new ArrayList<>(); - - if (transaction.getTransactionType() == Transaction.TransactionType.CREDIT) { - errors.addAll(buildNullValidationError(transaction.getToFundId(), TO_FUND_ID)); - } else { - errors.addAll(buildNullValidationError(transaction.getFromFundId(), FROM_FUND_ID)); - } - if (isNotEmpty(errors)) { - throw new HttpException(422, JsonObject.mapFrom(new Errors().withErrors(errors) - .withTotalRecords(errors.size())) - .encode()); - } - } - - @Override - protected Future getRelatedTransaction(Transaction transaction, DBConn conn) { - - CriterionBuilder criterionBuilder; - if (transaction.getSourceInvoiceLineId() != null) { - criterionBuilder = new CriterionBuilder() - .withJson("fromFundId","=", transaction.getFromFundId()) - .withJson("sourceInvoiceId","=", transaction.getSourceInvoiceId()) - .withJson("sourceInvoiceLineId","=", transaction.getSourceInvoiceLineId()) - .withJson("transactionType","=", PENDING_PAYMENT.value()) - .withOperation("AND"); - } else { - criterionBuilder = new CriterionBuilder() - .withJson("fromFundId","=", transaction.getFromFundId()) - .withJson("sourceInvoiceId","=", transaction.getSourceInvoiceId()) - .withJson("sourceInvoiceLineId","IS NULL", null) - .withJson("transactionType","=", PENDING_PAYMENT.value()) - .withOperation("AND"); - } - - return transactionDAO.getTransactions(criterionBuilder.build(), conn) - .map(transactions -> transactions.isEmpty() ? null : transactions.get(0)); - } - -} diff --git a/src/main/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionService.java b/src/main/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionService.java deleted file mode 100644 index 6d0c8bc2..00000000 --- a/src/main/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionService.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.folio.service.transactions.restriction; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.AwaitingPayment; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.folio.service.ledger.LedgerService; -import org.javamoney.moneta.Money; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.folio.rest.persist.HelperUtils.buildNullValidationError; -import static org.folio.service.transactions.AbstractTransactionService.FROM_FUND_ID; - -public class PendingPaymentRestrictionService extends BaseTransactionRestrictionService { - - private final TransactionDAO transactionsDAO; - - public PendingPaymentRestrictionService(BudgetService budgetService, LedgerService ledgerService, TransactionDAO transactionsDAO) { - super(budgetService, ledgerService); - this.transactionsDAO = transactionsDAO; - } - - /** - * Calculates remaining amount for payment and pending payments - * [remaining amount] = (allocated + netTransfers) * allowableExpenditure - (encumbered + awaitingPayment + expended) + relatedEncumbered - * - * @param budget processed budget - * @param currency - * @param relatedTransaction - * @return remaining amount for payment - */ - @Override - public Money getBudgetRemainingAmount(Budget budget, String currency, Transaction relatedTransaction) { - Money allocated = Money.of(budget.getAllocated(), currency); - // get allowableExpenditure from percentage value - double allowableExpenditure = Money.of(budget.getAllowableExpenditure(), currency).divide(100d).getNumber().doubleValue(); - Money expended = Money.of(budget.getExpenditures(), currency); - Money encumbered = Money.of(budget.getEncumbered(), currency); - Money awaitingPayment = Money.of(budget.getAwaitingPayment(), currency); - Money relatedEncumbered = relatedTransaction == null ? Money.of(0d, currency) : Money.of(relatedTransaction.getAmount(), currency); - Money netTransfers = Money.of(budget.getNetTransfers(), currency); - - Money totalFunding = allocated.add(netTransfers); - Money unavailable = encumbered.add(awaitingPayment).add(expended); - - return totalFunding.multiply(allowableExpenditure).subtract(unavailable).add(relatedEncumbered); - } - - @Override - protected Future getRelatedTransaction(Transaction transaction, DBConn conn) { - - String encumbranceId = Optional.ofNullable(transaction) - .map(Transaction::getAwaitingPayment) - .map(AwaitingPayment::getEncumbranceId) - .orElse(EMPTY); - - if (encumbranceId.isEmpty()) { - return Future.succeededFuture(null); - } - - CriterionBuilder criterion = new CriterionBuilder() - .with("id", encumbranceId); - - return transactionsDAO.getTransactions(criterion.build(), conn) - .map(transactions -> transactions.isEmpty() ? null : transactions.get(0)); - } - - @Override - boolean isTransactionOverspendRestricted(Ledger ledger, Budget budget) { - return ledger.getRestrictExpenditures() && budget.getAllowableExpenditure() != null; - } - - @Override - public void handleValidationError(Transaction transaction) { - - List errors = new ArrayList<>(buildNullValidationError(transaction.getFromFundId(), FROM_FUND_ID)); - - if (isNotEmpty(errors)) { - throw new HttpException(422, JsonObject.mapFrom(new Errors().withErrors(errors) - .withTotalRecords(errors.size())) - .encode()); - } - } - -} diff --git a/src/main/java/org/folio/service/transactions/restriction/TransactionRestrictionService.java b/src/main/java/org/folio/service/transactions/restriction/TransactionRestrictionService.java deleted file mode 100644 index fa7fd1e2..00000000 --- a/src/main/java/org/folio/service/transactions/restriction/TransactionRestrictionService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.folio.service.transactions.restriction; - -import io.vertx.core.Future; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; - -public interface TransactionRestrictionService { - - void handleValidationError(Transaction transaction); - Future verifyBudgetHasEnoughMoney(Transaction transaction, DBConn conn); -} diff --git a/src/main/resources/data/invoice-transaction-summaries/invoice-transaction-summary.json b/src/main/resources/data/invoice-transaction-summaries/invoice-transaction-summary.json deleted file mode 100644 index 80fffbcb..00000000 --- a/src/main/resources/data/invoice-transaction-summaries/invoice-transaction-summary.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "041c1fe8-adfa-44eb-b6b3-f85acdd71f81", - "numPendingPayments": 2, - "numPaymentsCredits": 2 -} diff --git a/src/main/resources/data/order-transaction-summaries/order-306857_transaction-summary.json b/src/main/resources/data/order-transaction-summaries/order-306857_transaction-summary.json deleted file mode 100644 index 26abf2e2..00000000 --- a/src/main/resources/data/order-transaction-summaries/order-306857_transaction-summary.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "e41e0161-2bc6-41f3-a6e7-34fc13250bf1", - "numTransactions": 1 -} diff --git a/src/main/resources/templates/db_scripts/migration/update_processed_order_transaction_summary.sql b/src/main/resources/templates/db_scripts/migration/update_processed_order_transaction_summary.sql deleted file mode 100644 index 1d6f3e68..00000000 --- a/src/main/resources/templates/db_scripts/migration/update_processed_order_transaction_summary.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE ${myuniversity}_${mymodule}.order_transaction_summaries AS summary - SET jsonb = jsonb_set(jsonb, '{numTransactions}', to_jsonb(-(jsonb->>'numTransactions')::integer), false) - WHERE (SELECT COUNT(*) FROM ${myuniversity}_${mymodule}.transaction AS transaction - WHERE summary.id::text = transaction.jsonb->'encumbrance'->>'sourcePurchaseOrderId') > 0 - AND (jsonb->>'numTransactions')::integer > 0; \ No newline at end of file diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index 6dfd44b8..5adeaca5 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -51,11 +51,6 @@ "snippetPath": "migration/fill_budget_net_transfer.sql", "fromModuleVersion": "mod-finance-storage-6.0.0" }, - { - "run": "after", - "snippetPath": "migration/update_processed_order_transaction_summary.sql", - "fromModuleVersion": "mod-finance-storage-6.0.0" - }, { "run": "after", "snippetPath": "migration/update_order_encumbrance.sql", @@ -366,7 +361,7 @@ { "tableName": "fund_distribution", "fromModuleVersion": "mod-finance-storage-4.2.1", - "mode": "delete", + "mode": "DELETE", "auditingTableName": "NOT_EXISTING_AUDITING_TABLE" }, { @@ -479,7 +474,7 @@ { "tableName": "ledgerFY", "fromModuleVersion": "mod-finance-storage-6.0.0", - "mode": "delete", + "mode": "DELETE", "auditingTableName": "NOT_EXISTING_AUDITING_TABLE" }, { @@ -542,11 +537,13 @@ }, { "tableName": "order_transaction_summaries", - "fromModuleVersion": "mod-finance-storage-4.0.0" + "fromModuleVersion": "mod-finance-storage-4.0.0", + "mode": "DELETE" }, { "tableName": "temporary_order_transactions", "fromModuleVersion": "mod-finance-storage-6.0.0", + "mode": "DELETE", "foreignKeys": [ { "fieldName": "encumbrance.sourcePurchaseOrderId", @@ -577,11 +574,13 @@ }, { "tableName": "invoice_transaction_summaries", - "fromModuleVersion": "mod-finance-storage-4.2.1" + "fromModuleVersion": "mod-finance-storage-4.2.1", + "mode": "DELETE" }, { "tableName": "temporary_invoice_transactions", "fromModuleVersion": "mod-finance-storage-6.0.0", + "mode": "DELETE", "uniqueIndex": [ { "fieldName": "temp_invoice_tx", @@ -624,7 +623,7 @@ { "tableName": "temporary_invoice_payments", "fromModuleVersion": "mod-finance-storage-4.2.1", - "mode": "delete", + "mode": "DELETE", "auditingTableName": "NOT_EXISTING_AUDITING_TABLE" }, { diff --git a/src/test/java/org/folio/StorageTestSuite.java b/src/test/java/org/folio/StorageTestSuite.java index 37f5dc9f..82cdd2c6 100644 --- a/src/test/java/org/folio/StorageTestSuite.java +++ b/src/test/java/org/folio/StorageTestSuite.java @@ -17,13 +17,10 @@ import org.folio.dao.rollover.LedgerFiscalYearRolloverDAOTest; import org.folio.dao.rollover.RolloverErrorDAOTest; import org.folio.dao.rollover.RolloverProgressDAOTest; -import org.folio.dao.transactions.BaseTransactionDAOTest; -import org.folio.dao.transactions.PendingPaymentDAOTest; import org.folio.postgres.testing.PostgresTesterContainer; import org.folio.rest.RestVerticle; import org.folio.rest.core.RestClientTest; import org.folio.rest.impl.BudgetTest; -import org.folio.rest.impl.EncumbrancesTest; import org.folio.rest.impl.EntitiesCrudTest; import org.folio.rest.impl.GroupBudgetTest; import org.folio.rest.impl.GroupFundFYTest; @@ -31,10 +28,8 @@ import org.folio.rest.impl.HelperUtilsTest; import org.folio.rest.impl.LedgerFundBudgetStatusTest; import org.folio.rest.impl.LedgerRolloverBudgetTest; -import org.folio.rest.impl.PaymentsCreditsTest; import org.folio.rest.impl.TenantSampleDataTest; import org.folio.rest.impl.TransactionTest; -import org.folio.rest.impl.TransactionsSummariesTest; import org.folio.rest.jaxrs.model.TenantJob; import org.folio.rest.persist.PostgresClient; import org.folio.rest.tools.utils.NetworkUtils; @@ -43,17 +38,10 @@ import org.folio.service.rollover.LedgerRolloverServiceTest; import org.folio.service.rollover.RolloverProgressServiceTest; import org.folio.service.rollover.RolloverValidationServiceTest; -import org.folio.service.summary.PendingPaymentTransactionSummaryServiceTest; -import org.folio.service.transactions.AllocationServiceTest; -import org.folio.service.transactions.BatchTransactionServiceTest; -import org.folio.service.transactions.EncumbranceServiceTest; -import org.folio.service.transactions.PaymentCreditServiceTest; -import org.folio.service.transactions.PendingPaymentServiceTest; -import org.folio.service.transactions.cancel.CancelPaymentCreditServiceTest; -import org.folio.service.transactions.cancel.CancelTransactionServiceTest; -import org.folio.service.transactions.restriction.EncumbranceRestrictionServiceTest; -import org.folio.service.transactions.restriction.PaymentCreditRestrictionServiceTest; -import org.folio.service.transactions.restriction.PendingPaymentRestrictionServiceTest; +import org.folio.service.transactions.AllocationTransferTest; +import org.folio.service.transactions.EncumbranceTest; +import org.folio.service.transactions.PaymentCreditTest; +import org.folio.service.transactions.PendingPaymentTest; import org.folio.utils.CalculationUtilsTest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -190,44 +178,6 @@ class GroupBudgetTestNested extends GroupBudgetTest { class HelperUtilsTestNested extends HelperUtilsTest { } - @Nested - class TransactionsSummariesTestNested extends TransactionsSummariesTest { - } - - @Nested - class PaymentsCreditsTestNested extends PaymentsCreditsTest { - } - - @Nested - class PaymentCreditServiceTestNested extends PaymentCreditServiceTest { - } - - @Nested - class EncumbrancesTestNested extends EncumbrancesTest { - } - - @Nested - class PendingPaymentServiceTestNested extends PendingPaymentServiceTest { - } - - @Nested - class BaseTransactionDAOTestNested extends BaseTransactionDAOTest {} - - @Nested - class PendingPaymentDAOTestNested extends PendingPaymentDAOTest {} - - @Nested - class PendingPaymentTransactionSummaryServiceTestNested extends PendingPaymentTransactionSummaryServiceTest {} - - @Nested - class EncumbranceRestrictionServiceTestNested extends EncumbranceRestrictionServiceTest {} - - @Nested - class PaymentCreditRestrictionServiceTestNested extends PaymentCreditRestrictionServiceTest {} - - @Nested - class PendingPaymentRestrictionServiceTestNested extends PendingPaymentRestrictionServiceTest {} - @Nested class LedgerRolloverServiceTestNested extends LedgerRolloverServiceTest {} @@ -250,23 +200,20 @@ class RolloverErrorDAOTestNested extends RolloverErrorDAOTest {} class CalculationUtilsTestNested extends CalculationUtilsTest {} @Nested - class AllocationServiceTestNested extends AllocationServiceTest {} - - @Nested - class CancelPaymentCreditServiceNested extends CancelPaymentCreditServiceTest {} + class RolloverValidationServiceTestNested extends RolloverValidationServiceTest {} @Nested - class CancelTransactionServiceNested extends CancelTransactionServiceTest {} + class EmailServiceTestNested extends EmailServiceTest {} @Nested - class EncumbranceServiceTestNested extends EncumbranceServiceTest {} + class AllocationTransferTestNested extends AllocationTransferTest {} @Nested - class RolloverValidationServiceTestNested extends RolloverValidationServiceTest {} + class EncumbranceTestNested extends EncumbranceTest {} @Nested - class EmailServiceTestNested extends EmailServiceTest {} + class PaymentCreditTestNested extends PaymentCreditTest {} @Nested - class BatchTransactionServiceTestNested extends BatchTransactionServiceTest {} + class PendingPaymentTestNested extends PendingPaymentTest {} } diff --git a/src/test/java/org/folio/dao/rollover/RolloverBudgetDAOTest.java b/src/test/java/org/folio/dao/rollover/RolloverBudgetDAOTest.java index 162eea5f..91906e0c 100644 --- a/src/test/java/org/folio/dao/rollover/RolloverBudgetDAOTest.java +++ b/src/test/java/org/folio/dao/rollover/RolloverBudgetDAOTest.java @@ -62,7 +62,7 @@ public void shouldGetBudgets(VertxTestContext testContext) { LedgerFiscalYearRolloverBudget budget = new LedgerFiscalYearRolloverBudget().withId(id); Results results = new Results<>(); - results.setResults(Collections.singletonList(budget)); + results.setResults(List.of(budget)); doReturn(Future.succeededFuture(results)).when(conn) .get(eq(ROLLOVER_BUDGET_TABLE), eq(LedgerFiscalYearRolloverBudget.class), eq(criterion), anyBoolean()); diff --git a/src/test/java/org/folio/dao/rollover/RolloverProgressDAOTest.java b/src/test/java/org/folio/dao/rollover/RolloverProgressDAOTest.java index 56831520..690b8764 100644 --- a/src/test/java/org/folio/dao/rollover/RolloverProgressDAOTest.java +++ b/src/test/java/org/folio/dao/rollover/RolloverProgressDAOTest.java @@ -8,7 +8,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; -import java.util.Collections; +import java.util.List; import java.util.UUID; import org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverError; @@ -59,7 +59,7 @@ void shouldCompletedSuccessfullyWhenRetrieveRolloverErrors(VertxTestContext test LedgerFiscalYearRolloverError error = new LedgerFiscalYearRolloverError().withId(id); Results results = new Results<>(); - results.setResults(Collections.singletonList(error)); + results.setResults(List.of(error)); doReturn(Future.succeededFuture(results)).when(conn) .get(eq(LEDGER_FISCAL_YEAR_ROLLOVER_ERRORS_TABLE), eq(LedgerFiscalYearRolloverError.class), eq(criterion), anyBoolean()); diff --git a/src/test/java/org/folio/dao/transactions/BaseTransactionDAOTest.java b/src/test/java/org/folio/dao/transactions/BaseTransactionDAOTest.java deleted file mode 100644 index f3729d7f..00000000 --- a/src/test/java/org/folio/dao/transactions/BaseTransactionDAOTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.folio.dao.transactions; - -import static org.folio.dao.transactions.EncumbranceDAO.TRANSACTIONS_TABLE; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import io.vertx.ext.web.handler.HttpException; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.Conn; -import org.folio.rest.persist.DBClient; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.rest.persist.interfaces.Results; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import io.vertx.core.Future; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import io.vertx.pgclient.PgException; -import io.vertx.sqlclient.Tuple; - -@ExtendWith(VertxExtension.class) -public class BaseTransactionDAOTest { - - private AutoCloseable mockitoMocks; - - @Mock - private Conn conn; - @Mock - private DBClient dbClient; - - private BaseTransactionDAO baseTransactionDAO; - private DBConn dbConn; - - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - baseTransactionDAO = Mockito.mock( - BaseTransactionDAO.class, Mockito.CALLS_REAL_METHODS); - dbConn = new DBConn(dbClient, conn); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void getTransactionsWithGenericDatabaseException(VertxTestContext testContext) { - doReturn(Future.failedFuture(new PgException("Test", "Test", "22P02", "Test"))).when(conn) - .get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), any(Criterion.class)); - - testContext.assertFailure(baseTransactionDAO.getTransactions(new Criterion(), dbConn)) - .onComplete(event -> { - HttpException exception = (HttpException) event.cause(); - testContext.verify(() -> { - assertEquals(exception.getStatusCode() , 400); - assertEquals(exception.getPayload(), "Test"); - }); - testContext.completeNow(); - }); - } - - @Test - void getTransactionsWithConnection(VertxTestContext testContext) { - Results results = new Results<>(); - results.setResults(Collections.emptyList()); - doReturn(Future.succeededFuture(results)).when(conn) - .get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), any(Criterion.class)); - - testContext.assertComplete(baseTransactionDAO.getTransactions(new Criterion(), dbConn)) - .onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> assertThat(transactions , hasSize(0))); - testContext.completeNow(); - }); - } - - @Test - void saveTransactionsToPermanentTable(VertxTestContext testContext) { - when(dbClient.getTenantId()) - .thenReturn("test"); - - Mockito.when(baseTransactionDAO.createPermanentTransactionsQuery(anyString())) - .thenReturn("test.table"); - - doReturn(Future.failedFuture(new PgException("Test", "Test", "22P02", "Test"))).when(conn) - .execute(eq("test.table"), any(Tuple.class)); - - testContext.assertFailure(baseTransactionDAO.saveTransactionsToPermanentTable(UUID.randomUUID().toString(), dbConn)) - .onComplete(event -> { - HttpException exception = (HttpException) event.cause(); - testContext.verify(() -> { - assertEquals(exception.getStatusCode() , 400); - assertEquals(exception.getPayload(), "Test"); - }); - testContext.completeNow(); - }); - } - - @Test - void updatePermanentTransactionsWithEmptyList() { - baseTransactionDAO.updatePermanentTransactions(Collections.emptyList(), dbConn); - verify(conn, never()).execute(anyString()); - } -} diff --git a/src/test/java/org/folio/dao/transactions/PendingPaymentDAOTest.java b/src/test/java/org/folio/dao/transactions/PendingPaymentDAOTest.java deleted file mode 100644 index fa878626..00000000 --- a/src/test/java/org/folio/dao/transactions/PendingPaymentDAOTest.java +++ /dev/null @@ -1,312 +0,0 @@ -package org.folio.dao.transactions; - -import static java.util.stream.Collectors.toMap; -import static org.folio.dao.summary.InvoiceTransactionSummaryDAO.INVOICE_TRANSACTION_SUMMARIES; -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.RestVerticle.OKAPI_HEADER_TENANT; -import static org.folio.rest.impl.ExpenseClassAPI.EXPENSE_CLASS_TABLE; -import static org.folio.rest.impl.FiscalYearAPI.FISCAL_YEAR_TABLE; -import static org.folio.rest.impl.FundAPI.FUND_TABLE; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ALLOCATION; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.utils.TenantApiTestUtil.deleteTenant; -import static org.folio.rest.utils.TenantApiTestUtil.prepareTenant; -import static org.folio.rest.utils.TenantApiTestUtil.purge; -import static org.folio.service.transactions.AbstractTransactionService.TRANSACTION_TABLE; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.Function; - -import org.folio.rest.impl.TestBase; -import org.folio.rest.jaxrs.model.ExpenseClass; -import org.folio.rest.jaxrs.model.FiscalYear; -import org.folio.rest.jaxrs.model.Fund; -import org.folio.rest.jaxrs.model.InvoiceTransactionSummary; -import org.folio.rest.jaxrs.model.TenantJob; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBClient; -import org.folio.rest.persist.Criteria.Criteria; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.rest.persist.DBConn; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import io.restassured.http.Header; -import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.Vertx; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; - -@ExtendWith(VertxExtension.class) -public class PendingPaymentDAOTest extends TestBase { - - static final String TEST_TENANT = "test_tenant"; - private static final Header TEST_TENANT_HEADER = new Header(OKAPI_HEADER_TENANT, TEST_TENANT); - - private final PendingPaymentDAO pendingPaymentDAO = new PendingPaymentDAO(); - private static TenantJob tenantJob; - - @BeforeEach - void prepareData() throws Exception { - tenantJob = prepareTenant(TEST_TENANT_HEADER, false, false); - } - - @AfterEach - void cleanupData() { - purge(TEST_TENANT_HEADER); - } - - @AfterAll - static void deleteTable() { - deleteTenant(tenantJob, TEST_TENANT_HEADER); - } - - @Test - void tesGetTransactions(Vertx vertx, VertxTestContext testContext) { - String id = UUID.randomUUID().toString(); - Transaction transaction = new Transaction().withId(id); - Promise promise1 = Promise.promise(); - final DBClient client = new DBClient(vertx, TEST_TENANT); - client.getPgClient().save(TRANSACTION_TABLE, id, transaction, event -> { - promise1.complete(); - }); - Promise promise2 = Promise.promise(); - client.getPgClient().save(TRANSACTION_TABLE, transaction, event -> { - promise2.complete(); - }); - Criterion criterion = new Criterion().addCriterion(new Criteria().addField("id").setOperation("=").setVal(id).setJSONB(false)); - testContext.assertComplete(promise1.future() - .compose(aVoid -> promise2.future()) - .compose(o -> client.withConn(conn -> pendingPaymentDAO.getTransactions(criterion, conn)))) - .onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> { - assertThat(transactions, hasSize(1)); - assertThat(transactions.get(0).getId(), is(id)); - }); - testContext.completeNow(); - }); - } - - @Test - void testUpdatePermanentTransactions(Vertx vertx, VertxTestContext testContext) { - Transaction emptyTransaction = new Transaction(); - Transaction t1 = new Transaction(); - Transaction t2 = new Transaction(); - Promise promise1 = Promise.promise(); - final DBClient client = new DBClient(vertx, TEST_TENANT); - client.getPgClient().save(TRANSACTION_TABLE, emptyTransaction, event -> { - promise1.complete(event.result()); - }); - Promise promise2 = Promise.promise(); - client.getPgClient().save(TRANSACTION_TABLE, emptyTransaction, event -> { - promise2.complete(event.result()); - }); - testContext.assertComplete( - client.withTrans(conn -> promise1.future() - .compose(id1 -> promise2.future()) - .compose(id2 -> pendingPaymentDAO.getTransactions(new Criterion(), conn)) - .compose(transactions -> { - assertThat(transactions, hasSize(2)); - t1.setId(transactions.get(0).getId()); - t2.setId(transactions.get(1).getId()); - transactions.get(0).withTransactionType(PENDING_PAYMENT); - transactions.get(1).withTransactionType(ALLOCATION); - return pendingPaymentDAO.updatePermanentTransactions(transactions, conn); - }) - ) - .compose(o -> client.withConn(conn -> pendingPaymentDAO.getTransactions(new Criterion(), conn))) - ).onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> { - assertThat(transactions, hasSize(2)); - Map transactionMap = transactions.stream().collect(toMap(Transaction::getId, Function.identity())); - assertThat(transactionMap.get(t1.getId()).getId(), is(t1.getId())); - assertThat(transactionMap.get(t2.getId()).getId(), is(t2.getId())); - assertThat(transactionMap.get(t1.getId()).getTransactionType(), is(PENDING_PAYMENT)); - assertThat(transactionMap.get(t2.getId()).getTransactionType(), is(ALLOCATION)); - }); - testContext.completeNow(); - }); - } - - @Test - void testSaveTransactionsToPermanentTableOnlyPendingPayments(Vertx vertx, VertxTestContext testContext) { - String summaryId = UUID.randomUUID().toString(); - Transaction tmpTransaction = new Transaction() - .withId(UUID.randomUUID().toString()) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(summaryId); - - final DBClient client = new DBClient(vertx, TEST_TENANT); - - testContext.assertComplete( - client.withTrans(conn -> createSummary(summaryId, conn) - .compose(s -> createTmpTransaction(tmpTransaction, conn)) - .compose(id1 -> pendingPaymentDAO.saveTransactionsToPermanentTable(summaryId, conn)) - ).compose(o -> client.withConn(conn -> pendingPaymentDAO.getTransactions(new Criterion(), conn))) - ).onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> { - assertThat(transactions, hasSize(1)); - assertThat(transactions.get(0).getId(), is(tmpTransaction.getId())); - assertThat(transactions.get(0).getTransactionType(), is(PENDING_PAYMENT)); - }); - testContext.completeNow(); - }); - } - - @Test - void shouldDoNothingOnConflictWhenSaveTransactionsToTransactionTable(Vertx vertx, VertxTestContext testContext) { - - Fund fund = new Fund() - .withId(UUID.randomUUID().toString()); - - FiscalYear fiscalYear = new FiscalYear() - .withId(UUID.randomUUID().toString()); - - ExpenseClass expenseClass = new ExpenseClass() - .withId(UUID.randomUUID().toString()); - - String summaryId = UUID.randomUUID().toString(); - Transaction tmpTransaction1 = new Transaction() - .withId(UUID.randomUUID().toString()) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(summaryId) - .withSourceInvoiceLineId(UUID.randomUUID().toString()) - .withAmount(100d) - .withFromFundId(fund.getId()) - .withExpenseClassId(expenseClass.getId()) - .withFiscalYearId(fiscalYear.getId()); - - - Transaction tmpTransaction2 = new Transaction() - .withId(UUID.randomUUID().toString()) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(summaryId) - .withSourceInvoiceLineId(tmpTransaction1.getSourceInvoiceLineId()) - .withAmount(tmpTransaction1.getAmount()) - .withFromFundId(tmpTransaction1.getFromFundId()) - .withExpenseClassId(tmpTransaction1.getExpenseClassId()) - .withFiscalYearId(tmpTransaction1.getFiscalYearId()) - .withCurrency("TEST"); - - final DBClient client = new DBClient(vertx, TEST_TENANT); - - testContext.assertComplete( - client.withTrans(conn -> createFund(fund, conn) - .compose(s -> createFiscalYear(fiscalYear, conn)) - .compose(s -> createExpenseClass(expenseClass, conn)) - .compose(client1 -> createSummary(summaryId, conn)) - .compose(s -> createTmpTransaction(tmpTransaction1, conn)) - .compose(id1 -> pendingPaymentDAO.saveTransactionsToPermanentTable(summaryId, conn)) - .compose(integer -> deleteTmpTransaction(tmpTransaction1, conn)) - .compose(aVoid -> createTmpTransaction(tmpTransaction2, conn)) - .compose(id1 -> pendingPaymentDAO.saveTransactionsToPermanentTable(summaryId, conn)) - ).compose(o -> client.withConn(conn -> pendingPaymentDAO.getTransactions(new Criterion(), conn))) - ).onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> { - assertThat(transactions, Matchers.hasSize(1)); - assertThat(transactions.get(0).getId(), is(tmpTransaction1.getId())); - assertThat(transactions.get(0).getTransactionType(), is(PENDING_PAYMENT)); - assertNull(transactions.get(0).getCurrency()); - }); - testContext.completeNow(); - }); - } - - @Test - void shouldCreateBothTransactionsWhenSaveTransactionsWithDifferentExpenseClassIds(Vertx vertx, VertxTestContext testContext) { - - Fund fund = new Fund() - .withId(UUID.randomUUID().toString()); - - FiscalYear fiscalYear = new FiscalYear() - .withId(UUID.randomUUID().toString()); - - ExpenseClass expenseClass1 = new ExpenseClass() - .withId(UUID.randomUUID().toString()); - - ExpenseClass expenseClass2 = new ExpenseClass() - .withId(UUID.randomUUID().toString()); - - String summaryId = UUID.randomUUID().toString(); - Transaction tmpTransaction1 = new Transaction() - .withId(UUID.randomUUID().toString()) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(summaryId) - .withSourceInvoiceLineId(UUID.randomUUID().toString()) - .withAmount(100d) - .withFromFundId(fund.getId()) - .withExpenseClassId(expenseClass1.getId()) - .withFiscalYearId(fiscalYear.getId()); - - - Transaction tmpTransaction2 = new Transaction() - .withId(UUID.randomUUID().toString()) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(summaryId) - .withSourceInvoiceLineId(tmpTransaction1.getSourceInvoiceLineId()) - .withAmount(tmpTransaction1.getAmount()) - .withFromFundId(tmpTransaction1.getFromFundId()) - .withExpenseClassId(expenseClass2.getId()) - .withFiscalYearId(tmpTransaction1.getFiscalYearId()); - - final DBClient client = new DBClient(vertx, TEST_TENANT); - - testContext.assertComplete( - client.withTrans(conn -> createFund(fund, conn) - .compose(s -> createFiscalYear(fiscalYear, conn)) - .compose(s -> createExpenseClass(expenseClass1, conn)) - .compose(s -> createExpenseClass(expenseClass2, conn)) - .compose(client1 -> createSummary(summaryId, conn)) - .compose(s -> createTmpTransaction(tmpTransaction1, conn)) - .compose(id1 -> pendingPaymentDAO.saveTransactionsToPermanentTable(summaryId, conn)) - .compose(integer -> deleteTmpTransaction(tmpTransaction1, conn)) - .compose(aVoid -> createTmpTransaction(tmpTransaction2, conn)) - .compose(id1 -> pendingPaymentDAO.saveTransactionsToPermanentTable(summaryId, conn)) - ).compose(o -> client.withConn(conn -> pendingPaymentDAO.getTransactions(new Criterion(), conn))) - ).onComplete(event -> { - List transactions = event.result(); - testContext.verify(() -> assertThat(transactions, Matchers.hasSize(2))); - testContext.completeNow(); - }); - } - - private Future createTmpTransaction(Transaction tmpTransaction, DBConn conn) { - return conn.save(TEMPORARY_INVOICE_TRANSACTIONS, tmpTransaction.getId(), tmpTransaction); - } - - private Future deleteTmpTransaction(Transaction tmpTransaction, DBConn conn) { - return conn.delete(TEMPORARY_INVOICE_TRANSACTIONS, tmpTransaction.getId()) - .mapEmpty(); - } - - private Future createSummary(String summaryId, DBConn conn) { - return conn.save(INVOICE_TRANSACTION_SUMMARIES, summaryId, new InvoiceTransactionSummary().withNumPendingPayments(1).withId(summaryId)); - } - - private Future createFund(Fund fund, DBConn conn) { - return conn.save(FUND_TABLE, fund.getId(), fund); - } - - private Future createFiscalYear(FiscalYear fiscalYear, DBConn conn) { - return conn.save(FISCAL_YEAR_TABLE, fiscalYear.getId(), fiscalYear); - } - - private Future createExpenseClass(ExpenseClass expenseClass, DBConn conn) { - return conn.save(EXPENSE_CLASS_TABLE, expenseClass.getId(), expenseClass); - } -} diff --git a/src/test/java/org/folio/rest/impl/BudgetTest.java b/src/test/java/org/folio/rest/impl/BudgetTest.java index 17d1647e..a53f40ac 100644 --- a/src/test/java/org/folio/rest/impl/BudgetTest.java +++ b/src/test/java/org/folio/rest/impl/BudgetTest.java @@ -1,8 +1,7 @@ package org.folio.rest.impl; import static org.folio.rest.RestVerticle.OKAPI_HEADER_TENANT; -import static org.folio.rest.impl.TransactionsSummariesTest.ORDERS_SUMMARY_SAMPLE; -import static org.folio.rest.impl.TransactionsSummariesTest.ORDER_TRANSACTION_SUMMARIES_ENDPOINT; +import static org.folio.rest.util.ErrorCodes.TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR; import static org.folio.rest.utils.TenantApiTestUtil.deleteTenant; import static org.folio.rest.utils.TenantApiTestUtil.purge; import static org.folio.rest.utils.TenantApiTestUtil.prepareTenant; @@ -13,19 +12,19 @@ import static org.folio.rest.utils.TestEntities.GROUP; import static org.folio.rest.utils.TestEntities.GROUP_FUND_FY; import static org.folio.rest.utils.TestEntities.LEDGER; -import static org.folio.rest.utils.TestEntities.ALLOCATION_TRANSACTION; -import static org.folio.service.budget.BudgetService.TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import java.util.List; import java.util.UUID; +import io.vertx.core.json.Json; import org.apache.commons.lang3.tuple.Pair; +import org.folio.rest.jaxrs.model.Batch; import org.folio.rest.jaxrs.model.Budget; import org.folio.rest.jaxrs.model.GroupFundFiscalYear; -import org.folio.rest.jaxrs.model.OrderTransactionSummary; import org.folio.rest.jaxrs.model.TenantJob; import org.folio.rest.jaxrs.model.Transaction; import org.folio.rest.util.ErrorCodes; @@ -40,10 +39,12 @@ public class BudgetTest extends TestBase { + private static final String ALLOCATION_SAMPLE_PATH = "data/transactions/allocations-8.4.0/allocation_AFRICAHIST-FY24.json"; + private static final String ENCUMBRANCE_SAMPLE_PATH = "data/transactions/encumbrances/encumbrance_AFRICAHIST_306857_1.json"; + private static final String BATCH_TRANSACTION_ENDPOINT = "/finance-storage/transactions/batch-all-or-nothing"; private static final String BUDGET_ENDPOINT = TestEntities.BUDGET.getEndpoint(); private static final String BUDGET_TEST_TENANT = "budgettesttenantapi"; private static final Header BUDGET_TENANT_HEADER = new Header(OKAPI_HEADER_TENANT, BUDGET_TEST_TENANT); - private static final String ENCUMBR_SAMPLE = "data/transactions/encumbrances/encumbrance_AFRICAHIST_306857_1.json"; private static TenantJob tenantJob; @AfterAll @@ -80,8 +81,14 @@ void testAbleToDeleteBudgetWithExistingOnlyAllocationTransactions() { Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), Pair.of(LEDGER, LEDGER.getPathToSampleFile()), Pair.of(FUND, FUND.getPathToSampleFile()), - Pair.of(BUDGET, BUDGET.getPathToSampleFile()), - Pair.of(ALLOCATION_TRANSACTION, ALLOCATION_TRANSACTION.getPathToSampleFile())); + Pair.of(BUDGET, BUDGET.getPathToSampleFile())); + + String allocationSample = getFile(ALLOCATION_SAMPLE_PATH); + Transaction allocation = Json.decodeValue(allocationSample, Transaction.class); + Batch batch = new Batch().withTransactionsToCreate(List.of(allocation)); + postData(BATCH_TRANSACTION_ENDPOINT, JsonObject.mapFrom(batch).encodePrettily(), BUDGET_TENANT_HEADER) + .then() + .statusCode(204); deleteData(BUDGET.getEndpointWithId(), BUDGET.getId(), BUDGET_TENANT_HEADER).then() .statusCode(204); @@ -101,22 +108,18 @@ void testDeleteBudgetFailedWhenExistOtherThenAllocationTransactions() { String orderId = UUID.randomUUID().toString(); - OrderTransactionSummary sample = new JsonObject(getFile(ORDERS_SUMMARY_SAMPLE)).mapTo(OrderTransactionSummary.class); - sample.setId(orderId); - postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), BUDGET_TENANT_HEADER).as(OrderTransactionSummary.class); - - - Transaction transaction = new JsonObject(getFile(ENCUMBR_SAMPLE)).mapTo(Transaction.class); + Transaction transaction = new JsonObject(getFile(ENCUMBRANCE_SAMPLE_PATH)).mapTo(Transaction.class); transaction.getEncumbrance().setSourcePurchaseOrderId(orderId); - postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(transaction).encodePrettily(), BUDGET_TENANT_HEADER) + Batch batch = new Batch().withTransactionsToCreate(List.of(transaction)); + + postData(BATCH_TRANSACTION_ENDPOINT, JsonObject.mapFrom(batch).encodePrettily(), BUDGET_TENANT_HEADER) .then() - .statusCode(201); + .statusCode(204); deleteData(BUDGET.getEndpointWithId(), BUDGET.getId(), BUDGET_TENANT_HEADER).then() .statusCode(400) - .body(containsString(TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR)); + .body(containsString(TRANSACTION_IS_PRESENT_BUDGET_DELETE_ERROR.getDescription())); purge(BUDGET_TENANT_HEADER); } diff --git a/src/test/java/org/folio/rest/impl/EncumbrancesTest.java b/src/test/java/org/folio/rest/impl/EncumbrancesTest.java deleted file mode 100644 index f1719c01..00000000 --- a/src/test/java/org/folio/rest/impl/EncumbrancesTest.java +++ /dev/null @@ -1,898 +0,0 @@ -package org.folio.rest.impl; - -import static org.folio.rest.impl.TransactionTest.TRANSACTION_TENANT_HEADER; -import static org.folio.rest.impl.TransactionsSummariesTest.INVOICE_TRANSACTION_SUMMARIES_ENDPOINT; -import static org.folio.rest.impl.TransactionsSummariesTest.ORDER_TRANSACTION_SUMMARIES_ENDPOINT; -import static org.folio.rest.impl.TransactionsSummariesTest.ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID; -import static org.folio.rest.jaxrs.model.Encumbrance.Status.PENDING; -import static org.folio.rest.util.ErrorCodes.BUDGET_NOT_FOUND_FOR_TRANSACTION; -import static org.folio.rest.utils.TenantApiTestUtil.deleteTenant; -import static org.folio.rest.utils.TenantApiTestUtil.prepareTenant; -import static org.folio.rest.utils.TenantApiTestUtil.purge; -import static org.folio.rest.utils.TestEntities.BUDGET; -import static org.folio.rest.utils.TestEntities.FISCAL_YEAR; -import static org.folio.rest.utils.TestEntities.FUND; -import static org.folio.rest.utils.TestEntities.LEDGER; -import static org.folio.rest.utils.TestEntities.ALLOCATION_TRANSACTION; -import static org.folio.service.transactions.AllOrNothingTransactionService.ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED; -import static org.folio.service.transactions.restriction.BaseTransactionRestrictionService.FUND_CANNOT_BE_PAID; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.UUID; - -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.FiscalYear; -import org.folio.rest.jaxrs.model.Fund; -import org.folio.rest.jaxrs.model.InvoiceTransactionSummary; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.OrderTransactionSummary; -import org.folio.rest.jaxrs.model.TenantJob; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import io.vertx.core.json.JsonObject; - -public class EncumbrancesTest extends TestBase { - - public static final String ENCUMBRANCE_SAMPLE = "data/transactions/encumbrances/encumbrance_AFRICAHIST_306857_1.json"; - private static TenantJob tenantJob; - - @BeforeEach - void prepareData() { - tenantJob = prepareTenant(TRANSACTION_TENANT_HEADER, false, true); - } - - @AfterEach - void deleteData() { - purge(TRANSACTION_TENANT_HEADER); - } - - @AfterAll - public static void after() { - deleteTenant(tenantJob, TRANSACTION_TENANT_HEADER); - } - - @Test - void testCreateEncumbranceAllOrNothingIdempotent() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget budgetBefore = buildBudget(fiscalYearId, fundId); - String budgetId = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(budgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 2); - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - - Transaction encumbrance1 = jsonTx.mapTo(Transaction.class); - encumbrance1.getEncumbrance().setSourcePurchaseOrderId(orderId); - - Transaction encumbrance2 = jsonTx.mapTo(Transaction.class); - encumbrance2.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance2.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - - - // create 1st Encumbrance, expected number is 2 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance1).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(404); - - // create 2nd Encumbrance - String encumbrance2Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance2).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // 2 encumbrances appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - - Budget budgetAfter = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - // check source budget and ledger totals - final double amount = sumValues(encumbrance1.getAmount(), encumbrance2.getAmount()); - double expectedBudgetsAvailable; - double expectedBudgetsUnavailable; - double expectedBudgetsEncumbered; - - - expectedBudgetsEncumbered = sumValues(budgetBefore.getEncumbered(), amount); - expectedBudgetsAvailable = subtractValues(budgetBefore.getAvailable(), amount); - expectedBudgetsUnavailable = sumValues(budgetBefore.getUnavailable(), amount); - - assertEquals(expectedBudgetsEncumbered, budgetAfter.getEncumbered()); - assertEquals(expectedBudgetsAvailable , budgetAfter.getAvailable()); - assertEquals(expectedBudgetsUnavailable, budgetAfter.getUnavailable()); - verifyBudgetTotalsAfter(budgetAfter); - - //create same encumbrances again - postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance1).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED)); - - budgetAfter = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().extract().as(Budget.class); - - // check source budget and ledger totals not changed - assertEquals(expectedBudgetsEncumbered, budgetAfter.getEncumbered()); - assertEquals(expectedBudgetsAvailable , budgetAfter.getAvailable()); - assertEquals(expectedBudgetsUnavailable, budgetAfter.getUnavailable()); - verifyBudgetTotalsAfter(budgetAfter); - - } - - @Test - void testCreateEncumbranceWithNotEnoughBudgetMoney() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.setAmount(1000000d); - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - - // create Encumbrance - postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(FUND_CANNOT_BE_PAID)); - - } - - @Test - void testCreateEncumbranceFromInactiveBudget() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId).withBudgetStatus(Budget.BudgetStatus.INACTIVE); - postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.setAmount((double) Integer.MAX_VALUE); - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - - // create Encumbrance - Errors errors = postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(400).extract().as(Errors.class); - assertThat(errors.getErrors(), hasSize(1)); - - } - - @Test - void testCreateEncumbranceWithoutSummary() { - - String orderId = UUID.randomUUID().toString(); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(404); - - } - - @Test - void testCreateEncumbranceWithoutBudget() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 2); - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.setFiscalYearId(UUID.randomUUID().toString()); - - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(404).body(containsString(BUDGET_NOT_FOUND_FOR_TRANSACTION.getDescription())); - - } - - protected void createOrderSummary(String orderId, int encumbranceNumber) { - OrderTransactionSummary summary = new OrderTransactionSummary().withId(orderId).withNumTransactions(encumbranceNumber); - postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(summary) - .encodePrettily(), TRANSACTION_TENANT_HEADER); - } - - protected void updateOrderSummary(String orderId, int encumbranceNumber) { - OrderTransactionSummary summary = getDataById(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, orderId, TRANSACTION_TENANT_HEADER) - .as(OrderTransactionSummary.class); - summary.setNumTransactions(encumbranceNumber); - putData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, orderId, JsonObject.mapFrom(summary).encodePrettily(), TRANSACTION_TENANT_HEADER) - .then() - .statusCode(204); - - } - - protected void createInvoiceSummary(String invoiceId, int numEncumbrances) { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary().withId(invoiceId).withNumPaymentsCredits(numEncumbrances).withNumPendingPayments(numEncumbrances); - postData(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(summary) - .encodePrettily(), TRANSACTION_TENANT_HEADER); - } - - @Test - void testCreateEncumbranceWithMissedRequiredFields() { - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - - encumbrance.setEncumbrance(null); - encumbrance.setFromFundId(null); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - Errors errors = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(422).extract().as(Errors.class); - assertThat(errors.getErrors(), hasSize(2)); - - } - - @Test - void testCreateEncumbrancesDuplicateInTemporaryTable() { - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - - postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class); - - String orderId = UUID.randomUUID().toString(); - - createOrderSummary(orderId, 2); - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - String encumbranceId = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Transaction.class).getId(); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbranceId, TRANSACTION_TENANT_HEADER).then().statusCode(404); - - // create encumbrance again - postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbranceId, TRANSACTION_TENANT_HEADER).then().statusCode(404); - - } - - @Test - void testCreateEncumbrancesDuplicateInTransactionTable() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - - postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 2); - - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - Transaction encumbrance1 = jsonTx.mapTo(Transaction.class); - encumbrance1.getEncumbrance().setSourcePurchaseOrderId(orderId); - - Transaction encumbrance2 = jsonTx.mapTo(Transaction.class); - encumbrance2.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance2.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - - String transactionSample1 = JsonObject.mapFrom(encumbrance1).encodePrettily(); - - // create 1st Encumbrance, expected number is 2 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample1, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(404); - - String transactionSample2 = JsonObject.mapFrom(encumbrance2).encodePrettily(); - - // create 2nd Encumbrance - String encumbrance2Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample2, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // 2 encumbrances appear in transaction table, temp transactions deleted - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - - postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample1, TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED)); - - } - - @Test - void testUpdateEncumbranceAllOrNothing() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - - String budgetId = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 3); - - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fundId); - Transaction encumbrance1 = jsonTx.mapTo(Transaction.class); - encumbrance1.getEncumbrance().setSourcePurchaseOrderId(orderId); - - Transaction encumbrance2 = jsonTx.mapTo(Transaction.class); - encumbrance2.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance2.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - encumbrance2.setAmount(0d); - encumbrance2.getEncumbrance().setInitialAmountEncumbered(0d); - encumbrance2.getEncumbrance().setStatus(PENDING); - - Transaction encumbrance3 = jsonTx.mapTo(Transaction.class); - encumbrance3.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance3.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - - String transactionSample = JsonObject.mapFrom(encumbrance1).encodePrettily(); - - - // create 1st Encumbrance, expected number is 2 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(404); - - transactionSample = JsonObject.mapFrom(encumbrance2).encodePrettily(); - - // create 2nd Encumbrance - String encumbrance2Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // create 3rd Encumbrance - transactionSample = JsonObject.mapFrom(encumbrance3).encodePrettily(); - String encumbrance3Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED)); - - // 3 encumbrances appear in transaction table - encumbrance1 = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Transaction.class); - encumbrance2 = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Transaction.class); - encumbrance3 = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance3Id, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Transaction.class); - Budget fromBudgetBeforeUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - verifyBudgetTotalsAfter(fromBudgetBeforeUpdate); - double releasedAmount = encumbrance1.getAmount(); - - encumbrance1.getEncumbrance().setStatus(Encumbrance.Status.RELEASED); - encumbrance2.setAmount(200d); - encumbrance2.getEncumbrance().setInitialAmountEncumbered(200d); - - encumbrance2.getEncumbrance().setStatus(Encumbrance.Status.UNRELEASED); - - updateOrderSummary(orderId, 3); - // First encumbrance update, save to temp table, changes won't get to transaction table - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, JsonObject.mapFrom(encumbrance1).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - Transaction transaction1FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(Encumbrance.Status.UNRELEASED, transaction1FromStorage.getEncumbrance().getStatus()); - assertEquals(transaction1FromStorage.getAmount(), releasedAmount); - - // Second encumbrance update, changes won't get to transaction table - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, JsonObject.mapFrom(encumbrance2).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - - // Third encumbrance update with pending status, changes for three encumbrances will get to transaction table - encumbrance3.getEncumbrance().setStatus(PENDING); - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance3Id, JsonObject.mapFrom(encumbrance3).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - - - // Check all-or-nothing results - // 1st transaction - transaction1FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(Encumbrance.Status.RELEASED, transaction1FromStorage.getEncumbrance().getStatus()); - assertEquals(0d, transaction1FromStorage.getAmount()); - - // 2nd transaction - Transaction transaction2FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(transaction2FromStorage.getEncumbrance().getAmountAwaitingPayment(), encumbrance2.getEncumbrance().getAmountAwaitingPayment()); - - double expectedAmount2 = subtractValues(encumbrance2.getAmount(), encumbrance2.getEncumbrance().getAmountAwaitingPayment(), encumbrance2.getEncumbrance().getAmountExpended()); - assertEquals(expectedAmount2, transaction2FromStorage.getAmount()); - - // 3rd transaction - Transaction transaction3FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance3Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(0, transaction3FromStorage.getAmount()); - assertEquals(0, transaction3FromStorage.getEncumbrance().getInitialAmountEncumbered()); - - - // check budget updates - Budget fromBudgetAfterUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - double transactionsTotalAmount = sumValues(encumbrance1.getAmount(), encumbrance3.getAmount(), -transaction2FromStorage.getAmount()); - double expectedBudgetsEncumbered = subtractValues(fromBudgetBeforeUpdate.getEncumbered(), transactionsTotalAmount); - - double expectedBudgetsAvailable = sumValues(fromBudgetBeforeUpdate.getAvailable(), transactionsTotalAmount); - double expectedBudgetsUnavailable = subtractValues(fromBudgetBeforeUpdate.getUnavailable(), transactionsTotalAmount); - expectedBudgetsUnavailable = expectedBudgetsUnavailable < 0 ? 0 : expectedBudgetsUnavailable; - - assertEquals(expectedBudgetsEncumbered, fromBudgetAfterUpdate.getEncumbered()); - assertEquals(expectedBudgetsAvailable , fromBudgetAfterUpdate.getAvailable()); - assertEquals(expectedBudgetsUnavailable, fromBudgetAfterUpdate.getUnavailable()); - - verifyBudgetTotalsAfter(fromBudgetAfterUpdate); - - } - - @Test - void testUpdateAlreadyReleasedEncumbranceBudgetNotUpdated() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - - String budgetId = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance.getEncumbrance().setStatus(Encumbrance.Status.RELEASED); - encumbrance.setAmount(0d); - encumbrance.getEncumbrance().setAmountAwaitingPayment(10d); - encumbrance.setSourceFiscalYearId(fiscalYearId); - encumbrance.setFiscalYearId(fiscalYearId); - encumbrance.setFromFundId(fundId); - encumbrance.setSourceInvoiceId(invoiceId); - - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - // create 1st Encumbrance, expected number is 1 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance appears in transaction table - encumbrance = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - - - updateOrderSummary(orderId, 1); - - encumbrance.getEncumbrance().setAmountAwaitingPayment(5d); - encumbrance.setAmount(2d); - - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - Transaction transaction1FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(10d, transaction1FromStorage.getEncumbrance().getAmountAwaitingPayment()); - assertEquals(0d, transaction1FromStorage.getAmount()); - Budget fromBudgetAfterUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - checkBudgetTotalsNotChanged(fromBudgetBefore, fromBudgetAfterUpdate); - - } - - @Test - void testUpdateReleasedToUnreleased() { - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - String fromBudgetBeforeSerialized = JsonObject.mapFrom(fromBudgetBefore).encodePrettily(); - String budgetId = postData(BUDGET.getEndpoint(), fromBudgetBeforeSerialized, TRANSACTION_TENANT_HEADER) - .then().statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.withAmount(0d) - .withSourceFiscalYearId(fiscalYearId) - .withFiscalYearId(fiscalYearId) - .withFromFundId(fundId) - .withSourceInvoiceId(invoiceId); - encumbrance.getEncumbrance() - .withSourcePurchaseOrderId(orderId) - .withStatus(Encumbrance.Status.RELEASED) - .withAmountExpended(10d) - .withAmountAwaitingPayment(5d) - .withInitialAmountEncumbered(100d); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - // create 1st encumbrance, expected number is 1 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER) - .then().statusCode(201).extract().as(Transaction.class).getId(); - - // encumbrance appears in transaction table - String transactionById = ALLOCATION_TRANSACTION.getEndpointWithId(); - encumbrance = getDataById(transactionById, encumbrance1Id, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Transaction.class); - assertEquals(0d, encumbrance.getAmount()); - assertEquals(100d, encumbrance.getEncumbrance().getInitialAmountEncumbered()); - - // unrelease the encumbrance - encumbrance.getEncumbrance().setStatus(Encumbrance.Status.UNRELEASED); - updateOrderSummary(orderId, 1); - String encumbranceSerialized = JsonObject.mapFrom(encumbrance).encodePrettily(); - putData(transactionById, encumbrance1Id, encumbranceSerialized, TRANSACTION_TENANT_HEADER) - .then().statusCode(204); - Transaction transaction1FromStorage = getDataById(transactionById, encumbrance1Id, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Transaction.class); - assertEquals(Encumbrance.Status.UNRELEASED, transaction1FromStorage.getEncumbrance().getStatus()); - assertEquals(85d, transaction1FromStorage.getAmount()); - - // check the budget - Budget fromBudgetAfterUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER) - .then().statusCode(200).extract().as(Budget.class); - assertEquals(sumValues(fromBudgetBefore.getEncumbered(), 85d), fromBudgetAfterUpdate.getEncumbered()); - assertEquals(subtractValues(fromBudgetBefore.getAvailable(), 85d), fromBudgetAfterUpdate.getAvailable()); - assertEquals(sumValues(fromBudgetBefore.getUnavailable(), 85d), fromBudgetAfterUpdate.getUnavailable()); - assertEquals(fromBudgetBefore.getAwaitingPayment(), fromBudgetAfterUpdate.getAwaitingPayment()); - } - - @Test - void testUpdateEncumbranceWithoutStatusChangeBudgetUpdated() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget fromBudgetBefore = buildBudget(fiscalYearId, fundId); - - String budgetId = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance.getEncumbrance().setStatus(Encumbrance.Status.UNRELEASED); - encumbrance.setAmount(10d); - encumbrance.getEncumbrance().setAmountAwaitingPayment(10d); - encumbrance.getEncumbrance().setInitialAmountEncumbered(20d); - encumbrance.setSourceFiscalYearId(fiscalYearId); - encumbrance.setFiscalYearId(fiscalYearId); - encumbrance.setFromFundId(fundId); - encumbrance.setSourceInvoiceId(invoiceId); - - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - // create 1st Encumbrance, expected number is 1 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance appears in transaction table - Budget fromBudgetBeforeUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - encumbrance = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - - - updateOrderSummary(orderId, 1); - - encumbrance.setAmount(30d); - - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - - Budget fromBudgetAfterUpdate = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - assertEquals(subtractValues(fromBudgetBeforeUpdate.getAvailable(), 20d), fromBudgetAfterUpdate.getAvailable()); - assertEquals(sumValues(fromBudgetBeforeUpdate.getUnavailable(), 20d), fromBudgetAfterUpdate.getUnavailable()); - assertEquals(sumValues(fromBudgetBeforeUpdate.getEncumbered(), 20d), fromBudgetAfterUpdate.getEncumbered()); - - } - - @Test - void testUpdateEncumbranceNotFound() { - - String invoiceId = UUID.randomUUID().toString(); - createInvoiceSummary(invoiceId, 2); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - encumbrance.setSourceInvoiceId(invoiceId); - - // Try to update non-existent transaction - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), UUID.randomUUID().toString(), JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(404); - - } - - @Test - void tesPostEncumbranceUnavailableMustNotIncludeOverEncumberedAmounts() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fundId = createFund(ledgerId); - - Budget budgetBefore = buildBudget(fiscalYearId, fundId); - - String budgetId = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(budgetBefore).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - Transaction encumbrance = jsonTx.mapTo(Transaction.class); - - encumbrance.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance.setFromFundId(fundId); - encumbrance.setFiscalYearId(fiscalYearId); - encumbrance.setSourceFiscalYearId(null); - - final double transactionAmount = 10000d; - encumbrance.setAmount(transactionAmount); - - String transactionSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - String encumbranceId = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Transaction.class).getId(); - - // encumbrance do not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbranceId, TRANSACTION_TENANT_HEADER).then().statusCode(200); - - Budget budgetAfter = getDataById(BUDGET.getEndpointWithId(), budgetId, TRANSACTION_TENANT_HEADER).then() - .statusCode(200).extract().as(Budget.class); - - assertEquals(sumValues(budgetBefore.getEncumbered(), transactionAmount), budgetAfter.getEncumbered()); - verifyBudgetTotalsAfter(budgetAfter); - - } - - @Test - void testUpdateAndCreateEncumbrancesAllOrNothing() { - - String fiscalYearId = createFiscalYear(); - String ledgerId = createLedger(fiscalYearId, true); - String fund1Id = createFund(ledgerId); - - Fund fund2 = new JsonObject(getFile(FUND.getPathToSampleFile())).mapTo(Fund.class).withId(null).withLedgerId(ledgerId).withCode("test2"); - String fund2Id = postData(FUND.getEndpoint(), JsonObject.mapFrom(fund2).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Fund.class).getId(); - - Budget fromBudget1Before = buildBudget(fiscalYearId, fund1Id); - Budget fromBudget2Before = buildBudget(fiscalYearId, fund2Id).withName("test2"); - - String budget1Id = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudget1Before).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - String budget2Id = postData(BUDGET.getEndpoint(), JsonObject.mapFrom(fromBudget2Before).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Budget.class).getId(); - - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 1); - - JsonObject jsonTx = prepareEncumbrance(fiscalYearId, fund1Id); - Transaction encumbrance1 = jsonTx.mapTo(Transaction.class); - encumbrance1.getEncumbrance().setSourcePurchaseOrderId(orderId); - - Transaction encumbrance2 = jsonTx.mapTo(Transaction.class); - encumbrance2.setFromFundId(fund2Id); - encumbrance2.getEncumbrance().setSourcePurchaseOrderId(orderId); - encumbrance2.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - encumbrance2.setAmount(encumbrance2.getEncumbrance().getInitialAmountEncumbered()); - encumbrance2.getEncumbrance().setAmountAwaitingPayment(0d); - encumbrance2.getEncumbrance().setAmountExpended(0d); - - String transactionSample = JsonObject.mapFrom(encumbrance1).encodePrettily(); - - // create 1st Encumbrance, expected number is 1 - String encumbrance1Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // encumbrance appears in transaction table - encumbrance1 = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - - Budget fromBudget1BeforeUpdate = getDataById(BUDGET.getEndpointWithId(), budget1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - verifyBudgetTotalsAfter(fromBudget1BeforeUpdate); - double releasedAmount = encumbrance1.getAmount(); - - encumbrance1.getEncumbrance().setStatus(Encumbrance.Status.RELEASED); - - updateOrderSummary(orderId, 2); - // First encumbrance update, save to temp table, changes won't get to transaction table - putData(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, JsonObject.mapFrom(encumbrance1).encodePrettily(), TRANSACTION_TENANT_HEADER).then().statusCode(204); - Transaction transaction1FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(Encumbrance.Status.UNRELEASED, transaction1FromStorage.getEncumbrance().getStatus()); - assertEquals(transaction1FromStorage.getAmount(), releasedAmount); - - // Second encumbrance creation, changes won't get to transaction table - String encumbrance2Id = postData(ALLOCATION_TRANSACTION.getEndpoint(), JsonObject.mapFrom(encumbrance2).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class).getId(); - - // Check all-or-nothing results - // 1st transaction - transaction1FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(Encumbrance.Status.RELEASED, transaction1FromStorage.getEncumbrance().getStatus()); - assertEquals(0d, transaction1FromStorage.getAmount()); - - // 2nd transaction - Transaction transaction2FromStorage = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), encumbrance2Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Transaction.class); - assertEquals(transaction2FromStorage.getEncumbrance().getAmountAwaitingPayment(), encumbrance2.getEncumbrance().getAmountAwaitingPayment()); - - double expectedAmount2 = subtractValues(encumbrance2.getAmount(), encumbrance2.getEncumbrance().getAmountAwaitingPayment(), encumbrance2.getEncumbrance().getAmountExpended()); - assertEquals(expectedAmount2, transaction2FromStorage.getAmount()); - - // check budget updates - Budget fromBudget1AfterUpdate = getDataById(BUDGET.getEndpointWithId(), budget1Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - - double expectedBudget1Encumbered = subtractValues(fromBudget1BeforeUpdate.getEncumbered(), releasedAmount); - double expectedBudget1Available = sumValues(fromBudget1BeforeUpdate.getAvailable(), releasedAmount); - double expectedBudget1Unavailable = subtractValues(fromBudget1BeforeUpdate.getUnavailable(), releasedAmount); - expectedBudget1Unavailable = expectedBudget1Unavailable < 0 ? 0 : expectedBudget1Unavailable; - - assertEquals(expectedBudget1Encumbered, fromBudget1AfterUpdate.getEncumbered()); - assertEquals(expectedBudget1Available , fromBudget1AfterUpdate.getAvailable()); - assertEquals(expectedBudget1Unavailable, fromBudget1AfterUpdate.getUnavailable()); - - Budget fromBudget2AfterUpdate = getDataById(BUDGET.getEndpointWithId(), budget2Id, TRANSACTION_TENANT_HEADER).then().statusCode(200).extract().as(Budget.class); - - - double expectedBudget2Encumbered = sumValues(fromBudget2Before.getEncumbered(), transaction2FromStorage.getAmount()); - double expectedBudget2Available = subtractValues(fromBudget2Before.getAvailable(), transaction2FromStorage.getAmount()); - double expectedBudget2Unavailable = sumValues(fromBudget2Before.getUnavailable(), transaction2FromStorage.getAmount()); - expectedBudget2Unavailable = expectedBudget2Unavailable < 0 ? 0 : expectedBudget2Unavailable; - - assertEquals(expectedBudget2Encumbered, fromBudget2AfterUpdate.getEncumbered()); - assertEquals(expectedBudget2Available , fromBudget2AfterUpdate.getAvailable()); - assertEquals(expectedBudget2Unavailable, fromBudget2AfterUpdate.getUnavailable()); - - verifyBudgetTotalsAfter(fromBudget1AfterUpdate); - } - - - private void verifyBudgetTotalsAfter(Budget budget) { - assertTrue(budget.getUnavailable() >= 0); - double expectedUnavailable = sumValues(budget.getEncumbered(), budget.getAwaitingPayment(), budget.getExpenditures()); - assertEquals(expectedUnavailable, budget.getUnavailable()); - assertEquals(subtractValues(sumValues(budget.getInitialAllocation(), budget.getAllocationTo()), budget.getAllocationFrom()), budget.getAllocated()); - } - - - private String createFund(String ledgerId) { - Fund fund = new JsonObject(getFile(FUND.getPathToSampleFile())).mapTo(Fund.class).withId(null).withLedgerId(ledgerId); - return postData(FUND.getEndpoint(), JsonObject.mapFrom(fund).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Fund.class).getId(); - } - - private String createFiscalYear() { - FiscalYear fiscalYear = new JsonObject(getFile(FISCAL_YEAR.getPathToSampleFile())).mapTo(FiscalYear.class).withId(null); - return postData(FISCAL_YEAR.getEndpoint(), JsonObject.mapFrom(fiscalYear).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(FiscalYear.class).getId(); - } - - private String createLedger(String fiscalYearId, boolean restrictEncumbrance) { - Ledger ledger = new JsonObject(getFile(LEDGER.getPathToSampleFile())).mapTo(Ledger.class) - .withId(null) - .withFiscalYearOneId(fiscalYearId) - .withRestrictEncumbrance(restrictEncumbrance); - return postData(LEDGER.getEndpoint(), JsonObject.mapFrom(ledger).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Ledger.class).getId(); - } - - private Budget buildBudget(String fiscalYearId, String fundId) { - final double allocated = 10000d; - final double available = 7000d; - final double unavailable = 3000d; - final double overEncumbrance = 150d; - return new JsonObject(getFile(BUDGET.getPathToSampleFile())).mapTo(Budget.class) - .withId(null) - .withFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withBudgetStatus(Budget.BudgetStatus.ACTIVE) - .withInitialAllocation(allocated) - .withAvailable(available) - .withExpenditures(1500d) - .withAwaitingPayment(1500d) - .withUnavailable(unavailable) - .withAllowableEncumbrance(overEncumbrance) - .withEncumbered(0d) - .withOverEncumbrance(0d); - } - - private JsonObject prepareEncumbrance(String fiscalYearId, String fundId) { - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - jsonTx.put("fiscalYearId", fiscalYearId); - jsonTx.put("fromFundId", fundId); - jsonTx.remove("sourceFiscalYearId"); - return jsonTx; - } - - private void checkBudgetTotalsNotChanged(Budget fromBudgetBefore, Budget fromBudgetAfterUpdate) { - assertEquals(fromBudgetBefore.getEncumbered(), fromBudgetAfterUpdate.getEncumbered()); - assertEquals(fromBudgetBefore.getAvailable() , fromBudgetAfterUpdate.getAvailable()); - assertEquals(fromBudgetBefore.getUnavailable(), fromBudgetAfterUpdate.getUnavailable()); - assertEquals(fromBudgetBefore.getAwaitingPayment(), fromBudgetAfterUpdate.getAwaitingPayment()); - } - -} diff --git a/src/test/java/org/folio/rest/impl/EntitiesCrudTest.java b/src/test/java/org/folio/rest/impl/EntitiesCrudTest.java index ea88b34d..12bd3bd0 100644 --- a/src/test/java/org/folio/rest/impl/EntitiesCrudTest.java +++ b/src/test/java/org/folio/rest/impl/EntitiesCrudTest.java @@ -13,12 +13,8 @@ import static org.folio.rest.utils.TestEntities.LEDGER_FISCAL_YEAR_ROLLOVER_LOG; import static org.folio.rest.utils.TestEntities.LEDGER_FISCAL_YEAR_ROLLOVER_ERROR; import static org.folio.rest.utils.TestEntities.LEDGER_FISCAL_YEAR_ROLLOVER_PROGRESS; -import static org.folio.rest.utils.TestEntities.ALLOCATION_TRANSACTION; -import static org.folio.rest.utils.TestEntities.ENCUMBRANCE_TRANSACTION; -import static org.folio.rest.utils.TestEntities.ORDER_SUMMARY; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; -import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -54,7 +50,7 @@ public class EntitiesCrudTest extends TestBase { * */ static Stream deleteOrder() { - return Stream.of(ORDER_SUMMARY, ALLOCATION_TRANSACTION, ENCUMBRANCE_TRANSACTION, GROUP_FUND_FY, BUDGET_EXPENSE_CLASS, BUDGET, LEDGER_FISCAL_YEAR_ROLLOVER_ERROR, + return Stream.of(GROUP_FUND_FY, BUDGET_EXPENSE_CLASS, BUDGET, LEDGER_FISCAL_YEAR_ROLLOVER_ERROR, LEDGER_FISCAL_YEAR_ROLLOVER, FUND, FUND_TYPE, LEDGER, FISCAL_YEAR, GROUP, EXPENSE_CLASS); } @@ -81,9 +77,7 @@ static Stream createDuplicateRecords() { @ParameterizedTest @Order(1) - @EnumSource(value = TestEntities.class, - names = {"ORDER_SUMMARY"}, - mode = EnumSource.Mode.EXCLUDE) + @EnumSource(value = TestEntities.class) void testVerifyCollection(TestEntities testEntity) { logger.info(String.format("--- mod-finance-storage %s test: Verifying database's initial state ... ", testEntity.name())); verifyCollectionQuantity(testEntity.getEndpoint(), 0); @@ -149,19 +143,10 @@ void testPostFailsOnUniqueConstraint(TestEntities testEntity) { @ParameterizedTest @Order(4) - @EnumSource(value = TestEntities.class, - names = {"ORDER_SUMMARY"}, - mode = EnumSource.Mode.EXCLUDE) + @EnumSource(value = TestEntities.class) void testVerifyCollectionQuantity(TestEntities testEntity) { logger.info(String.format("--- mod-finance-storage %s test: Verifying only 1 adjustment was created ... ", testEntity.name())); - int quantity = 1; - if(List.of(ALLOCATION_TRANSACTION, ENCUMBRANCE_TRANSACTION).contains(testEntity)) { - // both have the finance-storage/transactions endpoint in TestEntities, so each will return all transactions - // we use a preview for the fiscal year rollover, so a new encumbrance is not created - quantity = 2; - } - verifyCollectionQuantity(testEntity.getEndpoint(), quantity); - + verifyCollectionQuantity(testEntity.getEndpoint(), 1); } @ParameterizedTest @@ -200,7 +185,7 @@ void testPutById(TestEntities testEntity) { @ParameterizedTest @Order(7) @EnumSource(value = TestEntities.class, - names = {"LEDGER_FISCAL_YEAR_ROLLOVER_LOG", "ORDER_SUMMARY"}, + names = {"LEDGER_FISCAL_YEAR_ROLLOVER_LOG"}, mode = EnumSource.Mode.EXCLUDE) void testVerifyPut(TestEntities testEntity) { logger.info(String.format("--- mod-finance-storage %s test: Fetching updated %s with ID %s", testEntity.name(), testEntity.name(), testEntity.getId())); @@ -289,9 +274,7 @@ void testDeleteEntityWithNonExistedId(TestEntities testEntity) { } @ParameterizedTest - @EnumSource(value = TestEntities.class, - names = {"ORDER_SUMMARY"}, - mode = EnumSource.Mode.EXCLUDE) + @EnumSource(value = TestEntities.class) void testGetEntitiesWithInvalidCQLQuery(TestEntities testEntity) { logger.info(String.format("--- mod-finance-storage %s test: Invalid CQL query", testEntity.name())); testInvalidCQLQuery(testEntity.getEndpoint() + "?query=invalid-query"); diff --git a/src/test/java/org/folio/rest/impl/PaymentsCreditsTest.java b/src/test/java/org/folio/rest/impl/PaymentsCreditsTest.java deleted file mode 100644 index 6be284e7..00000000 --- a/src/test/java/org/folio/rest/impl/PaymentsCreditsTest.java +++ /dev/null @@ -1,510 +0,0 @@ -package org.folio.rest.impl; - -import static org.folio.rest.impl.EncumbrancesTest.ENCUMBRANCE_SAMPLE; -import static org.folio.rest.impl.TransactionTest.BUDGETS; -import static org.folio.rest.impl.TransactionTest.BUDGETS_QUERY; -import static org.folio.rest.impl.TransactionTest.TRANSACTION_ENDPOINT; -import static org.folio.rest.impl.TransactionTest.TRANSACTION_TENANT_HEADER; -import static org.folio.rest.impl.TransactionsSummariesTest.INVOICE_TRANSACTION_SUMMARIES_ENDPOINT; -import static org.folio.rest.impl.TransactionsSummariesTest.ORDER_TRANSACTION_SUMMARIES_ENDPOINT; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.utils.TenantApiTestUtil.deleteTenant; -import static org.folio.rest.utils.TenantApiTestUtil.purge; -import static org.folio.rest.utils.TenantApiTestUtil.prepareTenant; -import static org.folio.rest.utils.TestEntities.ALLOCATION_TRANSACTION; -import static org.folio.service.transactions.AllOrNothingTransactionService.ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED; -import static org.folio.service.transactions.restriction.BaseTransactionRestrictionService.FUND_CANNOT_BE_PAID; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import java.util.UUID; -import java.util.stream.Stream; - -import org.folio.rest.jaxrs.model.AwaitingPayment; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.BudgetCollection; -import org.folio.rest.jaxrs.model.InvoiceTransactionSummary; -import org.folio.rest.jaxrs.model.OrderTransactionSummary; -import org.folio.rest.jaxrs.model.TenantJob; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.jaxrs.model.TransactionCollection; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; - -import io.vertx.core.json.JsonObject; - -public class PaymentsCreditsTest extends TestBase { - private static final String CREDIT_SAMPLE = "data/transactions/credits/credit_CANHIST_30121.json"; - private static final String PAYMENT_SAMPLE = "data/transactions/payments/payment_ENDOW-SUBN_30121.json"; - private static final String TRANSACTION_SAMPLE = "data/transactions/encumbrances/encumbrance_ENDOW-SUBN_S60402_80.json"; - private static TenantJob tenantJob; - - @BeforeEach - void prepareData() { - tenantJob = prepareTenant(TRANSACTION_TENANT_HEADER, true, true); - } - - @AfterEach - void cleanupData() { - purge(TRANSACTION_TENANT_HEADER); - } - - @AfterAll - public static void after() { - deleteTenant(tenantJob, TRANSACTION_TENANT_HEADER); - } - - @Test - void testCreatePaymentsCreditsAllOrNothing() { - - String invoiceId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - createOrderSummary(orderId, 2); - createInvoiceSummary(invoiceId, 2); - JsonObject jsonTx = new JsonObject(getFile(ENCUMBRANCE_SAMPLE)); - jsonTx.remove("id"); - - Transaction paymentEncumbranceBefore = jsonTx.mapTo(Transaction.class); - paymentEncumbranceBefore.getEncumbrance() - .setSourcePurchaseOrderId(orderId); - - Transaction creditEncumbranceBefore = jsonTx.mapTo(Transaction.class); - creditEncumbranceBefore.getEncumbrance() - .setSourcePurchaseOrderId(orderId); - creditEncumbranceBefore.getEncumbrance() - .setSourcePoLineId(UUID.randomUUID() - .toString()); - String fY = paymentEncumbranceBefore.getFiscalYearId(); - String fromFundId = paymentEncumbranceBefore.getFromFundId(); - - // prepare budget queries - String budgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - - // create 1st Encumbrance, expected number is 2 - String paymentEncumbranceId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(paymentEncumbranceBefore) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - // create 2nd Encumbrance - String creditEncumbranceId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(creditEncumbranceBefore) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - JsonObject paymentJsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - paymentJsonTx.remove("id"); - - Transaction payment = paymentJsonTx.mapTo(Transaction.class); - payment.setSourceInvoiceId(invoiceId); - payment.setFiscalYearId(fY); - payment.setFromFundId(fromFundId); - payment.setPaymentEncumbranceId(paymentEncumbranceId); - - Transaction pendingPaymentForPayment = new Transaction() - .withFromFundId(payment.getFromFundId()) - .withAmount(payment.getAmount()) - .withCurrency(payment.getCurrency()) - .withFiscalYearId(fY) - .withSource(Transaction.Source.INVOICE) - .withSourceInvoiceId(invoiceId) - .withTransactionType(PENDING_PAYMENT) - .withAwaitingPayment(new AwaitingPayment() - .withReleaseEncumbrance(false) - .withEncumbranceId(paymentEncumbranceId)); - - - JsonObject creditJsonTx = new JsonObject(getFile(CREDIT_SAMPLE)); - creditJsonTx.remove("id"); - - Transaction credit = creditJsonTx.mapTo(Transaction.class); - credit.setSourceInvoiceId(invoiceId); - credit.setFiscalYearId(fY); - credit.setToFundId(fromFundId); - credit.setPaymentEncumbranceId(creditEncumbranceId); - - Transaction pendingPaymentForCredit = new Transaction() - .withFromFundId(credit.getToFundId()) - .withAmount(-credit.getAmount()) - .withCurrency(credit.getCurrency()) - .withFiscalYearId(fY) - .withSource(Transaction.Source.INVOICE) - .withSourceInvoiceId(invoiceId) - .withTransactionType(PENDING_PAYMENT) - .withAwaitingPayment(new AwaitingPayment() - .withReleaseEncumbrance(false) - .withEncumbranceId(creditEncumbranceId)); - - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(pendingPaymentForPayment).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201); - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(pendingPaymentForCredit).encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201); - - - Transaction paymentEncumbranceBeforePayment= getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentEncumbranceId, - TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - Transaction creditEncumbranceBeforePayment = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), creditEncumbranceId, - TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - - Budget budgetBefore = getBudgetAndValidate(budgetEndpointWithQueryParams); - - String paymentId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(payment) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - // payment does not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentId, TRANSACTION_TENANT_HEADER).then() - .statusCode(404); - - String creditId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(credit) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(credit) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(ALL_EXPECTED_TRANSACTIONS_ALREADY_PROCESSED)); - - // 2 transactions(each for a payment and credit) appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentId, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), creditId, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - - Transaction paymentEncumbranceAfter = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentEncumbranceId, - TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - Transaction creditEncumbranceAfter = getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), creditEncumbranceId, - TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - - // Check pending payments deleted - TransactionCollection transactionCollection = getData(String.format("%s?query=sourceInvoiceId==%s AND transactionType==%s", TRANSACTION_ENDPOINT, invoiceId, PENDING_PAYMENT.value()), TRANSACTION_TENANT_HEADER) - .then().statusCode(200) - .extract() - .as(TransactionCollection.class); - - assertEquals(0, transactionCollection.getTotalRecords()); - - // Encumbrance Changes for payment - assertEquals(payment.getAmount(), subtractValues(paymentEncumbranceAfter.getEncumbrance() - .getAmountExpended(), - paymentEncumbranceBeforePayment.getEncumbrance() - .getAmountExpended())); - - assertEquals(payment.getAmount(), subtractValues(paymentEncumbranceBeforePayment.getEncumbrance() - .getAmountAwaitingPayment(), - paymentEncumbranceAfter.getEncumbrance() - .getAmountAwaitingPayment())); - - // Encumbrance Changes for credit - assertEquals(-credit.getAmount(), subtractValues(creditEncumbranceAfter.getEncumbrance() - .getAmountExpended(), - creditEncumbranceBeforePayment.getEncumbrance() - .getAmountExpended())); - - assertEquals(-credit.getAmount(), subtractValues(creditEncumbranceBeforePayment.getEncumbrance() - .getAmountAwaitingPayment(), - creditEncumbranceAfter.getEncumbrance() - .getAmountAwaitingPayment())); - - Budget budgetAfter = getBudgetAndValidate(budgetEndpointWithQueryParams); - - // awaiting payment must decreases by payment amount - assertEquals(0d, subtractValues(budgetAfter.getAwaitingPayment(), budgetBefore.getAwaitingPayment())); - - // expenditures must increase by payment amt and decrease by credit amount - assertEquals(subtractValues(payment.getAmount(), credit.getAmount()), - subtractValues(budgetAfter.getExpenditures(), budgetBefore.getExpenditures())); - - } - - @Test - void testCreatePaymentsCreditsAllOrNothingWithNoEncumbrances() { - - String invoiceId = UUID.randomUUID().toString(); - createInvoiceSummary(invoiceId, 3); - - JsonObject paymentJsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - paymentJsonTx.remove("id"); - - Transaction payment = paymentJsonTx.mapTo(Transaction.class); - payment.setSourceInvoiceId(invoiceId); - payment.setPaymentEncumbranceId(null); - - String fY = payment.getFiscalYearId(); - String fromFundId = payment.getFromFundId(); - - - // prepare budget queries - String paymentBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - - Budget paymentBudgetBefore = getBudgetAndValidate(paymentBudgetEndpointWithQueryParams); - - String paymentId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(payment) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - // payment does not appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentId, TRANSACTION_TENANT_HEADER).then() - .statusCode(404); - - JsonObject creditJsonTx = new JsonObject(getFile(CREDIT_SAMPLE)); - creditJsonTx.remove("id"); - - Transaction credit = creditJsonTx.mapTo(Transaction.class); - credit.setSourceInvoiceId(invoiceId); - credit.setFiscalYearId(fY); - credit.setPaymentEncumbranceId(null); - credit.setAmount(30d); - - String creditBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, credit.getToFundId()); - Budget creditBudgetBefore = getBudgetAndValidate(creditBudgetEndpointWithQueryParams); - - String creditId = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(credit) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - String creditId1 = postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(credit.withSourceInvoiceLineId(UUID.randomUUID().toString())) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - // 3 transactions appear in transaction table - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), paymentId, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), creditId, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - getDataById(ALLOCATION_TRANSACTION.getEndpointWithId(), creditId1, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .extract() - .as(Transaction.class); - - Budget paymentBudgetAfter = getBudgetAndValidate(paymentBudgetEndpointWithQueryParams); - Budget creditBudgetAfter = getBudgetAndValidate(creditBudgetEndpointWithQueryParams); - - // payment changes awaiting payment must decreases, expenditures increase - assertEquals(paymentBudgetAfter.getAwaitingPayment(), subtractValues(paymentBudgetBefore.getAwaitingPayment(), payment.getAmount())); - assertEquals(paymentBudgetAfter.getExpenditures(), sumValues(paymentBudgetBefore.getExpenditures(), payment.getAmount())); - - // available, unavailable, encumbrances must remain the same - assertEquals(paymentBudgetAfter.getAvailable(), paymentBudgetBefore.getAvailable()); - assertEquals(paymentBudgetAfter.getUnavailable(), paymentBudgetBefore.getUnavailable()); - assertEquals(paymentBudgetAfter.getEncumbered(), paymentBudgetBefore.getEncumbered()); - assertEquals(creditBudgetAfter.getAvailable(), creditBudgetBefore.getAvailable()); - assertEquals(creditBudgetAfter.getUnavailable(), creditBudgetBefore.getUnavailable()); - - - // credit changes awaiting payment must increase, expenditures decreases - assertEquals(creditBudgetAfter.getAwaitingPayment(), sumValues(creditBudgetBefore.getAwaitingPayment(), credit.getAmount(), credit.getAmount())); - assertEquals(creditBudgetAfter.getExpenditures(), subtractValues(creditBudgetBefore.getExpenditures(), credit.getAmount(), credit.getAmount())); - - } - - @Test - void testCreatePaymentWithoutInvoiceSummary() { - - String invoiceId = UUID.randomUUID() - .toString(); - - JsonObject jsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - jsonTx.remove("id"); - Transaction payment = jsonTx.mapTo(Transaction.class); - - payment.setSourceInvoiceId(invoiceId); - - String transactionSample = JsonObject.mapFrom(payment) - .encodePrettily(); - - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(400); - - } - - @Test - void testPaymentsIdempotentInTemporaryTable() { - - JsonObject encumbranceJson = new JsonObject(getFile(TRANSACTION_SAMPLE)); - Transaction encumbrance = encumbranceJson.mapTo(Transaction.class); - String encumbranceSample = JsonObject.mapFrom(encumbrance).encodePrettily(); - - createOrderSummary(encumbrance.getEncumbrance().getSourcePurchaseOrderId(), 1); - - postData(ALLOCATION_TRANSACTION.getEndpoint(), encumbranceSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201).extract().as(Transaction.class); - - String invoiceId = UUID.randomUUID() - .toString(); - createInvoiceSummary(invoiceId, 2); - - JsonObject jsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - jsonTx.remove("id"); - Transaction payment = jsonTx.mapTo(Transaction.class); - - payment.setSourceInvoiceId(invoiceId); - String transactionSample = JsonObject.mapFrom(payment) - .encodePrettily(); - - // to support retrying a payment , just id is updated and a duplicate entry is not created - String id = postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - String retryId = postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class) - .getId(); - - assertNotEquals(retryId, id); - - } - - @Test - void testPaymentsWithInvalidPaymentEncumbranceInTemporaryTable() { - - String invoiceId = UUID.randomUUID() - .toString(); - createInvoiceSummary(invoiceId, 2); - - JsonObject jsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - jsonTx.remove("id"); - Transaction payment = jsonTx.mapTo(Transaction.class); - - payment.setSourceInvoiceId(invoiceId); - payment.setPaymentEncumbranceId(UUID.randomUUID() - .toString()); - String transactionSample = JsonObject.mapFrom(payment) - .encodePrettily(); - - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(400); - - } - - @Test - void testCreatePaymentWithRestrictedLedgerAndNotEnoughMoney() { - - String invoiceId = UUID.randomUUID().toString(); - createInvoiceSummary(invoiceId, 1); - - JsonObject paymentJsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - paymentJsonTx.remove("id"); - - Transaction payment = paymentJsonTx.mapTo(Transaction.class); - payment.setSourceInvoiceId(invoiceId); - payment.setPaymentEncumbranceId(null); - payment.setAmount((double) Integer.MAX_VALUE); - - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(payment) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(400) - .body(containsString(FUND_CANNOT_BE_PAID)); - - } - - @RepeatedTest(3) - void testCreateAllOrNothing10Payments() { - int numberOfPayments = 10; - int initialNumberOfTransactions = getData(TRANSACTION_ENDPOINT, TRANSACTION_TENANT_HEADER).then() - .extract() - .as(TransactionCollection.class) - .getTotalRecords(); - - String invoiceId = UUID.randomUUID().toString(); - createInvoiceSummary(invoiceId, numberOfPayments); - - JsonObject paymentJsonTx = new JsonObject(getFile(PAYMENT_SAMPLE)); - paymentJsonTx.remove("id"); - - Transaction payment = paymentJsonTx.mapTo(Transaction.class); - payment.setSourceInvoiceId(invoiceId); - payment.setPaymentEncumbranceId(null); - payment.setAmount(1d); - Stream.generate(() -> paymentJsonTx.mapTo(Transaction.class) - .withSourceInvoiceId(invoiceId) - .withPaymentEncumbranceId(null) - .withSourceInvoiceLineId(UUID.randomUUID().toString()) - .withAmount(1d)) - .limit(numberOfPayments) - .parallel() - .forEach(transaction -> { - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(transaction) - .encodePrettily(), TRANSACTION_TENANT_HEADER).then() - .statusCode(201); - }); - - int newNumberOfTransactions = getData(TRANSACTION_ENDPOINT, TRANSACTION_TENANT_HEADER).then() - .extract() - .as(TransactionCollection.class) - .getTotalRecords(); - - Assertions.assertEquals(initialNumberOfTransactions + numberOfPayments, newNumberOfTransactions, - String.format("initialNum = %s, newNum = %s", initialNumberOfTransactions, newNumberOfTransactions)); - } - - protected void createOrderSummary(String orderId, int encumbranceNumber) { - OrderTransactionSummary summary = new OrderTransactionSummary().withId(orderId).withNumTransactions(encumbranceNumber); - postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(summary) - .encodePrettily(), TRANSACTION_TENANT_HEADER); - } - - protected void createInvoiceSummary(String invoiceId, int numPaymentsCredits) { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary().withId(invoiceId).withNumPaymentsCredits(numPaymentsCredits).withNumPendingPayments(numPaymentsCredits); - postData(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(summary) - .encodePrettily(), TRANSACTION_TENANT_HEADER); - } - - protected Budget getBudgetAndValidate(String endpoint) { - return getData(endpoint, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .body(BUDGETS, hasSize(1)) - .extract() - .as(BudgetCollection.class).getBudgets().get(0); - } - -} diff --git a/src/test/java/org/folio/rest/impl/TenantSampleDataTest.java b/src/test/java/org/folio/rest/impl/TenantSampleDataTest.java index 0a5b4dd9..e00250db 100644 --- a/src/test/java/org/folio/rest/impl/TenantSampleDataTest.java +++ b/src/test/java/org/folio/rest/impl/TenantSampleDataTest.java @@ -57,10 +57,8 @@ void testLoadSampleDataWithoutUpgrade() { try { tenantJob = postTenant(ANOTHER_TENANT_HEADER, TenantApiTestUtil.prepareTenantBody(true, true)); for (TestEntities entity : TestEntities.values()) { - if (!entity.equals(TestEntities.ORDER_SUMMARY)) { - logger.info("Test expected quantity for " + entity.name()); - verifyCollectionQuantity(entity.getEndpoint(), entity.getInitialQuantity(), ANOTHER_TENANT_HEADER); - } + logger.info("Test expected quantity for " + entity.name()); + verifyCollectionQuantity(entity.getEndpoint(), entity.getInitialQuantity(), ANOTHER_TENANT_HEADER); } } finally { purge(ANOTHER_TENANT_HEADER); @@ -78,7 +76,7 @@ void testLoadReferenceData() { for (TestEntities entity : TestEntities.values()) { //category is the only reference data, which must be loaded - if (!entity.equals(TestEntities.FUND_TYPE) && !entity.equals(TestEntities.EXPENSE_CLASS) && !entity.equals(TestEntities.ORDER_SUMMARY)) { + if (!entity.equals(TestEntities.FUND_TYPE) && !entity.equals(TestEntities.EXPENSE_CLASS)) { logger.info("Test sample data not loaded for " + entity.name()); verifyCollectionQuantity(entity.getEndpoint(), 0, ANOTHER_TENANT_HEADER); } @@ -129,10 +127,8 @@ private TenantJob upgradeTenantWithSampleDataLoad() { TenantJob tenantJob = postTenant(ANOTHER_TENANT_HEADER, TenantApiTestUtil.prepareTenantBody(true, true)); for (TestEntities entity : TestEntities.values()) { - if (!entity.equals(TestEntities.ORDER_SUMMARY)) { - logger.info("Test expected quantity for " + entity.name()); - verifyCollectionQuantity(entity.getEndpoint(), entity.getInitialQuantity(), ANOTHER_TENANT_HEADER); - } + logger.info("Test expected quantity for " + entity.name()); + verifyCollectionQuantity(entity.getEndpoint(), entity.getInitialQuantity(), ANOTHER_TENANT_HEADER); } return tenantJob; } diff --git a/src/test/java/org/folio/rest/impl/TestBase.java b/src/test/java/org/folio/rest/impl/TestBase.java index 09ede6fc..8112001e 100644 --- a/src/test/java/org/folio/rest/impl/TestBase.java +++ b/src/test/java/org/folio/rest/impl/TestBase.java @@ -10,11 +10,9 @@ import static org.junit.Assert.assertThat; import java.io.InputStream; -import java.math.BigDecimal; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; @@ -166,13 +164,6 @@ void deleteDataSuccess(String endpoint, String id) { .statusCode(204); } - void deleteDataSuccess(TestEntities testEntity, String id) { - logger.info(String.format("--- %s test: Deleting record with ID %s", testEntity.name(), id)); - deleteData(testEntity.getEndpointWithId(), id) - .then().log().ifValidationFails() - .statusCode(204); - } - Response deleteData(String endpoint, String id) { return deleteData(endpoint, id, TENANT_HEADER); } @@ -249,12 +240,4 @@ void testVerifyEntityDeletion(String endpoint, String id) { .statusCode(404); } - protected double subtractValues(double d1, Double ... subtrahends) { - BigDecimal subtrahendSum = Stream.of(subtrahends).map(BigDecimal::valueOf).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); - return BigDecimal.valueOf(d1).subtract(subtrahendSum).doubleValue(); - } - - protected double sumValues(Double ... doubles) { - return Stream.of(doubles).map(BigDecimal::valueOf).reduce(BigDecimal::add).orElse(BigDecimal.ZERO).doubleValue(); - } } diff --git a/src/test/java/org/folio/rest/impl/TransactionTest.java b/src/test/java/org/folio/rest/impl/TransactionTest.java index 78105beb..d1998662 100644 --- a/src/test/java/org/folio/rest/impl/TransactionTest.java +++ b/src/test/java/org/folio/rest/impl/TransactionTest.java @@ -1,7 +1,5 @@ package org.folio.rest.impl; -import static io.restassured.RestAssured.given; -import static org.folio.StorageTestSuite.storageUrl; import static org.folio.rest.RestVerticle.OKAPI_HEADER_TENANT; import static org.folio.rest.utils.TenantApiTestUtil.deleteTenant; import static org.folio.rest.utils.TenantApiTestUtil.purge; @@ -10,19 +8,11 @@ import static org.folio.rest.utils.TestEntities.FISCAL_YEAR; import static org.folio.rest.utils.TestEntities.FUND; import static org.folio.rest.utils.TestEntities.LEDGER; -import static org.folio.rest.utils.TestEntities.ALLOCATION_TRANSACTION; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.UUID; - -import org.apache.commons.lang3.StringUtils; +import io.vertx.core.json.JsonObject; import org.apache.commons.lang3.tuple.Pair; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.BudgetCollection; +import org.folio.rest.jaxrs.model.Batch; import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.OrderTransactionSummary; import org.folio.rest.jaxrs.model.TenantJob; import org.folio.rest.jaxrs.model.Transaction; import org.junit.jupiter.api.AfterAll; @@ -30,33 +20,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.restassured.http.ContentType; import io.restassured.http.Header; -import io.vertx.core.json.JsonObject; + +import java.util.List; +import java.util.UUID; public class TransactionTest extends TestBase { - protected static final String TRANSACTION_ENDPOINT = ALLOCATION_TRANSACTION.getEndpoint(); - protected static final String TRANSACTION_ENDPOINT_BY_ID = ALLOCATION_TRANSACTION.getEndpointWithId(); protected static final String TRANSACTION_TEST_TENANT = "transactiontesttenant"; protected static final Header TRANSACTION_TENANT_HEADER = new Header(OKAPI_HEADER_TENANT, TRANSACTION_TEST_TENANT); - private static final String FY_FUND_QUERY = "?query=fiscalYearId==%s AND fundId==%s"; - public static final String ALLOCATION_SAMPLE = "data/transactions/zallocation_AFRICAHIST-FY24_ANZHIST-FY24.json"; - - static String BUDGETS_QUERY = BUDGET.getEndpoint() + FY_FUND_QUERY; - static final String BUDGETS = "budgets"; - public static final String FISCAL_YEAR_18_SAMPLE_PATH = "data/fiscal-years-8.4.0/fy18.json"; - public static final String LEDGER_MAIN_LIBRARY_SAMPLE_PATH = "data/ledgers-8.4.0/MainLibrary.json"; - public static final String ALLOCATION_FROM_FUND_SAMPLE_PATH = "data/funds-8.4.0/CANLATHIST.json"; - public static final String ALLOCATION_TO_FUND_SAMPLE_PATH = "data/funds-8.4.0/ANZHIST.json"; - public static final String ALLOCATION_FROM_BUDGET_SAMPLE_PATH = "data/budgets-8.4.0/CANLATHIST-FY24-closed.json"; - public static final String ALLOCATION_TO_BUDGET_SAMPLE_PATH = "data/budgets-8.4.0/ANZHIST-FY24.json"; - public static final String ALLOCATION_SAMPLE_PATH = "data/transactions/allocations-8.4.0/allocation_CANLATHIST-FY24.json"; - public static final String ORDER_TRANSACTION_SUMMARIES_ENDPOINT = "/finance-storage/order-transaction-summaries"; private static final String BATCH_TRANSACTION_SAMPLE = "data/transactions/batch/batch_with_patch.json"; private static final String BATCH_TRANSACTION_ENDPOINT = "/finance-storage/transactions/batch-all-or-nothing"; - private static final String ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID = ORDER_TRANSACTION_SUMMARIES_ENDPOINT + "/{id}"; + private static final String TRANSACTION_ENDPOINT_BY_ID = "/finance-storage/transactions/{id}"; private static TenantJob tenantJob; @BeforeEach @@ -75,285 +51,10 @@ public static void after() { } @Test - void testCreateAllocation() { - - givenTestData(TRANSACTION_TENANT_HEADER, - Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), - Pair.of(FISCAL_YEAR, FISCAL_YEAR_18_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER_MAIN_LIBRARY_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER.getPathToSampleFile()), - Pair.of(FUND, ALLOCATION_TO_FUND_SAMPLE_PATH), - Pair.of(FUND, ALLOCATION_FROM_FUND_SAMPLE_PATH), - Pair.of(BUDGET, ALLOCATION_TO_BUDGET_SAMPLE_PATH), - Pair.of(BUDGET, ALLOCATION_FROM_BUDGET_SAMPLE_PATH), - Pair.of(ALLOCATION_TRANSACTION, ALLOCATION_SAMPLE_PATH)); - - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - jsonTx.remove("id"); - String transactionSample = jsonTx.toString(); - - String fY = jsonTx.getString("fiscalYearId"); - String fromFundId = jsonTx.getString("fromFundId"); - String toFundId = jsonTx.getString("toFundId"); - - // prepare budget queries - String fromBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - String toBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, toFundId); - - Budget fromBudgetBefore = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetBefore = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - - // create Allocation - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - Budget fromBudgetAfter = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetAfter = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - // check source budget totals - final Double amount = jsonTx.getDouble("amount"); - double expectedBudgetsAvailable; - double expectedBudgetsAllocated; - - if (StringUtils.isNotEmpty(jsonTx.getString("fromFundId"))){ - expectedBudgetsAllocated = subtractValues(fromBudgetBefore.getAllocated(), amount); - expectedBudgetsAvailable = subtractValues(fromBudgetBefore.getAvailable(), amount); - - assertEquals(expectedBudgetsAllocated, fromBudgetAfter.getAllocated()); - assertEquals(expectedBudgetsAvailable, fromBudgetAfter.getAvailable()); - - } - - // check destination budget totals - expectedBudgetsAllocated = sumValues(toBudgetBefore.getAllocated(), amount); - expectedBudgetsAvailable = sumValues(toBudgetBefore.getAvailable(), amount); - - assertEquals(expectedBudgetsAvailable, toBudgetAfter.getAvailable()); - assertEquals(expectedBudgetsAllocated, toBudgetAfter.getAllocated()); - } - - @Test - void testCreateAllocationWithSourceBudgetNotExist() { - givenTestData(TRANSACTION_TENANT_HEADER, - Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), - Pair.of(FISCAL_YEAR, FISCAL_YEAR_18_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER_MAIN_LIBRARY_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER.getPathToSampleFile()), - Pair.of(FUND, ALLOCATION_TO_FUND_SAMPLE_PATH), - Pair.of(FUND, ALLOCATION_FROM_FUND_SAMPLE_PATH), - Pair.of(BUDGET, ALLOCATION_TO_BUDGET_SAMPLE_PATH)); - - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - jsonTx.remove("id"); - String transactionSample = jsonTx.toString(); - - String fY = jsonTx.getString("fiscalYearId"); - String toFundId = jsonTx.getString("toFundId"); - - // prepare budget queries - String toBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, toFundId); - - Budget toBudgetBefore = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - // try to create Allocation - given() - .header(TRANSACTION_TENANT_HEADER) - .accept(ContentType.TEXT) - .contentType(ContentType.JSON) - .body(transactionSample) - .log().all() - .post(storageUrl(TRANSACTION_ENDPOINT)) - .then() - .statusCode(404); - - Budget toBudgetAfter = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - // verify budget values not changed - if (StringUtils.isNotEmpty(jsonTx.getString("fromFundId"))){ - assertEquals(toBudgetBefore.getAllocated(), toBudgetAfter.getAllocated()); - assertEquals(toBudgetBefore.getAvailable(), toBudgetAfter.getAvailable()); - assertEquals(toBudgetBefore.getUnavailable() , toBudgetAfter.getUnavailable()); - } - - } - - @Test - void testDecreaseAllocationWhenAvailableLessThanAllocation() { - - givenTestData(TRANSACTION_TENANT_HEADER, - Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), - Pair.of(FISCAL_YEAR, FISCAL_YEAR_18_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER_MAIN_LIBRARY_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER.getPathToSampleFile()), - Pair.of(FUND, ALLOCATION_TO_FUND_SAMPLE_PATH), - Pair.of(FUND, ALLOCATION_FROM_FUND_SAMPLE_PATH), - Pair.of(BUDGET, ALLOCATION_TO_BUDGET_SAMPLE_PATH), - Pair.of(BUDGET, ALLOCATION_FROM_BUDGET_SAMPLE_PATH), - Pair.of(ALLOCATION_TRANSACTION, ALLOCATION_SAMPLE_PATH)); - - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - jsonTx.remove("id"); - jsonTx.remove("toFundId"); - - String fY = jsonTx.getString("fiscalYearId"); - String fromFundId = jsonTx.getString("fromFundId"); - - // prepare budget queries - String fromBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - - Budget fromBudgetBefore = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - jsonTx.put("amount", fromBudgetBefore.getAvailable() + 10d); - String transactionSample = jsonTx.toString(); - - // create Allocation - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - Budget fromBudgetAfter = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - - // check source budget totals - final Double amount = jsonTx.getDouble("amount"); - double expectedBudgetsAvailable; - double expectedBudgetsAllocated; - - expectedBudgetsAllocated = subtractValues(fromBudgetBefore.getAllocated(), amount); - expectedBudgetsAvailable = subtractValues(fromBudgetBefore.getAvailable(), amount); - - assertTrue(fromBudgetAfter.getAvailable() < 0); - assertEquals(expectedBudgetsAllocated, fromBudgetAfter.getAllocated()); - assertEquals(expectedBudgetsAvailable, fromBudgetAfter.getAvailable()); - } - - @Test - void testCreateTransfer() { - - givenTestData(TRANSACTION_TENANT_HEADER, - Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), - Pair.of(FISCAL_YEAR, FISCAL_YEAR_18_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER_MAIN_LIBRARY_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER.getPathToSampleFile()), - Pair.of(FUND, ALLOCATION_TO_FUND_SAMPLE_PATH), - Pair.of(FUND, FUND.getPathToSampleFile()), - Pair.of(BUDGET, BUDGET.getPathToSampleFile()), - Pair.of(BUDGET, ALLOCATION_TO_BUDGET_SAMPLE_PATH)); - - JsonObject jsonAllocation = new JsonObject(getFile("data/transactions/allocations-8.4.0/allocation_AFRICAHIST-FY24.json")); - jsonAllocation.remove("id"); - String allocationSample = jsonAllocation.toString(); - - postData(TRANSACTION_ENDPOINT, allocationSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - JsonObject jsonTx = new JsonObject(getFile("data/transactions/transfers-8.4.0/transfer.json")); - jsonTx.remove("id"); - String transactionSample = jsonTx.toString(); - - String fY = jsonTx.getString("fiscalYearId"); - String fromFundId = jsonTx.getString("fromFundId"); - String toFundId = jsonTx.getString("toFundId"); - - // prepare budget queries - String fromBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - String toBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, toFundId); - - Budget fromBudgetBefore = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetBefore = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - // create Transfer - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - Budget fromBudgetAfter = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetAfter = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - // check source budget totals - final Double amount = jsonTx.getDouble("amount"); - double expectedBudgetsAvailable; - - if (StringUtils.isNotEmpty(jsonTx.getString("fromFundId"))) { - expectedBudgetsAvailable = subtractValues(fromBudgetBefore.getAvailable(), amount); - - - assertEquals(expectedBudgetsAvailable, fromBudgetAfter.getAvailable()); - assertEquals(fromBudgetBefore.getUnavailable() , fromBudgetAfter.getUnavailable()); - } - - // check destination budget totals - expectedBudgetsAvailable = sumValues(toBudgetBefore.getAvailable(), amount); - - assertEquals(expectedBudgetsAvailable, toBudgetAfter.getAvailable()); - - } - - - @Test - void testCreateTransferThatAvailableNegativeNumberOfBudget() { - - givenTestData(TRANSACTION_TENANT_HEADER, - Pair.of(FISCAL_YEAR, FISCAL_YEAR.getPathToSampleFile()), - Pair.of(FISCAL_YEAR, FISCAL_YEAR_18_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER_MAIN_LIBRARY_SAMPLE_PATH), - Pair.of(LEDGER, LEDGER.getPathToSampleFile()), - Pair.of(FUND, ALLOCATION_TO_FUND_SAMPLE_PATH), - Pair.of(FUND, FUND.getPathToSampleFile()), - Pair.of(BUDGET, BUDGET.getPathToSampleFile()), - Pair.of(BUDGET, ALLOCATION_TO_BUDGET_SAMPLE_PATH)); - - JsonObject jsonAllocation = new JsonObject(getFile("data/transactions/allocations-8.4.0/allocation_AFRICAHIST-FY24.json")); - jsonAllocation.remove("id"); - String allocationSample = jsonAllocation.toString(); - - postData(TRANSACTION_ENDPOINT, allocationSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - JsonObject jsonTx = new JsonObject(getFile("data/transactions/transfers-8.4.0/transfer.json")); - jsonTx.remove("id"); - jsonTx.put("amount","21001"); - String transactionSample = jsonTx.toString(); - - // prepare budget queries - String fY = jsonTx.getString("fiscalYearId"); - String fromFundId = jsonTx.getString("fromFundId"); - String toFundId = jsonTx.getString("toFundId"); - - String fromBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, fromFundId); - String toBudgetEndpointWithQueryParams = String.format(BUDGETS_QUERY, fY, toFundId); - - Budget fromBudgetBefore = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetBefore = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - assertEquals(21000, fromBudgetBefore.getAvailable()); - assertEquals(0, toBudgetBefore.getAvailable()); - - // create Transfer - postData(TRANSACTION_ENDPOINT, transactionSample, TRANSACTION_TENANT_HEADER).then() - .statusCode(201) - .extract() - .as(Transaction.class); - - Budget fromBudgetAfter = getBudgetAndValidate(fromBudgetEndpointWithQueryParams); - Budget toBudgetAfter = getBudgetAndValidate(toBudgetEndpointWithQueryParams); - - assertEquals(-1, fromBudgetAfter.getAvailable()); - assertEquals(21001, toBudgetAfter.getAvailable()); - } - - protected Budget getBudgetAndValidate(String endpoint) { - return getData(endpoint, TRANSACTION_TENANT_HEADER).then() - .statusCode(200) - .body(BUDGETS, hasSize(1)) - .extract() - .as(BudgetCollection.class).getBudgets().get(0); + void testBatchTransactionsPatch() { + String batchAsString = getFile(BATCH_TRANSACTION_SAMPLE); + postData(BATCH_TRANSACTION_ENDPOINT, batchAsString, TRANSACTION_TENANT_HEADER).then() + .statusCode(500); } @Test @@ -366,11 +67,6 @@ void testUpdateEncumbranceConflict() { String orderId = UUID.randomUUID().toString(); String orderLineId = UUID.randomUUID().toString(); - OrderTransactionSummary postSummary = new OrderTransactionSummary() - .withId(orderId) - .withNumTransactions(1); - postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(postSummary).encodePrettily(), TRANSACTION_TENANT_HEADER) - .then().statusCode(201); String encumbranceId = UUID.randomUUID().toString(); Transaction encumbrance = new Transaction() @@ -389,38 +85,29 @@ void testUpdateEncumbranceConflict() { .withInitialAmountEncumbered(10d) .withSubscription(false) .withReEncumber(false)); - postData(TRANSACTION_ENDPOINT, JsonObject.mapFrom(encumbrance).encodePrettily(), TRANSACTION_TENANT_HEADER) - .then().statusCode(201); - Transaction encumbrance2 = JsonObject.mapFrom(encumbrance).mapTo(Transaction.class) + Batch batch1 = new Batch() + .withTransactionsToCreate(List.of(encumbrance)); + postData(BATCH_TRANSACTION_ENDPOINT, JsonObject.mapFrom(batch1).encodePrettily(), TRANSACTION_TENANT_HEADER) + .then().statusCode(204); + + Transaction createdEncumbrance = getDataById(TRANSACTION_ENDPOINT_BY_ID, encumbranceId, TRANSACTION_TENANT_HEADER) + .as(Transaction.class); + + Transaction encumbrance2 = JsonObject.mapFrom(createdEncumbrance).mapTo(Transaction.class) .withAmount(9.0) .withVersion(1); - OrderTransactionSummary putSummary1 = new OrderTransactionSummary() - .withId(orderId) - .withNumTransactions(1); - putData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, orderId, JsonObject.mapFrom(putSummary1).encodePrettily(), - TRANSACTION_TENANT_HEADER) - .then().statusCode(204); - putData(TRANSACTION_ENDPOINT_BY_ID, encumbranceId, JsonObject.mapFrom(encumbrance2).encodePrettily(), TRANSACTION_TENANT_HEADER) + Batch batch2 = new Batch() + .withTransactionsToUpdate(List.of(encumbrance2)); + postData(BATCH_TRANSACTION_ENDPOINT, JsonObject.mapFrom(batch2).encodePrettily(), TRANSACTION_TENANT_HEADER) .then().statusCode(204); - Transaction encumbrance3 = JsonObject.mapFrom(encumbrance).mapTo(Transaction.class) + Transaction encumbrance3 = JsonObject.mapFrom(createdEncumbrance).mapTo(Transaction.class) .withAmount(8.0); - OrderTransactionSummary putSummary2 = new OrderTransactionSummary() - .withId(orderId) - .withNumTransactions(1); - putData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, orderId, JsonObject.mapFrom(putSummary2).encodePrettily(), - TRANSACTION_TENANT_HEADER) - .then().statusCode(204); - putData(TRANSACTION_ENDPOINT_BY_ID, encumbranceId, JsonObject.mapFrom(encumbrance3).encodePrettily(), TRANSACTION_TENANT_HEADER) + Batch batch3 = new Batch() + .withTransactionsToUpdate(List.of(encumbrance3)); + postData(BATCH_TRANSACTION_ENDPOINT, JsonObject.mapFrom(batch3).encodePrettily(), TRANSACTION_TENANT_HEADER) .then().statusCode(409); } - @Test - void testBatchTransactionsPatch() { - String batchAsString = getFile(BATCH_TRANSACTION_SAMPLE); - postData(BATCH_TRANSACTION_ENDPOINT, batchAsString, TRANSACTION_TENANT_HEADER).then() - .statusCode(500); - } - } diff --git a/src/test/java/org/folio/rest/impl/TransactionsSummariesTest.java b/src/test/java/org/folio/rest/impl/TransactionsSummariesTest.java deleted file mode 100644 index eb98e48c..00000000 --- a/src/test/java/org/folio/rest/impl/TransactionsSummariesTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.folio.rest.impl; - -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - - -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.InvoiceTransactionSummary; -import org.folio.rest.jaxrs.model.OrderTransactionSummary; -import org.junit.jupiter.api.Test; - -import io.vertx.core.json.JsonObject; - -public class TransactionsSummariesTest extends TestBase { - - static final String ORDER_TRANSACTION_SUMMARIES_ENDPOINT = "/finance-storage/order-transaction-summaries"; - static final String ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID = ORDER_TRANSACTION_SUMMARIES_ENDPOINT + "/{id}"; - public static final String ORDERS_SUMMARY_SAMPLE = "data/order-transaction-summaries/order-306857_transaction-summary.json"; - public static final String INVOICE_SUMMARY_SAMPLE = "data/invoice-transaction-summaries/invoice-transaction-summary.json"; - - static final String INVOICE_TRANSACTION_SUMMARIES_ENDPOINT = "/finance-storage/invoice-transaction-summaries"; - private static final String INVOICE_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID = INVOICE_TRANSACTION_SUMMARIES_ENDPOINT + "/{id}"; - private static final int NUM_TRANSACTIONS = 123; - - - @Test - void testOrderTransactionSummaries() { - OrderTransactionSummary sample = new JsonObject(getFile(ORDERS_SUMMARY_SAMPLE)).mapTo(OrderTransactionSummary.class); - postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), TENANT_HEADER).as(OrderTransactionSummary.class); - - OrderTransactionSummary createdSummary = postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), TENANT_HEADER).as(OrderTransactionSummary.class); - - createdSummary.setNumTransactions(NUM_TRANSACTIONS); - putData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId(), JsonObject.mapFrom(createdSummary) - .encodePrettily(), TENANT_HEADER).then() - .statusCode(204); - - testEntitySuccessfullyFetched(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId()); - assertEquals(NUM_TRANSACTIONS, createdSummary.getNumTransactions()); - - deleteDataSuccess(ORDER_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId()); - } - - - @Test - void testOrderTransactionSummariesWithValidationError() { - OrderTransactionSummary sample = new JsonObject(getFile(ORDERS_SUMMARY_SAMPLE)).mapTo(OrderTransactionSummary.class); - sample.setNumTransactions(0); - Error error = postData(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), TENANT_HEADER).then().statusCode(422).contentType(APPLICATION_JSON) - .extract().as(Errors.class).getErrors().get(0); - - assertThat(error.getParameters().get(0).getKey(), equalTo("numOfTransactions")); - assertThat(error.getParameters().get(0).getValue(), equalTo("0")); - } - - @Test - void testInvoiceTransactionSummaries() { - InvoiceTransactionSummary sample = new JsonObject(getFile(INVOICE_SUMMARY_SAMPLE)).mapTo(InvoiceTransactionSummary.class); - - InvoiceTransactionSummary createdSummary = postData(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), TENANT_HEADER).as(InvoiceTransactionSummary.class); - - testEntitySuccessfullyFetched(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId()); - - createdSummary.setNumPaymentsCredits(NUM_TRANSACTIONS); - putData(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId(), JsonObject.mapFrom(createdSummary) - .encodePrettily(), TENANT_HEADER).then() - .statusCode(204); - - testEntitySuccessfullyFetched(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId()); - assertEquals(NUM_TRANSACTIONS, createdSummary.getNumPaymentsCredits()); - - - deleteDataSuccess(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT_WITH_ID, createdSummary.getId()); - } - - @Test - void testInvoiceTransactionSummariesWithValidationError() { - InvoiceTransactionSummary sample = new JsonObject(getFile(INVOICE_SUMMARY_SAMPLE)).mapTo(InvoiceTransactionSummary.class); - sample.setNumPaymentsCredits(0); - Error error = postData(INVOICE_TRANSACTION_SUMMARIES_ENDPOINT, JsonObject.mapFrom(sample) - .encodePrettily(), TENANT_HEADER).then().statusCode(422).contentType(APPLICATION_JSON) - .extract().as(Errors.class).getErrors().get(0); - - assertThat(error.getParameters().get(0).getKey(), equalTo("numOfTransactions")); - assertThat(error.getParameters().get(0).getValue(), equalTo("0")); - - } -} diff --git a/src/test/java/org/folio/rest/utils/DBClientTest.java b/src/test/java/org/folio/rest/utils/DBClientTest.java index 0c227e5c..a8366086 100644 --- a/src/test/java/org/folio/rest/utils/DBClientTest.java +++ b/src/test/java/org/folio/rest/utils/DBClientTest.java @@ -7,7 +7,7 @@ import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import io.vertx.sqlclient.Tuple; @@ -71,7 +71,7 @@ void withTrans(Vertx vertx, VertxTestContext vtc) { private static void assertHttpException(Throwable e, String expectedSubstring1, String expectedSubstring2) { assertThat(e, instanceOf(HttpException.class)); - assertThat(((HttpException)e).getPayload(), containsString(expectedSubstring1)); - assertThat(((HttpException)e).getPayload(), containsString(expectedSubstring2)); + assertThat(e.getMessage(), containsString(expectedSubstring1)); + assertThat(e.getMessage(), containsString(expectedSubstring2)); } } diff --git a/src/test/java/org/folio/rest/utils/TestEntities.java b/src/test/java/org/folio/rest/utils/TestEntities.java index dce5eb49..ec6a055d 100644 --- a/src/test/java/org/folio/rest/utils/TestEntities.java +++ b/src/test/java/org/folio/rest/utils/TestEntities.java @@ -4,8 +4,6 @@ import org.folio.rest.jaxrs.resource.*; import org.folio.rest.persist.HelperUtils; -import static org.folio.rest.impl.TransactionTest.ORDER_TRANSACTION_SUMMARIES_ENDPOINT; - public enum TestEntities { //The Order is important because of the foreign key relationships EXPENSE_CLASS(HelperUtils.getEndpoint(FinanceStorageExpenseClasses.class), ExpenseClass.class, "data/expense-classes-4.0.0/", "elec.json", "name", "Electronic", 2, true), @@ -15,9 +13,6 @@ public enum TestEntities { FUND(HelperUtils.getEndpoint(FinanceStorageFunds.class), Fund.class, "data/funds-8.4.0/", "AFRICAHIST.json", "name", "African History", 23, true), BUDGET(HelperUtils.getEndpoint(FinanceStorageBudgets.class), Budget.class, "data/budgets-8.4.0/", "AFRICAHIST-FY24.json", "name", "AFRICAHIST-FY24", 23, true), BUDGET_EXPENSE_CLASS(HelperUtils.getEndpoint(FinanceStorageBudgetExpenseClasses.class), BudgetExpenseClass.class, "data/budget-expense-classes-8.4.0/", "AFRICAHIST-FY24-elec.json", "status", "Inactive", 1, true), - ORDER_SUMMARY(ORDER_TRANSACTION_SUMMARIES_ENDPOINT, OrderTransactionSummary.class, "data/order-transaction-summaries/", "order-306857_transaction-summary.json", "numTransactions", "1", 1, false), - ALLOCATION_TRANSACTION(HelperUtils.getEndpoint(FinanceStorageTransactions.class), Transaction.class, "data/transactions/", "allocations-8.4.0/allocation_AFRICAHIST-FY24.json", "source", "Invoice", 16, true), - ENCUMBRANCE_TRANSACTION(HelperUtils.getEndpoint(FinanceStorageTransactions.class), Transaction.class, "data/transactions/", "encumbrances/encumbrance_AFRICAHIST_306857_2.json", "source", "Invoice", 16, true), GROUP(HelperUtils.getEndpoint(FinanceStorageGroups.class), Group.class, "data/groups-3.2.0/", "HIST.json", "name", "New name", 1, true), GROUP_FUND_FY(HelperUtils.getEndpoint(FinanceStorageGroupFundFiscalYears.class), GroupFundFiscalYear.class, "data/group-fund-fiscal-years-8.4.0/", "AFRICAHIST-FY24.json", "fundId", "7fbd5d84-62d1-44c6-9c45-6cb173998bbd", 12, true), LEDGER_FISCAL_YEAR_ROLLOVER(HelperUtils.getEndpoint(FinanceStorageLedgerRollovers.class), LedgerFiscalYearRollover.class, "data/ledger-fiscal-year-rollovers/", "main-library.json", "restrictEncumbrance", "true", 0, true), @@ -42,7 +37,6 @@ public enum TestEntities { private String endpoint; private String sampleFileName; private String sampleId; - private Integer version; private String pathToSamples; private String updatedFieldName; private String updatedFieldValue; diff --git a/src/test/java/org/folio/service/rollover/RolloverProgressServiceTest.java b/src/test/java/org/folio/service/rollover/RolloverProgressServiceTest.java index 2beade27..d493001b 100644 --- a/src/test/java/org/folio/service/rollover/RolloverProgressServiceTest.java +++ b/src/test/java/org/folio/service/rollover/RolloverProgressServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import java.util.Collections; +import java.util.List; import org.folio.dao.rollover.RolloverProgressDAO; import org.folio.rest.jaxrs.model.LedgerFiscalYearRolloverError; @@ -60,7 +61,7 @@ void shouldUpdateRolloverProgressWithErrorOverallStatusWhenThereAreRolloverError LedgerFiscalYearRolloverError error = new LedgerFiscalYearRolloverError(); when(rolloverErrorService.getRolloverErrors(any(), any())) - .thenReturn(Future.succeededFuture(Collections.singletonList(error))); + .thenReturn(Future.succeededFuture(List.of(error))); when(rolloverProgressDAO.update(refEq(progress), any())).thenReturn(Future.succeededFuture()); testContext.assertComplete(rolloverProgressService.calculateAndUpdateOverallProgressStatus(progress, conn)) @@ -103,7 +104,7 @@ void shouldUpdateRolloverProgressWithErrorFinancialStatusWhenThereAreRolloverErr LedgerFiscalYearRolloverError error = new LedgerFiscalYearRolloverError(); when(rolloverErrorService.getRolloverErrors(any(), any())) - .thenReturn(Future.succeededFuture(Collections.singletonList(error))); + .thenReturn(Future.succeededFuture(List.of(error))); when(rolloverProgressDAO.update(refEq(progress), any())).thenReturn(Future.succeededFuture()); testContext.assertComplete(rolloverProgressService.calculateAndUpdateOverallProgressStatus(progress, conn)) diff --git a/src/test/java/org/folio/service/rollover/RolloverValidationServiceTest.java b/src/test/java/org/folio/service/rollover/RolloverValidationServiceTest.java index e8035149..5838db14 100644 --- a/src/test/java/org/folio/service/rollover/RolloverValidationServiceTest.java +++ b/src/test/java/org/folio/service/rollover/RolloverValidationServiceTest.java @@ -25,7 +25,7 @@ import org.mockito.MockitoAnnotations; import io.vertx.core.Future; -import io.vertx.ext.web.handler.HttpException; +import org.folio.rest.exception.HttpException; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import io.vertx.pgclient.impl.RowImpl; @@ -102,8 +102,8 @@ void shouldValidationFailed(VertxTestContext testContext) { .onComplete(event -> { HttpException exception = (HttpException) event.cause(); testContext.verify(() -> { - assertEquals(exception.getStatusCode() , 409); - assertEquals(exception.getPayload(), "Not unique pair ledgerId and fromFiscalYearId"); + assertEquals(exception.getCode() , 409); + assertEquals(exception.getMessage(), "Not unique pair ledgerId and fromFiscalYearId"); }); testContext.completeNow(); })); diff --git a/src/test/java/org/folio/service/summary/PendingPaymentTransactionSummaryServiceTest.java b/src/test/java/org/folio/service/summary/PendingPaymentTransactionSummaryServiceTest.java deleted file mode 100644 index 50b147dd..00000000 --- a/src/test/java/org/folio/service/summary/PendingPaymentTransactionSummaryServiceTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.folio.service.summary; - -import io.vertx.core.json.JsonObject; -import org.folio.dao.summary.InvoiceTransactionSummaryDAO; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.InvoiceTransactionSummary; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class PendingPaymentTransactionSummaryServiceTest { - - private final PendingPaymentTransactionSummaryService pendingPaymentSummaryService = new PendingPaymentTransactionSummaryService(new InvoiceTransactionSummaryDAO()); - - @Test - void testGetSummaryId() { - String invoiceId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - - Transaction transaction = new Transaction() - .withSourceInvoiceId(invoiceId) - .withEncumbrance(new Encumbrance().withSourcePurchaseOrderId(orderId)); - - assertEquals(pendingPaymentSummaryService.getSummaryId(transaction), invoiceId); - } - - @Test - void testIsProcessedTrue() { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary() - .withNumPendingPayments(-2) - .withNumPaymentsCredits(2); - assertTrue(pendingPaymentSummaryService.isProcessed(JsonObject.mapFrom(summary))); - } - - @Test - void testIsProcessedFalse() { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary() - .withNumPendingPayments(2) - .withNumPaymentsCredits(-2); - assertFalse(pendingPaymentSummaryService.isProcessed(JsonObject.mapFrom(summary))); - } - - @Test - void setTransactionsSummariesProcessed() { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary() - .withNumPendingPayments(2) - .withNumPaymentsCredits(2); - JsonObject jsonSummary = (JsonObject.mapFrom(summary)); - pendingPaymentSummaryService.setTransactionsSummariesProcessed(jsonSummary); - InvoiceTransactionSummary updatedSummary = jsonSummary.mapTo(InvoiceTransactionSummary.class); - assertEquals(2, updatedSummary.getNumPaymentsCredits()); - assertEquals(-2, updatedSummary.getNumPendingPayments()); - } - - @Test - void getNumTransactions() { - InvoiceTransactionSummary summary = new InvoiceTransactionSummary() - .withNumPendingPayments(-2) - .withNumPaymentsCredits(2); - assertEquals(-2, pendingPaymentSummaryService.getNumTransactions(JsonObject.mapFrom(summary))); - } -} diff --git a/src/test/java/org/folio/service/transactions/AllocationServiceTest.java b/src/test/java/org/folio/service/transactions/AllocationServiceTest.java deleted file mode 100644 index 66046f6c..00000000 --- a/src/test/java/org/folio/service/transactions/AllocationServiceTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.folio.service.transactions; - -import static org.folio.rest.impl.TestBase.getFile; -import static org.folio.rest.impl.TransactionTest.ALLOCATION_SAMPLE; -import static org.folio.rest.util.ErrorCodes.ALLOCATION_MUST_BE_POSITIVE; -import static org.folio.rest.util.ErrorCodes.MISSING_FUND_ID; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.vertx.ext.web.handler.HttpException; -import org.folio.dao.transactions.DefaultTransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; - -@ExtendWith(VertxExtension.class) -public class AllocationServiceTest { - - private AutoCloseable mockitoMocks; - private AllocationService allocationService; - @Mock - private BudgetService budgetService; - @Mock - private DBConn conn; - - @BeforeEach - public void initMocks() { - mockitoMocks = MockitoAnnotations.openMocks(this); - DefaultTransactionDAO defaultTransactionDAO = new DefaultTransactionDAO(); - allocationService = new AllocationService(budgetService, defaultTransactionDAO); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void shouldThrowExceptionWithMissingFundIdCodeWhenCreateAllocationWithEmptyFromFundIdEndToFundId(VertxTestContext testContext) { - - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - jsonTx.remove("toFundId"); - jsonTx.remove("fromFundId"); - Transaction transactionSample = jsonTx.mapTo(Transaction.class); - testContext.assertFailure(allocationService.createTransaction(transactionSample, conn)) - .onFailure(thrown -> { - testContext.verify(() -> { - assertThat(thrown, instanceOf(HttpException.class)); - assertThat(((HttpException) thrown).getPayload(), containsString(MISSING_FUND_ID.getCode())); - }); - testContext.completeNow(); - }); - - } - - @Test - void shouldThrowExceptionWithMustBePositiveCodeWhenCreateAllocationWithNegativeAmount(VertxTestContext testContext) { - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - Transaction transactionSample = jsonTx.mapTo(Transaction.class); - transactionSample.setAmount(-10d); - testContext.assertFailure(allocationService.createTransaction(transactionSample, conn)) - .onFailure(thrown -> { - testContext.verify(() -> { - assertThat(thrown, instanceOf(HttpException.class)); - assertThat(((HttpException) thrown).getPayload(), containsString(ALLOCATION_MUST_BE_POSITIVE.getCode())); - }); - testContext.completeNow(); - }); - - } - - @Test - void shouldCreateAllocationAndUpdateBudgetSpecifiedInFromFundIdWhenCreateAllocationAndToFundIdNotSpecified( - VertxTestContext testContext) { - JsonObject jsonTx = new JsonObject(getFile(ALLOCATION_SAMPLE)); - Transaction transactionSample = jsonTx.mapTo(Transaction.class); - transactionSample.setToFundId(null); - transactionSample.setAmount(40d); - - when(budgetService.checkBudgetHaveMoneyForTransaction(any(), any())) - .thenReturn(Future.succeededFuture()); - doReturn(Future.succeededFuture(transactionSample)) - .when(conn).saveAndReturnUpdatedEntity(any(), any(), any()); - when(budgetService.getBudgetByFiscalYearIdAndFundIdForUpdate(anyString(), anyString(), any())) - .thenReturn(Future.succeededFuture(new Budget().withInitialAllocation(50d))); - doNothing().when(budgetService) - .updateBudgetMetadata(any(), any()); - when(budgetService.updateBatchBudgets(any(), any())) - .thenReturn(Future.succeededFuture()); - - testContext.assertComplete(allocationService.createTransaction(transactionSample, conn)) - .onSuccess(transaction -> { - testContext.verify(() -> { - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Budget.class); - verify(budgetService).updateBudgetMetadata(argumentCaptor.capture(), any()); - Budget budget = argumentCaptor.getValue(); - assertEquals(40d, budget.getAllocationFrom()); - }); - testContext.completeNow(); - }); - - } -} diff --git a/src/test/java/org/folio/service/transactions/AllocationTransferTest.java b/src/test/java/org/folio/service/transactions/AllocationTransferTest.java new file mode 100644 index 00000000..13a33582 --- /dev/null +++ b/src/test/java/org/folio/service/transactions/AllocationTransferTest.java @@ -0,0 +1,425 @@ +package org.folio.service.transactions; + +import io.vertx.junit5.VertxTestContext; +import io.vertx.sqlclient.Tuple; +import org.folio.rest.exception.HttpException; +import org.folio.rest.jaxrs.model.Batch; +import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Fund; +import org.folio.rest.jaxrs.model.Ledger; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.jaxrs.model.Transaction; +import org.folio.rest.persist.Criteria.Criterion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.dao.ledger.LedgerPostgresDAO.LEDGER_TABLE; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; +import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; +import static org.folio.rest.impl.FundAPI.FUND_TABLE; +import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.ACTIVE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ALLOCATION; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.TRANSFER; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class AllocationTransferTest extends BatchTransactionServiceTestBase { + + @Test + void testCreateInitialAllocation(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withToFundId(fundId) + .withTransactionType(ALLOCATION) + .withAmount(5d) + .withFiscalYearId(fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify allocation creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); + assertThat(savedTransaction.getTransactionType(), equalTo(ALLOCATION)); + assertNotNull(savedTransaction.getMetadata()); + assertThat(savedTransaction.getAmount(), equalTo(allocation.getAmount())); + + // Verify budget update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getInitialAllocation(), equalTo(10d)); + assertThat(savedBudget.getAllocationTo(), equalTo(5d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateAllocation(VertxTestContext testContext) { + String fundId1 = UUID.randomUUID().toString(); + String fundId2 = UUID.randomUUID().toString(); + String budgetId1 = UUID.randomUUID().toString(); + String budgetId2 = UUID.randomUUID().toString(); + String transactionId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId1) + .withToFundId(fundId2) + .withTransactionType(ALLOCATION) + .withAmount(5d) + .withFiscalYearId(fiscalYearId); + + setup2Funds2Budgets1Ledger(fundId1, fundId2, budgetId1, budgetId2, fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify allocation creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); + assertNotNull(savedTransaction.getMetadata()); + assertThat(savedTransaction.getAmount(), equalTo(allocation.getAmount())); + + // Verify budget updates + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + Budget savedBudget1 = (Budget)(updateEntities.get(0).get(0)); + assertThat(savedBudget1.getId(), equalTo(budgetId1)); + assertNotNull(savedBudget1.getMetadata().getUpdatedDate()); + assertThat(savedBudget1.getAllocationFrom(), equalTo(5d)); + Budget savedBudget2 = (Budget)(updateEntities.get(0).get(1)); + assertThat(savedBudget2.getId(), equalTo(budgetId2)); + assertNotNull(savedBudget2.getMetadata().getUpdatedDate()); + assertThat(savedBudget2.getAllocationTo(), equalTo(5d)); + }); + testContext.completeNow(); + }); + } + + @ParameterizedTest + @ValueSource(doubles = {5d, 10d}) + void testCreateTransfer(double transferAmount, VertxTestContext testContext) { + String fundId1 = UUID.randomUUID().toString(); + String fundId2 = UUID.randomUUID().toString(); + String budgetId1 = UUID.randomUUID().toString(); + String budgetId2 = UUID.randomUUID().toString(); + String transactionId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction transfer = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId1) + .withToFundId(fundId2) + .withTransactionType(TRANSFER) + .withAmount(transferAmount) + .withFiscalYearId(fiscalYearId); + + setup2Funds2Budgets1Ledger(fundId1, fundId2, budgetId1, budgetId2, fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(transfer); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify transfer creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); + assertThat(savedTransaction.getTransactionType(), equalTo(TRANSFER)); + assertNotNull(savedTransaction.getMetadata()); + assertThat(savedTransaction.getAmount(), equalTo(transfer.getAmount())); + + // Verify budget updates + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + Budget savedBudget1 = (Budget)(updateEntities.get(0).get(0)); + assertThat(savedBudget1.getId(), equalTo(budgetId1)); + assertNotNull(savedBudget1.getMetadata().getUpdatedDate()); + assertThat(savedBudget1.getNetTransfers(), equalTo(-transferAmount)); + assertThat(savedBudget1.getAllocationFrom(), equalTo(0d)); + assertThat(savedBudget1.getAllocationTo(), equalTo(0d)); + Budget savedBudget2 = (Budget)(updateEntities.get(0).get(1)); + assertThat(savedBudget2.getId(), equalTo(budgetId2)); + assertNotNull(savedBudget2.getMetadata().getUpdatedDate()); + assertThat(savedBudget2.getNetTransfers(), equalTo(transferAmount)); + assertThat(savedBudget2.getAllocationFrom(), equalTo(0d)); + assertThat(savedBudget2.getAllocationTo(), equalTo(0d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateAllocationWithMissingSourceBudget(VertxTestContext testContext) { + String fundId1 = UUID.randomUUID().toString(); + String fundId2 = UUID.randomUUID().toString(); + String budgetId2 = UUID.randomUUID().toString(); + String transactionId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String tenantId = "tenantname"; + String ledgerId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId1) + .withToFundId(fundId2) + .withTransactionType(ALLOCATION) + .withAmount(5d) + .withFiscalYearId(fiscalYearId); + + Ledger ledger = new Ledger() + .withId(ledgerId); + + Fund fund1 = new Fund() + .withId(fundId1) + .withLedgerId(ledgerId); + + Fund fund2 = new Fund() + .withId(fundId2) + .withLedgerId(ledgerId); + + Budget budget2 = new Budget() + .withId(budgetId2) + .withFiscalYearId(fiscalYearId) + .withFundId(fundId2) + .withBudgetStatus(ACTIVE) + .withInitialAllocation(10d) + .withNetTransfers(0d) + .withMetadata(new Metadata()); + + doReturn(tenantId) + .when(conn).getTenantId(); + + Criterion fundCriterion = createCriterionByIds(List.of(fundId1, fundId2)); + doReturn(succeededFuture(createResults(List.of(fund1, fund2)))) + .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( + crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); + + String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '%s' OR fundId = '%s')) FOR UPDATE"; + doReturn(succeededFuture(createRowSet(List.of(budget2)))) + .when(conn).execute(argThat(s -> + s.equals(String.format(sql, fundId1, fundId2)) || s.equals(String.format(sql, fundId2, fundId1))), + any(Tuple.class)); + + Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); + doReturn(succeededFuture(createResults(List.of(ledger)))) + .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( + crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + HttpException exception = (HttpException)event.cause(); + assertThat(exception.getCode(), equalTo(500)); + assertThat(exception.getMessage(), startsWith("Could not find some budgets in the database")); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateAllocationBeyondAvailableBudget(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ALLOCATION) + .withAmount(15d) + .withFiscalYearId(fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify budget update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); + assertThat(savedBudget.getInitialAllocation(), equalTo(10d)); + assertThat(savedBudget.getAllocationFrom(), equalTo(15d)); + assertThat(savedBudget.getAllocationTo(), equalTo(0d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateAllocationWithoutFundId(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withTransactionType(ALLOCATION) + .withAmount(5d) + .withFiscalYearId(fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + HttpException exception = (HttpException)event.cause(); + assertThat(exception.getCode(), equalTo(400)); + assertEquals(exception.getErrors().getErrors().get(0).getCode(), "missingFundId"); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateAllocationWithNegativeAmount(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + + Transaction allocation = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withTransactionType(ALLOCATION) + .withAmount(-5d) + .withFiscalYearId(fiscalYearId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(allocation); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + HttpException exception = (HttpException)event.cause(); + assertThat(exception.getCode(), equalTo(400)); + assertEquals(exception.getErrors().getErrors().get(0).getCode(), "allocationMustBePositive"); + }); + testContext.completeNow(); + }); + } + +} diff --git a/src/test/java/org/folio/service/transactions/BatchTransactionServiceTest.java b/src/test/java/org/folio/service/transactions/BatchTransactionServiceTest.java deleted file mode 100644 index 3ccb4b95..00000000 --- a/src/test/java/org/folio/service/transactions/BatchTransactionServiceTest.java +++ /dev/null @@ -1,1324 +0,0 @@ -package org.folio.service.transactions; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import io.vertx.pgclient.impl.RowImpl; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowSet; -import io.vertx.sqlclient.Tuple; -import io.vertx.sqlclient.impl.RowDesc; -import org.folio.dao.budget.BudgetDAO; -import org.folio.dao.budget.BudgetPostgresDAO; -import org.folio.dao.fund.FundDAO; -import org.folio.dao.fund.FundPostgresDAO; -import org.folio.dao.ledger.LedgerDAO; -import org.folio.dao.ledger.LedgerPostgresDAO; -import org.folio.dao.transactions.BatchTransactionDAO; -import org.folio.dao.transactions.BatchTransactionPostgresDAO; -import org.folio.rest.core.model.RequestContext; -import org.folio.rest.exception.HttpException; -import org.folio.rest.jaxrs.model.AwaitingPayment; -import org.folio.rest.jaxrs.model.Batch; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Fund; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Metadata; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.jaxrs.model.TransactionPatch; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.rest.persist.CriterionBuilder; -import org.folio.rest.persist.DBClient; -import org.folio.rest.persist.DBClientFactory; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.helpers.LocalRowDesc; -import org.folio.rest.persist.helpers.LocalRowSet; -import org.folio.rest.persist.interfaces.Results; -import org.folio.service.budget.BudgetService; -import org.folio.service.fund.FundService; -import org.folio.service.fund.StorageFundService; -import org.folio.service.ledger.LedgerService; -import org.folio.service.ledger.StorageLedgerService; -import org.folio.service.transactions.batch.BatchAllocationService; -import org.folio.service.transactions.batch.BatchEncumbranceService; -import org.folio.service.transactions.batch.BatchPaymentCreditService; -import org.folio.service.transactions.batch.BatchPendingPaymentService; -import org.folio.service.transactions.batch.BatchTransactionService; -import org.folio.service.transactions.batch.BatchTransactionServiceInterface; -import org.folio.service.transactions.batch.BatchTransferService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.function.Function; - -import static io.vertx.core.Future.succeededFuture; -import static org.folio.dao.ledger.LedgerPostgresDAO.LEDGER_TABLE; -import static org.folio.dao.transactions.BatchTransactionPostgresDAO.TRANSACTIONS_TABLE; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.rest.impl.FundAPI.FUND_TABLE; -import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.ACTIVE; -import static org.folio.rest.jaxrs.model.Encumbrance.Status.RELEASED; -import static org.folio.rest.jaxrs.model.Encumbrance.Status.UNRELEASED; -import static org.folio.rest.jaxrs.model.Transaction.Source.PO_LINE; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ALLOCATION; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ENCUMBRANCE; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PAYMENT; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; -import static org.folio.rest.jaxrs.model.Transaction.TransactionType.TRANSFER; -import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_ENCUMBRANCE_ERROR; -import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_EXPENDITURES_ERROR; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@ExtendWith(VertxExtension.class) -public class BatchTransactionServiceTest { - private AutoCloseable mockitoMocks; - private BatchTransactionService batchTransactionService; - @Mock - private DBClientFactory dbClientFactory; - @Mock - private RequestContext requestContext; - @Mock - private DBClient dbClient; - @Mock - private DBConn conn; - - @BeforeEach - public void init() { - mockitoMocks = MockitoAnnotations.openMocks(this); - FundDAO fundDAO = new FundPostgresDAO(); - FundService fundService = new StorageFundService(fundDAO); - BudgetDAO budgetDAO = new BudgetPostgresDAO(); - BudgetService budgetService = new BudgetService(budgetDAO); - LedgerDAO ledgerDAO = new LedgerPostgresDAO(); - LedgerService ledgerService = new StorageLedgerService(ledgerDAO, fundService); - Set batchTransactionStrategies = new HashSet<>(); - batchTransactionStrategies.add(new BatchEncumbranceService()); - batchTransactionStrategies.add(new BatchPendingPaymentService()); - batchTransactionStrategies.add(new BatchPaymentCreditService()); - batchTransactionStrategies.add(new BatchAllocationService()); - batchTransactionStrategies.add(new BatchTransferService()); - BatchTransactionDAO transactionDAO = new BatchTransactionPostgresDAO(); - batchTransactionService = new BatchTransactionService(dbClientFactory, transactionDAO, fundService, budgetService, - ledgerService, batchTransactionStrategies); - doReturn(dbClient) - .when(dbClientFactory).getDbClient(eq(requestContext)); - doAnswer(invocation -> { - Function> function = invocation.getArgument(0); - return function.apply(conn); - }).when(dbClient).withTrans(any()); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void testBatchEntityValidity(VertxTestContext testContext) { - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - Transaction pendingPayment = new Transaction() - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withCurrency(currency) - .withTransactionType(PENDING_PAYMENT); - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(pendingPayment); - testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) - .onFailure(thrown -> { - testContext.verify(() -> { - assertThat(thrown, instanceOf(HttpException.class)); - assertThat(((HttpException) thrown).getCode(), equalTo(400)); - assertThat(thrown.getMessage(), equalTo("Id is required in transactions to create.")); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreateEncumbrance(VertxTestContext testContext) { - String transactionId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - - Transaction encumbrance = new Transaction() - .withId(transactionId) - .withCurrency("USD") - .withFromFundId(fundId) - .withTransactionType(ENCUMBRANCE) - .withAmount(5d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(Encumbrance.Status.PENDING) - .withSourcePurchaseOrderId(orderId) - .withInitialAmountEncumbered(5d)); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(encumbrance); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false); - - Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); - doReturn(succeededFuture(createResults(new ArrayList()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify encumbrance creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); - assertNotNull(savedTransaction.getMetadata()); - assertThat(savedTransaction.getAmount(), equalTo(encumbrance.getAmount())); - - // Verify budget update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(5d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testUpdateEncumbrance(VertxTestContext testContext) { - String encumbranceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withCurrency("USD") - .withFromFundId(fundId) - .withTransactionType(ENCUMBRANCE) - .withAmount(5d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(Encumbrance.Status.PENDING) - .withSourcePurchaseOrderId(orderId) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Transaction newEncumbrance = JsonObject.mapFrom(existingEncumbrance).mapTo(Transaction.class); - newEncumbrance.setAmount(10d); - - Batch batch = new Batch(); - batch.getTransactionsToUpdate().add(newEncumbrance); - - setupFundBudgetLedger(fundId, fiscalYearId, 5d, 0d, 0d, false, false); - - Criterion transactionCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify encumbrance update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - - assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedTransaction = (Transaction)(updateEntities.get(0).get(0)); - assertNotNull(savedTransaction.getMetadata().getUpdatedDate()); - assertThat(savedTransaction.getAmount(), equalTo(10d)); - - // Verify budget update - assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(10d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreatePendingPaymentWithLinkedEncumbrance(VertxTestContext testContext) { - String pendingPaymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction pendingPayment = new Transaction() - .withId(pendingPaymentId) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withAwaitingPayment(new AwaitingPayment() - .withEncumbranceId(encumbranceId) - .withReleaseEncumbrance(true)); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withCurrency("USD") - .withFromFundId(fundId) - .withAmount(5d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(UNRELEASED) - .withAmountAwaitingPayment(0d) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(pendingPayment); - - setupFundBudgetLedger(fundId, fiscalYearId, 5d, 0d, 0d, false, false); - - Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); - doReturn(succeededFuture(createResults(List.of()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); - - Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(encumbranceCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify pending payment creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedPendingPayment = (Transaction)(saveEntities.get(0).get(0)); - assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); - assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); - assertThat(savedPendingPayment.getAmount(), equalTo(5d)); - - // Verify encumbrance update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - - assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(0)); - assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); - assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); - assertThat(savedEncumbrance.getAmount(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); - assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(5d)); - - // Verify budget update - assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(0d)); - assertThat(savedBudget.getAwaitingPayment(), equalTo(5d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testUpdatePendingPaymentWithLinkedEncumbrance(VertxTestContext testContext) { - String pendingPaymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction existingPendingPayment = new Transaction() - .withId(pendingPaymentId) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withAwaitingPayment(new AwaitingPayment() - .withEncumbranceId(encumbranceId) - .withReleaseEncumbrance(true)) - .withMetadata(new Metadata()); - - Transaction newPendingPayment = JsonObject.mapFrom(existingPendingPayment).mapTo(Transaction.class); - newPendingPayment.setAmount(10d); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withCurrency("USD") - .withFromFundId(fundId) - .withAmount(0d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(RELEASED) - .withAmountAwaitingPayment(5d) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Batch batch = new Batch(); - batch.getTransactionsToUpdate().add(newPendingPayment); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false); - - Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); - doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); - - Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(encumbranceCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify pending payment update - ArgumentCaptor tableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> entitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(tableNamesCaptor.capture(), entitiesCaptor.capture()); - List tableNames = tableNamesCaptor.getAllValues(); - List> entities = entitiesCaptor.getAllValues(); - assertThat(tableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - - Transaction savedPendingPayment = (Transaction)(entities.get(0).get(0)); - assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); - assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); - assertThat(savedPendingPayment.getAmount(), equalTo(10d)); - - // Verify encumbrance update - Transaction savedEncumbrance = (Transaction)(entities.get(0).get(1)); - assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); - assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); - assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); - assertThat(savedEncumbrance.getAmount(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(10d)); - - // Verify budget update - assertThat(tableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(entities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(0d)); - assertThat(savedBudget.getAwaitingPayment(), equalTo(10d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testCancelPendingPaymentWithLinkedEncumbrance(VertxTestContext testContext) { - String pendingPaymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction existingPendingPayment = new Transaction() - .withId(pendingPaymentId) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withAwaitingPayment(new AwaitingPayment() - .withEncumbranceId(encumbranceId) - .withReleaseEncumbrance(true)) - .withMetadata(new Metadata()); - - Transaction newPendingPayment = JsonObject.mapFrom(existingPendingPayment).mapTo(Transaction.class); - newPendingPayment.setInvoiceCancelled(true); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withCurrency("USD") - .withFromFundId(fundId) - .withAmount(0d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(RELEASED) - .withAmountAwaitingPayment(5d) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Batch batch = new Batch(); - batch.getTransactionsToUpdate().add(newPendingPayment); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false); - - Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); - doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); - - Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(encumbranceCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify pending payment update - ArgumentCaptor tableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> entitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(tableNamesCaptor.capture(), entitiesCaptor.capture()); - List tableNames = tableNamesCaptor.getAllValues(); - List> entities = entitiesCaptor.getAllValues(); - assertThat(tableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - - Transaction savedPendingPayment = (Transaction)(entities.get(0).get(0)); - assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); - assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); - assertThat(savedPendingPayment.getAmount(), equalTo(0d)); - assertThat(savedPendingPayment.getVoidedAmount(), equalTo(5d)); - - // Verify encumbrance update - Transaction savedEncumbrance = (Transaction)(entities.get(0).get(1)); - assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); - assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); - assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); - assertThat(savedEncumbrance.getAmount(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); - - // Verify budget update - assertThat(tableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(entities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(0d)); - assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreatePaymentWithLinkedEncumbrance(VertxTestContext testContext) { - String paymentId = UUID.randomUUID().toString(); - String pendingPaymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction payment = new Transaction() - .withId(paymentId) - .withTransactionType(PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withPaymentEncumbranceId(encumbranceId); - - Transaction existingPendingPayment = new Transaction() - .withId(pendingPaymentId) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withAwaitingPayment(new AwaitingPayment() - .withEncumbranceId(encumbranceId) - .withReleaseEncumbrance(true)); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withCurrency("USD") - .withFromFundId(fundId) - .withAmount(0d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(RELEASED) - .withAmountAwaitingPayment(5d) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(payment); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false); - - Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); - doReturn(succeededFuture(createResults(List.of()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(paymentCriterion.toString()))); - - Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(encumbranceCriterion.toString()))); - - String snippet = "WHERE (jsonb->>'transactionType') = 'Pending payment' AND ( (jsonb->>'sourceInvoiceId') = '" + invoiceId + "') "; - doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(snippet))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(List.of(existingPendingPayment)))) - .when(conn).delete(anyString(), any(Criterion.class)); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify payment creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedPayment = (Transaction)(saveEntities.get(0).get(0)); - assertThat(savedPayment.getTransactionType(), equalTo(PAYMENT)); - assertNotNull(savedPayment.getMetadata().getCreatedDate()); - assertThat(savedPayment.getAmount(), equalTo(5d)); - - // Verify encumbrance update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - - assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(0)); - assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); - assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); - assertThat(savedEncumbrance.getAmount(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountExpended(), equalTo(5d)); - - // Verify budget update - assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(0d)); - assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); - assertThat(savedBudget.getExpenditures(), equalTo(5d)); - - // Verify pending payment deletion - ArgumentCaptor deleteTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor deleteCriterionCaptor = ArgumentCaptor.forClass(Criterion.class); - verify(conn, times(1)).delete(deleteTableNamesCaptor.capture(), deleteCriterionCaptor.capture()); - List deleteTableNames = deleteTableNamesCaptor.getAllValues(); - List deleteCriterions = deleteCriterionCaptor.getAllValues(); - - Criterion pendingPaymentCriterionByIds = createCriterionByIds(List.of(pendingPaymentId)); - assertThat(deleteTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Criterion deleteCriterion = deleteCriterions.get(0); - assertThat(deleteCriterion.toString(), equalTo(pendingPaymentCriterionByIds.toString())); - }); - testContext.completeNow(); - }); - } - - @Test - void testCancelPaymentWithLinkedEncumbrance(VertxTestContext testContext) { - String paymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction existingPayment = new Transaction() - .withId(paymentId) - .withTransactionType(PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withPaymentEncumbranceId(encumbranceId) - .withMetadata(new Metadata()); - - Transaction newPayment = JsonObject.mapFrom(existingPayment).mapTo(Transaction.class); - newPayment.setInvoiceCancelled(true); - - Transaction existingEncumbrance = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withCurrency("USD") - .withFromFundId(fundId) - .withAmount(0d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(RELEASED) - .withAmountAwaitingPayment(0d) - .withAmountExpended(5d) - .withInitialAmountEncumbered(5d)) - .withMetadata(new Metadata()); - - Batch batch = new Batch(); - batch.getTransactionsToUpdate().add(newPayment); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 5d, false, false); - - Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); - doReturn(succeededFuture(createResults(List.of(existingPayment)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(paymentCriterion.toString()))); - - Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(encumbranceCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify payment update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - - assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedPayment = (Transaction)(updateEntities.get(0).get(0)); - assertThat(savedPayment.getTransactionType(), equalTo(PAYMENT)); - assertNotNull(savedPayment.getMetadata().getUpdatedDate()); - assertThat(savedPayment.getAmount(), equalTo(0d)); - assertThat(savedPayment.getVoidedAmount(), equalTo(5d)); - - // Verify encumbrance update - assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(1)); - assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); - assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); - assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); - assertThat(savedEncumbrance.getAmount(), equalTo(5d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); - assertThat(savedEncumbrance.getEncumbrance().getAmountExpended(), equalTo(0d)); - - // Verify budget update - assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); - Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getEncumbered(), equalTo(0d)); - assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); - assertThat(savedBudget.getExpenditures(), equalTo(0d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testDeleteTransaction(VertxTestContext testContext) { - String tenantId = "tenantname"; - String encumbranceId = UUID.randomUUID().toString(); - Transaction transaction = new Transaction() - .withId(encumbranceId) - .withTransactionType(ENCUMBRANCE) - .withEncumbrance(new Encumbrance() - .withStatus(RELEASED)); - - Batch batch = new Batch(); - batch.getIdsOfTransactionsToDelete().add(encumbranceId); - - doReturn(tenantId) - .when(conn).getTenantId(); - - Criterion criterion = createCriterionByIds(List.of(encumbranceId)); - doReturn(succeededFuture(createResults(List.of(transaction)))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(criterion.toString()))); - - CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); - criterionBuilder.withJson("awaitingPayment.encumbranceId", "=", encumbranceId); - doReturn(succeededFuture(createResults(List.of()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(criterionBuilder.build().toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(List.of(transaction)))) - .when(conn).delete(anyString(), any(Criterion.class)); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify transaction deletion - ArgumentCaptor deleteTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor deleteCriterionCaptor = ArgumentCaptor.forClass(Criterion.class); - verify(conn, times(1)).delete(deleteTableNamesCaptor.capture(), deleteCriterionCaptor.capture()); - List deleteTableNames = deleteTableNamesCaptor.getAllValues(); - List deleteCriterions = deleteCriterionCaptor.getAllValues(); - - Criterion expectedCriterion = createCriterionByIds(List.of(encumbranceId)); - assertThat(deleteTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Criterion deleteCriterion = deleteCriterions.get(0); - assertThat(deleteCriterion.toString(), equalTo(expectedCriterion.toString())); - }); - testContext.completeNow(); - }); - } - - @Test - void testPatchTransaction(VertxTestContext testContext) { - String transactionId = UUID.randomUUID().toString(); - TransactionPatch transactionPatch = new TransactionPatch() - .withId(transactionId) - .withAdditionalProperty("encumbrance", new LinkedHashMap() - .put("orderStatus", Encumbrance.OrderStatus.CLOSED.value())); - Batch batch = new Batch(); - batch.getTransactionPatches().add(transactionPatch); - testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) - .onFailure(thrown -> { - testContext.verify(() -> { - assertThat(thrown, instanceOf(HttpException.class)); - assertThat(((HttpException) thrown).getCode(), equalTo(500)); - assertThat(thrown.getMessage(), equalTo("transactionPatches: not implemented")); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreateInitialAllocation(VertxTestContext testContext) { - String transactionId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - - Transaction allocation = new Transaction() - .withId(transactionId) - .withCurrency("USD") - .withToFundId(fundId) - .withTransactionType(ALLOCATION) - .withAmount(5d) - .withFiscalYearId(fiscalYearId); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(allocation); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false); - - Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); - doReturn(succeededFuture(createResults(new ArrayList()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify allocation creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); - assertNotNull(savedTransaction.getMetadata()); - assertThat(savedTransaction.getAmount(), equalTo(allocation.getAmount())); - - // Verify budget update - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); - assertNotNull(savedBudget.getMetadata().getUpdatedDate()); - assertThat(savedBudget.getInitialAllocation(), equalTo(10d)); - assertThat(savedBudget.getAllocationTo(), equalTo(5d)); - - }); - testContext.completeNow(); - }); - } - - @Test - void testCreateAllocation(VertxTestContext testContext) { - String fundId1 = UUID.randomUUID().toString(); - String fundId2 = UUID.randomUUID().toString(); - String budgetId1 = UUID.randomUUID().toString(); - String budgetId2 = UUID.randomUUID().toString(); - String transactionId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - - Transaction allocation = new Transaction() - .withId(transactionId) - .withCurrency("USD") - .withFromFundId(fundId1) - .withToFundId(fundId2) - .withTransactionType(ALLOCATION) - .withAmount(5d) - .withFiscalYearId(fiscalYearId); - - setup2Funds2Budgets1Ledger(fundId1, fundId2, budgetId1, budgetId2, fiscalYearId); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(allocation); - - Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); - doReturn(succeededFuture(createResults(new ArrayList()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify allocation creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); - assertNotNull(savedTransaction.getMetadata()); - assertThat(savedTransaction.getAmount(), equalTo(allocation.getAmount())); - - // Verify budget updates - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - Budget savedBudget1 = (Budget)(updateEntities.get(0).get(0)); - assertThat(savedBudget1.getId(), equalTo(budgetId1)); - assertNotNull(savedBudget1.getMetadata().getUpdatedDate()); - assertThat(savedBudget1.getAllocationFrom(), equalTo(5d)); - Budget savedBudget2 = (Budget)(updateEntities.get(0).get(1)); - assertThat(savedBudget2.getId(), equalTo(budgetId2)); - assertNotNull(savedBudget2.getMetadata().getUpdatedDate()); - assertThat(savedBudget2.getAllocationTo(), equalTo(5d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreateTransfer(VertxTestContext testContext) { - String fundId1 = UUID.randomUUID().toString(); - String fundId2 = UUID.randomUUID().toString(); - String budgetId1 = UUID.randomUUID().toString(); - String budgetId2 = UUID.randomUUID().toString(); - String transactionId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - - Transaction transfer = new Transaction() - .withId(transactionId) - .withCurrency("USD") - .withFromFundId(fundId1) - .withToFundId(fundId2) - .withTransactionType(TRANSFER) - .withAmount(5d) - .withFiscalYearId(fiscalYearId); - - setup2Funds2Budgets1Ledger(fundId1, fundId2, budgetId1, budgetId2, fiscalYearId); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(transfer); - - Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); - doReturn(succeededFuture(createResults(new ArrayList()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).saveBatch(anyString(), anyList()); - - doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) - .when(conn).updateBatch(anyString(), anyList()); - - testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - // Verify allocation creation - ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> saveEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); - List saveTableNames = saveTableNamesCaptor.getAllValues(); - List> saveEntities = saveEntitiesCaptor.getAllValues(); - assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); - Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); - assertNotNull(savedTransaction.getMetadata()); - assertThat(savedTransaction.getAmount(), equalTo(transfer.getAmount())); - - // Verify budget updates - ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor> updateEntitiesCaptor = ArgumentCaptor.forClass(List.class); - verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); - List updateTableNames = updateTableNamesCaptor.getAllValues(); - assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); - List> updateEntities = updateEntitiesCaptor.getAllValues(); - Budget savedBudget1 = (Budget)(updateEntities.get(0).get(0)); - assertThat(savedBudget1.getId(), equalTo(budgetId1)); - assertNotNull(savedBudget1.getMetadata().getUpdatedDate()); - assertThat(savedBudget1.getNetTransfers(), equalTo(-5d)); - Budget savedBudget2 = (Budget)(updateEntities.get(0).get(1)); - assertThat(savedBudget2.getId(), equalTo(budgetId2)); - assertNotNull(savedBudget2.getMetadata().getUpdatedDate()); - assertThat(savedBudget2.getNetTransfers(), equalTo(5d)); - }); - testContext.completeNow(); - }); - } - - @Test - void testExpenditureRestrictionsWhenCreatingTwoPendingPayments(VertxTestContext testContext) { - String pendingPaymentId1 = UUID.randomUUID().toString(); - String pendingPaymentId2 = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction pendingPayment1 = new Transaction() - .withId(pendingPaymentId1) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(7d) - .withCurrency(currency) - .withInvoiceCancelled(false); - - Transaction pendingPayment2 = new Transaction() - .withId(pendingPaymentId2) - .withTransactionType(PENDING_PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(8d) - .withCurrency(currency) - .withInvoiceCancelled(false); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().addAll(List.of(pendingPayment1, pendingPayment2)); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, true, false); - - Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId1, pendingPaymentId2)); - doReturn(succeededFuture(createResults(List.of()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); - - testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) - .onFailure(thrown -> { - HttpException exception = (HttpException)thrown; - testContext.verify(() -> { - assertThat(exception.getCode(), equalTo(422)); - assertThat(exception.getErrors().getErrors().get(0).getMessage(), - equalTo(BUDGET_RESTRICTED_EXPENDITURES_ERROR.getDescription())); - }); - testContext.completeNow(); - }); - } - - @Test - void testEncumbranceRestrictionsWhenCreatingTwoEncumbrances(VertxTestContext testContext) { - String transactionId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - - Transaction encumbrance = new Transaction() - .withId(transactionId) - .withCurrency("USD") - .withFromFundId(fundId) - .withTransactionType(ENCUMBRANCE) - .withAmount(10d) - .withFiscalYearId(fiscalYearId) - .withSource(PO_LINE) - .withEncumbrance(new Encumbrance() - .withStatus(Encumbrance.Status.PENDING) - .withSourcePurchaseOrderId(orderId) - .withInitialAmountEncumbered(10d)); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(encumbrance); - - setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, true); - - Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); - doReturn(succeededFuture(createResults(new ArrayList()))) - .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( - crit -> crit.toString().equals(transactionCriterion.toString()))); - - testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) - .onFailure(thrown -> { - HttpException exception = (HttpException)thrown; - testContext.verify(() -> { - assertThat(exception.getCode(), equalTo(422)); - assertThat(exception.getErrors().getErrors().get(0).getMessage(), - equalTo(BUDGET_RESTRICTED_ENCUMBRANCE_ERROR.getDescription())); - }); - testContext.completeNow(); - }); - } - - @Test - void testCreatePaymentWithNegativeAmount(VertxTestContext testContext) { - String paymentId = UUID.randomUUID().toString(); - String encumbranceId = UUID.randomUUID().toString(); - String invoiceId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String fiscalYearId = UUID.randomUUID().toString(); - String currency = "USD"; - - Transaction payment = new Transaction() - .withId(paymentId) - .withTransactionType(PAYMENT) - .withSourceInvoiceId(invoiceId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withAmount(-5d) - .withCurrency(currency) - .withInvoiceCancelled(false) - .withPaymentEncumbranceId(encumbranceId); - - Batch batch = new Batch(); - batch.getTransactionsToCreate().add(payment); - - testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) - .onComplete(event -> { - testContext.verify(() -> { - assertThat(event.cause(), instanceOf(HttpException.class)); - assertThat(((HttpException) event.cause()).getCode(), equalTo(422)); - }); - testContext.completeNow(); - }); - } - - private void setupFundBudgetLedger(String fundId, String fiscalYearId, double budgetEncumbered, - double budgetAwaitingPayment, double budgetExpenditures, boolean restrictExpenditures, boolean restrictEncumbrance) { - String tenantId = "tenantname"; - String budgetId = UUID.randomUUID().toString(); - String ledgerId = UUID.randomUUID().toString(); - - Fund fund = new Fund() - .withId(fundId) - .withLedgerId(ledgerId); - - Budget budget = new Budget() - .withId(budgetId) - .withFiscalYearId(fiscalYearId) - .withFundId(fundId) - .withBudgetStatus(ACTIVE) - .withInitialAllocation(10d) - .withEncumbered(budgetEncumbered) - .withAwaitingPayment(budgetAwaitingPayment) - .withExpenditures(budgetExpenditures) - .withAllowableExpenditure(90d) - .withAllowableEncumbrance(90d) - .withMetadata(new Metadata()); - - Ledger ledger = new Ledger() - .withId(ledgerId) - .withRestrictExpenditures(restrictExpenditures) - .withRestrictEncumbrance(restrictEncumbrance); - - doReturn(tenantId) - .when(conn).getTenantId(); - - Criterion fundCriterion = createCriterionByIds(List.of(fundId)); - doReturn(succeededFuture(createResults(List.of(fund)))) - .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( - crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); - - String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '" + fundId + "')) FOR UPDATE"; - doReturn(succeededFuture(createRowSet(List.of(budget)))) - .when(conn).execute(eq(sql), any(Tuple.class)); - - Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); - doReturn(succeededFuture(createResults(List.of(ledger)))) - .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( - crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); - } - - private void setup2Funds2Budgets1Ledger(String fundId1, String fundId2, String budgetId1, String budgetId2, - String fiscalYearId) { - String tenantId = "tenantname"; - String ledgerId = UUID.randomUUID().toString(); - Ledger ledger = new Ledger() - .withId(ledgerId); - - Fund fund1 = new Fund() - .withId(fundId1) - .withLedgerId(ledgerId); - - Fund fund2 = new Fund() - .withId(fundId2) - .withLedgerId(ledgerId); - - Budget budget1 = new Budget() - .withId(budgetId1) - .withFiscalYearId(fiscalYearId) - .withFundId(fundId1) - .withBudgetStatus(ACTIVE) - .withInitialAllocation(5d) - .withNetTransfers(0d) - .withMetadata(new Metadata()); - - Budget budget2 = new Budget() - .withId(budgetId2) - .withFiscalYearId(fiscalYearId) - .withFundId(fundId2) - .withBudgetStatus(ACTIVE) - .withInitialAllocation(10d) - .withNetTransfers(0d) - .withMetadata(new Metadata()); - - doReturn(tenantId) - .when(conn).getTenantId(); - - Criterion fundCriterion = createCriterionByIds(List.of(fundId1, fundId2)); - doReturn(succeededFuture(createResults(List.of(fund1, fund2)))) - .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( - crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); - - String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '%s' OR fundId = '%s')) FOR UPDATE"; - doReturn(succeededFuture(createRowSet(List.of(budget1, budget2)))) - .when(conn).execute(argThat(s -> - s.equals(String.format(sql, fundId1, fundId2)) || s.equals(String.format(sql, fundId2, fundId1))), - any(Tuple.class)); - - Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); - doReturn(succeededFuture(createResults(List.of(ledger)))) - .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( - crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); - } - - private Criterion createCriterionByIds(List ids) { - CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); - ids.forEach(id -> criterionBuilder.with("id", id)); - return criterionBuilder.build(); - } - - private Results createResults(List list) { - Results results = new Results<>(); - results.setResults(list); - return results; - } - - private RowSet createRowSet(List list) { - RowDesc rowDesc = new LocalRowDesc(List.of("foo")); - List rows = list.stream().map(item -> { - Row row = new RowImpl(rowDesc); - row.addJsonObject(JsonObject.mapFrom(item)); - return row; - }).toList(); - return new LocalRowSet(list.size()) - .withRows(rows); - } - -} diff --git a/src/test/java/org/folio/service/transactions/BatchTransactionServiceTestBase.java b/src/test/java/org/folio/service/transactions/BatchTransactionServiceTestBase.java new file mode 100644 index 00000000..2cee6cb7 --- /dev/null +++ b/src/test/java/org/folio/service/transactions/BatchTransactionServiceTestBase.java @@ -0,0 +1,273 @@ +package org.folio.service.transactions; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.pgclient.impl.RowImpl; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.impl.RowDesc; +import org.folio.dao.budget.BudgetDAO; +import org.folio.dao.budget.BudgetPostgresDAO; +import org.folio.dao.fund.FundDAO; +import org.folio.dao.fund.FundPostgresDAO; +import org.folio.dao.ledger.LedgerDAO; +import org.folio.dao.ledger.LedgerPostgresDAO; +import org.folio.dao.transactions.BatchTransactionDAO; +import org.folio.dao.transactions.BatchTransactionPostgresDAO; +import org.folio.rest.core.model.RequestContext; +import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Fund; +import org.folio.rest.jaxrs.model.Ledger; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.persist.Criteria.Criterion; +import org.folio.rest.persist.CriterionBuilder; +import org.folio.rest.persist.DBClient; +import org.folio.rest.persist.DBClientFactory; +import org.folio.rest.persist.DBConn; +import org.folio.rest.persist.helpers.LocalRowDesc; +import org.folio.rest.persist.helpers.LocalRowSet; +import org.folio.rest.persist.interfaces.Results; +import org.folio.service.budget.BudgetService; +import org.folio.service.fund.FundService; +import org.folio.service.fund.StorageFundService; +import org.folio.service.ledger.LedgerService; +import org.folio.service.ledger.StorageLedgerService; +import org.folio.service.transactions.batch.BatchAllocationService; +import org.folio.service.transactions.batch.BatchEncumbranceService; +import org.folio.service.transactions.batch.BatchPaymentCreditService; +import org.folio.service.transactions.batch.BatchPendingPaymentService; +import org.folio.service.transactions.batch.BatchTransactionService; +import org.folio.service.transactions.batch.BatchTransactionServiceInterface; +import org.folio.service.transactions.batch.BatchTransferService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.dao.ledger.LedgerPostgresDAO.LEDGER_TABLE; +import static org.folio.rest.impl.FundAPI.FUND_TABLE; +import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.ACTIVE; +import static org.folio.rest.jaxrs.model.Budget.BudgetStatus.INACTIVE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(VertxExtension.class) +public abstract class BatchTransactionServiceTestBase { + private AutoCloseable mockitoMocks; + protected BatchTransactionService batchTransactionService; + + @Mock + private DBClientFactory dbClientFactory; + @Mock + protected RequestContext requestContext; + @Mock + private DBClient dbClient; + @Mock + protected DBConn conn; + @Captor + protected ArgumentCaptor> saveEntitiesCaptor; + @Captor + protected ArgumentCaptor> updateEntitiesCaptor; + + + @BeforeEach + public void init() { + mockitoMocks = MockitoAnnotations.openMocks(this); + FundDAO fundDAO = new FundPostgresDAO(); + FundService fundService = new StorageFundService(fundDAO); + BudgetDAO budgetDAO = new BudgetPostgresDAO(); + BudgetService budgetService = new BudgetService(budgetDAO); + LedgerDAO ledgerDAO = new LedgerPostgresDAO(); + LedgerService ledgerService = new StorageLedgerService(ledgerDAO, fundService); + Set batchTransactionStrategies = new HashSet<>(); + batchTransactionStrategies.add(new BatchEncumbranceService()); + batchTransactionStrategies.add(new BatchPendingPaymentService()); + batchTransactionStrategies.add(new BatchPaymentCreditService()); + batchTransactionStrategies.add(new BatchAllocationService()); + batchTransactionStrategies.add(new BatchTransferService()); + BatchTransactionDAO transactionDAO = new BatchTransactionPostgresDAO(); + batchTransactionService = new BatchTransactionService(dbClientFactory, transactionDAO, fundService, budgetService, + ledgerService, batchTransactionStrategies); + doReturn(dbClient) + .when(dbClientFactory).getDbClient(requestContext); + doAnswer(invocation -> { + Function> function = invocation.getArgument(0); + return function.apply(conn); + }).when(dbClient).withTrans(any()); + } + + @AfterEach + public void afterEach() throws Exception { + mockitoMocks.close(); + } + + protected void setupFundBudgetLedger(String fundId, String fiscalYearId, double budgetEncumbered, + double budgetAwaitingPayment, double budgetExpenditures, boolean restrictExpenditures, + boolean restrictEncumbrance, boolean inactiveBudget) { + String tenantId = "tenantname"; + String budgetId = UUID.randomUUID().toString(); + String ledgerId = UUID.randomUUID().toString(); + + Fund fund = new Fund() + .withId(fundId) + .withCode("FUND1") + .withLedgerId(ledgerId); + + Budget budget = new Budget() + .withId(budgetId) + .withFiscalYearId(fiscalYearId) + .withFundId(fundId) + .withBudgetStatus(inactiveBudget ? INACTIVE : ACTIVE) + .withInitialAllocation(10d) + .withEncumbered(budgetEncumbered) + .withAwaitingPayment(budgetAwaitingPayment) + .withExpenditures(budgetExpenditures) + .withAllowableExpenditure(90d) + .withAllowableEncumbrance(90d) + .withMetadata(new Metadata()); + + Ledger ledger = new Ledger() + .withId(ledgerId) + .withRestrictExpenditures(restrictExpenditures) + .withRestrictEncumbrance(restrictEncumbrance); + + doReturn(tenantId) + .when(conn).getTenantId(); + + Criterion fundCriterion = createCriterionByIds(List.of(fundId)); + doReturn(succeededFuture(createResults(List.of(fund)))) + .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( + crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); + + String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '" + fundId + "')) FOR UPDATE"; + doReturn(succeededFuture(createRowSet(List.of(budget)))) + .when(conn).execute(eq(sql), any(Tuple.class)); + + Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); + doReturn(succeededFuture(createResults(List.of(ledger)))) + .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( + crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); + } + + protected void setup2Funds2Budgets1Ledger(String fundId1, String fundId2, String budgetId1, String budgetId2, + String fiscalYearId) { + String tenantId = "tenantname"; + String ledgerId = UUID.randomUUID().toString(); + Ledger ledger = new Ledger() + .withId(ledgerId); + + Fund fund1 = new Fund() + .withId(fundId1) + .withLedgerId(ledgerId); + + Fund fund2 = new Fund() + .withId(fundId2) + .withLedgerId(ledgerId); + + Budget budget1 = new Budget() + .withId(budgetId1) + .withFiscalYearId(fiscalYearId) + .withFundId(fundId1) + .withBudgetStatus(ACTIVE) + .withInitialAllocation(5d) + .withMetadata(new Metadata()); + + Budget budget2 = new Budget() + .withId(budgetId2) + .withFiscalYearId(fiscalYearId) + .withFundId(fundId2) + .withBudgetStatus(ACTIVE) + .withInitialAllocation(10d) + .withMetadata(new Metadata()); + + doReturn(tenantId) + .when(conn).getTenantId(); + + Criterion fundCriterion = createCriterionByIds(List.of(fundId1, fundId2)); + doReturn(succeededFuture(createResults(List.of(fund1, fund2)))) + .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( + crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); + + String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '%s' OR fundId = '%s')) FOR UPDATE"; + doReturn(succeededFuture(createRowSet(List.of(budget1, budget2)))) + .when(conn).execute(argThat(s -> + s.equals(String.format(sql, fundId1, fundId2)) || s.equals(String.format(sql, fundId2, fundId1))), + any(Tuple.class)); + + Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); + doReturn(succeededFuture(createResults(List.of(ledger)))) + .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( + crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); + } + + protected void setupFundWithMissingBudget(String fundId, String fiscalYearId) { + String tenantId = "tenantname"; + String ledgerId = UUID.randomUUID().toString(); + + Fund fund = new Fund() + .withId(fundId) + .withLedgerId(ledgerId); + + Ledger ledger = new Ledger() + .withId(ledgerId) + .withRestrictExpenditures(false) + .withRestrictEncumbrance(false); + + doReturn(tenantId) + .when(conn).getTenantId(); + + Criterion fundCriterion = createCriterionByIds(List.of(fundId)); + doReturn(succeededFuture(createResults(List.of(fund)))) + .when(conn).get(eq(FUND_TABLE), eq(Fund.class), argThat( + crit -> crit.toString().equals(fundCriterion.toString())), eq(false)); + + String sql = "SELECT jsonb FROM " + tenantId + "_mod_finance_storage.budget WHERE (fiscalYearId = '" + fiscalYearId + "' AND (fundId = '" + fundId + "')) FOR UPDATE"; + doReturn(succeededFuture(createRowSet(Collections.emptyList()))) + .when(conn).execute(eq(sql), any(Tuple.class)); + + Criterion ledgerCriterion = createCriterionByIds(List.of(ledgerId)); + doReturn(succeededFuture(createResults(List.of(ledger)))) + .when(conn).get(eq(LEDGER_TABLE), eq(Ledger.class), argThat( + crit -> crit.toString().equals(ledgerCriterion.toString())), eq(false)); + } + + protected Criterion createCriterionByIds(List ids) { + CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); + ids.forEach(id -> criterionBuilder.with("id", id)); + return criterionBuilder.build(); + } + + protected Results createResults(List list) { + Results results = new Results<>(); + results.setResults(list); + return results; + } + + protected RowSet createRowSet(List list) { + RowDesc rowDesc = new LocalRowDesc(List.of("foo")); + List rows = list.stream().map(item -> { + Row row = new RowImpl(rowDesc); + row.addJsonObject(JsonObject.mapFrom(item)); + return row; + }).toList(); + return new LocalRowSet(list.size()) + .withRows(rows); + } + +} diff --git a/src/test/java/org/folio/service/transactions/EncumbranceServiceTest.java b/src/test/java/org/folio/service/transactions/EncumbranceServiceTest.java deleted file mode 100644 index 9b65e098..00000000 --- a/src/test/java/org/folio/service/transactions/EncumbranceServiceTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.folio.service.transactions; - -import static io.vertx.core.Future.succeededFuture; - -import io.vertx.core.Future; -import org.folio.rest.persist.DBConn; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import java.util.Collection; -import java.util.List; -import java.util.UUID; -import java.util.function.Function; - -import org.folio.dao.transactions.TemporaryTransactionDAO; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBClient; -import org.folio.rest.persist.Criteria.Criterion; -import org.folio.service.budget.BudgetService; -import org.folio.service.summary.TransactionSummaryService; -import org.folio.service.transactions.restriction.TransactionRestrictionService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import io.vertx.sqlclient.Tuple; - -@ExtendWith(VertxExtension.class) -public class EncumbranceServiceTest { - private EncumbranceService encumbranceService; - - private AllOrNothingTransactionService mockAllOrNothingEncumbranceService; - - private AutoCloseable mockitoMocks; - @Mock - private TransactionDAO transactionDAO; - @Mock - private TemporaryTransactionDAO temporaryTransactionDAO; - @Mock - private TransactionSummaryService transactionSummaryService; - @Mock - private TransactionRestrictionService transactionRestrictionService; - @Mock - private BudgetService budgetService; - @Mock - private DBClient dbClient; - @Mock - private DBConn conn; - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - - mockAllOrNothingEncumbranceService = spy(new AllOrNothingTransactionService(transactionDAO, temporaryTransactionDAO, - transactionSummaryService, transactionRestrictionService)); - - encumbranceService = new EncumbranceService(mockAllOrNothingEncumbranceService, transactionDAO, budgetService); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void shouldUpdateOrdersStatusToClosedIfEncumbranceAlreadyReleased(VertxTestContext testContext) { - String transactionId = UUID.randomUUID().toString(); - String fundId = UUID.randomUUID().toString(); - String orderId = UUID.randomUUID().toString(); - - Transaction tmpTransaction = new Transaction().withId(transactionId).withAmount(0.0).withCurrency("USD") - .withFromFundId(fundId).withTransactionType(Transaction.TransactionType.ENCUMBRANCE) - .withEncumbrance(new Encumbrance().withStatus(Encumbrance.Status.RELEASED).withSourcePurchaseOrderId(orderId) - .withInitialAmountEncumbered(10d).withAmountExpended(10d).withOrderStatus(Encumbrance.OrderStatus.OPEN)); - - Transaction incomingTransaction = new Transaction().withId(transactionId).withAmount(0.0).withCurrency("USD") - .withFromFundId(fundId).withTransactionType(Transaction.TransactionType.ENCUMBRANCE) - .withEncumbrance(new Encumbrance().withStatus(Encumbrance.Status.RELEASED).withSourcePurchaseOrderId(orderId) - .withInitialAmountEncumbered(10d).withAmountExpended(10d).withOrderStatus(Encumbrance.OrderStatus.CLOSED)); - - JsonObject trSummary = new JsonObject().put("id", orderId).put("numTransactions", 1); - - Budget budget = new Budget().withId(UUID.randomUUID().toString()).withFundId(fundId); - - doReturn("testTenant") - .when(conn).getTenantId(); - doReturn(succeededFuture(List.of(budget))) - .when(budgetService).getBudgets(any(String.class), any(Tuple.class), eq(conn)); - doReturn(succeededFuture()) - .when(budgetService).updateBatchBudgets(anyList(), eq(conn)); - - doReturn(succeededFuture(List.of(tmpTransaction))) - .when(transactionDAO).getTransactions(any(Criterion.class), eq(conn)); - doReturn(succeededFuture(null)) - .when(transactionDAO).updatePermanentTransactions(anyList(), eq(conn)); - - doReturn(succeededFuture(trSummary)) - .when(transactionSummaryService).getAndCheckTransactionSummary(eq(incomingTransaction), eq(conn)); - doReturn(orderId) - .when(transactionSummaryService).getSummaryId(eq(incomingTransaction)); - doReturn(succeededFuture(trSummary)) - .when(transactionSummaryService).getTransactionSummaryWithLocking(eq(orderId), eq(conn)); - doReturn(succeededFuture(incomingTransaction)) - .when(temporaryTransactionDAO).createTempTransaction(eq(incomingTransaction), eq(orderId), eq("testTenant"), eq(conn)); - doReturn(succeededFuture(List.of(incomingTransaction))) - .when(temporaryTransactionDAO).getTempTransactionsBySummaryId(eq(orderId), eq(conn)); - doAnswer(invocation -> { - Function> function = invocation.getArgument(0); - return function.apply(conn); - }).when(dbClient).withTrans(any()); - doReturn(1) - .when(transactionSummaryService).getNumTransactions(eq(trSummary)); - doReturn(succeededFuture(null)) - .when(transactionSummaryService).setTransactionsSummariesProcessed(eq(trSummary), eq(conn)); - doReturn(succeededFuture(1)) - .when(temporaryTransactionDAO).deleteTempTransactions(eq(orderId), eq(conn)); - - encumbranceService.updateTransaction(incomingTransaction, conn) - .onComplete(event -> { - testContext.verify(() -> { - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionDAO).updatePermanentTransactions(argumentCaptor.capture(), eq(conn)); - List transactions = argumentCaptor.getValue(); - assertEquals(Encumbrance.OrderStatus.CLOSED, transactions.get(0).getEncumbrance().getOrderStatus()); - assertEquals(Encumbrance.Status.RELEASED, transactions.get(0).getEncumbrance().getStatus()); - }); - testContext.completeNow(); - }); - } -} diff --git a/src/test/java/org/folio/service/transactions/EncumbranceTest.java b/src/test/java/org/folio/service/transactions/EncumbranceTest.java new file mode 100644 index 00000000..ff4a8e38 --- /dev/null +++ b/src/test/java/org/folio/service/transactions/EncumbranceTest.java @@ -0,0 +1,430 @@ +package org.folio.service.transactions; + +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxTestContext; +import org.folio.rest.exception.HttpException; +import org.folio.rest.jaxrs.model.Batch; +import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Encumbrance; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.jaxrs.model.Transaction; +import org.folio.rest.jaxrs.model.TransactionPatch; +import org.folio.rest.persist.Criteria.Criterion; +import org.folio.rest.persist.CriterionBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; +import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; +import static org.folio.rest.jaxrs.model.Encumbrance.Status.RELEASED; +import static org.folio.rest.jaxrs.model.Encumbrance.Status.UNRELEASED; +import static org.folio.rest.jaxrs.model.Transaction.Source.PO_LINE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ENCUMBRANCE; +import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_ENCUMBRANCE_ERROR; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class EncumbranceTest extends BatchTransactionServiceTestBase { + + @Test + void testCreateEncumbrance(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction encumbrance = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(5d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(Encumbrance.Status.PENDING) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(5d)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(encumbrance); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify encumbrance creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(saveEntities.get(0).get(0)); + assertNotNull(savedTransaction.getMetadata()); + assertThat(savedTransaction.getAmount(), equalTo(encumbrance.getAmount())); + + // Verify budget update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(5d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testUpdateEncumbrance(VertxTestContext testContext) { + String encumbranceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(5d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(Encumbrance.Status.PENDING) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Transaction newEncumbrance = JsonObject.mapFrom(existingEncumbrance).mapTo(Transaction.class); + newEncumbrance.setAmount(10d); + + Batch batch = new Batch(); + batch.getTransactionsToUpdate().add(newEncumbrance); + + setupFundBudgetLedger(fundId, fiscalYearId, 5d, 0d, 0d, false, false, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify encumbrance update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(updateEntities.get(0).get(0)); + assertNotNull(savedTransaction.getMetadata().getUpdatedDate()); + assertThat(savedTransaction.getAmount(), equalTo(10d)); + + // Verify budget update + assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(10d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testDeleteTransaction(VertxTestContext testContext) { + String tenantId = "tenantname"; + String encumbranceId = UUID.randomUUID().toString(); + Transaction transaction = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED)); + + Batch batch = new Batch(); + batch.getIdsOfTransactionsToDelete().add(encumbranceId); + + doReturn(tenantId) + .when(conn).getTenantId(); + + Criterion criterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(transaction)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(criterion.toString()))); + + CriterionBuilder criterionBuilder = new CriterionBuilder("OR"); + criterionBuilder.withJson("awaitingPayment.encumbranceId", "=", encumbranceId); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(criterionBuilder.build().toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(List.of(transaction)))) + .when(conn).delete(anyString(), any(Criterion.class)); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify transaction deletion + ArgumentCaptor deleteTableNamesCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor deleteCriterionCaptor = ArgumentCaptor.forClass(Criterion.class); + verify(conn, times(1)).delete(deleteTableNamesCaptor.capture(), deleteCriterionCaptor.capture()); + List deleteTableNames = deleteTableNamesCaptor.getAllValues(); + List deleteCriterions = deleteCriterionCaptor.getAllValues(); + + Criterion expectedCriterion = createCriterionByIds(List.of(encumbranceId)); + assertThat(deleteTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Criterion deleteCriterion = deleteCriterions.get(0); + assertThat(deleteCriterion.toString(), equalTo(expectedCriterion.toString())); + }); + testContext.completeNow(); + }); + } + + @Test + void testPatchTransaction(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + TransactionPatch transactionPatch = new TransactionPatch() + .withId(transactionId) + .withAdditionalProperty("encumbrance", new LinkedHashMap() + .put("orderStatus", Encumbrance.OrderStatus.CLOSED.value())); + Batch batch = new Batch(); + batch.getTransactionPatches().add(transactionPatch); + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onFailure(thrown -> { + testContext.verify(() -> { + assertThat(thrown, instanceOf(HttpException.class)); + assertThat(((HttpException) thrown).getCode(), equalTo(500)); + assertThat(thrown.getMessage(), equalTo("transactionPatches: not implemented")); + }); + testContext.completeNow(); + }); + } + + @Test + void testEncumbranceRestrictionsWhenCreatingTwoEncumbrances(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction encumbrance = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(10d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(Encumbrance.Status.PENDING) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(10d)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(encumbrance); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, true, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onFailure(thrown -> { + HttpException exception = (HttpException)thrown; + testContext.verify(() -> { + assertThat(exception.getCode(), equalTo(422)); + assertThat(exception.getErrors().getErrors().get(0).getMessage(), + equalTo(BUDGET_RESTRICTED_ENCUMBRANCE_ERROR.getDescription())); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateEncumbranceWithInactiveBudget(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction encumbrance = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(5d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(Encumbrance.Status.PENDING) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(5d)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(encumbrance); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, false, false, true); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + HttpException exception = (HttpException)event.cause(); + testContext.verify(() -> { + assertEquals(exception.getCode(), 400); + assertEquals(exception.getErrors().getErrors().get(0).getCode(), "budgetIsNotActiveOrPlanned"); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreateEncumbranceWithMissingBudget(VertxTestContext testContext) { + String transactionId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction encumbrance = new Transaction() + .withId(transactionId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(5d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(Encumbrance.Status.PENDING) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(5d)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(encumbrance); + + setupFundWithMissingBudget(fundId, fiscalYearId); + + Criterion transactionCriterion = createCriterionByIds(List.of(transactionId)); + doReturn(succeededFuture(createResults(new ArrayList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + HttpException exception = (org.folio.rest.exception.HttpException)event.cause(); + testContext.verify(() -> { + assertEquals(exception.getCode(), 500); + assertThat(exception.getMessage(), startsWith("Could not find some budgets in the database")); + }); + testContext.completeNow(); + }); + } + + @Test + void testUnreleaseEncumbrance(VertxTestContext testContext) { + String encumbranceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String orderId = UUID.randomUUID().toString(); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withCurrency("USD") + .withFromFundId(fundId) + .withTransactionType(ENCUMBRANCE) + .withAmount(0d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED) + .withSourcePurchaseOrderId(orderId) + .withInitialAmountEncumbered(8d) + .withAmountAwaitingPayment(5d) + .withAmountExpended(1d)) + .withMetadata(new Metadata()); + + Transaction newEncumbrance = JsonObject.mapFrom(existingEncumbrance).mapTo(Transaction.class); + newEncumbrance.getEncumbrance().setStatus(UNRELEASED); + + Batch batch = new Batch(); + batch.getTransactionsToUpdate().add(newEncumbrance); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 1d, false, false, false); + + Criterion transactionCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(transactionCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify encumbrance update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedTransaction = (Transaction)(updateEntities.get(0).get(0)); + assertThat(savedTransaction.getAmount(), equalTo(2d)); + + // Verify budget update + assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(2d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(5d)); + assertThat(savedBudget.getExpenditures(), equalTo(1d)); + }); + testContext.completeNow(); + }); + } + +} diff --git a/src/test/java/org/folio/service/transactions/PaymentCreditServiceTest.java b/src/test/java/org/folio/service/transactions/PaymentCreditServiceTest.java deleted file mode 100644 index c3fc248a..00000000 --- a/src/test/java/org/folio/service/transactions/PaymentCreditServiceTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.folio.service.transactions; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.service.transactions.cancel.CancelTransactionService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.List; - -import static java.util.UUID.randomUUID; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class PaymentCreditServiceTest { - - private AutoCloseable mockitoMocks; - @InjectMocks - private PaymentCreditService paymentCreditService; - @Mock - private CancelTransactionService cancelTransactionService; - @Mock - private TransactionDAO transactionsDAO; - @Mock - private DBConn conn; - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void testCancelTransactions() { - Transaction newPayment = new Transaction() - .withId(randomUUID().toString()) - .withSourceInvoiceId(randomUUID().toString()) - .withFromFundId(randomUUID().toString()) - .withFiscalYearId(randomUUID().toString()) - .withCurrency("USD") - .withTransactionType(Transaction.TransactionType.PAYMENT) - .withAmount(10d) - .withInvoiceCancelled(true); - Transaction newCredit = new Transaction() - .withId(randomUUID().toString()) - .withSourceInvoiceId(randomUUID().toString()) - .withFromFundId(randomUUID().toString()) - .withFiscalYearId(randomUUID().toString()) - .withCurrency("USD") - .withTransactionType(Transaction.TransactionType.CREDIT) - .withAmount(5d) - .withInvoiceCancelled(true); - Transaction newPaymentToIgnore = JsonObject.mapFrom(newPayment).mapTo(Transaction.class) - .withId(randomUUID().toString()); - List newTransactions = List.of(newPayment, newCredit, newPaymentToIgnore); - Transaction existingPayment = JsonObject.mapFrom(newPayment).mapTo(Transaction.class) - .withInvoiceCancelled(false); - Transaction existingCredit = JsonObject.mapFrom(newCredit).mapTo(Transaction.class) - .withInvoiceCancelled(false); - List existingTransactions = List.of(existingPayment, existingCredit, newPaymentToIgnore); - - when(transactionsDAO.getTransactions(anyList(), any())) - .thenReturn(Future.succeededFuture(existingTransactions)); - when(cancelTransactionService.cancelTransactions(anyList(), any())) - .then(args -> Future.succeededFuture(args.getArgument(0))); - - PaymentCreditService spyService = Mockito.spy(paymentCreditService); - - spyService.updateTransactions(newTransactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - verify(transactionsDAO, times(1)).getTransactions(anyList(), eq(conn)); - verify(cancelTransactionService, times(1)).cancelTransactions(argThat(list -> list.size() == 2), eq(conn)); - } - -} diff --git a/src/test/java/org/folio/service/transactions/PaymentCreditTest.java b/src/test/java/org/folio/service/transactions/PaymentCreditTest.java new file mode 100644 index 00000000..e906a312 --- /dev/null +++ b/src/test/java/org/folio/service/transactions/PaymentCreditTest.java @@ -0,0 +1,452 @@ +package org.folio.service.transactions; + +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxTestContext; +import org.folio.rest.exception.HttpException; +import org.folio.rest.jaxrs.model.AwaitingPayment; +import org.folio.rest.jaxrs.model.Batch; +import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Encumbrance; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.jaxrs.model.Transaction; +import org.folio.rest.persist.Criteria.Criterion; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; +import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; +import static org.folio.rest.jaxrs.model.Encumbrance.Status.RELEASED; +import static org.folio.rest.jaxrs.model.Transaction.Source.PO_LINE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ENCUMBRANCE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PAYMENT; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class PaymentCreditTest extends BatchTransactionServiceTestBase { + + @Test + void testCreatePaymentWithoutLinkedEncumbrance(VertxTestContext testContext) { + String paymentId = UUID.randomUUID().toString(); + String pendingPaymentId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction payment = new Transaction() + .withId(paymentId) + .withTransactionType(PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false); + + Transaction existingPendingPayment = new Transaction() + .withId(pendingPaymentId) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withAwaitingPayment(new AwaitingPayment() + .withReleaseEncumbrance(true)); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(payment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false, false); + + Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(paymentCriterion.toString()))); + + String snippet = "WHERE (jsonb->>'transactionType') = 'Pending payment' AND ( (jsonb->>'sourceInvoiceId') = '" + invoiceId + "') "; + doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(snippet))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(List.of(existingPendingPayment)))) + .when(conn).delete(anyString(), any(Criterion.class)); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify payment creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedPayment = (Transaction)(saveEntities.get(0).get(0)); + assertThat(savedPayment.getTransactionType(), equalTo(PAYMENT)); + assertNotNull(savedPayment.getMetadata().getCreatedDate()); + assertThat(savedPayment.getAmount(), equalTo(5d)); + + // Verify budget update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(0).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); + assertThat(savedBudget.getExpenditures(), equalTo(5d)); + + // Verify pending payment deletion + ArgumentCaptor deleteTableNamesCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor deleteCriterionCaptor = ArgumentCaptor.forClass(Criterion.class); + verify(conn, times(1)).delete(deleteTableNamesCaptor.capture(), deleteCriterionCaptor.capture()); + List deleteTableNames = deleteTableNamesCaptor.getAllValues(); + List deleteCriterions = deleteCriterionCaptor.getAllValues(); + + Criterion pendingPaymentCriterionByIds = createCriterionByIds(List.of(pendingPaymentId)); + assertThat(deleteTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Criterion deleteCriterion = deleteCriterions.get(0); + assertThat(deleteCriterion.toString(), equalTo(pendingPaymentCriterionByIds.toString())); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreatePaymentWithLinkedEncumbrance(VertxTestContext testContext) { + String paymentId = UUID.randomUUID().toString(); + String pendingPaymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction payment = new Transaction() + .withId(paymentId) + .withTransactionType(PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withPaymentEncumbranceId(encumbranceId); + + Transaction existingPendingPayment = new Transaction() + .withId(pendingPaymentId) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withAwaitingPayment(new AwaitingPayment() + .withEncumbranceId(encumbranceId) + .withReleaseEncumbrance(true)); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withCurrency("USD") + .withFromFundId(fundId) + .withAmount(0d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED) + .withAmountAwaitingPayment(5d) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(payment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false, false); + + Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(paymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + String snippet = "WHERE (jsonb->>'transactionType') = 'Pending payment' AND ( (jsonb->>'sourceInvoiceId') = '" + invoiceId + "') "; + doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(snippet))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(List.of(existingPendingPayment)))) + .when(conn).delete(anyString(), any(Criterion.class)); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify payment creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedPayment = (Transaction)(saveEntities.get(0).get(0)); + assertThat(savedPayment.getTransactionType(), equalTo(PAYMENT)); + assertNotNull(savedPayment.getMetadata().getCreatedDate()); + assertThat(savedPayment.getAmount(), equalTo(5d)); + + // Verify encumbrance update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(0)); + assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); + assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); + assertThat(savedEncumbrance.getAmount(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountExpended(), equalTo(5d)); + + // Verify budget update + assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); + assertThat(savedBudget.getExpenditures(), equalTo(5d)); + + // Verify pending payment deletion + ArgumentCaptor deleteTableNamesCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor deleteCriterionCaptor = ArgumentCaptor.forClass(Criterion.class); + verify(conn, times(1)).delete(deleteTableNamesCaptor.capture(), deleteCriterionCaptor.capture()); + List deleteTableNames = deleteTableNamesCaptor.getAllValues(); + List deleteCriterions = deleteCriterionCaptor.getAllValues(); + + Criterion pendingPaymentCriterionByIds = createCriterionByIds(List.of(pendingPaymentId)); + assertThat(deleteTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Criterion deleteCriterion = deleteCriterions.get(0); + assertThat(deleteCriterion.toString(), equalTo(pendingPaymentCriterionByIds.toString())); + }); + testContext.completeNow(); + }); + } + + @Test + void testCancelPaymentWithLinkedEncumbrance(VertxTestContext testContext) { + String paymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction existingPayment = new Transaction() + .withId(paymentId) + .withTransactionType(PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withPaymentEncumbranceId(encumbranceId) + .withMetadata(new Metadata()); + + Transaction newPayment = JsonObject.mapFrom(existingPayment).mapTo(Transaction.class); + newPayment.setInvoiceCancelled(true); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withCurrency("USD") + .withFromFundId(fundId) + .withAmount(0d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED) + .withAmountAwaitingPayment(0d) + .withAmountExpended(5d) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Batch batch = new Batch(); + batch.getTransactionsToUpdate().add(newPayment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 5d, false, false, false); + + Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); + doReturn(succeededFuture(createResults(List.of(existingPayment)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(paymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify payment update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedPayment = (Transaction)(updateEntities.get(0).get(0)); + assertThat(savedPayment.getTransactionType(), equalTo(PAYMENT)); + assertNotNull(savedPayment.getMetadata().getUpdatedDate()); + assertThat(savedPayment.getAmount(), equalTo(0d)); + assertThat(savedPayment.getVoidedAmount(), equalTo(5d)); + + // Verify encumbrance update + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(1)); + assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); + assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); + assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); + assertThat(savedEncumbrance.getAmount(), equalTo(5d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountExpended(), equalTo(0d)); + + // Verify budget update + assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); + assertThat(savedBudget.getExpenditures(), equalTo(0d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreatePaymentWithNegativeAmount(VertxTestContext testContext) { + String paymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction payment = new Transaction() + .withId(paymentId) + .withTransactionType(PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(-5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withPaymentEncumbranceId(encumbranceId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(payment); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + HttpException exception = (HttpException)(event.cause()); + assertEquals(exception.getErrors().getErrors().get(0).getCode(), "paymentOrCreditHasNegativeAmount"); + assertThat(exception.getCode(), equalTo(422)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCreatePaymentWithBadEncumbranceLink(VertxTestContext testContext) { + String paymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction payment = new Transaction() + .withId(paymentId) + .withTransactionType(PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withPaymentEncumbranceId(encumbranceId); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(payment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false, false); + + Criterion paymentCriterion = createCriterionByIds(List.of(paymentId)); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(paymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(Collections.emptyList()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + assertThat(event.cause(), instanceOf(HttpException.class)); + assertThat(((HttpException) event.cause()).getCode(), equalTo(400)); + }); + testContext.completeNow(); + }); + } + +} diff --git a/src/test/java/org/folio/service/transactions/PendingPaymentServiceTest.java b/src/test/java/org/folio/service/transactions/PendingPaymentServiceTest.java deleted file mode 100644 index f2237fb7..00000000 --- a/src/test/java/org/folio/service/transactions/PendingPaymentServiceTest.java +++ /dev/null @@ -1,832 +0,0 @@ -package org.folio.service.transactions; - -import static org.folio.dao.transactions.TemporaryInvoiceTransactionDAO.TEMPORARY_INVOICE_TRANSACTIONS; -import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; -import static org.folio.service.transactions.PendingPaymentService.SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import org.folio.dao.transactions.EncumbranceDAO; -import org.folio.rest.exception.HttpException; -import org.folio.rest.jaxrs.model.AwaitingPayment; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Parameter; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.persist.DBConn; -import org.folio.rest.persist.HelperUtils; -import org.folio.service.budget.BudgetService; -import org.folio.service.transactions.cancel.CancelTransactionService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import io.vertx.core.Future; -import io.vertx.core.json.JsonObject; -import io.vertx.sqlclient.Tuple; - -public class PendingPaymentServiceTest { - - private static final String TENANT_ID = "tenant"; - - private AutoCloseable mockitoMocks; - - @InjectMocks - private PendingPaymentService pendingPaymentService; - - @Mock - private BudgetService budgetService; - @Mock - private CancelTransactionService cancelTransactionService; - @Mock - private EncumbranceDAO transactionsDAO; - @Mock - private DBConn conn; - - private final String summaryId = UUID.randomUUID().toString(); - private final String encumbranceId = UUID.randomUUID().toString(); - private final String fundId = UUID.randomUUID().toString(); - private final String fiscalYearId = UUID.randomUUID().toString(); - private final String currency = "USD"; - private Transaction linkedTransaction; - private Transaction notLinkedTransaction; - private Transaction encumbrance; - private Budget budget; - - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - when(conn.getTenantId()).thenReturn(TENANT_ID); - } - - @BeforeEach - public void prepareData(){ - - linkedTransaction = new Transaction() - .withAwaitingPayment(new AwaitingPayment().withEncumbranceId(encumbranceId).withReleaseEncumbrance(false)) - .withSourceInvoiceId(summaryId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withCurrency(currency) - .withTransactionType(Transaction.TransactionType.PENDING_PAYMENT); - - notLinkedTransaction = new Transaction() - .withSourceInvoiceId(summaryId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withCurrency(currency) - .withTransactionType(Transaction.TransactionType.PENDING_PAYMENT); - - encumbrance = new Transaction() - .withId(encumbranceId) - .withCurrency(currency) - .withFromFundId(fundId) - .withTransactionType(Transaction.TransactionType.ENCUMBRANCE) - .withEncumbrance(new Encumbrance() - .withInitialAmountEncumbered(10d)); - - budget = new Budget() - .withFiscalYearId(fiscalYearId) - .withAwaitingPayment(0d) - .withAllocated(100d) - .withAvailable(100d) - .withEncumbered(0d) - .withUnavailable(0d) - .withFundId(fundId); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void testProcessTemporaryToPermanentTransactionsWithLinkedAndNotLinkedPendingPayments() { - BigDecimal linkedAmount = BigDecimal.valueOf(9.1); - linkedTransaction.withAmount(linkedAmount.doubleValue()); - BigDecimal notLinkedAmount = BigDecimal.valueOf(1.5); - notLinkedTransaction.withAmount(notLinkedAmount.doubleValue()); - - List transactions = Arrays.asList(linkedTransaction, notLinkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - encumbrance.withAmount(encumbered.doubleValue()); - List encumbrances = Collections.singletonList(encumbrance); - - when(transactionsDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future result = spyService.createTransactions(transactions, conn); - - assertTrue(result.succeeded()); - - final ArgumentCaptor> idsArgumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).getTransactions(idsArgumentCaptor.capture(), eq(conn)); - final List ids = idsArgumentCaptor.getValue(); - assertThat(ids, contains(encumbranceId)); - - final ArgumentCaptor> encumbranceUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).updatePermanentTransactions(encumbranceUpdateArgumentCapture.capture(), eq(conn)); - Transaction updatedEncumbrance = encumbranceUpdateArgumentCapture.getValue().get(0); - assertThat(updatedEncumbrance.getAmount(), is(encumbered.subtract(linkedAmount).doubleValue())); - assertThat(updatedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), is(BigDecimal.ZERO.add(linkedAmount).doubleValue())); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(linkedAmount).add(notLinkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.subtract(linkedAmount).doubleValue())); - - - } - - @Test - void testProcessTemporaryToPermanentTransactionsWithLinkedPendingPaymentReleaseEncumbrance() { - BigDecimal linkedAmount = BigDecimal.valueOf(9.1); - linkedTransaction.withAmount(linkedAmount.doubleValue()); - linkedTransaction.getAwaitingPayment().setReleaseEncumbrance(true); - - List transactions = Collections.singletonList(linkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ONE; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(9d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - encumbrance.withAmount(encumbered.doubleValue()); - encumbrance.getEncumbrance().setAmountAwaitingPayment(awaitingPayment.doubleValue()); - List encumbrances = Collections.singletonList(encumbrance); - - when(transactionsDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future result = spyService.createTransactions(transactions, conn); - - assertTrue(result.succeeded()); - - final ArgumentCaptor> idsArgumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).getTransactions(idsArgumentCaptor.capture(), eq(conn)); - final List ids = idsArgumentCaptor.getValue(); - assertThat(ids, contains(encumbranceId)); - - final ArgumentCaptor> encumbranceUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).updatePermanentTransactions(encumbranceUpdateArgumentCapture.capture(), eq(conn)); - Transaction updatedEncumbrance = encumbranceUpdateArgumentCapture.getValue().get(0); - assertThat(updatedEncumbrance.getAmount(), is(0.0)); - assertThat(updatedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), is(10.1d)); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(linkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(0.0)); - - } - - @Test - void testProcessTemporaryToPermanentTransactionsWithLinkedPendingPaymentGreaterThanEncumbrance() { - BigDecimal linkedAmount = BigDecimal.valueOf(11d); - linkedTransaction.withAmount(linkedAmount.doubleValue()); - linkedTransaction.getAwaitingPayment().setReleaseEncumbrance(false); - - List transactions = Collections.singletonList(linkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - encumbrance.withAmount(encumbered.doubleValue()); - List encumbrances = Collections.singletonList(encumbrance); - - when(transactionsDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future result = spyService.createTransactions(transactions, conn); - - assertTrue(result.succeeded()); - - final ArgumentCaptor> listArgumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).getTransactions(listArgumentCaptor.capture(), eq(conn)); - final List idsArgument = listArgumentCaptor.getValue(); - assertThat(idsArgument, contains(linkedTransaction.getAwaitingPayment().getEncumbranceId())); - - final ArgumentCaptor> encumbranceUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).updatePermanentTransactions(encumbranceUpdateArgumentCapture.capture(), eq(conn)); - Transaction updatedEncumbrance = encumbranceUpdateArgumentCapture.getValue().get(0); - assertThat(updatedEncumbrance.getAmount(), is(0.0)); - assertThat(updatedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), is(linkedAmount.doubleValue())); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(linkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(0d)); - - } - - @Test - void testProcessTemporaryToPermanentTransactionsWithLinkedPendingPaymentGreaterThanBudgetRemainingAmount() { - BigDecimal linkedAmount = BigDecimal.valueOf(110d); - linkedTransaction.withAmount(linkedAmount.doubleValue()); - linkedTransaction.getAwaitingPayment().setReleaseEncumbrance(false); - - List transactions = Collections.singletonList(linkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal allocated = BigDecimal.valueOf(50d); - BigDecimal netTransfer = BigDecimal.valueOf(50d); - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - encumbrance.withAmount(encumbered.doubleValue()); - List encumbrances = Collections.singletonList(encumbrance); - - when(transactionsDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future result = spyService.createTransactions(transactions, conn); - - assertTrue(result.succeeded()); - - final ArgumentCaptor> listArgumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).getTransactions(listArgumentCaptor.capture(), eq(conn)); - final List idsArgument = listArgumentCaptor.getValue(); - assertThat(idsArgument, contains(linkedTransaction.getAwaitingPayment().getEncumbranceId())); - - final ArgumentCaptor> encumbranceUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO).updatePermanentTransactions(encumbranceUpdateArgumentCapture.capture(), eq(conn)); - Transaction updatedEncumbrance = encumbranceUpdateArgumentCapture.getValue().get(0); - assertThat(updatedEncumbrance.getAmount(), is(0.0)); - assertThat(updatedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), is(linkedAmount.doubleValue())); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(linkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(0d)); - - } - - @Test - void testProcessTemporaryToPermanentTransactionsWithNotLinkedPendingPayment() { - BigDecimal notLinkedAmount = BigDecimal.valueOf(1.5); - notLinkedTransaction.withAmount(notLinkedAmount.doubleValue()); - - List transactions = Collections.singletonList(notLinkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - spyService.createTransactions(transactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - - verify(transactionsDAO, never()).getTransactions(anyList(), any()); - verify(transactionsDAO, never()).updatePermanentTransactions(anyList(), any()); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(notLinkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.doubleValue())); - - } - - - @Test - void overExpendedShouldBeIncreasedWhenProcessTemporaryToPermanentTransactionsWithNotLinkedPendingPaymentWithAmountGreaterThenBudgetRemaining() { - BigDecimal notLinkedAmount = BigDecimal.valueOf(150); - notLinkedTransaction.withAmount(notLinkedAmount.doubleValue()); - - List transactions = Collections.singletonList(notLinkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - spyService.createTransactions(transactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - - verify(transactionsDAO, never()).getTransactions(anyList(), any()); - verify(transactionsDAO, never()).updatePermanentTransactions(anyList(), any()); - - verify(transactionsDAO).saveTransactionsToPermanentTable(eq(summaryId), eq(conn)); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(notLinkedAmount).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.doubleValue())); - - } - - @Test - void shouldUpdateBudgetsTotalsWhenUpdateNotLinkedToEncumbrancePendingPaymentsAmount() { - BigDecimal newAmount = BigDecimal.valueOf(1.5); - BigDecimal amount = BigDecimal.valueOf(2d); - notLinkedTransaction.withAmount(newAmount.doubleValue()); - Transaction existingTransaction = JsonObject.mapFrom(notLinkedTransaction).mapTo(Transaction.class) - .withAmount(amount.doubleValue()); - List transactions = Collections.singletonList(notLinkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - when(transactionsDAO.getTransactions(anyList(), any())) - .thenReturn(Future.succeededFuture(Collections.singletonList(existingTransaction))); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - spyService.cancelAndUpdateTransactions(transactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - verify(transactionsDAO).getTransactions(anyList(), any()); - verify(transactionsDAO).updatePermanentTransactions(anyList(), any()); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - BigDecimal amountDifference = newAmount.subtract(amount); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(amountDifference).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.doubleValue())); - - } - - @Test - void shouldUpdateBudgetsAndEncumbrancesTotalsWhenUpdateLinkedToEncumbrancePendingPaymentsAmount() { - BigDecimal newAmount = BigDecimal.valueOf(11d); - BigDecimal amount = BigDecimal.valueOf(9.99); - linkedTransaction.withId(UUID.randomUUID().toString()) - .withAmount(newAmount.doubleValue()); - linkedTransaction.getAwaitingPayment().setReleaseEncumbrance(false); - - Transaction existingTransaction = JsonObject.mapFrom(linkedTransaction).mapTo(Transaction.class) - .withAmount(amount.doubleValue()); - List transactions = Collections.singletonList(linkedTransaction); - - BigDecimal awaitingPayment = BigDecimal.ZERO; - BigDecimal available = BigDecimal.valueOf(90d); - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(10d); - - budget.withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - encumbrance.withAmount(encumbered.doubleValue()); - List encumbrances = Collections.singletonList(encumbrance); - - when(transactionsDAO.getTransactions(eq(Collections.singletonList(linkedTransaction.getAwaitingPayment().getEncumbranceId())), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - when(transactionsDAO.getTransactions(eq(Collections.singletonList(linkedTransaction.getId())), any())) - .thenReturn(Future.succeededFuture(Collections.singletonList(existingTransaction))); - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - when(transactionsDAO.saveTransactionsToPermanentTable(anyString(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future result = spyService.cancelAndUpdateTransactions(transactions, conn); - - assertTrue(result.succeeded()); - - final ArgumentCaptor> criterionArgumentCaptor = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO, times(2)).getTransactions(criterionArgumentCaptor.capture(), eq(conn)); - final List> idsArguments = criterionArgumentCaptor.getAllValues(); - assertThat(idsArguments, containsInAnyOrder(Collections.singletonList(linkedTransaction.getId()), - Collections.singletonList(linkedTransaction.getAwaitingPayment().getEncumbranceId()))); - - final ArgumentCaptor> updateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(transactionsDAO, times(2)).updatePermanentTransactions(updateArgumentCapture.capture(), eq(conn)); - List> updateArguments = updateArgumentCapture.getAllValues(); - Transaction updatedEncumbrance = updateArguments.stream().flatMap(Collection::stream) - .filter(transaction -> transaction.getTransactionType() == Transaction.TransactionType.ENCUMBRANCE) - .findFirst().orElse(null); - BigDecimal amountDifference = newAmount.subtract(amount); - - assertThat(updatedEncumbrance.getAmount(), is(encumbered.subtract(amountDifference).doubleValue())); - assertThat(updatedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), is(awaitingPayment.add(amountDifference).doubleValue())); - - Transaction updatedPendingPayment = updateArguments.stream().flatMap(Collection::stream) - .filter(transaction -> transaction.getTransactionType() == Transaction.TransactionType.PENDING_PAYMENT) - .findFirst().orElse(null); - - assertThat(updatedPendingPayment.getAmount(), is(newAmount.doubleValue())); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(awaitingPayment.add(amountDifference).doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.subtract(amountDifference).doubleValue())); - - } - - @Test - void shouldUpdateBudgetsWithOverExpendedWhenUpdateNotLinkedToEncumbrancePendingPaymentsAmount() { - BigDecimal newAmount = BigDecimal.valueOf(1.5); - BigDecimal amount = BigDecimal.valueOf(20000d); - notLinkedTransaction.withAmount(newAmount.doubleValue()); - Transaction existingTransaction = JsonObject.mapFrom(notLinkedTransaction).mapTo(Transaction.class) - .withAmount(amount.doubleValue()); - List transactions = Collections.singletonList(notLinkedTransaction); - - BigDecimal allocated = BigDecimal.valueOf(100d); - BigDecimal awaitingPayment = BigDecimal.valueOf(20000d); - BigDecimal available = BigDecimal.ZERO; - BigDecimal encumbered = BigDecimal.valueOf(10d); - BigDecimal unavailable = BigDecimal.valueOf(100d); - BigDecimal overExpended = BigDecimal.valueOf(19910d); - budget.withAllocated(allocated.doubleValue()) - .withAwaitingPayment(awaitingPayment.doubleValue()) - .withAvailable(available.doubleValue()) - .withEncumbered(encumbered.doubleValue()) - .withUnavailable(unavailable.doubleValue()) - .withOverExpended(overExpended.doubleValue()); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))).thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - when(transactionsDAO.getTransactions(anyList(), any())) - .thenReturn(Future.succeededFuture(Collections.singletonList(existingTransaction))); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - spyService.cancelAndUpdateTransactions(transactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - verify(transactionsDAO).getTransactions(anyList(), any()); - verify(transactionsDAO).updatePermanentTransactions(anyList(), any()); - - String budgetTableName = HelperUtils.getFullTableName(TENANT_ID, BUDGET_TABLE); - String transactionTableName = HelperUtils.getFullTableName(TENANT_ID, TEMPORARY_INVOICE_TRANSACTIONS); - String sql = String.format(SELECT_BUDGETS_BY_INVOICE_ID_FOR_UPDATE, budgetTableName, budgetTableName, transactionTableName); - - final ArgumentCaptor paramsArgumentCapture = ArgumentCaptor.forClass(Tuple.class); - verify(budgetService).getBudgets(eq(sql), paramsArgumentCapture.capture(), eq(conn)); - - Tuple params = paramsArgumentCapture.getValue(); - assertThat(params.get(UUID.class, 0), is(UUID.fromString(summaryId))); - - final ArgumentCaptor> budgetUpdateArgumentCapture = ArgumentCaptor.forClass(List.class); - verify(budgetService).updateBatchBudgets(budgetUpdateArgumentCapture.capture(), eq(conn)); - List updatedBudgets = budgetUpdateArgumentCapture.getValue(); - Budget updatedBudget = updatedBudgets.get(0); - - BigDecimal amountDifference = newAmount.subtract(amount); - BigDecimal expectedAwaitingPayment = awaitingPayment.add(amountDifference); - BigDecimal expectedUnavailable = expectedAwaitingPayment.add(encumbered); - - assertThat(updatedBudget.getAvailable(), is(available.doubleValue())); - assertThat(updatedBudget.getUnavailable(), is(unavailable.doubleValue())); - assertThat(updatedBudget.getAwaitingPayment(), is(expectedAwaitingPayment.doubleValue())); - assertThat(updatedBudget.getEncumbered(), is(encumbered.doubleValue())); - - } - - @Test - void testCancelTransactions() { - Transaction newTransaction = JsonObject.mapFrom(notLinkedTransaction).mapTo(Transaction.class) - .withId(UUID.randomUUID().toString()) - .withAmount(2d) - .withInvoiceCancelled(true); - List newTransactions = Collections.singletonList(newTransaction); - Transaction existingTransaction = JsonObject.mapFrom(newTransaction).mapTo(Transaction.class) - .withInvoiceCancelled(false); - List existingTransactions = Collections.singletonList(existingTransaction); - - when(transactionsDAO.getTransactions(anyList(), any())).thenReturn(Future.succeededFuture(existingTransactions)); - when(cancelTransactionService.cancelTransactions(anyList(), any())).thenReturn(Future.succeededFuture(null)); - - PendingPaymentService spyService = Mockito.spy(pendingPaymentService); - - spyService.cancelAndUpdateTransactions(newTransactions, conn) - .onComplete(res -> assertTrue(res.succeeded())); - - verify(transactionsDAO).getTransactions(anyList(), any()); - verify(cancelTransactionService, times(1)).cancelTransactions(anyList(), eq(conn)); - } - - @Test - void outdatedFundIdInEncumbrance() { - BigDecimal linkedAmount = BigDecimal.valueOf(2.0); - linkedTransaction.withAmount(linkedAmount.doubleValue()); - List pendingPayments = Collections.singletonList(linkedTransaction); - encumbrance.withAmount(1.0); - encumbrance.getEncumbrance().setSourcePoLineId(UUID.randomUUID().toString()); - List encumbrances = Collections.singletonList(encumbrance); - List budgets = List.of(); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(transactionsDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - - pendingPaymentService.createTransactions(pendingPayments, conn) - .onComplete(event -> { - assertThat(event.cause(), instanceOf(HttpException.class)); - HttpException thrown = (HttpException)event.cause(); - assertThat(thrown.getCode(), is(500)); - Error error = thrown.getErrors().getErrors().get(0); - assertThat(error.getCode(), is("outdatedFundIdInEncumbrance")); - List parameters = error.getParameters(); - assertThat(parameters.get(0).getKey(), is("encumbranceId")); - assertThat(parameters.get(0).getValue(), is(encumbrance.getId())); - assertThat(parameters.get(1).getKey(), is("fundId")); - assertThat(parameters.get(1).getValue(), is(fundId)); - assertThat(parameters.get(2).getKey(), is("poLineId")); - assertThat(parameters.get(2).getValue(), is(encumbrance.getEncumbrance().getSourcePoLineId())); - } - ); - } -} diff --git a/src/test/java/org/folio/service/transactions/PendingPaymentTest.java b/src/test/java/org/folio/service/transactions/PendingPaymentTest.java new file mode 100644 index 00000000..a2ca2bb1 --- /dev/null +++ b/src/test/java/org/folio/service/transactions/PendingPaymentTest.java @@ -0,0 +1,410 @@ +package org.folio.service.transactions; + +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxTestContext; +import org.folio.rest.exception.HttpException; +import org.folio.rest.jaxrs.model.AwaitingPayment; +import org.folio.rest.jaxrs.model.Batch; +import org.folio.rest.jaxrs.model.Budget; +import org.folio.rest.jaxrs.model.Encumbrance; +import org.folio.rest.jaxrs.model.Metadata; +import org.folio.rest.jaxrs.model.Transaction; +import org.folio.rest.persist.Criteria.Criterion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import java.util.List; +import java.util.UUID; + +import static io.vertx.core.Future.succeededFuture; +import static org.folio.dao.transactions.BatchTransactionDAO.TRANSACTIONS_TABLE; +import static org.folio.rest.impl.BudgetAPI.BUDGET_TABLE; +import static org.folio.rest.jaxrs.model.Encumbrance.Status.RELEASED; +import static org.folio.rest.jaxrs.model.Encumbrance.Status.UNRELEASED; +import static org.folio.rest.jaxrs.model.Transaction.Source.PO_LINE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.ENCUMBRANCE; +import static org.folio.rest.jaxrs.model.Transaction.TransactionType.PENDING_PAYMENT; +import static org.folio.rest.util.ErrorCodes.BUDGET_RESTRICTED_EXPENDITURES_ERROR; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class PendingPaymentTest extends BatchTransactionServiceTestBase { + + @Test + void testBatchEntityValidity(VertxTestContext testContext) { + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + Transaction pendingPayment = new Transaction() + .withTransactionType(PENDING_PAYMENT) + .withAmount(0d) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withCurrency(currency); + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(pendingPayment); + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onFailure(thrown -> { + testContext.verify(() -> { + assertThat(thrown, instanceOf(HttpException.class)); + HttpException exception = (HttpException)thrown; + assertThat(exception.getCode(), equalTo(400)); + assertEquals(exception.getErrors().getErrors().get(0).getCode(), "idIsRequiredInTransactions"); + assertThat(exception.getMessage(), equalTo("Id is required in transactions to create.")); + }); + testContext.completeNow(); + }); + } + + @ParameterizedTest + @ValueSource(doubles = {5d, 15d}) + void testCreatePendingPaymentWithLinkedEncumbrance(double pendingPaymentAmount, VertxTestContext testContext) { + // In the second test, the pending payment amount is greater than available budget, and also greater than the encumbrance + // Expenditure restrictions are disabled + String pendingPaymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction pendingPayment = new Transaction() + .withId(pendingPaymentId) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(pendingPaymentAmount) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withAwaitingPayment(new AwaitingPayment() + .withEncumbranceId(encumbranceId) + .withReleaseEncumbrance(true)); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withCurrency("USD") + .withFromFundId(fundId) + .withAmount(5d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(UNRELEASED) + .withAmountAwaitingPayment(0d) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().add(pendingPayment); + + setupFundBudgetLedger(fundId, fiscalYearId, 5d, 0d, 0d, false, false, false); + + Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).saveBatch(anyString(), anyList()); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify pending payment creation + ArgumentCaptor saveTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(1)).saveBatch(saveTableNamesCaptor.capture(), saveEntitiesCaptor.capture()); + List saveTableNames = saveTableNamesCaptor.getAllValues(); + List> saveEntities = saveEntitiesCaptor.getAllValues(); + + assertThat(saveTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedPendingPayment = (Transaction)(saveEntities.get(0).get(0)); + assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); + assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); + assertThat(savedPendingPayment.getAmount(), equalTo(pendingPaymentAmount)); + + // Verify encumbrance update + ArgumentCaptor updateTableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(updateTableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List updateTableNames = updateTableNamesCaptor.getAllValues(); + List> updateEntities = updateEntitiesCaptor.getAllValues(); + + assertThat(updateTableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + Transaction savedEncumbrance = (Transaction)(updateEntities.get(0).get(0)); + assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); + assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); + assertThat(savedEncumbrance.getAmount(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); + assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(pendingPaymentAmount)); + + // Verify budget update + assertThat(updateTableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(updateEntities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(pendingPaymentAmount)); + }); + testContext.completeNow(); + }); + } + + @Test + void testUpdatePendingPaymentWithLinkedEncumbrance(VertxTestContext testContext) { + String pendingPaymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction existingPendingPayment = new Transaction() + .withId(pendingPaymentId) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withAwaitingPayment(new AwaitingPayment() + .withEncumbranceId(encumbranceId) + .withReleaseEncumbrance(true)) + .withMetadata(new Metadata()); + + Transaction newPendingPayment = JsonObject.mapFrom(existingPendingPayment).mapTo(Transaction.class); + newPendingPayment.setAmount(10d); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withCurrency("USD") + .withFromFundId(fundId) + .withAmount(0d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED) + .withAmountAwaitingPayment(5d) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Batch batch = new Batch(); + batch.getTransactionsToUpdate().add(newPendingPayment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false, false); + + Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); + doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify pending payment update + ArgumentCaptor tableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(tableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List tableNames = tableNamesCaptor.getAllValues(); + List> entities = updateEntitiesCaptor.getAllValues(); + assertThat(tableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + + Transaction savedPendingPayment = (Transaction)(entities.get(0).get(0)); + assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); + assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); + assertThat(savedPendingPayment.getAmount(), equalTo(10d)); + + // Verify encumbrance update + Transaction savedEncumbrance = (Transaction)(entities.get(0).get(1)); + assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); + assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); + assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); + assertThat(savedEncumbrance.getAmount(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(10d)); + + // Verify budget update + assertThat(tableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(entities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(10d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testCancelPendingPaymentWithLinkedEncumbrance(VertxTestContext testContext) { + String pendingPaymentId = UUID.randomUUID().toString(); + String encumbranceId = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction existingPendingPayment = new Transaction() + .withId(pendingPaymentId) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(5d) + .withCurrency(currency) + .withInvoiceCancelled(false) + .withAwaitingPayment(new AwaitingPayment() + .withEncumbranceId(encumbranceId) + .withReleaseEncumbrance(true)) + .withMetadata(new Metadata()); + + Transaction newPendingPayment = JsonObject.mapFrom(existingPendingPayment).mapTo(Transaction.class); + newPendingPayment.setInvoiceCancelled(true); + + Transaction existingEncumbrance = new Transaction() + .withId(encumbranceId) + .withTransactionType(ENCUMBRANCE) + .withCurrency("USD") + .withFromFundId(fundId) + .withAmount(0d) + .withFiscalYearId(fiscalYearId) + .withSource(PO_LINE) + .withEncumbrance(new Encumbrance() + .withStatus(RELEASED) + .withAmountAwaitingPayment(5d) + .withInitialAmountEncumbered(5d)) + .withMetadata(new Metadata()); + + Batch batch = new Batch(); + batch.getTransactionsToUpdate().add(newPendingPayment); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 5d, 0d, false, false, false); + + Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId)); + doReturn(succeededFuture(createResults(List.of(existingPendingPayment)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); + + Criterion encumbranceCriterion = createCriterionByIds(List.of(encumbranceId)); + doReturn(succeededFuture(createResults(List.of(existingEncumbrance)))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(encumbranceCriterion.toString()))); + + doAnswer(invocation -> succeededFuture(createRowSet(invocation.getArgument(1)))) + .when(conn).updateBatch(anyString(), anyList()); + + testContext.assertComplete(batchTransactionService.processBatch(batch, requestContext)) + .onComplete(event -> { + testContext.verify(() -> { + // Verify pending payment update + ArgumentCaptor tableNamesCaptor = ArgumentCaptor.forClass(String.class); + verify(conn, times(2)).updateBatch(tableNamesCaptor.capture(), updateEntitiesCaptor.capture()); + List tableNames = tableNamesCaptor.getAllValues(); + List> entities = updateEntitiesCaptor.getAllValues(); + assertThat(tableNames.get(0), equalTo(TRANSACTIONS_TABLE)); + + Transaction savedPendingPayment = (Transaction)(entities.get(0).get(0)); + assertThat(savedPendingPayment.getTransactionType(), equalTo(PENDING_PAYMENT)); + assertNotNull(savedPendingPayment.getMetadata().getUpdatedDate()); + assertThat(savedPendingPayment.getAmount(), equalTo(0d)); + assertThat(savedPendingPayment.getVoidedAmount(), equalTo(5d)); + + // Verify encumbrance update + Transaction savedEncumbrance = (Transaction)(entities.get(0).get(1)); + assertThat(savedEncumbrance.getTransactionType(), equalTo(ENCUMBRANCE)); + assertThat(savedEncumbrance.getEncumbrance().getStatus(), equalTo(RELEASED)); + assertNotNull(savedEncumbrance.getMetadata().getUpdatedDate()); + assertThat(savedEncumbrance.getAmount(), equalTo(0d)); + assertThat(savedEncumbrance.getEncumbrance().getAmountAwaitingPayment(), equalTo(0d)); + + // Verify budget update + assertThat(tableNames.get(1), equalTo(BUDGET_TABLE)); + Budget savedBudget = (Budget)(entities.get(1).get(0)); + assertNotNull(savedBudget.getMetadata().getUpdatedDate()); + assertThat(savedBudget.getEncumbered(), equalTo(0d)); + assertThat(savedBudget.getAwaitingPayment(), equalTo(0d)); + }); + testContext.completeNow(); + }); + } + + @Test + void testExpenditureRestrictionsWhenCreatingTwoPendingPayments(VertxTestContext testContext) { + String pendingPaymentId1 = UUID.randomUUID().toString(); + String pendingPaymentId2 = UUID.randomUUID().toString(); + String invoiceId = UUID.randomUUID().toString(); + String fundId = UUID.randomUUID().toString(); + String fiscalYearId = UUID.randomUUID().toString(); + String currency = "USD"; + + Transaction pendingPayment1 = new Transaction() + .withId(pendingPaymentId1) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(7d) + .withCurrency(currency) + .withInvoiceCancelled(false); + + Transaction pendingPayment2 = new Transaction() + .withId(pendingPaymentId2) + .withTransactionType(PENDING_PAYMENT) + .withSourceInvoiceId(invoiceId) + .withFromFundId(fundId) + .withFiscalYearId(fiscalYearId) + .withAmount(8d) + .withCurrency(currency) + .withInvoiceCancelled(false); + + Batch batch = new Batch(); + batch.getTransactionsToCreate().addAll(List.of(pendingPayment1, pendingPayment2)); + + setupFundBudgetLedger(fundId, fiscalYearId, 0d, 0d, 0d, true, false, false); + + Criterion pendingPaymentCriterion = createCriterionByIds(List.of(pendingPaymentId1, pendingPaymentId2)); + doReturn(succeededFuture(createResults(List.of()))) + .when(conn).get(eq(TRANSACTIONS_TABLE), eq(Transaction.class), argThat( + crit -> crit.toString().equals(pendingPaymentCriterion.toString()))); + + testContext.assertFailure(batchTransactionService.processBatch(batch, requestContext)) + .onFailure(thrown -> { + HttpException exception = (HttpException)thrown; + testContext.verify(() -> { + assertThat(exception.getCode(), equalTo(422)); + assertThat(exception.getErrors().getErrors().get(0).getMessage(), + equalTo(BUDGET_RESTRICTED_EXPENDITURES_ERROR.getDescription())); + }); + testContext.completeNow(); + }); + } + +} diff --git a/src/test/java/org/folio/service/transactions/cancel/CancelPaymentCreditServiceTest.java b/src/test/java/org/folio/service/transactions/cancel/CancelPaymentCreditServiceTest.java deleted file mode 100644 index 6ff5cb77..00000000 --- a/src/test/java/org/folio/service/transactions/cancel/CancelPaymentCreditServiceTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.folio.service.transactions.cancel; - -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class CancelPaymentCreditServiceTest { - - private AutoCloseable mockitoMocks; - - @InjectMocks - private CancelPaymentCreditService cancelPaymentCreditService; - - private final String currency = "USD"; - - - @BeforeEach - public void initMocks() { - mockitoMocks = MockitoAnnotations.openMocks(this); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - @DisplayName("Credit should be canceled") - void creditShouldBeCanceled() { - Transaction transaction = new Transaction() - .withId(UUID.randomUUID().toString()) - .withAmount(-1000.0) - .withCurrency(currency) - .withTransactionType(Transaction.TransactionType.CREDIT); - List transactionList = List.of(transaction); - - Budget budget = new Budget() - .withId(UUID.randomUUID().toString()) - .withExpenditures(2000.0); - - Budget resultBudget = cancelPaymentCreditService.budgetMoneyBack(budget, transactionList); - - assertEquals(transaction.getAmount(), 0.0); - assertEquals(transaction.getVoidedAmount(), -1000.0); - assertEquals(resultBudget.getExpenditures(), 1000.0); - } - - @Test - @DisplayName("Payment should be canceled") - void paymentShouldBeCanceled() { - Transaction transaction = new Transaction() - .withId(UUID.randomUUID().toString()) - .withAmount(1000.0) - .withCurrency(currency) - .withTransactionType(Transaction.TransactionType.PAYMENT); - List transactionList = List.of(transaction); - - Budget budget = new Budget() - .withId(UUID.randomUUID().toString()) - .withExpenditures(2000.0); - - Budget resultBudget = cancelPaymentCreditService.budgetMoneyBack(budget, transactionList); - - assertEquals(transaction.getAmount(), 0.0); - assertEquals(transaction.getVoidedAmount(), 1000.0); - assertEquals(resultBudget.getExpenditures(), 1000.0); - } -} diff --git a/src/test/java/org/folio/service/transactions/cancel/CancelTransactionServiceTest.java b/src/test/java/org/folio/service/transactions/cancel/CancelTransactionServiceTest.java deleted file mode 100644 index 1f5aae5b..00000000 --- a/src/test/java/org/folio/service/transactions/cancel/CancelTransactionServiceTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package org.folio.service.transactions.cancel; - -import io.vertx.core.Future; -import io.vertx.sqlclient.Tuple; -import org.folio.dao.transactions.TransactionDAO; -import org.folio.rest.jaxrs.model.AwaitingPayment; -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Encumbrance; -import org.folio.rest.jaxrs.model.Transaction; -import org.folio.rest.jaxrs.model.Transaction.TransactionType; -import org.folio.rest.persist.DBConn; -import org.folio.service.budget.BudgetService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class CancelTransactionServiceTest { - - private AutoCloseable mockitoMocks; - @InjectMocks - CancelPendingPaymentService cancelPendingPaymentService; - @InjectMocks - CancelPaymentCreditService cancelPaymentCreditService; - @Mock - TransactionDAO transactionsDAO; - @Mock - TransactionDAO encumbranceDAO; - @Mock - BudgetService budgetService; - @Mock - private DBConn conn; - - private static final String TENANT_ID = "tenant"; - private final String summaryId = UUID.randomUUID().toString(); - private final String fundId = UUID.randomUUID().toString(); - private final String fiscalYearId = UUID.randomUUID().toString(); - private final String currency = "USD"; - private Transaction transaction, encumbrance; - private Budget budget; - - - @BeforeEach - public void initMocks() { - mockitoMocks = MockitoAnnotations.openMocks(this); - when(conn.getTenantId()) - .thenReturn(TENANT_ID); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @BeforeEach - public void prepareData(){ - transaction = new Transaction() - .withId(UUID.randomUUID().toString()) - .withSourceInvoiceId(summaryId) - .withFromFundId(fundId) - .withFiscalYearId(fiscalYearId) - .withCurrency(currency) - .withAmount(10.0) - .withInvoiceCancelled(true); - - encumbrance = new Transaction() - .withId(UUID.randomUUID().toString()) - .withCurrency(currency) - .withFromFundId(fundId) - .withTransactionType(Transaction.TransactionType.ENCUMBRANCE) - .withEncumbrance(new Encumbrance() - .withInitialAmountEncumbered(10d)) - .withAmount(10.0); - - budget = new Budget() - .withFiscalYearId(fiscalYearId) - .withAwaitingPayment(0d) - .withAllocated(100d) - .withAvailable(100d) - .withEncumbered(0d) - .withUnavailable(0d) - .withFundId(fundId); - } - - @Test - void cancelPendingPaymentTransaction() { - cancelPendingPaymentService = new CancelPendingPaymentService(budgetService, transactionsDAO, encumbranceDAO); - - transaction.setTransactionType(TransactionType.PENDING_PAYMENT); - transaction.setAwaitingPayment(new AwaitingPayment().withReleaseEncumbrance(true).withEncumbranceId(encumbrance.getId())); - List transactions = List.of(transaction); - encumbrance.getEncumbrance().setAmountAwaitingPayment(10.0); - List encumbrances = List.of(encumbrance); - - budget.withAwaitingPayment(100d) - .withAvailable(90d) - .withEncumbered(10d) - .withUnavailable(10d) - .withExpenditures(100d); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - when(encumbranceDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - when(encumbranceDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future cancelResult = cancelPendingPaymentService.cancelTransactions(transactions, conn); - assertTrue(cancelResult.succeeded()); - - verify(budgetService, times(1)) - .updateBatchBudgets(argThat(budgetColl -> budgetColl.stream().allMatch( - b -> b.getAwaitingPayment() == 90d && b.getExpenditures() == 100d - )), eq(conn)); - verify(transactionsDAO, times(1)).updatePermanentTransactions(argThat(trList -> { - if (trList.size() != 1) - return false; - Transaction first = trList.get(0); - return first.getTransactionType() == TransactionType.PENDING_PAYMENT && - first.getInvoiceCancelled() && first.getVoidedAmount() == 10d; - }), eq(conn)); - verify(encumbranceDAO, times(1)).updatePermanentTransactions(argThat(trList -> { - if (trList.size() != 1) - return false; - Transaction first = trList.get(0); - return first.getTransactionType() == TransactionType.ENCUMBRANCE && - first.getEncumbrance().getAmountAwaitingPayment() == 0d; - }), eq(conn)); - } - - @Test - void cancelPaymentCreditTransaction() { - cancelPaymentCreditService = new CancelPaymentCreditService(budgetService, transactionsDAO, encumbranceDAO); - - transaction.setTransactionType(TransactionType.PAYMENT); - transaction.setPaymentEncumbranceId(encumbrance.getId()); - List transactions = List.of(transaction); - encumbrance.getEncumbrance().setAmountExpended(10.0); - List encumbrances = List.of(encumbrance); - - budget.withAwaitingPayment(100d) - .withAvailable(90d) - .withEncumbered(10d) - .withUnavailable(10d) - .withExpenditures(100d); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - when(encumbranceDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - when(encumbranceDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future cancelResult = cancelPaymentCreditService.cancelTransactions(transactions, conn); - assertTrue(cancelResult.succeeded()); - - verify(budgetService, times(1)) - .updateBatchBudgets(argThat(budgetColl -> budgetColl.stream().allMatch( - b -> b.getAwaitingPayment() == 100d && b.getExpenditures() == 90d - )), eq(conn)); - verify(transactionsDAO, times(1)).updatePermanentTransactions(argThat(trList -> { - if (trList.size() != 1) - return false; - Transaction first = trList.get(0); - return first.getTransactionType() == TransactionType.PAYMENT && - first.getInvoiceCancelled() && first.getVoidedAmount() == 10d; - }), eq(conn)); - verify(encumbranceDAO, times(1)).updatePermanentTransactions(argThat(trList -> { - if (trList.size() != 1) - return false; - Transaction first = trList.get(0); - return first.getTransactionType() == TransactionType.ENCUMBRANCE && - first.getEncumbrance().getAmountExpended() == 0d; - }), eq(conn)); - } - - @Test - void cancelTransactionWithTypeCredit() { - cancelPaymentCreditService = new CancelPaymentCreditService(budgetService, transactionsDAO, encumbranceDAO); - - transaction.setTransactionType(TransactionType.CREDIT); - transaction.setPaymentEncumbranceId(encumbrance.getId()); - List transactions = List.of(transaction); - encumbrance.getEncumbrance().setAmountExpended(10.0); - List encumbrances = List.of(encumbrance); - - budget.withAwaitingPayment(100d) - .withAvailable(90d) - .withEncumbered(10d) - .withUnavailable(10d) - .withExpenditures(100d); - List budgets = Collections.singletonList(budget); - - when(budgetService.getBudgets(anyString(), any(Tuple.class), eq(conn))) - .thenReturn(Future.succeededFuture(budgets)); - when(budgetService.updateBatchBudgets(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - when(transactionsDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - when(encumbranceDAO.getTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture(encumbrances)); - when(encumbranceDAO.updatePermanentTransactions(anyList(), eq(conn))) - .thenReturn(Future.succeededFuture()); - - Future cancelResult = cancelPaymentCreditService.cancelTransactions(transactions, conn); - assertTrue(cancelResult.succeeded()); - } -} diff --git a/src/test/java/org/folio/service/transactions/restriction/EncumbranceRestrictionServiceTest.java b/src/test/java/org/folio/service/transactions/restriction/EncumbranceRestrictionServiceTest.java deleted file mode 100644 index 4362ea31..00000000 --- a/src/test/java/org/folio/service/transactions/restriction/EncumbranceRestrictionServiceTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.folio.service.transactions.restriction; - -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; - -import javax.money.MonetaryAmount; -import java.util.UUID; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class EncumbranceRestrictionServiceTest { - - private AutoCloseable mockitoMocks; - - @InjectMocks - private EncumbranceRestrictionService restrictionService; - - private String fundId = UUID.randomUUID().toString(); - private String fiscalYearId = UUID.randomUUID().toString(); - private Budget budget; - private String currency = "USD"; - - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - - budget = new Budget() - .withFiscalYearId(fiscalYearId) - .withAwaitingPayment(0d) - .withAllocated(100d) - .withAvailable(100d) - .withEncumbered(0d) - .withUnavailable(0d) - .withFundId(fundId); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void testGetBudgetRemainingAmountForEncumbrance() { - budget.withNetTransfers(20d) - .withAllowableEncumbrance(110d) - .withEncumbered(10d) - .withAwaitingPayment(11d) - .withExpenditures(90d) - .withAvailable(21d) // should not be used - .withUnavailable(22d); // should not be used - MonetaryAmount amount = restrictionService.getBudgetRemainingAmount(budget, currency, new Transaction().withAmount(5d)); - assertThat(amount.getNumber().doubleValue(), is(21d)); - } - -} diff --git a/src/test/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionServiceTest.java b/src/test/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionServiceTest.java deleted file mode 100644 index 151a31ed..00000000 --- a/src/test/java/org/folio/service/transactions/restriction/PaymentCreditRestrictionServiceTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.folio.service.transactions.restriction; - -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; - -import javax.money.MonetaryAmount; -import java.util.UUID; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class PaymentCreditRestrictionServiceTest { - - private AutoCloseable mockitoMocks; - - @InjectMocks - private PaymentCreditRestrictionService restrictionService; - - private String fundId = UUID.randomUUID().toString(); - private String fiscalYearId = UUID.randomUUID().toString(); - private Budget budget; - private String currency = "USD"; - - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - - budget = new Budget() - .withFiscalYearId(fiscalYearId) - .withAwaitingPayment(0d) - .withAllocated(100d) - .withAvailable(100d) - .withEncumbered(0d) - .withUnavailable(0d) - .withFundId(fundId); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void testGetBudgetRemainingAmountForEncumbrance() { - budget.withNetTransfers(20d) - .withAllowableExpenditure(110d) - .withEncumbered(10d) - .withAwaitingPayment(11d) - .withExpenditures(90d) - .withAvailable(21d) // should not be used - .withUnavailable(22d); // should not be used - MonetaryAmount amount = restrictionService.getBudgetRemainingAmount(budget, currency, new Transaction().withAmount(5d)); - assertThat(amount.getNumber().doubleValue(), is(26d)); - } - - @Test - void testGetBudgetRemainingAmountForEncumbranceWhenUnavailableGreaterThanTotalFunding() { - budget.withNetTransfers(0d) - .withAllowableExpenditure(100d) - .withEncumbered(100d) - .withAwaitingPayment(10d) - .withExpenditures(0d) - .withAllocated(100d); - MonetaryAmount amount = restrictionService.getBudgetRemainingAmount(budget, currency, new Transaction().withAmount(10d)); - assertThat(amount.getNumber().doubleValue(), is(10d)); - } - -} diff --git a/src/test/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionServiceTest.java b/src/test/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionServiceTest.java deleted file mode 100644 index 4de3b234..00000000 --- a/src/test/java/org/folio/service/transactions/restriction/PendingPaymentRestrictionServiceTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.folio.service.transactions.restriction; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Collections; -import java.util.UUID; - -import javax.money.MonetaryAmount; - -import org.folio.rest.jaxrs.model.Budget; -import org.folio.rest.jaxrs.model.Error; -import org.folio.rest.jaxrs.model.Errors; -import org.folio.rest.jaxrs.model.Ledger; -import org.folio.rest.jaxrs.model.Parameter; -import org.folio.rest.jaxrs.model.Transaction; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; - -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.handler.HttpException; - -public class PendingPaymentRestrictionServiceTest { - - private AutoCloseable mockitoMocks; - - @InjectMocks - private PendingPaymentRestrictionService restrictionService; - - private String fundId = UUID.randomUUID().toString(); - private String fiscalYearId = UUID.randomUUID().toString(); - private Budget budget; - private String currency = "USD"; - - @BeforeEach - public void initMocks(){ - mockitoMocks = MockitoAnnotations.openMocks(this); - - budget = new Budget() - .withFiscalYearId(fiscalYearId) - .withAwaitingPayment(0d) - .withAllocated(100d) - .withAvailable(100d) - .withEncumbered(0d) - .withUnavailable(0d) - .withFundId(fundId); - } - - @AfterEach - public void afterEach() throws Exception { - mockitoMocks.close(); - } - - @Test - void TestHandleValidationErrorExceptionThrown() { - HttpException thrown = assertThrows( - HttpException.class, - () -> restrictionService.handleValidationError(new Transaction()), - "Expected handleValidationError() to throw, but it didn't" - ); - - Parameter parameter = new Parameter().withKey("fromFundId") - .withValue("null"); - Error error = new Error().withCode("-1") - .withMessage("may not be null") - .withParameters(Collections.singletonList(parameter)); - Errors errors = new Errors().withErrors(Collections.singletonList(error)).withTotalRecords(1); - assertThat(thrown.getStatusCode(), is(422)); - assertThat(thrown.getPayload(), is(JsonObject.mapFrom(errors).encode())); - } - - @Test - void testHandleValidationErrorValidTransaction() { - assertDoesNotThrow(() -> restrictionService.handleValidationError(new Transaction().withFromFundId(fundId))); - } - - @Test - void testGetBudgetRemainingAmountForEncumbrance() { - budget.withNetTransfers(20d) - .withAllowableExpenditure(110d) - .withEncumbered(10d) - .withAwaitingPayment(11d) - .withExpenditures(90d) - .withAvailable(21d) // should not be used - .withUnavailable(22d); // should not be used - MonetaryAmount amount = restrictionService.getBudgetRemainingAmount(budget, currency, new Transaction().withAmount(5d)); - assertThat(amount.getNumber().doubleValue(), is(26d)); - } - - @Test - void testIsTransactionOverspendRestrictedWithEmptyAllowableExpenditure() { - Assertions.assertFalse(restrictionService.isTransactionOverspendRestricted(new Ledger().withRestrictExpenditures(true), budget.withAllowableExpenditure(null))); - } - - @Test - void testIsTransactionOverspendRestrictedWithRestrictExpendituresIsFalse() { - Assertions.assertFalse(restrictionService.isTransactionOverspendRestricted(new Ledger().withRestrictExpenditures(false), budget.withAllowableExpenditure(110d))); - } - - @Test - void testIsTransactionOverspendRestrictedWithRestrictExpendituresIsTrueWithSpecifiedAllowableExpenditure() { - Assertions.assertTrue(restrictionService.isTransactionOverspendRestricted(new Ledger().withRestrictExpenditures(true), budget.withAllowableExpenditure(110d))); - } -} diff --git a/src/test/resources/data/transactions/zallocation_AFRICAHIST-FY24_ANZHIST-FY24.json b/src/test/resources/data/transactions/zallocation_AFRICAHIST-FY24_ANZHIST-FY24.json deleted file mode 100644 index 2719dc6a..00000000 --- a/src/test/resources/data/transactions/zallocation_AFRICAHIST-FY24_ANZHIST-FY24.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "c0c32f67-99e2-4cc7-9b80-cfa8fffc7de2", - "amount": 11000.00, - "currency": "USD", - "description": "PO_Line: History of Incas", - "fiscalYearId": "7a4c4d30-3b63-4102-8e2d-3ee5792d7d02", - "fromFundId": "67cd0046-e4f1-4e4f-9024-adf0b0039d09", - "source": "User", - "sourceFiscalYearId": "7a4c4d30-3b63-4102-8e2d-3ee5792d7d02", - "toFundId": "69640328-788e-43fc-9c3c-af39e243f3b7", - "transactionType": "Allocation" -}