From 566b065dcd5a4cbe065fabd376b153ce1a46fd9d Mon Sep 17 00:00:00 2001 From: Carlos Belizon Date: Mon, 13 Jun 2022 17:33:53 +0200 Subject: [PATCH] Release 4.12.0 --- README.md | 101 +++--- gradle.properties | 2 +- infrastructure/build.gradle | 2 + .../batchjob/AbstractBatchJob.java | 35 ++ .../batchjob/AbstractExtractBatchJob.java | 14 + .../batchjob/AbstractRetryBatchJob.java | 14 + .../infrastructure/batchjob/BatchJob.java | 12 + .../batchjob/BatchJobContext.java | 10 + .../batchjob/BatchJobExecutor.java | 87 ++++- .../BatchJobFailedItemRepository.java | 15 + .../BatchJobFailedItemServiceImpl.java | 27 +- .../batchjob/BatchJobItemEnricher.java | 20 ++ .../BatchJobItemValidationResult.java | 20 ++ .../BatchJobItemValidationStatus.java | 18 ++ .../batchjob/BatchJobItemValidator.java | 19 ++ .../batchjob/BatchJobPreProcessor.java | 21 ++ .../batchjob/BatchJobProcessingListener.java | 28 ++ .../infrastructure/batchjob/BatchJobType.java | 20 ++ ...ractBatchJobProcessingListenerSupport.java | 22 ++ ...FailureBatchJobItemProcessingListener.java | 5 +- ...LoggingBatchJobItemProcessingListener.java | 11 + .../quartz/BatchJobContextQuartzAdapter.java | 9 + .../quartz/QuartzBatchJobAdapter.java | 2 +- .../quartz/QuartzBatchJobContextFactory.java | 6 +- .../QuartzBatchJobContextFactoryImpl.java | 6 +- .../InfrastructureDatasourceConfig.java | 8 +- .../itemlinks/ItemLinksConfiguration.java | 14 + .../ItemLinksModelEntityConverter.java | 24 ++ .../itemlinks/entities/ItemLinkEntity.java | 38 +++ .../model/HyperwalletItemLinkLocator.java | 24 ++ .../itemlinks/model/HyperwalletItemTypes.java | 10 + .../model/ItemLinkExternalSystem.java | 11 + .../itemlinks/model/ItemLinkLocator.java | 29 ++ .../model/MiraklItemLinkLocator.java | 24 ++ .../itemlinks/model/MiraklItemTypes.java | 10 + .../repository/ItemLinkRepository.java | 16 + .../itemlinks/services/ItemLinksService.java | 49 +++ .../services/ItemLinksServiceImpl.java | 70 ++++ .../main/resources/infrastructure.properties | 1 + .../batchjob/AbstractBatchJobTest.java | 17 +- .../batchjob/AbstractExtractBatchJobTest.java | 35 ++ .../batchjob/AbstractRetryBatchJobTest.java | 35 ++ .../batchjob/BatchJobExecutorTest.java | 99 +++++- .../BatchJobFailedItemServiceImplITTest.java | 70 ++++ ...JobFailedItemServiceImplITTestContext.java | 23 ++ .../BatchJobFailedItemServiceImplTest.java | 45 ++- .../AbstractBatchJobTestSupport.java | 166 +++++++++- ...chJobFailedItemCacheTestContextITTest.java | 2 +- .../BatchJobFailedItemRetryITTest.java | 80 +++-- ...tContext.java => BatchJobTestContext.java} | 2 +- ...ureBatchJobItemProcessingListenerTest.java | 25 +- ...ingBatchJobItemProcessingListenerTest.java | 14 + .../quartz/QuartzBatchJobAdapterTest.java | 3 +- .../QuartzBatchJobContextFactoryImplTest.java | 6 +- .../InfrastructureDatasourceConfigTest.java | 5 +- .../itemlinks/ItemLinksITTest.java | 106 ++++++ .../itemlinks/ItemLinksITTestContext.java | 25 ++ .../ItemLinksModelEntityConverterTest.java | 54 ++++ .../services/ItemLinksServiceImplTest.java | 95 ++++++ .../resources/infrastructure-test.properties | 1 + .../AbstractAccountingDocumentBatchJob.java | 41 +++ ...bstractAccountingDocumentBatchJobItem.java | 21 ++ ...ccountingDocumentBatchJobItemEnricher.java | 66 ++++ ...countingDocumentBatchJobItemValidator.java | 40 +++ ...ccountingDocumentBatchJobPreProcessor.java | 41 +++ .../creditnotes/CreditNoteExtractJobItem.java | 9 +- .../CreditNotesExtractBatchJob.java | 23 +- .../creditnotes/CreditNotesRetryBatchJob.java | 23 +- .../invoices/InvoiceExtractJobItem.java | 9 +- .../invoices/InvoicesExtractBatchJob.java | 23 +- .../invoices/InvoicesRetryBatchJob.java | 22 +- .../hmc/AccountingDocumentsLinksService.java | 45 +++ .../AccountingDocumentsLinksServiceImpl.java | 89 +++++ .../mirakl/MiraklInvoiceLinksService.java | 32 ++ ...AccountingDocumentsExtractServiceImpl.java | 134 +------- .../MiraklCreditNotesExtractServiceImpl.java | 6 +- .../impl/MiraklInvoiceLinksServiceImpl.java | 120 +++++++ .../MiraklInvoicesExtractServiceImpl.java | 6 +- .../MiraklInvoicesExtractServiceMock.java | 6 +- ...bstractAccountingDocumentBatchJobTest.java | 76 +++++ ...ngDocumentBatchJobHandlersTestSupport.java | 32 ++ ...ntingDocumentBatchJobItemEnricherTest.java | 65 ++++ ...tingDocumentBatchJobItemValidatorTest.java | 68 ++++ ...ntingDocumentBatchJobPreProcessorTest.java | 50 +++ .../CreditNoteExtractJobItemTest.java | 17 +- .../CreditNotesExtractBatchJobTest.java | 6 + .../CreditNotesRetryBatchJobTest.java | 8 +- .../invoices/InvoiceExtractJobItemTest.java | 17 +- .../invoices/InvoicesExtractBatchJobTest.java | 6 + .../invoices/InvoicesRetryBatchJobTest.java | 8 +- ...countingDocumentsLinksServiceImplTest.java | 101 ++++++ ...untingDocumentsExtractServiceImplTest.java | 167 +++++----- ...raklCreditNotesExtractServiceImplTest.java | 305 +----------------- .../MiraklInvoiceLinksServiceImplTest.java | 192 +++++++++++ .../MiraklInvoicesExtractServiceImplTest.java | 298 +---------------- ...sStakeholdersDocumentsExtractBatchJob.java | 2 +- .../SellersDocumentsExtractBatchJob.java | 2 +- .../BankAccountExtractBatchJob.java | 2 +- .../bankaccount/BankAccountRetryBatchJob.java | 7 +- .../BusinessStakeholdersExtractBatchJob.java | 2 +- .../BusinessStakeholdersRetryBatchJob.java | 7 +- ...akeholdersRetryBatchJobItemsExtractor.java | 35 +- .../IndividualSellersExtractBatchJob.java | 7 +- .../IndividualSellersRetryBatchJob.java | 8 +- .../ProfessionalSellersExtractBatchJob.java | 2 +- .../ProfessionalSellersRetryBatchJob.java | 7 +- ...oldersRetryBatchJobItemsExtractorTest.java | 46 ++- 107 files changed, 3091 insertions(+), 970 deletions(-) create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJob.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJob.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemEnricher.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationResult.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationStatus.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidator.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobPreProcessor.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobType.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/ItemLinksConfiguration.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverter.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/entities/ItemLinkEntity.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemLinkLocator.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemTypes.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkExternalSystem.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkLocator.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemLinkLocator.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemTypes.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/repository/ItemLinkRepository.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksService.java create mode 100644 infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImpl.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJobTest.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJobTest.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTest.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTestContext.java rename infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/{BatchJobFailedItemCacheTestContext.java => BatchJobTestContext.java} (96%) create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTest.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTestContext.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverterTest.java create mode 100644 infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImplTest.java create mode 100644 invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJob.java create mode 100644 invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobItem.java create mode 100644 invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricher.java create mode 100644 invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidator.java create mode 100644 invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessor.java create mode 100644 invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/AccountingDocumentsLinksService.java create mode 100644 invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImpl.java create mode 100644 invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/MiraklInvoiceLinksService.java create mode 100644 invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImpl.java create mode 100644 invoices/src/test/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobTest.java create mode 100644 invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobHandlersTestSupport.java create mode 100644 invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricherTest.java create mode 100644 invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidatorTest.java create mode 100644 invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessorTest.java create mode 100644 invoices/src/test/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImplTest.java create mode 100644 invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImplTest.java diff --git a/README.md b/README.md index 4f542de9..6c492a4d 100644 --- a/README.md +++ b/README.md @@ -22,57 +22,58 @@ The following table provides every environment variable, and describes whether t optional (either by having a default value or only being used when specific features are enabled), and a description and example. -| ENVIRONMENT VARIABLE | MANDATORY | DESCRIPTION | EXAMPLE VALUE | -|-------------------------------------------------------------------|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| -| `PAYPAL_MIRAKL_SDK_USER` | YES | Mirakl username for accessing the Mirakl Java SDK. | `yourCompanyName` | -| `PAYPAL_MIRAKL_SDK_PASSWORD` | YES | Mirakl password, for accessing the Mirakl Java SDK. | `secret` | -| `PAYPAL_MIRAKL_OPERATOR_API_KEY` | YES | The Mirakl operator API key generated for your operator account. | `c262b297-c8a7-45a5-a22f-a0d9fe25132a` | -| `PAYPAL_MIRAKL_OPERATOR_TIME_ZONE` | NO (default value: `UTC`) | The Mirakl time zone of your Mirakl instance. Possible values are documented [here](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html). | `GMT`, `Europe/London` | -| `PAYPAL_MIRAKL_ENVIRONMENT` | YES | The URL for your Mirakl environment's API (provided by Mirakl). | `https://yourCompany.mirakl.net/api` | -| `PAYPAL_HYPERWALLET_API_SERVER` | YES | The URL for your Hyperwallet environment's API (provided by Hyperwallet). | `https://uat-api.paylution.com` | -| `PAYPAL_HYPERWALLET_API_USERNAME` | YES | Hyperwallet environment username (provided by Hyperwallet). | `restapiuser@000001` | -| `PAYPAL_HYPERWALLET_API_PASSWORD` | YES | Hyperwallet environment password (provided by Hyperwallet). | `yourSecret` | -| `PAYPAL_HYPERWALLET_PROGRAM_TOKENS` | YES | A set of comma separated values based on the program hierarchy provided by Hyperwallet (See the Program Configuration section below). | `DEFAULT` | -| `PAYPAL_HYPERWALLET_PROGRAM_TOKEN_USERS_DEFAULT` | YES | The program token for contacting the Hyperwallet API on the `/users` path for `DEFAULT` program. | `prg-6541532-as1a23s242-12as124-as2454` | -| `PAYPAL_HYPERWALLET_PROGRAM_TOKEN_PAYMENTS_DEFAULT` | YES (default value: `DEFAULT`) | The program token for contacting the Hyperwallet API on the `/payments` path for `DEFAULT` program. | `prg-54545a532-asda2refs43-as2fd35-das233` | -| `PAYPAL_HYPERWALLET_OPERATOR_BANK_ACCOUNT_TOKEN_DEFAULT` | NO | The transfer bank account token where commissions will be paid out too. Only needed if the operator commissions feature is being used (see the Operator Commissions section below). | `trm-2646asas54-21asdas5642-xasa45sxx` | -| `PAYPAL_HYPERWALLET_OPERATOR_COMMISSIONS_ENABLED` | NO (default value: `true`) | By default, the operator commissions feature is enabled. | Possible values:`true` or `false` | -| `PAYPAL_HYPERWALLET_OPERATOR_CREDIT_NOTES_ENABLED` | NO (default value: `false`) | By default, the direct processing of manual credit notes is disabled. This can be enabled to facilitate manual testing of payout scenarios. | Possible values:`true` or `false` | -| `PAYPAL_BRAINTREE_MERCHANT_ID` | NO | BrainTree merchant id provided by BrainTree. | `myBrainTreeMerchantId` | -| `PAYPAL_BRAINTREE_PUBLIC_KEY` | NO | BrainTree public key provided by BrainTree. | `myBrainTreePublicKey` | -| `PAYPAL_BRAINTREE_PRIVATE_KEY` | NO | BrainTree private key provided by BrainTree. | `myBrainTreePrivateKey` | -| `PAYPAL_BRAINTREE_REPORT_ENVIRONMENT` | NO | By default the value is pointing to the `sandbox` environment. In case you want to point to production environment, set the value to `production`. | `sandbox` | -| `PAYPAL_SERVER_EMAIL_HOST` | NO | The URL where your POP3/SMTP server is hosted. If you're using the Docker Compose script provided in this repo, use `smtp`. | `smtp.example.com` | -| `PAYPAL_SERVER_EMAIL_PORT` | NO | The port used by your POP3/SMTP server. If you're using the Docker Compose script provided in this repo, use `1025`. | `1025` | -| `PAYPAL_MAIL_SMTP_AUTH` | NO | Whether or not authentication is needed for accessing the POP3/SMTP mail server. | Possible values: `true` or `false` | -| `PAYPAL_MAIL_USER_NAME` | NO | The username credential for using the POP3/SMTP server. It can be left empty if `PAYPAL_MAIL_SMTP_AUTH` is set to `false`. | `smtp-username` | -| `PAYPAL_MAIL_USER_PASSWORD` | NO | The password credential for using the POP3/SMTP server. It can be left empty if `PAYPAL_MAIL_SMTP_AUTH` is set to `false`. | `smtp-pass` | -| `PAYPAL_MAIL_SMTP_STARTTLS_ENABLE` | NO | Whether or not TLS is needed for establishing connection with the POP3/SMTP server. | Possible values:`true` or `false` | -| `PAYPAL_HYPERWALLET_MAIL_RECIPIENT` | NO (default value: `recipient1@test.com` | The email recipient for the errors thrown by the connector. | `recipient@email.com` | -| `PAYPAL_HYPERWALLET_MAIL_FROM` | NO (default value: `from@email.com`) | The from email that appears on the emails sent by the connector. | `from@email.com` | -| `PAYPAL_SPRING_PROFILE_ACTIVE` | YES | The Profile to execute/deploy the connector service on. Possible options: `dev`, `qa`, `prod`, `encrypted`, `financial-report`. `prod` should be used when in production and during user testing, whenever connecting to Hyperwallet and Mirakl platforms. `qa` or `dev` provide levels of mocking when Hyperwallet or Mirakl platforms are not available and should only be used in advanced cases. | `prod,financial-report` | -| `PAYPAL_HYPERWALLET_EXTRACT_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0 0 1/1 * ? *` ) | The cron expression to trigger periodically the Sellers Extract Job. | `0 0 0 1/1 * ? *` | -| `PAYPAL_HYPERWALLET_RETRY_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *` ) | The cron expression to trigger periodically the Sellers Retry Job. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_EXTRACT_PROFESSIONAL_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0 0 1/1 * ? *`) | The cron expression to trigger periodically the Professional Sellers Extract Job. | `0 0 0 1/1 * ? *` | -| `PAYPAL_HYPERWALLET_RETRY_PROFESSIONAL_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Professional Sellers Retry Job. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_RETRY_BUSINESS_STAKEHOLDERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Business Stakeholders Retry Job. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_BANK_ACCOUNT_EXTRACT_CRON_EXPRESSION` | NO (default value: `0 30 0 1/1 * ? *`) | The cron expression to trigger periodically the Bank account Extract Job. | `0 30 0 1/1 * ? *` | -| `PAYPAL_HYPERWALLET_BANK_ACCOUNT_RETRY_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Bank account Retry Job. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_EXTRACT_INVOICES_CRON_EXPRESSION` | NO (default value: `1 0 0 1/1 * ? *`) | The cron expression to trigger periodically the Invoices Extract Job,. | `1 0 0 1/1 * ? *` | -| `PAYPAL_HYPERWALLET_RETRY_INVOICES_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Invoices Retry Job,. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_RETRY_CREDITNOTES_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Credit Notes Retry Job,. | `0 0/15 * ? * * *` | -| `PAYPAL_HYPERWALLET_RETRY_FAILED_NOTIFICATIONS_CRON_EXPRESSION` | NO (default value: `0 0/15 * * * ? *`) | The cron expression to trigger periodically the Failed Notifications Retry Job. | `1 30 0 1/1 * ? *` | -| `PAYPAL_HYPERWALLET_KEY_SET_LOCATION` | NO (default value: `https://uat-api.paylution.com/jwkset`) | The key set uri. For pointing to production, replace the value by `https://api.paylution.com/jwkset` | `https://uat-api.paylution.com/jwkset` | -| `PAYPAL_HYPERWALLET_ENCRYPTION_ALGORITHM` | NO | The algorithm used for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `RSA-OAEP-256` | -| `PAYPAL_HYPERWALLET_SIGN_ALGORITHM` | NO | The sign algorithm for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `RS256` | -| `PAYPAL_HYPERWALLET_ENCRYPTION_METHOD` | NO | The encryption method used for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `A256CBC-HS512` | -| `PAYPAL_HYPERWALLET_PRIVATE_JWK_JSON_LOCATION` | NO | The private/public JWK set location | `/your/path/to/private/keys/jwk_set.key` | -| `PAYPAL_HYPERWALLET_PUBLIC_JWK_JSON_LOCATION` | NO | The public JWK set location. | `https://example.com/hw/shared` | -| `PAYPAL_HYPERWALLET_RETRY_NOTIFICATIONS` | NO (default value: `true`) | Whether or not Hyperwallet notifications should be retried when an error occurs (e.g. connection issues). If set to `true`, any notification that fails is stored in the database and automatically restarted up to `PAYPAL_HYPERWALLET_MAX_AMOUNT_OF_NOTIFICATION_RETRIES` times. If set to `false`, notifications are not stored or retried | Possible values:`true` or `false` | -| `PAYPAL_HYPERWALLET_MAX_AMOUNT_OF_NOTIFICATION_RETRIES` | NO (default value: `5`) | Sets the amount of retries a Hyperwallet notification operation can be retried before it is discarded. Whenever a notification is discarded, an email is sent to the integrators so it can be analyzed and addressed | Possible values: Any positive integer | -| `PAYPAL_MOCK_SERVER_URL` | YES | The URL to your webhook/mock server. Only used when running with the `qa` Spring profile. | `https://mockserver.example.com` | +| ENVIRONMENT VARIABLE | MANDATORY | DESCRIPTION | EXAMPLE VALUE | +|-------------------------------------------------------------------|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| +| `PAYPAL_MIRAKL_SDK_USER` | YES | Mirakl username for accessing the Mirakl Java SDK. | `yourCompanyName` | +| `PAYPAL_MIRAKL_SDK_PASSWORD` | YES | Mirakl password, for accessing the Mirakl Java SDK. | `secret` | +| `PAYPAL_MIRAKL_OPERATOR_API_KEY` | YES | The Mirakl operator API key generated for your operator account. | `c262b297-c8a7-45a5-a22f-a0d9fe25132a` | +| `PAYPAL_MIRAKL_OPERATOR_TIME_ZONE` | NO (default value: `UTC`) | The Mirakl time zone of your Mirakl instance. Possible values are documented [here](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html). | `GMT`, `Europe/London` | +| `PAYPAL_MIRAKL_ENVIRONMENT` | YES | The URL for your Mirakl environment's API (provided by Mirakl). | `https://yourCompany.mirakl.net/api` | +| `PAYPAL_HYPERWALLET_API_SERVER` | YES | The URL for your Hyperwallet environment's API (provided by Hyperwallet). | `https://uat-api.paylution.com` | +| `PAYPAL_HYPERWALLET_API_USERNAME` | YES | Hyperwallet environment username (provided by Hyperwallet). | `restapiuser@000001` | +| `PAYPAL_HYPERWALLET_API_PASSWORD` | YES | Hyperwallet environment password (provided by Hyperwallet). | `yourSecret` | +| `PAYPAL_HYPERWALLET_PROGRAM_TOKENS` | YES | A set of comma separated values based on the program hierarchy provided by Hyperwallet (See the Program Configuration section below). | `DEFAULT` | +| `PAYPAL_HYPERWALLET_PROGRAM_TOKEN_USERS_DEFAULT` | YES | The program token for contacting the Hyperwallet API on the `/users` path for `DEFAULT` program. | `prg-6541532-as1a23s242-12as124-as2454` | +| `PAYPAL_HYPERWALLET_PROGRAM_TOKEN_PAYMENTS_DEFAULT` | YES (default value: `DEFAULT`) | The program token for contacting the Hyperwallet API on the `/payments` path for `DEFAULT` program. | `prg-54545a532-asda2refs43-as2fd35-das233` | +| `PAYPAL_HYPERWALLET_OPERATOR_BANK_ACCOUNT_TOKEN_DEFAULT` | NO | The transfer bank account token where commissions will be paid out too. Only needed if the operator commissions feature is being used (see the Operator Commissions section below). | `trm-2646asas54-21asdas5642-xasa45sxx` | +| `PAYPAL_HYPERWALLET_OPERATOR_COMMISSIONS_ENABLED` | NO (default value: `true`) | By default, the operator commissions feature is enabled. | Possible values:`true` or `false` | +| `PAYPAL_HYPERWALLET_OPERATOR_CREDIT_NOTES_ENABLED` | NO (default value: `false`) | By default, the direct processing of manual credit notes is disabled. This can be enabled to facilitate manual testing of payout scenarios. | Possible values:`true` or `false` | +| `PAYPAL_BRAINTREE_MERCHANT_ID` | NO | BrainTree merchant id provided by BrainTree. | `myBrainTreeMerchantId` | +| `PAYPAL_BRAINTREE_PUBLIC_KEY` | NO | BrainTree public key provided by BrainTree. | `myBrainTreePublicKey` | +| `PAYPAL_BRAINTREE_PRIVATE_KEY` | NO | BrainTree private key provided by BrainTree. | `myBrainTreePrivateKey` | +| `PAYPAL_BRAINTREE_REPORT_ENVIRONMENT` | NO | By default the value is pointing to the `sandbox` environment. In case you want to point to production environment, set the value to `production`. | `sandbox` | +| `PAYPAL_SERVER_EMAIL_HOST` | NO | The URL where your POP3/SMTP server is hosted. If you're using the Docker Compose script provided in this repo, use `smtp`. | `smtp.example.com` | +| `PAYPAL_SERVER_EMAIL_PORT` | NO | The port used by your POP3/SMTP server. If you're using the Docker Compose script provided in this repo, use `1025`. | `1025` | +| `PAYPAL_MAIL_SMTP_AUTH` | NO | Whether or not authentication is needed for accessing the POP3/SMTP mail server. | Possible values: `true` or `false` | +| `PAYPAL_MAIL_USER_NAME` | NO | The username credential for using the POP3/SMTP server. It can be left empty if `PAYPAL_MAIL_SMTP_AUTH` is set to `false`. | `smtp-username` | +| `PAYPAL_MAIL_USER_PASSWORD` | NO | The password credential for using the POP3/SMTP server. It can be left empty if `PAYPAL_MAIL_SMTP_AUTH` is set to `false`. | `smtp-pass` | +| `PAYPAL_MAIL_SMTP_STARTTLS_ENABLE` | NO | Whether or not TLS is needed for establishing connection with the POP3/SMTP server. | Possible values:`true` or `false` | +| `PAYPAL_HYPERWALLET_MAIL_RECIPIENT` | NO (default value: `recipient1@test.com` | The email recipient for the errors thrown by the connector. | `recipient@email.com` | +| `PAYPAL_HYPERWALLET_MAIL_FROM` | NO (default value: `from@email.com`) | The from email that appears on the emails sent by the connector. | `from@email.com` | +| `PAYPAL_SPRING_PROFILE_ACTIVE` | YES | The Profile to execute/deploy the connector service on. Possible options: `dev`, `qa`, `prod`, `encrypted`, `financial-report`. `prod` should be used when in production and during user testing, whenever connecting to Hyperwallet and Mirakl platforms. `qa` or `dev` provide levels of mocking when Hyperwallet or Mirakl platforms are not available and should only be used in advanced cases. | `prod,financial-report` | +| `PAYPAL_HYPERWALLET_EXTRACT_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0 0 1/1 * ? *` ) | The cron expression to trigger periodically the Sellers Extract Job. | `0 0 0 1/1 * ? *` | +| `PAYPAL_HYPERWALLET_RETRY_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *` ) | The cron expression to trigger periodically the Sellers Retry Job. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_EXTRACT_PROFESSIONAL_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0 0 1/1 * ? *`) | The cron expression to trigger periodically the Professional Sellers Extract Job. | `0 0 0 1/1 * ? *` | +| `PAYPAL_HYPERWALLET_RETRY_PROFESSIONAL_SELLERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Professional Sellers Retry Job. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_RETRY_BUSINESS_STAKEHOLDERS_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Business Stakeholders Retry Job. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_BANK_ACCOUNT_EXTRACT_CRON_EXPRESSION` | NO (default value: `0 30 0 1/1 * ? *`) | The cron expression to trigger periodically the Bank account Extract Job. | `0 30 0 1/1 * ? *` | +| `PAYPAL_HYPERWALLET_BANK_ACCOUNT_RETRY_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Bank account Retry Job. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_EXTRACT_INVOICES_CRON_EXPRESSION` | NO (default value: `1 0 0 1/1 * ? *`) | The cron expression to trigger periodically the Invoices Extract Job,. | `1 0 0 1/1 * ? *` | +| `PAYPAL_HYPERWALLET_RETRY_INVOICES_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Invoices Retry Job,. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_RETRY_CREDITNOTES_CRON_EXPRESSION` | NO (default value: `0 0/15 * ? * * *`) | The cron expression to trigger periodically the Credit Notes Retry Job,. | `0 0/15 * ? * * *` | +| `PAYPAL_HYPERWALLET_RETRY_FAILED_NOTIFICATIONS_CRON_EXPRESSION` | NO (default value: `0 0/15 * * * ? *`) | The cron expression to trigger periodically the Failed Notifications Retry Job. | `1 30 0 1/1 * ? *` | +| `PAYPAL_HYPERWALLET_KEY_SET_LOCATION` | NO (default value: `https://uat-api.paylution.com/jwkset`) | The key set uri. For pointing to production, replace the value by `https://api.paylution.com/jwkset` | `https://uat-api.paylution.com/jwkset` | +| `PAYPAL_HYPERWALLET_ENCRYPTION_ALGORITHM` | NO | The algorithm used for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `RSA-OAEP-256` | +| `PAYPAL_HYPERWALLET_SIGN_ALGORITHM` | NO | The sign algorithm for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `RS256` | +| `PAYPAL_HYPERWALLET_ENCRYPTION_METHOD` | NO | The encryption method used for Layer7 encryption ([Hyperwallet encryption](https://docs.hyperwallet.com/content/api/v4/overview/payload-encryption)) | `A256CBC-HS512` | +| `PAYPAL_HYPERWALLET_PRIVATE_JWK_JSON_LOCATION` | NO | The private/public JWK set location | `/your/path/to/private/keys/jwk_set.key` | +| `PAYPAL_HYPERWALLET_PUBLIC_JWK_JSON_LOCATION` | NO | The public JWK set location. | `https://example.com/hw/shared` | +| `PAYPAL_HYPERWALLET_RETRY_NOTIFICATIONS` | NO (default value: `true`) | Whether or not Hyperwallet notifications should be retried when an error occurs (e.g. connection issues). If set to `true`, any notification that fails is stored in the database and automatically restarted up to `PAYPAL_HYPERWALLET_MAX_AMOUNT_OF_NOTIFICATION_RETRIES` times. If set to `false`, notifications are not stored or retried | Possible values:`true` or `false` | +| `PAYPAL_HYPERWALLET_MAX_AMOUNT_OF_NOTIFICATION_RETRIES` | NO (default value: `5`) | Sets the amount of retries a Hyperwallet notification operation can be retried before it is discarded. Whenever a notification is discarded, an email is sent to the integrators so it can be analyzed and addressed | Possible values: Any positive integer | +| `PAYPAL_MOCK_SERVER_URL` | YES | The URL to your webhook/mock server. Only used when running with the `qa` Spring profile. | `https://mockserver.example.com` | | `PAYPAL_HYPERWALLET_STK_MANDATORY_EMAIL` | NO (default value: `false`) | By default, the business stakeholder email is not mandatory in Hyperwallet. | Possible values:`true` or `false` | -| `PAYPAL_HYPERWALLET_SEARCH_INVOICES_MAX_DAYS` | NO (default value: `15`) | Size in days of the search window when searching invoices by id. Used by invoice retry jobs. | Possible values: Any positive integer | +| `PAYPAL_HYPERWALLET_SEARCH_INVOICES_MAX_DAYS` | NO (default value: `15`) | Size in days of the search window when searching invoices by id. Used by invoice retry jobs. | Possible values: Any positive integer | +| `PAYPAL_HYPERWALLET_MAX_FAILED_ITEMS_TO_BE_PROCESSED` | NO (default value: `100`) | As some Mirakl APIs have a maximun number of items to be requested it sets the amount of max number failed items to be processed on retry jobs | Possible values: Any positive integer | A sample .env file is provided in this repository, primarily for use in the Docker container deployment scenario ( documented below). The .env file can also be used to source environment variables for use in local deployment, if you diff --git a/gradle.properties b/gradle.properties index 2deb5fb4..73729203 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ springBoot = 2.6.6 dependencyManagement = 1.0.11.RELEASE -projectVersion = 4.11.0 +projectVersion = 4.12.0 paypalHyperwalletDockerRepository = hyperwallet-mirakl-connector org.gradle.jvmargs = -XX:PermSize=1024M -XX:MaxPermSize=1024M diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 1d5899da..e0b129ae 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -32,6 +32,8 @@ dependencies { testImplementation 'com.callibrity.logging:log-tracker:1.0.1' testImplementation 'org.awaitility:awaitility' + testAnnotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final" + runtimeOnly 'net.minidev:json-smart:2.3' api 'com.hyperwallet:sdk:2.2.5' api 'com.mirakl:mmp-sdk-operator:4.10.0' diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractBatchJob.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractBatchJob.java index 724a46de..78eb8f1a 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractBatchJob.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractBatchJob.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.Collection; +import java.util.Optional; /** * Base class for all batch jobs. Managed the job execution main flow including batch @@ -15,6 +16,18 @@ public abstract class AbstractBatchJob getBatchJobItemsExtractor(); + protected Optional> getBatchJobItemValidator() { + return Optional.empty(); + } + + protected Optional> getBatchJobPreProcessor() { + return Optional.empty(); + } + + protected Optional> getBatchJobItemEnricher() { + return Optional.empty(); + } + /** * {@inheritDoc} */ @@ -31,4 +44,26 @@ public void processItem(final C ctx, final T jobItem) { getBatchJobItemProcessor().processItem(ctx, jobItem); } + @Override + public void prepareForItemProcessing(C ctx, Collection itemsToBeProcessed) { + getBatchJobPreProcessor().ifPresent(it -> it.prepareForProcessing(ctx, itemsToBeProcessed)); + } + + @Override + public T enrichItem(C ctx, T jobItem) { + BatchJobItemEnricher batchJobItemEnricher = getBatchJobItemEnricher().orElse(null); + return batchJobItemEnricher != null ? batchJobItemEnricher.enrichItem(ctx, jobItem) : jobItem; + } + + @Override + public BatchJobItemValidationResult validateItem(C ctx, T jobItem) { + BatchJobItemValidator batchJobItemValidator = getBatchJobItemValidator().orElse(null); + return batchJobItemValidator != null ? batchJobItemValidator.validateItem(ctx, jobItem) + : buildValidValidationResult(); + } + + private BatchJobItemValidationResult buildValidValidationResult() { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.VALID).build(); + } + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJob.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJob.java new file mode 100644 index 00000000..2435b1e2 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJob.java @@ -0,0 +1,14 @@ +package com.paypal.infrastructure.batchjob; + +/** + * Abstract class for all jobs of type Extract + */ +public abstract class AbstractExtractBatchJob> + extends AbstractBatchJob { + + @Override + public BatchJobType getType() { + return BatchJobType.EXTRACT; + } + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJob.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJob.java new file mode 100644 index 00000000..615cb983 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJob.java @@ -0,0 +1,14 @@ +package com.paypal.infrastructure.batchjob; + +/** + * Abstract class for all jobs of type Retry + */ +public abstract class AbstractRetryBatchJob> + extends AbstractBatchJob { + + @Override + public BatchJobType getType() { + return BatchJobType.RETRY; + } + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJob.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJob.java index 087dfb3f..62cdea69 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJob.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJob.java @@ -16,6 +16,12 @@ public interface BatchJob> */ Collection getItems(C ctx); + void prepareForItemProcessing(C ctx, Collection itemsToBeProcessed); + + T enrichItem(C ctx, final T jobItem); + + BatchJobItemValidationResult validateItem(final C ctx, final T jobItem); + /** * Process an item with the given context. * @param ctx the batch job context. @@ -23,4 +29,10 @@ public interface BatchJob> */ void processItem(final C ctx, final T jobItem); + /** + * Return the type of this job + * @return a {@link BatchJobType}+ + */ + BatchJobType getType(); + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobContext.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobContext.java index 96255068..340465fe 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobContext.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobContext.java @@ -13,6 +13,10 @@ public interface BatchJobContext { */ String getJobName(); + /** + * Returns the job unique identifier. + * @return the job unique identifier. + */ String getJobUuid(); /** @@ -87,4 +91,10 @@ public interface BatchJobContext { */ JobExecutionContext getJobExecutionContext(); + /** + * Returns the job which is being executed. + * @return the {@link BatchJob}. + */ + > BatchJob getBatchJob(); + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobExecutor.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobExecutor.java index 37984edd..276d55bb 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobExecutor.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobExecutor.java @@ -20,7 +20,11 @@ public > void execute(Batch try { reportBatchJobStarted(ctx); - retrieveBatchItems(job, ctx).forEach(i -> processItem(job, ctx, i)); + Collection itemsToBeProcessed = retrieveBatchItems(job, ctx); + + prepareForProcessing(job, ctx, itemsToBeProcessed); + + itemsToBeProcessed.forEach(i -> processItem(job, ctx, i)); reportBatchJobFinished(ctx); } @@ -47,12 +51,42 @@ private > Collection ret } } + private , C extends BatchJobContext> void prepareForProcessing(BatchJob job, + C context, Collection itemsToBeProcessed) { + try { + reportPreparationForProcessingStarted(context); + + job.prepareForItemProcessing(context, itemsToBeProcessed); + + reportPreparationForProcessingFinished(context); + } + catch (final RuntimeException e) { + reportPreparationForProcessingFailure(context, e); + } + } + private > void processItem(BatchJob job, final C context, final T item) { try { reportItemProcessingStarted(context, item); - job.processItem(context, item); - reportItemProcessingFinished(context, item); + + T enrichedItem = job.enrichItem(context, item); + BatchJobItemValidationResult validationResult = job.validateItem(context, enrichedItem); + switch (validationResult.getStatus()) { + case INVALID: + reportItemProcessingValidationFailure(context, item, validationResult); + reportItemProcessingFailure(context, item, null); + break; + case WARNING: + reportItemProcessingValidationFailure(context, item, validationResult); + job.processItem(context, enrichedItem); + reportItemProcessingFinished(context, item); + break; + case VALID: + job.processItem(context, enrichedItem); + reportItemProcessingFinished(context, item); + break; + } } catch (final RuntimeException e) { reportItemProcessingFailure(context, item, e); @@ -98,6 +132,18 @@ private void reportBatchJobFailure(final C ctx, fina } } + private > void reportItemProcessingValidationFailure(C ctx, + T item, BatchJobItemValidationResult validationResult) { + for (final var batchJobProcessingListener : batchJobProcessingListeners) { + try { + batchJobProcessingListener.onItemProcessingValidationFailure(ctx, item, validationResult); + } + catch (final RuntimeException e) { + log.error(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER, e); + } + } + } + private > void reportItemProcessingFailure(final C ctx, final T item, final RuntimeException e) { ctx.incrementFailedItems(); @@ -174,4 +220,39 @@ private void reportItemExtractionFailure(final C ctx } } + private void reportPreparationForProcessingStarted(C context) { + for (final var batchJobProcessingListener : batchJobProcessingListeners) { + try { + batchJobProcessingListener.onPreparationForProcessingStarted(context); + } + catch (final RuntimeException e) { + log.error(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER, e); + } + } + + } + + private void reportPreparationForProcessingFinished(C context) { + for (final var batchJobProcessingListener : batchJobProcessingListeners) { + try { + batchJobProcessingListener.onPreparationForProcessingFinished(context); + } + catch (final RuntimeException e) { + log.error(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER, e); + } + } + } + + private void reportPreparationForProcessingFailure(C context, RuntimeException e) { + for (final var batchJobProcessingListener : batchJobProcessingListeners) { + try { + batchJobProcessingListener.onPreparationForProcessingFailure(context, e); + } + catch (final RuntimeException e1) { + log.error(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER, e1); + } + } + + } + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemRepository.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemRepository.java index 62d63aa5..ce733b6f 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemRepository.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemRepository.java @@ -1,5 +1,6 @@ package com.paypal.infrastructure.batchjob; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -21,6 +22,20 @@ public interface BatchJobFailedItemRepository extends JpaRepository findByTypeAndStatus(String type, BatchJobFailedItemStatus status); + /** + * Retrieves a {@link List} of {@link BatchJobFailedItem} by the given type and + * {@link BatchJobFailedItemStatus} status ordered by + * {@link BatchJobFailedItem#getLastRetryTimestamp()} asc. + * @param type the {@link BatchJobFailedItem} type. + * @param status the {@link BatchJobFailedItem} status. + * @param pageable the {@link Pageable} pageable data. + * @return a {@link List} of {@link BatchJobFailedItem} by the given type and + * {@link BatchJobFailedItemStatus} status ordered by + * {@link BatchJobFailedItem#getLastRetryTimestamp()} asc. + */ + List findByTypeAndStatusOrderByLastRetryTimestampAsc(String type, + BatchJobFailedItemStatus status, Pageable pageable); + /** * Retrieves a {@link List} of {@link BatchJobFailedItem} by the given type. * @param type the {@link BatchJobFailedItem} type. diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImpl.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImpl.java index 5034fe79..de8fc47a 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImpl.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImpl.java @@ -3,11 +3,14 @@ import com.paypal.infrastructure.batchjob.entities.BatchJobItemTrackInfoEntity; import com.paypal.infrastructure.mail.MailNotificationUtil; import com.paypal.infrastructure.util.TimeMachine; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -26,6 +29,9 @@ public class BatchJobFailedItemServiceImpl implements BatchJobFailedItemService private final MailNotificationUtil mailNotificationUtil; + @Value("${retry.maxFailedItemsToProcessed}") + private int maxNumberOfFailedItems; + public BatchJobFailedItemServiceImpl(final BatchJobFailedItemRepository failedItemRepository, BatchJobTrackingService batchJobTrackingService, List batchJobFailedItemRetryPolicies, @@ -90,8 +96,8 @@ public > void removeItemProcessed(final T item) { */ @Override public List getFailedItemsForRetry(String itemType) { - List failedItems = failedItemRepository.findByTypeAndStatus(itemType, - BatchJobFailedItemStatus.RETRY_PENDING); + List failedItems = failedItemRepository.findByTypeAndStatusOrderByLastRetryTimestampAsc( + itemType, BatchJobFailedItemStatus.RETRY_PENDING, Pageable.ofSize(getMaxNumberOfFailedItems())); Set itemsBeingProcessedIds = batchJobTrackingService.getItemsBeingProcessedOrEnquedToProcess(itemType) .stream().map(BatchJobItemTrackInfoEntity::getItemId).collect(Collectors.toSet()); @@ -112,7 +118,18 @@ public List getFailedItems(String itemType, BatchJobFailedIt @Override public > void checkUpdatedFailedItems(Collection extractedItems) { - // Nothing to do + + //@formatter:off + extractedItems.stream() + .map(batchJobItem -> new BatchJobFailedItemId(batchJobItem.getItemId(), batchJobItem.getItemType())) + .map(failedItemRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(batchJobFailedItem -> { + batchJobFailedItem.setNumberOfRetries(0); + failedItemRepository.save(batchJobFailedItem); + }); + //@formatter:on } private boolean shouldRetryFailedItem(BatchJobFailedItem item) { @@ -138,4 +155,8 @@ private BatchJobFailedItem updateFailedItem(BatchJobFailedItem failedItem) { return failedItem; } + protected int getMaxNumberOfFailedItems() { + return maxNumberOfFailedItems; + } + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemEnricher.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemEnricher.java new file mode 100644 index 00000000..9a2989c1 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemEnricher.java @@ -0,0 +1,20 @@ +package com.paypal.infrastructure.batchjob; + +/** + * BatchJobs will use classes implementing this interface for enriching items before + * processing them. + * + * @param the job context type. + * @param the item type. + */ +public interface BatchJobItemEnricher> { + + /** + * Enrichs the information of an item. + * @param ctx the batch job context. + * @param jobItem the item to be processed. + * @return the enriched item. + */ + T enrichItem(C ctx, T jobItem); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationResult.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationResult.java new file mode 100644 index 00000000..386e89a9 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationResult.java @@ -0,0 +1,20 @@ +package com.paypal.infrastructure.batchjob; + +import lombok.Builder; +import lombok.Value; + +import java.util.Optional; + +/** + * This class holds the result of a batch job item validation. + */ +@Value +@Builder +public class BatchJobItemValidationResult { + + private BatchJobItemValidationStatus status; + + @Builder.Default + private Optional reason = Optional.empty(); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationStatus.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationStatus.java new file mode 100644 index 00000000..84cc42e9 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidationStatus.java @@ -0,0 +1,18 @@ +package com.paypal.infrastructure.batchjob; + +/** + * List of all possible item validation outcomes. + * + *
    + *
  • VALID: the item is correct and processing will continue.
  • + *
  • INVALID: the item is not valid so the item is not going to be processed and added + * for future retry.
  • + *
  • WARNING: the item is not valid but the processing will continue even if it can + * potentially fail.
  • + *
+ */ +public enum BatchJobItemValidationStatus { + + VALID, INVALID, WARNING + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidator.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidator.java new file mode 100644 index 00000000..6312a872 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobItemValidator.java @@ -0,0 +1,19 @@ +package com.paypal.infrastructure.batchjob; + +/** + * BatchJobs will use classes implementing this interface for validating items. + * + * @param the job context type. + * @param the item type. + */ +public interface BatchJobItemValidator> { + + /** + * Validates an item. + * @param ctx the batch job context. + * @param jobItem the item to be validated. + * @return the result of the validation. + */ + BatchJobItemValidationResult validateItem(C ctx, T jobItem); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobPreProcessor.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobPreProcessor.java new file mode 100644 index 00000000..607a12cb --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobPreProcessor.java @@ -0,0 +1,21 @@ +package com.paypal.infrastructure.batchjob; + +import java.util.Collection; + +/** + * BatchJobs will use classes implementing this interface for doing preparation tasks + * needed before start processing items. + * + * @param the job context type. + * @param the item type. + */ +public interface BatchJobPreProcessor> { + + /** + * Do preparation tasks before processing the provided items. + * @param ctx the batch context. + * @param itemsToBeProcessed the items that are going to be processed. + */ + void prepareForProcessing(C ctx, Collection itemsToBeProcessed); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobProcessingListener.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobProcessingListener.java index bbbc5901..bce4e334 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobProcessingListener.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobProcessingListener.java @@ -34,6 +34,15 @@ public interface BatchJobProcessingListener { */ void beforeProcessingItem(BatchJobContext ctx, BatchJobItem item); + /** + * Handler on validation failure while item processing + * @param ctx the job context. + * @param item the item. + * @param validationResult the validation result. + */ + void onItemProcessingValidationFailure(BatchJobContext ctx, BatchJobItem item, + BatchJobItemValidationResult validationResult); + /** * Handler on failure item processing. * @param ctx the job context. @@ -68,4 +77,23 @@ public interface BatchJobProcessingListener { */ void onBatchJobFailure(BatchJobContext ctx, Exception e); + /** + * Handler on preparation for processing phase started + * @param ctx the job context. + */ + void onPreparationForProcessingStarted(BatchJobContext ctx); + + /** + * Handler on preparation for processing phase finished + * @param ctx the job context. + */ + void onPreparationForProcessingFinished(BatchJobContext ctx); + + /** + * Handler on preparation for processing phase failed + * @param ctx the job context. + * @param e the exception. + */ + void onPreparationForProcessingFailure(BatchJobContext ctx, RuntimeException e); + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobType.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobType.java new file mode 100644 index 00000000..a07569d2 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/BatchJobType.java @@ -0,0 +1,20 @@ +package com.paypal.infrastructure.batchjob; + +/** + * Batch Job types + */ +public enum BatchJobType { + + /** + * Extract batch jobs extract items from Mirakl so they can be pushed/updated into + * Hyperwallet + */ + EXTRACT, + + /** + * Retry batch jobs take items that have failed in previous job executions and try to + * process them again. + */ + RETRY + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/AbstractBatchJobProcessingListenerSupport.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/AbstractBatchJobProcessingListenerSupport.java index 6ddefb9c..03083a29 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/AbstractBatchJobProcessingListenerSupport.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/AbstractBatchJobProcessingListenerSupport.java @@ -2,6 +2,7 @@ import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobItemValidationResult; import com.paypal.infrastructure.batchjob.BatchJobProcessingListener; import java.util.Collection; @@ -43,6 +44,12 @@ public void beforeProcessingItem(BatchJobContext ctx, BatchJobItem item) { // empty method } + @Override + public void onItemProcessingValidationFailure(BatchJobContext ctx, BatchJobItem item, + BatchJobItemValidationResult validationResult) { + // empty method + } + /** * {@inheritDoc} */ @@ -83,4 +90,19 @@ public void onBatchJobFailure(BatchJobContext ctx, Exception e) { // empty method } + @Override + public void onPreparationForProcessingStarted(BatchJobContext ctx) { + // empty method + } + + @Override + public void onPreparationForProcessingFinished(BatchJobContext ctx) { + // empty method + } + + @Override + public void onPreparationForProcessingFailure(BatchJobContext ctx, RuntimeException e) { + // empty method + } + } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListener.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListener.java index 456a4a91..d8a3f692 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListener.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListener.java @@ -3,6 +3,7 @@ import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobFailedItemService; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobType; import org.springframework.stereotype.Component; import java.util.Collection; @@ -40,7 +41,9 @@ public void onItemProcessingSuccess(BatchJobContext ctx, BatchJobItem item) { */ @Override public void onItemExtractionSuccessful(BatchJobContext ctx, Collection> extractedItems) { - batchJobFailedItemService.checkUpdatedFailedItems(extractedItems); + if (BatchJobType.EXTRACT.equals(ctx.getBatchJob().getType())) { + batchJobFailedItemService.checkUpdatedFailedItems(extractedItems); + } } } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListener.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListener.java index b8bd4941..623ee6a7 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListener.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListener.java @@ -2,6 +2,7 @@ import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobItemValidationResult; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -67,6 +68,16 @@ public void onItemProcessingSuccess(BatchJobContext ctx, BatchJobItem item) { logBatchProgress(ctx); } + /** + * {@inheritDoc} + */ + @Override + public void onItemProcessingValidationFailure(BatchJobContext ctx, BatchJobItem item, + BatchJobItemValidationResult validationResult) { + log.warn("[{}] Validation of item of type {} with id: {} has failed with the following message: {}", + ctx.getJobName(), item.getItemType(), item.getItemId(), validationResult.getReason().orElse("")); + } + private void logBatchProgress(BatchJobContext ctx) { log.info("[{}] {} items processed successfully. {} items failed. {} items remaining", ctx.getJobName(), ctx.getNumberOfItemsProcessed(), ctx.getNumberOfItemsFailed(), ctx.getNumberOfItemsRemaining()); diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/BatchJobContextQuartzAdapter.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/BatchJobContextQuartzAdapter.java index ed00e6fc..48a23481 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/BatchJobContextQuartzAdapter.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/BatchJobContextQuartzAdapter.java @@ -1,6 +1,8 @@ package com.paypal.infrastructure.batchjob.quartz; +import com.paypal.infrastructure.batchjob.BatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItem; import com.paypal.infrastructure.batchjob.BatchJobStatus; import org.apache.commons.lang3.StringUtils; import org.quartz.JobExecutionContext; @@ -10,6 +12,8 @@ public class BatchJobContextQuartzAdapter implements BatchJobContext { + public static final String KEY_BATCH_JOB = "batchJob"; + private static final String KEY_BATCH_JOB_EXECUTION_UUID = "batchJobUuid"; private static final String KEY_BATCH_JOB_STATUS = "batchJobStatus"; @@ -166,6 +170,11 @@ public JobExecutionContext getJobExecutionContext() { return jobExecutionContext; } + @Override + public > BatchJob getBatchJob() { + return (BatchJob) jobExecutionContext.getJobDetail().getJobDataMap().get(KEY_BATCH_JOB); + } + private void setIntValue(final String key, final int value) { jobExecutionContext.getJobDetail().getJobDataMap().put(key, Integer.valueOf(value)); } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapter.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapter.java index c331ce91..2409cfe2 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapter.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapter.java @@ -34,7 +34,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } private BatchJobContext getBatchJobContext(final JobExecutionContext jobExecutionContext) { - return quartzBatchJobContextFactory.getBatchJobContext(jobExecutionContext); + return quartzBatchJobContextFactory.getBatchJobContext(batchJob, jobExecutionContext); } } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactory.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactory.java index 4cd9ef60..104e5782 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactory.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactory.java @@ -1,15 +1,19 @@ package com.paypal.infrastructure.batchjob.quartz; +import com.paypal.infrastructure.batchjob.BatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItem; import org.quartz.JobExecutionContext; public interface QuartzBatchJobContextFactory { /** * Creates a {@link BatchJobContext} by the given {@link JobExecutionContext}. + * @param batchJob a {@link BatchJob}. * @param jobExecutionContext a {@link JobExecutionContext}. * @return a {@link BatchJobContext}. */ - BatchJobContext getBatchJobContext(JobExecutionContext jobExecutionContext); + BatchJobContext getBatchJobContext(BatchJob> batchJob, + JobExecutionContext jobExecutionContext); } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImpl.java b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImpl.java index 80c2a94f..45ae71ba 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImpl.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImpl.java @@ -1,6 +1,8 @@ package com.paypal.infrastructure.batchjob.quartz; +import com.paypal.infrastructure.batchjob.BatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItem; import org.quartz.JobExecutionContext; import org.springframework.stereotype.Service; @@ -11,7 +13,9 @@ public class QuartzBatchJobContextFactoryImpl implements QuartzBatchJobContextFa * {@inheritDoc} */ @Override - public BatchJobContext getBatchJobContext(JobExecutionContext jobExecutionContext) { + public BatchJobContext getBatchJobContext(BatchJob> batchJob, + JobExecutionContext jobExecutionContext) { + jobExecutionContext.getJobDetail().getJobDataMap().put(BatchJobContextQuartzAdapter.KEY_BATCH_JOB, batchJob); return new BatchJobContextQuartzAdapter(jobExecutionContext); } diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfig.java b/infrastructure/src/main/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfig.java index 019738ed..08b9f694 100644 --- a/infrastructure/src/main/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfig.java +++ b/infrastructure/src/main/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfig.java @@ -1,8 +1,6 @@ package com.paypal.infrastructure.configuration; -import com.paypal.infrastructure.batchjob.BatchJobFailedItem; -import com.paypal.infrastructure.model.entity.JobExecutionInformationEntity; -import com.paypal.infrastructure.model.entity.NotificationInfoEntity; +import com.paypal.infrastructure.InfrastructureConnectorApplication; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -62,9 +60,7 @@ public DataSource applicationDataSource(final DataSourceProperties applicationDa @Bean(name = "applicationEntityManagerFactory") public LocalContainerEntityManagerFactoryBean applicationEntityManagerFactory( final EntityManagerFactoryBuilder builder, final DataSource applicationDataSource) { - return builder.dataSource(applicationDataSource) - .packages(JobExecutionInformationEntity.class, NotificationInfoEntity.class, BatchJobFailedItem.class) - .build(); + return builder.dataSource(applicationDataSource).packages(InfrastructureConnectorApplication.class).build(); } /** diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/ItemLinksConfiguration.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/ItemLinksConfiguration.java new file mode 100644 index 00000000..b7ac9967 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/ItemLinksConfiguration.java @@ -0,0 +1,14 @@ +package com.paypal.infrastructure.itemlinks; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaRepositories(basePackages = { "com.paypal.infrastructure.itemlinks" }, + entityManagerFactoryRef = "applicationEntityManagerFactory", + transactionManagerRef = "applicationTransactionManager") +@ComponentScan +public class ItemLinksConfiguration { + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverter.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverter.java new file mode 100644 index 00000000..f1737faf --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverter.java @@ -0,0 +1,24 @@ +package com.paypal.infrastructure.itemlinks.converters; + +import com.paypal.infrastructure.itemlinks.entities.ItemLinkEntity; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface ItemLinksModelEntityConverter { + + @Mapping(target = "sourceId", source = "source.id") + @Mapping(target = "sourceType", source = "source.type") + @Mapping(target = "sourceSystem", source = "source.system") + @Mapping(target = "targetId", source = "target.id") + @Mapping(target = "targetType", source = "target.type") + @Mapping(target = "targetSystem", source = "target.system") + ItemLinkEntity from(MiraklItemLinkLocator source, HyperwalletItemLinkLocator target); + + @Mapping(target = "id", source = "targetId") + @Mapping(target = "type", source = "targetType") + HyperwalletItemLinkLocator hyperwalletLocatorFromLinkTarget(ItemLinkEntity itemLinkEntity); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/entities/ItemLinkEntity.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/entities/ItemLinkEntity.java new file mode 100644 index 00000000..b9ba847a --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/entities/ItemLinkEntity.java @@ -0,0 +1,38 @@ +package com.paypal.infrastructure.itemlinks.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.io.Serializable; + +@Data +@Entity +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@IdClass(ItemLinkEntity.class) +@Table(indexes = { @Index(columnList = "sourceId,sourceType,sourceSystem,targetSystem,targetType") }) +public class ItemLinkEntity implements Serializable { + + @Id + private String sourceId; + + @Id + private String sourceType; + + @Id + private String sourceSystem; + + @Id + private String targetId; + + @Id + private String targetType; + + @Id + private String targetSystem; + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemLinkLocator.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemLinkLocator.java new file mode 100644 index 00000000..544af7b6 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemLinkLocator.java @@ -0,0 +1,24 @@ +package com.paypal.infrastructure.itemlinks.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * Represents a reference to a Hyperwallet item. + */ +@Data +@Builder +@AllArgsConstructor +public class HyperwalletItemLinkLocator implements ItemLinkLocator { + + private String id; + + private HyperwalletItemTypes type; + + @Override + public ItemLinkExternalSystem getSystem() { + return ItemLinkExternalSystem.HYPERWALLET; + } + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemTypes.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemTypes.java new file mode 100644 index 00000000..165cec29 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/HyperwalletItemTypes.java @@ -0,0 +1,10 @@ +package com.paypal.infrastructure.itemlinks.model; + +/** + * List of all possible values of Hyperwallet items. + */ +public enum HyperwalletItemTypes { + + PROGRAM, BANK_ACCOUNT, USER, BUSINESS_STAKEHOLDER, PAYMENT + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkExternalSystem.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkExternalSystem.java new file mode 100644 index 00000000..e0fe3bad --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkExternalSystem.java @@ -0,0 +1,11 @@ +package com.paypal.infrastructure.itemlinks.model; + +/** + * All possible external systems available for tracking relationships between item + * references. + */ +public enum ItemLinkExternalSystem { + + MIRAKL, HYPERWALLET + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkLocator.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkLocator.java new file mode 100644 index 00000000..4d8f271b --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/ItemLinkLocator.java @@ -0,0 +1,29 @@ +package com.paypal.infrastructure.itemlinks.model; + +/** + * Represents a reference for an item in an external system. The item is identified by its + * Id and its Type. + * + * @param Enum with all possible values for item types. + */ +public interface ItemLinkLocator { + + /** + * Returns the id of the item. + * @return an Id. + */ + String getId(); + + /** + * Returns the type of the item. + * @return an enum value with the type. + */ + T getType(); + + /** + * Returns the system where the item is stored. + * @return an enum value with the system. + */ + ItemLinkExternalSystem getSystem(); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemLinkLocator.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemLinkLocator.java new file mode 100644 index 00000000..77fc841c --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemLinkLocator.java @@ -0,0 +1,24 @@ +package com.paypal.infrastructure.itemlinks.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * Represents a reference to a Mirakl item. + */ +@Data +@Builder +@AllArgsConstructor +public class MiraklItemLinkLocator implements ItemLinkLocator { + + private String id; + + private MiraklItemTypes type; + + @Override + public ItemLinkExternalSystem getSystem() { + return ItemLinkExternalSystem.MIRAKL; + } + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemTypes.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemTypes.java new file mode 100644 index 00000000..2ba836bd --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/model/MiraklItemTypes.java @@ -0,0 +1,10 @@ +package com.paypal.infrastructure.itemlinks.model; + +/** + * List of all possible values of Mirakl items. + */ +public enum MiraklItemTypes { + + SHOP, INVOICE + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/repository/ItemLinkRepository.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/repository/ItemLinkRepository.java new file mode 100644 index 00000000..eaa6f7c3 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/repository/ItemLinkRepository.java @@ -0,0 +1,16 @@ +package com.paypal.infrastructure.itemlinks.repository; + +import com.paypal.infrastructure.itemlinks.entities.ItemLinkEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +@Repository +public interface ItemLinkRepository extends JpaRepository { + + List findBySourceSystemAndSourceIdAndSourceTypeAndTargetSystemAndTargetTypeIn(String sourceSystem, + String sourceId, String sourceType, String targetSystem, Collection targetTypes); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksService.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksService.java new file mode 100644 index 00000000..f0107b1d --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksService.java @@ -0,0 +1,49 @@ +package com.paypal.infrastructure.itemlinks.services; + +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * This service is used to keep track of the relationships between Mirakl and Hyperwallet + * entities. This information in persisted in the database so is kept between restarts. + * + */ +@Service +public interface ItemLinksService { + + /** + * Stores the provided links. + * @param miraklItemLocator A reference to a Mirakl item. + * @param hyperwalletItemLocators A collection of references to Hyperwallet items. + */ + void createLinks(MiraklItemLinkLocator miraklItemLocator, + Collection hyperwalletItemLocators); + + /** + * Retrieves all the links of the requested types for the list of Mirakl references + * passed + * @param sourceItem A reference to a Mirakl item. + * @param targetTypes The types of Hyperwallet items to find. + * @return A map with a collection of Hyperwallet references for each of the requested + * Mirakl references. + */ + Map> findLinks( + Collection sourceItem, Set targetTypes); + + /** + * Retrieves all the links of the requested types for the list of Mirakl references + * passed + * @param sourceItem A reference to a Mirakl item. + * @param hyperwalletItemTypes The types of Hyperwallet items to find. + * @return all items relationships found. + */ + Collection findLinks(MiraklItemLinkLocator sourceItem, + Set hyperwalletItemTypes); + +} diff --git a/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImpl.java b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImpl.java new file mode 100644 index 00000000..306012b3 --- /dev/null +++ b/infrastructure/src/main/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImpl.java @@ -0,0 +1,70 @@ +package com.paypal.infrastructure.itemlinks.services; + +import com.paypal.infrastructure.itemlinks.converters.ItemLinksModelEntityConverter; +import com.paypal.infrastructure.itemlinks.entities.ItemLinkEntity; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.infrastructure.itemlinks.model.ItemLinkExternalSystem; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import com.paypal.infrastructure.itemlinks.repository.ItemLinkRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Transactional +public class ItemLinksServiceImpl implements ItemLinksService { + + private final ItemLinkRepository itemLinkRepository; + + private final ItemLinksModelEntityConverter itemLinksModelEntityConverter; + + public ItemLinksServiceImpl(ItemLinkRepository itemLinkRepository, + ItemLinksModelEntityConverter itemLinksModelEntityConverter) { + this.itemLinkRepository = itemLinkRepository; + this.itemLinksModelEntityConverter = itemLinksModelEntityConverter; + } + + @Override + public void createLinks(MiraklItemLinkLocator miraklItemLocator, + Collection hyperwalletItemLocators) { + // formatter:off + Collection itemLinkEntities = hyperwalletItemLocators.stream() + .map(hwItemLocator -> itemLinksModelEntityConverter.from(miraklItemLocator, hwItemLocator)) + .collect(Collectors.toSet()); + // formatter:on + + itemLinkRepository.saveAll(itemLinkEntities); + } + + @Override + public Map> findLinks( + Collection sourceItem, Set targetTypes) { + // formatter:off + return sourceItem.stream() + .collect(Collectors.toMap(Function.identity(), source -> findLinks(source, targetTypes))); + // formatter_on + } + + @Override + public Collection findLinks(MiraklItemLinkLocator sourceItem, + Set hyperwalletItemTypes) { + // formatter:off + List itemLinkEntityList = itemLinkRepository + .findBySourceSystemAndSourceIdAndSourceTypeAndTargetSystemAndTargetTypeIn( + sourceItem.getSystem().toString(), sourceItem.getId(), sourceItem.getType().toString(), + ItemLinkExternalSystem.HYPERWALLET.toString(), + hyperwalletItemTypes.stream().map(HyperwalletItemTypes::toString).collect(Collectors.toSet())); + // formatter:on + + return itemLinkEntityList.stream().map(itemLinksModelEntityConverter::hyperwalletLocatorFromLinkTarget) + .collect(Collectors.toSet()); + } + +} diff --git a/infrastructure/src/main/resources/infrastructure.properties b/infrastructure/src/main/resources/infrastructure.properties index 454f2778..3049c35d 100644 --- a/infrastructure/src/main/resources/infrastructure.properties +++ b/infrastructure/src/main/resources/infrastructure.properties @@ -3,3 +3,4 @@ mail.notifications.recipients = ${PAYPAL_HYPERWALLET_MAIL_RECIPIENT:recipi mail.notifications.from = ${PAYPAL_HYPERWALLET_MAIL_FROM:from@email.com} notifications.retry = ${PAYPAL_HYPERWALLET_RETRY_NOTIFICATIONS:true} notifications.max.retries = ${PAYPAL_HYPERWALLET_MAX_AMOUNT_OF_NOTIFICATION_RETRIES:5} +retry.maxFailedItemsToProcessed = ${PAYPAL_HYPERWALLET_MAX_FAILED_ITEMS_TO_BE_PROCESSED:100} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractBatchJobTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractBatchJobTest.java index 2e18b5d5..7b42568d 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractBatchJobTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractBatchJobTest.java @@ -48,7 +48,22 @@ void processItem_shouldCallItemsProcessor() { verify(batchJobItemProcessorMock, times(1)).processItem(batchJobContextMock, batchJobItemMock); } - private static class MyAbstractBatchJob extends AbstractBatchJob> { + @Test + void getBatchJobItemValidator_shouldReturnEmptyValue() { + assertThat(testObj.getBatchJobItemValidator()).isEmpty(); + } + + @Test + void getBatchJobPreProcessor_shouldReturnEmptyValue() { + assertThat(testObj.getBatchJobPreProcessor()).isEmpty(); + } + + @Test + void getBatchJobItemEnricher_shouldReturnEmptyValue() { + assertThat(testObj.getBatchJobItemEnricher()).isEmpty(); + } + + private static class MyAbstractBatchJob extends AbstractExtractBatchJob> { private final BatchJobItemProcessor> itemBatchJobItemProcessor; diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJobTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJobTest.java new file mode 100644 index 00000000..da7cbb57 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractExtractBatchJobTest.java @@ -0,0 +1,35 @@ +package com.paypal.infrastructure.batchjob; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class AbstractExtractBatchJobTest { + + @InjectMocks + private TestExtractBatchJob testObj; + + @Test + void getType_shouldReturnExtractType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.EXTRACT); + } + + static class TestExtractBatchJob extends AbstractExtractBatchJob> { + + @Override + protected BatchJobItemProcessor> getBatchJobItemProcessor() { + return null; + } + + @Override + protected BatchJobItemsExtractor> getBatchJobItemsExtractor() { + return null; + } + + } + +} \ No newline at end of file diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJobTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJobTest.java new file mode 100644 index 00000000..3e755820 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/AbstractRetryBatchJobTest.java @@ -0,0 +1,35 @@ +package com.paypal.infrastructure.batchjob; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class AbstractRetryBatchJobTest { + + @InjectMocks + private TestRetryBatchJob testObj; + + @Test + void getType_shouldReturnRetryType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.RETRY); + } + + static class TestRetryBatchJob extends AbstractRetryBatchJob> { + + @Override + protected BatchJobItemProcessor> getBatchJobItemProcessor() { + return null; + } + + @Override + protected BatchJobItemsExtractor> getBatchJobItemsExtractor() { + return null; + } + + } + +} \ No newline at end of file diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobExecutorTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobExecutorTest.java index e1878932..110866c1 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobExecutorTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobExecutorTest.java @@ -31,7 +31,7 @@ class BatchJobExecutorTest { private BatchJobExecutor testObj; @Mock - private BatchJob> batchJobMock; + private BatchJob> batchJobMock; @Mock private BatchJobContext batchJobContextMock; @@ -40,17 +40,28 @@ class BatchJobExecutorTest { private BatchJobProcessingListener listenerMock1, listenerMock2; @Mock - private BatchJobItem itemMock1, itemMock2; + private BatchJobItem itemMock1, itemMock2; - private Collection> itemCollection; + @Mock + private BatchJobItem enrichedItemMock1, enrichedItemMock2; + + private Collection> itemCollection; @BeforeEach public void setUp() { testObj.batchJobProcessingListeners = List.of(listenerMock1, listenerMock2); itemCollection = List.of(itemMock1, itemMock2); lenient().when(batchJobMock.getItems(any(BatchJobContext.class))).thenReturn(itemCollection); + + lenient().when(batchJobMock.validateItem(any(), any())) + .thenReturn(BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.VALID).build()); + lenient().when(batchJobMock.enrichItem(any(BatchJobContext.class), eq(itemMock1))) + .thenReturn(enrichedItemMock1); + lenient().when(batchJobMock.enrichItem(any(BatchJobContext.class), eq(itemMock2))) + .thenReturn(enrichedItemMock2); } + @SuppressWarnings("unchecked") @Test void execute_ShouldRetrieveAndProcessBatchItems() { @@ -63,22 +74,69 @@ void execute_ShouldRetrieveAndProcessBatchItems() { inOrder.verify(listenerMock1).beforeItemExtraction(any(BatchJobContext.class)); inOrder.verify(listenerMock2).beforeItemExtraction(any(BatchJobContext.class)); inOrder.verify(batchJobMock).getItems(any(BatchJobContext.class)); - inOrder.verify(listenerMock1).onItemExtractionSuccessful(any(BatchJobContext.class), eq(itemCollection)); - inOrder.verify(listenerMock2).onItemExtractionSuccessful(any(BatchJobContext.class), eq(itemCollection)); + inOrder.verify(listenerMock1).onItemExtractionSuccessful(any(BatchJobContext.class), + (Collection) eq(itemCollection)); + inOrder.verify(listenerMock2).onItemExtractionSuccessful(any(BatchJobContext.class), + (Collection) eq(itemCollection)); + + inOrder.verify(batchJobMock).prepareForItemProcessing(any(BatchJobContext.class), any()); + inOrder.verify(listenerMock1).beforeProcessingItem(any(BatchJobContext.class), eq(itemMock1)); inOrder.verify(listenerMock2).beforeProcessingItem(any(BatchJobContext.class), eq(itemMock1)); - inOrder.verify(batchJobMock).processItem(any(BatchJobContext.class), eq(itemMock1)); + inOrder.verify(batchJobMock).enrichItem(any(BatchJobContext.class), eq(itemMock1)); + inOrder.verify(batchJobMock).validateItem(any(BatchJobContext.class), eq(enrichedItemMock1)); + inOrder.verify(batchJobMock).processItem(any(BatchJobContext.class), eq(enrichedItemMock1)); inOrder.verify(listenerMock1).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock1)); inOrder.verify(listenerMock2).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock1)); inOrder.verify(listenerMock1).beforeProcessingItem(any(BatchJobContext.class), eq(itemMock2)); inOrder.verify(listenerMock2).beforeProcessingItem(any(BatchJobContext.class), eq(itemMock2)); - inOrder.verify(batchJobMock).processItem(any(BatchJobContext.class), eq(itemMock2)); + inOrder.verify(batchJobMock).enrichItem(any(BatchJobContext.class), eq(itemMock2)); + inOrder.verify(batchJobMock).validateItem(any(BatchJobContext.class), eq(enrichedItemMock2)); + inOrder.verify(batchJobMock).processItem(any(BatchJobContext.class), eq(enrichedItemMock2)); inOrder.verify(listenerMock1).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock2)); inOrder.verify(listenerMock2).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock2)); inOrder.verify(listenerMock1).onBatchJobFinished(any(BatchJobContext.class)); inOrder.verify(listenerMock2).onBatchJobFinished(any(BatchJobContext.class)); } + @Test + void execute_ShouldContinueProcessing_WhenItemValidationReturnsAWarning() { + + when(batchJobMock.validateItem(any(), eq(enrichedItemMock2))).thenReturn( + BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.WARNING).build()); + + testObj.execute(batchJobMock, batchJobContextMock); + + verify(batchJobMock).enrichItem(any(BatchJobContext.class), eq(itemMock2)); + verify(batchJobMock).validateItem(any(BatchJobContext.class), eq(enrichedItemMock2)); + verify(batchJobMock).processItem(any(BatchJobContext.class), eq(enrichedItemMock2)); + verify(listenerMock1).onItemProcessingValidationFailure(any(BatchJobContext.class), eq(itemMock2), + any(BatchJobItemValidationResult.class)); + verify(listenerMock2).onItemProcessingValidationFailure(any(BatchJobContext.class), eq(itemMock2), + any(BatchJobItemValidationResult.class)); + verify(listenerMock1).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock2)); + verify(listenerMock2).onItemProcessingSuccess(any(BatchJobContext.class), eq(itemMock2)); + } + + @Test + void execute_ShouldAbortItemProcessingAndRegisterFailure_WhenItemValidationReturnsAnInvalid() { + + when(batchJobMock.validateItem(any(), eq(enrichedItemMock2))).thenReturn( + BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.INVALID).build()); + + testObj.execute(batchJobMock, batchJobContextMock); + + verify(batchJobMock).enrichItem(any(BatchJobContext.class), eq(itemMock2)); + verify(batchJobMock).validateItem(any(BatchJobContext.class), eq(enrichedItemMock2)); + verify(batchJobMock, times(0)).processItem(any(BatchJobContext.class), eq(enrichedItemMock2)); + verify(listenerMock1).onItemProcessingValidationFailure(any(BatchJobContext.class), eq(itemMock2), + any(BatchJobItemValidationResult.class)); + verify(listenerMock2).onItemProcessingValidationFailure(any(BatchJobContext.class), eq(itemMock2), + any(BatchJobItemValidationResult.class)); + verify(listenerMock1).onItemProcessingFailure(any(BatchJobContext.class), eq(itemMock2), eq(null)); + verify(listenerMock2).onItemProcessingFailure(any(BatchJobContext.class), eq(itemMock2), eq(null)); + } + @Test void execute_ShouldLogAnError_WhenOnBatchJobStartedThrowsARunTimeException() { @@ -133,6 +191,31 @@ void execute_ShouldLogAnError_WhenOnItemExtractionFailureThrowsARunTimeException assertThat(logTrackerStub.contains(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER)).isTrue(); } + @Test + void execute_ShouldCallOnPreparationForProcessingFailure_WhenPreparationForProcessingThrowsARunTimeException() { + + doThrow(RuntimeException.class).when(batchJobMock).prepareForItemProcessing(any(), any()); + + testObj.execute(batchJobMock, batchJobContextMock); + + verify(listenerMock1).onPreparationForProcessingFailure(any(BatchJobContext.class), + any(RuntimeException.class)); + verify(listenerMock2).onPreparationForProcessingFailure(any(BatchJobContext.class), + any(RuntimeException.class)); + } + + @Test + void execute_ShouldLogAnError_WhenOnPreparationForProcessingFailureThrowsARunTimeException() { + + doThrow(RuntimeException.class).when(batchJobMock).prepareForItemProcessing(any(), any()); + doThrow(RuntimeException.class).when(listenerMock1) + .onPreparationForProcessingFailure(any(BatchJobContext.class), any(RuntimeException.class)); + + testObj.execute(batchJobMock, batchJobContextMock); + + assertThat(logTrackerStub.contains(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER)).isTrue(); + } + @Test void execute_ShouldLogAnError_WhenOnBatchJobFailureThrowsARunTimeException() { @@ -145,4 +228,4 @@ void execute_ShouldLogAnError_WhenOnBatchJobFailureThrowsARunTimeException() { assertThat(logTrackerStub.contains(MSG_ERROR_WHILE_INVOKING_BATCH_JOB_LISTENER)).isTrue(); } -} \ No newline at end of file +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTest.java new file mode 100644 index 00000000..b4422f28 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTest.java @@ -0,0 +1,70 @@ +package com.paypal.infrastructure.batchjob; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Tag("IntegrationTest") +@SpringBootTest(classes = BatchJobFailedItemServiceImplITTestContext.class) +@TestPropertySource( + locations = { "classpath:infrastructure-test.properties", "classpath:infrastructure-test-db.properties" }) +@ExtendWith(SpringExtension.class) +class BatchJobFailedItemServiceImplITTest { + + @Autowired + private BatchJobFailedItemService batchJobFailedItemService; + + /** + * For testing purpose setting up max failed item as five in + * infrastructure-test.properties -> retry.maxFailedItemsToProcessed = 5 + */ + @Test + void getFailedItemsForRetry_ShouldReturnNoMoreThanFiveFailedItems() { + + final MyItem failedItem1 = new MyItem("failedItem1"); + final MyItem failedItem2 = new MyItem("failedItem2"); + final MyItem failedItem3 = new MyItem("failedItem3"); + final MyItem failedItem4 = new MyItem("failedItem4"); + final MyItem failedItem5 = new MyItem("failedItem5"); + final MyItem failedItem6 = new MyItem("failedItem6"); + + batchJobFailedItemService.saveItemFailed(failedItem1); + batchJobFailedItemService.saveItemFailed(failedItem2); + batchJobFailedItemService.saveItemFailed(failedItem3); + batchJobFailedItemService.saveItemFailed(failedItem4); + batchJobFailedItemService.saveItemFailed(failedItem5); + batchJobFailedItemService.saveItemFailed(failedItem6); + + final List failedItemsForRetry = batchJobFailedItemService + .getFailedItemsForRetry(MyItem.class.getSimpleName()); + + assertThat(failedItemsForRetry).hasSize(5); + } + + static class MyItem extends AbstractBatchJobItem { + + protected MyItem(String item) { + super(item); + } + + @Override + public String getItemId() { + return getItem(); + } + + @Override + public String getItemType() { + return this.getClass().getSimpleName(); + } + + } + +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTestContext.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTestContext.java new file mode 100644 index 00000000..7c73b627 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplITTestContext.java @@ -0,0 +1,23 @@ +package com.paypal.infrastructure.batchjob; + +import com.paypal.infrastructure.configuration.InfrastructureDatasourceConfig; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@ComponentScan(basePackages = { "com.paypal.infrastructure.batchjob" }) +//@formatter:off +@Import({ + InfrastructureDatasourceConfig.class, + HibernateJpaAutoConfiguration.class +}) +//@formatter:on +@EnableAspectJAutoProxy +@EnableTransactionManagement +public class BatchJobFailedItemServiceImplITTestContext { + +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplTest.java index e2e5f956..8f40850e 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/BatchJobFailedItemServiceImplTest.java @@ -1,6 +1,5 @@ package com.paypal.infrastructure.batchjob; -import com.paypal.infrastructure.batchjob.cache.BatchJobFailedItemCacheService; import com.paypal.infrastructure.batchjob.entities.BatchJobItemTrackInfoEntity; import com.paypal.infrastructure.mail.MailNotificationUtil; import com.paypal.infrastructure.util.TimeMachine; @@ -9,9 +8,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; import java.time.LocalDateTime; import java.util.List; @@ -32,7 +31,6 @@ class BatchJobFailedItemServiceImplTest { private static final int INITIAL_NUMBER_OF_RETRIES = 3; - @InjectMocks private BatchJobFailedItemServiceImpl testObj; @Mock @@ -51,16 +49,19 @@ class BatchJobFailedItemServiceImplTest { private ArgumentCaptor batchJobFailedItemArgumentCaptor; @Mock - private BatchJobFailedItem batchJobFailedItem1Mock, batchJobFailedItem2Mock; + private BatchJobFailedItem batchJobFailedItem1Mock, batchJobFailedItem2Mock, batchJobFailedItem3Mock; @Mock private BatchJobItemTrackInfoEntity batchJobItemTrackInfoEntity1Mock; + @Mock + private BatchJobItem batchJobItem1Mock, batchJobItem2Mock, batchJobItem3Mock; + @BeforeEach void setUp() { - testObj = new BatchJobFailedItemServiceImpl(batchJobFailedItemRepositoryMock, batchJobTrackingServiceMock, - List.of(batchJobFailedItemRetryPolicyMock), mailNotificationUtilMock); + testObj = spy(new BatchJobFailedItemServiceImpl(batchJobFailedItemRepositoryMock, batchJobTrackingServiceMock, + List.of(batchJobFailedItemRetryPolicyMock), mailNotificationUtilMock)); } @Test @@ -201,13 +202,16 @@ void itemProcessed_ShouldShouldNitRemoveTheFailedItem_WhenFailedItemIsNotFound() @Test void getFailedItemsForRetry_ShouldReturnBatchJobFailedItemThatAreNotBeingProcessed() { + doReturn(5).when(testObj).getMaxNumberOfFailedItems(); + when(batchJobFailedItem1Mock.getId()).thenReturn(ID_001); when(batchJobFailedItem2Mock.getId()).thenReturn(ID_002); when(batchJobFailedItemRetryPolicyMock.shouldRetryFailedItem(batchJobFailedItem2Mock)).thenReturn(true); - when(batchJobFailedItemRepositoryMock.findByTypeAndStatus(SELLER_TYPE, BatchJobFailedItemStatus.RETRY_PENDING)) - .thenReturn(List.of(batchJobFailedItem1Mock, batchJobFailedItem2Mock)); + when(batchJobFailedItemRepositoryMock.findByTypeAndStatusOrderByLastRetryTimestampAsc(SELLER_TYPE, + BatchJobFailedItemStatus.RETRY_PENDING, Pageable.ofSize(5))) + .thenReturn(List.of(batchJobFailedItem1Mock, batchJobFailedItem2Mock)); when(batchJobItemTrackInfoEntity1Mock.getItemId()).thenReturn(ID_001); @@ -229,6 +233,31 @@ void getFailedItems_ShouldReturnAllFailedItemsOfAType() { assertThat(result).containsAll(batchJobFailedItems); } + @Test + void checkUpdatedFailedItems() { + when(batchJobItem1Mock.getItemId()).thenReturn("1"); + when(batchJobItem1Mock.getItemType()).thenReturn("test"); + when(batchJobItem2Mock.getItemId()).thenReturn("2"); + when(batchJobItem2Mock.getItemType()).thenReturn("test"); + when(batchJobItem3Mock.getItemId()).thenReturn("3"); + when(batchJobItem3Mock.getItemType()).thenReturn("test"); + + when(batchJobFailedItemRepositoryMock.findById(new BatchJobFailedItemId("1", "test"))) + .thenReturn(Optional.of(batchJobFailedItem1Mock)); + when(batchJobFailedItemRepositoryMock.findById(new BatchJobFailedItemId("2", "test"))) + .thenReturn(Optional.empty()); + when(batchJobFailedItemRepositoryMock.findById(new BatchJobFailedItemId("3", "test"))) + .thenReturn(Optional.of(batchJobFailedItem3Mock)); + + testObj.checkUpdatedFailedItems(List.of(batchJobItem1Mock, batchJobItem2Mock, batchJobItem3Mock)); + + verify(batchJobFailedItem1Mock).setNumberOfRetries(0); + verify(batchJobFailedItemRepositoryMock).save(batchJobFailedItem1Mock); + verify(batchJobFailedItemRepositoryMock, times(0)).save(batchJobFailedItem2Mock); + verify(batchJobFailedItem3Mock).setNumberOfRetries(0); + verify(batchJobFailedItemRepositoryMock).save(batchJobFailedItem3Mock); + } + private static class MySellerBatchJobItem extends AbstractBatchJobItem { protected MySellerBatchJobItem(final Object item) { diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/AbstractBatchJobTestSupport.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/AbstractBatchJobTestSupport.java index 553abb0d..e91f03fa 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/AbstractBatchJobTestSupport.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/AbstractBatchJobTestSupport.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; @@ -50,6 +51,15 @@ abstract class AbstractBatchJobTestSupport { @Autowired protected TestBatchJobItemProcessor testBatchJobItemProcessor; + @Autowired + protected TestBatchJobPreProcessor testBatchJobPreProcessor; + + @Autowired + protected TestBatchJobItemValidator testBatchJobItemValidator; + + @Autowired + protected TestBatchJobItemEnricher testBatchJobItemEnricher; + protected static AtomicBoolean jobRunning = new AtomicBoolean(false); @BeforeEach @@ -67,6 +77,13 @@ void cleanTimeMachineState() { TimeMachine.useSystemDefaultZoneClock(); } + @BeforeEach + void resetBatchJobsState() { + testBatchJobItemExtractor.itemsIdsToExtract = new ArrayList<>(); + testBatchJobItemProcessor.itemsIdsToFail = new HashSet<>(); + testBatchJobItemProcessor.itemsProcessedSuccesfully = new HashSet<>(); + } + @PostConstruct public void jobJobExecutionInformationListenerInit() throws SchedulerException { scheduler.getListenerManager().addJobListener(new JobRunningListener()); @@ -90,14 +107,50 @@ protected void waitForJobsToFinish() { await().atMost(2, TimeUnit.SECONDS).until(() -> !jobRunning.get()); } - static class TestRetryBatchJob extends AbstractBatchJob { + static abstract class AbstractTestBatchJob extends AbstractBatchJob { + + private final TestBatchJobItemEnricher testBatchJobItemEnricher; + + private final TestBatchJobItemValidator testBatchJobItemValidator; + + private final TestBatchJobPreProcessor testBatchJobPreProcessor; + + protected AbstractTestBatchJob(TestBatchJobItemEnricher testBatchJobItemEnricher, + TestBatchJobItemValidator testBatchJobItemValidator, + TestBatchJobPreProcessor testBatchJobPreProcessor) { + this.testBatchJobItemEnricher = testBatchJobItemEnricher; + this.testBatchJobItemValidator = testBatchJobItemValidator; + this.testBatchJobPreProcessor = testBatchJobPreProcessor; + } + + @Override + protected Optional> getBatchJobItemValidator() { + return Optional.of(testBatchJobItemValidator); + } + + @Override + protected Optional> getBatchJobPreProcessor() { + return Optional.of(testBatchJobPreProcessor); + } + + @Override + protected Optional> getBatchJobItemEnricher() { + return Optional.of(testBatchJobItemEnricher); + } + + } + + static class TestRetryBatchJob extends AbstractTestBatchJob { private final BatchJobFailedItemRetryITTest.TestRetryBatchJobItemExtractor testRetryBatchJobItemExtractor; private final BatchJobFailedItemRetryITTest.TestBatchJobItemProcessor testBatchJobItemProcessor; TestRetryBatchJob(BatchJobFailedItemRetryITTest.TestRetryBatchJobItemExtractor testRetryBatchJobItemExtractor, - BatchJobFailedItemRetryITTest.TestBatchJobItemProcessor testBatchJobItemProcessor) { + BatchJobFailedItemRetryITTest.TestBatchJobItemProcessor testBatchJobItemProcessor, + TestBatchJobItemEnricher testBatchJobItemEnricher, TestBatchJobItemValidator testBatchJobItemValidator, + TestBatchJobPreProcessor testBatchJobPreProcessor) { + super(testBatchJobItemEnricher, testBatchJobItemValidator, testBatchJobPreProcessor); this.testRetryBatchJobItemExtractor = testRetryBatchJobItemExtractor; this.testBatchJobItemProcessor = testBatchJobItemProcessor; } @@ -112,17 +165,24 @@ protected BatchJobItemsExtractor { + static class TestBatchJob extends AbstractTestBatchJob { private final BatchJobFailedItemRetryITTest.TestBatchJobItemExtractor testBatchJobItemExtractor; private final BatchJobFailedItemRetryITTest.TestBatchJobItemProcessor testBatchJobItemProcessor; - TestBatchJob(BatchJobFailedItemRetryITTest.TestBatchJobItemExtractor testBatchJobItemExtractor, - BatchJobFailedItemRetryITTest.TestBatchJobItemProcessor testBatchJobItemProcessor) { + TestBatchJob(TestBatchJobItemExtractor testBatchJobItemExtractor, + TestBatchJobItemProcessor testBatchJobItemProcessor, TestBatchJobItemEnricher testBatchJobItemEnricher, + TestBatchJobItemValidator testBatchJobItemValidator, + TestBatchJobPreProcessor testBatchJobPreProcessor) { + super(testBatchJobItemEnricher, testBatchJobItemValidator, testBatchJobPreProcessor); this.testBatchJobItemExtractor = testBatchJobItemExtractor; this.testBatchJobItemProcessor = testBatchJobItemProcessor; } @@ -137,6 +197,11 @@ protected BatchJobItemsExtractor getItems(Batch } + static class TestBatchJobPreProcessor + implements BatchJobPreProcessor { + + Set itemsIdsToFail = new HashSet<>(); + + Set itemsProcessedSuccesfully = new HashSet<>(); + + @Override + public void prepareForProcessing(BatchJobContext ctx, Collection itemsToBeProcessed) { + if (itemsToBeProcessed.stream().map(BatchJobItem::getItemId) + .anyMatch(itemId -> itemsIdsToFail.contains(itemId))) { + throw new RuntimeException("Item Failed"); + } + else { + itemsProcessedSuccesfully.addAll(itemsToBeProcessed); + } + } + + } + + static class TestBatchJobItemValidator + implements BatchJobItemValidator { + + Set itemsIdsToFail = new HashSet<>(); + + Set itemsIdsToWarn = new HashSet<>(); + + Set itemsIdsToReject = new HashSet<>(); + + @Override + public BatchJobItemValidationResult validateItem(BatchJobContext ctx, TestBatchJobItem jobItem) { + Integer itemId = Integer.valueOf(jobItem.getItemId()); + if (itemsIdsToReject.contains(itemId)) { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.INVALID).build(); + } + else if (itemsIdsToWarn.contains(itemId)) { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.WARNING).build(); + } + else if (itemsIdsToFail.contains(itemId)) { + throw new RuntimeException("Item validation exception"); + } + else { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.VALID).build(); + } + } + + } + + static class TestBatchJobItemEnricher + implements BatchJobItemEnricher { + + Function enrichmentFunction = Function.identity(); + + @Override + public TestBatchJobItem enrichItem(BatchJobContext ctx, TestBatchJobItem jobItem) { + return new TestBatchJobItem(jobItem.getItemId(), enrichmentFunction.apply(jobItem.getItem())); + } + + } + static class TestBatchJobItem implements BatchJobItem { private final String id; @@ -255,16 +380,37 @@ public TestBatchJobItemProcessor testBatchJobItemProcessor() { return new TestBatchJobItemProcessor(); } + @Bean + public TestBatchJobItemValidator testBatchJobItemValidator() { + return new TestBatchJobItemValidator(); + } + + @Bean + public TestBatchJobItemEnricher testBatchJobItemEnricher() { + return new TestBatchJobItemEnricher(); + } + + @Bean + public TestBatchJobPreProcessor testBatchJobPreProcessor() { + return new TestBatchJobPreProcessor(); + } + @Bean public TestRetryBatchJob testRetryBatchJob(TestRetryBatchJobItemExtractor testRetryBatchJobItemExtractor, - TestBatchJobItemProcessor testBatchJobItemProcessor) { - return new TestRetryBatchJob(testRetryBatchJobItemExtractor, testBatchJobItemProcessor); + TestBatchJobItemProcessor testBatchJobItemProcessor, TestBatchJobItemEnricher testBatchJobItemEnricher, + TestBatchJobItemValidator testBatchJobItemValidator, + TestBatchJobPreProcessor testBatchJobPreProcessor) { + return new TestRetryBatchJob(testRetryBatchJobItemExtractor, testBatchJobItemProcessor, + testBatchJobItemEnricher, testBatchJobItemValidator, testBatchJobPreProcessor); } @Bean public TestBatchJob testBatchJob(TestBatchJobItemExtractor testBatchJobItemExtractor, - TestBatchJobItemProcessor testBatchJobItemProcessor) { - return new TestBatchJob(testBatchJobItemExtractor, testBatchJobItemProcessor); + TestBatchJobItemProcessor testBatchJobItemProcessor, TestBatchJobItemEnricher testBatchJobItemEnricher, + TestBatchJobItemValidator testBatchJobItemValidator, + TestBatchJobPreProcessor testBatchJobPreProcessor) { + return new TestBatchJob(testBatchJobItemExtractor, testBatchJobItemProcessor, testBatchJobItemEnricher, + testBatchJobItemValidator, testBatchJobPreProcessor); } @Bean diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContextITTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContextITTest.java index 1af506cf..db068a14 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContextITTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContextITTest.java @@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; @Tag("IntegrationTest") -@SpringBootTest(classes = BatchJobFailedItemCacheTestContext.class) +@SpringBootTest(classes = BatchJobTestContext.class) @TestPropertySource( locations = { "classpath:infrastructure-test.properties", "classpath:infrastructure-test-db.properties" }) @ExtendWith(SpringExtension.class) diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemRetryITTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemRetryITTest.java index 95ed22fb..d8d2740b 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemRetryITTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemRetryITTest.java @@ -12,15 +12,13 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @Tag("IntegrationTest") -@SpringBootTest(classes = BatchJobFailedItemCacheTestContext.class) +@SpringBootTest(classes = BatchJobTestContext.class) @TestPropertySource( locations = { "classpath:infrastructure-test.properties", "classpath:infrastructure-test-db.properties" }) @ExtendWith(SpringExtension.class) @@ -30,42 +28,84 @@ class BatchJobFailedItemRetryITTest extends AbstractBatchJobTestSupport { private ExponentialBackOffItemRetryPolicy exponentialBackOffItemRetryPolicy; @BeforeEach - void resetBatchJobsState() { - testBatchJobItemExtractor.itemsIdsToExtract = new ArrayList<>(); - testBatchJobItemProcessor.itemsIdsToFail = new HashSet<>(); - testBatchJobItemProcessor.itemsProcessedSuccesfully = new HashSet<>(); + void resetBatchJobsPolicy() { exponentialBackOffItemRetryPolicy.setMinutesPerRetry(ExponentialBackOffItemRetryPolicy.MINUTES_PER_RETRY); } @Test void shouldRetryProcessingItemsReadingFromCache() throws SchedulerException, InterruptedException { + prepareItemsToExtractAndMarkOneForFailWhileProcessing(); + + runJobAndCheckThatItemThatFailsIsAddedForRetry(); + + runRetryJobAndCheckThatItemThatHasFailedIsRetried(); + + cleanItemsMarkedForFailWhileProcessing(); + + runRetryJobAndCheckThatItemThatPreviouslyFailedIsProcessedSuccesfully(); + } + + @Test + void shouldRetryProcessingItemsThatFailedOnValidation() throws SchedulerException, InterruptedException { + prepareItemsToExtractAndMarkOneForFailDuringValidation(); + + runJobAndCheckThatItemThatFailsIsAddedForRetry(); + + runRetryJobAndCheckThatItemThatHasFailedIsRetried(); + + cleanItemsMarkedForFailDuringValidation(); + + runRetryJobAndCheckThatItemThatPreviouslyFailedIsProcessedSuccesfully(); + } + + private void prepareItemsToExtractAndMarkOneForFailDuringValidation() { + testBatchJobItemExtractor.itemsIdsToExtract = List.of(1, 2); + testBatchJobItemValidator.itemsIdsToReject = Set.of(1); + exponentialBackOffItemRetryPolicy.setMinutesPerRetry(0L); + } + + private void prepareItemsToExtractAndMarkOneForFailWhileProcessing() { testBatchJobItemExtractor.itemsIdsToExtract = List.of(1, 2); testBatchJobItemProcessor.itemsIdsToFail = Set.of(1); exponentialBackOffItemRetryPolicy.setMinutesPerRetry(0L); + } - runJob(testJob); - assertThat(batchJobFailedItemCacheService.retrieveItem(TestBatchJobItem.class, - TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE, "1")).isPresent(); - assertThat(batchJobFailedItemCacheService.retrieveItem(TestBatchJobItem.class, - TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE, "2")).isNotPresent(); + private void cleanItemsMarkedForFailWhileProcessing() { + testBatchJobItemProcessor.itemsIdsToFail = Set.of(0); + } + + private void cleanItemsMarkedForFailDuringValidation() { + testBatchJobItemValidator.itemsIdsToReject = Set.of(); + } + + private void runRetryJobAndCheckThatItemThatPreviouslyFailedIsProcessedSuccesfully() + throws SchedulerException, InterruptedException { + runJob(testRetryJob); assertThat(batchJobFailedItemService.getFailedItemsForRetry(TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)) - .hasSize(1); - assertThat(batchJobFailedItemRepository - .findById(new BatchJobFailedItemId("1", TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)).get() - .getNumberOfRetries()).isZero(); + .isEmpty(); + assertThat(testBatchJobItemProcessor.itemsProcessedSuccesfully).hasSize(2); + } + private void runRetryJobAndCheckThatItemThatHasFailedIsRetried() throws SchedulerException, InterruptedException { runJob(testRetryJob); assertThat(batchJobFailedItemService.getFailedItemsForRetry(TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)) .hasSize(1); assertThat(batchJobFailedItemRepository .findById(new BatchJobFailedItemId("1", TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)).get() .getNumberOfRetries()).isEqualTo(1); + } - testBatchJobItemProcessor.itemsIdsToFail = Set.of(0); - runJob(testRetryJob); + private void runJobAndCheckThatItemThatFailsIsAddedForRetry() throws SchedulerException, InterruptedException { + runJob(testJob); + assertThat(batchJobFailedItemCacheService.retrieveItem(TestBatchJobItem.class, + TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE, "1")).isPresent(); + assertThat(batchJobFailedItemCacheService.retrieveItem(TestBatchJobItem.class, + TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE, "2")).isNotPresent(); assertThat(batchJobFailedItemService.getFailedItemsForRetry(TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)) - .isEmpty(); - assertThat(testBatchJobItemProcessor.itemsProcessedSuccesfully).hasSize(2); + .hasSize(1); + assertThat(batchJobFailedItemRepository + .findById(new BatchJobFailedItemId("1", TestBatchJobItem.TEST_BATCH_JOB_ITEM_TYPE)).get() + .getNumberOfRetries()).isZero(); } } diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContext.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobTestContext.java similarity index 96% rename from infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContext.java rename to infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobTestContext.java index ed500f63..642651e2 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobFailedItemCacheTestContext.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/integrationtests/BatchJobTestContext.java @@ -24,6 +24,6 @@ //@formatter:on @EnableAspectJAutoProxy @EnableTransactionManagement -public class BatchJobFailedItemCacheTestContext { +public class BatchJobTestContext { } diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListenerTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListenerTest.java index 232807b0..39bbc38b 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListenerTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/FailureBatchJobItemProcessingListenerTest.java @@ -1,8 +1,6 @@ package com.paypal.infrastructure.batchjob.listeners; -import com.paypal.infrastructure.batchjob.BatchJobContext; -import com.paypal.infrastructure.batchjob.BatchJobFailedItemService; -import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -12,7 +10,7 @@ import java.util.List; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class FailureBatchJobItemProcessingListenerTest { @@ -29,6 +27,9 @@ class FailureBatchJobItemProcessingListenerTest { @Mock private BatchJobItem batchJobItemMock; + @Mock + private BatchJob batchJobMock; + @Mock private Exception exceptionMock; @@ -48,12 +49,26 @@ void onItemProcessingSuccess_ShouldCallBatchJobFailedItemServiceItemProcessedMet verify(batchJobFailedItemServiceMock).saveItemFailed(batchJobItemMock); } + @SuppressWarnings("unchecked") @Test - void onItemItemExtractionSuccessful_ShouldCallBatchJobFailedItemServiceItemProcessedMethod() { + void onItemItemExtractionSuccessful_ShouldCallBatchJobFailedItemServiceItemProcessedMethod_ForExtractJobs() { + when(batchJobContextMock.getBatchJob()).thenReturn(batchJobMock); + when(batchJobMock.getType()).thenReturn(BatchJobType.EXTRACT); testObj.onItemExtractionSuccessful(batchJobContextMock, List.of(batchJobItemMock)); verify(batchJobFailedItemServiceMock).checkUpdatedFailedItems(argThat(list -> list.contains(batchJobItemMock))); } + @SuppressWarnings("unchecked") + @Test + void onItemItemExtractionSuccessful_ShouldCallBatchJobFailedItemServiceItemProcessedMethod_ForRetryJobs() { + when(batchJobContextMock.getBatchJob()).thenReturn(batchJobMock); + when(batchJobMock.getType()).thenReturn(BatchJobType.RETRY); + + testObj.onItemExtractionSuccessful(batchJobContextMock, List.of(batchJobItemMock)); + + verify(batchJobFailedItemServiceMock, times(0)).checkUpdatedFailedItems(any()); + } + } diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListenerTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListenerTest.java index 52794b00..f2147721 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListenerTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/listeners/LoggingBatchJobItemProcessingListenerTest.java @@ -4,6 +4,7 @@ import com.callibrity.logging.test.LogTrackerStub; import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobItemValidationResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,6 +50,9 @@ class LoggingBatchJobItemProcessingListenerTest { @Mock private BatchJobItem batchJobItemMock; + @Mock + private BatchJobItemValidationResult batchJobItemValidationResultMock; + @Mock private Collection> extractedItemsMock; @@ -137,4 +141,14 @@ void onItemProcessingSuccess_ShouldLogInfoMessages() { } + @Test + void onItemProcessingValidationFailure_ShouldLogWarnMessage() { + testObj.onItemProcessingValidationFailure(batchJobContextMock, batchJobItemMock, + batchJobItemValidationResultMock); + + assertThat(logTrackerStub.contains( + "[" + JOB_NAME + "] Validation of item of type " + ITEM_TYPE + " with id: " + ITEM_ID + " has failed")) + .isTrue(); + } + } diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapterTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapterTest.java index df858125..487343e7 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapterTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobAdapterTest.java @@ -9,7 +9,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.InjectMocks; -import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -40,7 +39,7 @@ class QuartzBatchJobAdapterTest { @Test void execute_ShouldExecuteAdaptedJob() throws JobExecutionException { - when(quartzBatchJobContextFactoryMock.getBatchJobContext(jobExecutionContextMock)) + when(quartzBatchJobContextFactoryMock.getBatchJobContext(batchJobMock, jobExecutionContextMock)) .thenReturn(batchJobContextMock); testObj.execute(jobExecutionContextMock); diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImplTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImplTest.java index 0071af2a..1f80943d 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImplTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/batchjob/quartz/QuartzBatchJobContextFactoryImplTest.java @@ -1,5 +1,6 @@ package com.paypal.infrastructure.batchjob.quartz; +import com.paypal.infrastructure.batchjob.BatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,11 +30,14 @@ class QuartzBatchJobContextFactoryImplTest { @Mock private JobDataMap jobDataMapMock; + @Mock + private BatchJob batchJobMock; + @Test void getBatchJobContext_ShouldCreateBatchJobContext() { when(jobExecutionContextMock.getJobDetail()).thenReturn(jobDetailMock); when(jobDetailMock.getJobDataMap()).thenReturn(jobDataMapMock); - BatchJobContext result = testObj.getBatchJobContext(jobExecutionContextMock); + BatchJobContext result = testObj.getBatchJobContext(batchJobMock, jobExecutionContextMock); assertThat(result).isNotNull(); } diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfigTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfigTest.java index 371b535a..6923712d 100644 --- a/infrastructure/src/test/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfigTest.java +++ b/infrastructure/src/test/java/com/paypal/infrastructure/configuration/InfrastructureDatasourceConfigTest.java @@ -1,5 +1,6 @@ package com.paypal.infrastructure.configuration; +import com.paypal.infrastructure.InfrastructureConnectorApplication; import com.paypal.infrastructure.batchjob.BatchJobFailedItem; import com.paypal.infrastructure.model.entity.JobExecutionInformationEntity; import com.paypal.infrastructure.model.entity.NotificationInfoEntity; @@ -75,8 +76,8 @@ void applicationEntityManagerFactory_scanPackageJobExecutionInformationEntity() testObj.applicationEntityManagerFactory(entityManagerFactoryBuilderMock, dataSourceMock); verify(builderMock).packages(packagesArgumentCaptor.capture()); - assertThat(packagesArgumentCaptor.getAllValues()).containsExactlyInAnyOrder(JobExecutionInformationEntity.class, - NotificationInfoEntity.class, BatchJobFailedItem.class); + assertThat(packagesArgumentCaptor.getAllValues()) + .containsExactlyInAnyOrder(InfrastructureConnectorApplication.class); } @Test diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTest.java new file mode 100644 index 00000000..e2084f5f --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTest.java @@ -0,0 +1,106 @@ +package com.paypal.infrastructure.itemlinks; + +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.MiraklItemTypes; +import com.paypal.infrastructure.itemlinks.services.ItemLinksService; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@Tag("IntegrationTest") +@SpringBootTest(classes = ItemLinksITTestContext.class) +@TestPropertySource( + locations = { "classpath:infrastructure-test.properties", "classpath:infrastructure-test-db.properties" }) +@ExtendWith(SpringExtension.class) +@Transactional +class ItemLinksITTest { + + @Autowired + private ItemLinksService itemLinksService; + + private ItemLinksMother itemLinksMother = new ItemLinksMother(); + + @Test + void shouldCreateAndRecoverLinks() { + MiraklItemLinkLocator miraklItemLinkLocator1 = itemLinksMother.aMiraklItemLinkLocator(1, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator2 = itemLinksMother.aMiraklItemLinkLocator(2, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator3 = itemLinksMother.aMiraklItemLinkLocator(3, MiraklItemTypes.SHOP); + + HyperwalletItemLinkLocator hyperwalletItemLinkLocator1 = itemLinksMother.aHyperwalletItemLinkLocator(1, + HyperwalletItemTypes.PROGRAM); + HyperwalletItemLinkLocator hyperwalletItemLinkLocator2 = itemLinksMother.aHyperwalletItemLinkLocator(1, + HyperwalletItemTypes.BANK_ACCOUNT); + HyperwalletItemLinkLocator hyperwalletItemLinkLocator3 = itemLinksMother.aHyperwalletItemLinkLocator(2, + HyperwalletItemTypes.PROGRAM); + + itemLinksService.createLinks(miraklItemLinkLocator1, + Set.of(hyperwalletItemLinkLocator1, hyperwalletItemLinkLocator2)); + itemLinksService.createLinks(miraklItemLinkLocator2, Set.of(hyperwalletItemLinkLocator3)); + itemLinksService.createLinks(miraklItemLinkLocator3, Set.of()); + + Map> miraklItemLinkLocatorLinks = itemLinksService + .findLinks(Set.of(miraklItemLinkLocator1, miraklItemLinkLocator2, miraklItemLinkLocator3), + Set.of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM)); + + assertThat(miraklItemLinkLocatorLinks.entrySet()).hasSize(3); + assertThat(miraklItemLinkLocatorLinks.get(miraklItemLinkLocator1)) + .containsExactlyInAnyOrder(hyperwalletItemLinkLocator1, hyperwalletItemLinkLocator2); + assertThat(miraklItemLinkLocatorLinks.get(miraklItemLinkLocator2)) + .containsExactlyInAnyOrder(hyperwalletItemLinkLocator3); + assertThat(miraklItemLinkLocatorLinks.get(miraklItemLinkLocator3)).isEmpty(); + } + + @Test + void shouldFilterLinksAccordingToTypes() { + MiraklItemLinkLocator miraklItemLinkLocator1 = itemLinksMother.aMiraklItemLinkLocator(1, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator2 = itemLinksMother.aMiraklItemLinkLocator(2, MiraklItemTypes.SHOP); + + HyperwalletItemLinkLocator hyperwalletItemLinkLocator1 = itemLinksMother.aHyperwalletItemLinkLocator(1, + HyperwalletItemTypes.PROGRAM); + HyperwalletItemLinkLocator hyperwalletItemLinkLocator2 = itemLinksMother.aHyperwalletItemLinkLocator(1, + HyperwalletItemTypes.BUSINESS_STAKEHOLDER); + HyperwalletItemLinkLocator hyperwalletItemLinkLocator3 = itemLinksMother.aHyperwalletItemLinkLocator(2, + HyperwalletItemTypes.PROGRAM); + + itemLinksService.createLinks(miraklItemLinkLocator1, + Set.of(hyperwalletItemLinkLocator1, hyperwalletItemLinkLocator2)); + itemLinksService.createLinks(miraklItemLinkLocator2, Set.of(hyperwalletItemLinkLocator3)); + + Map> miraklItemLinkLocatorLinks = itemLinksService + .findLinks(Set.of(miraklItemLinkLocator1, miraklItemLinkLocator2), + Set.of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM)); + + assertThat(miraklItemLinkLocatorLinks.entrySet()).hasSize(2); + assertThat(miraklItemLinkLocatorLinks.get(miraklItemLinkLocator1)) + .containsExactlyInAnyOrder(hyperwalletItemLinkLocator1); + assertThat(miraklItemLinkLocatorLinks.get(miraklItemLinkLocator2)) + .containsExactlyInAnyOrder(hyperwalletItemLinkLocator3); + } + + class ItemLinksMother { + + MiraklItemLinkLocator aMiraklItemLinkLocator(int id, MiraklItemTypes type) { + return new MiraklItemLinkLocator(String.format("M-%s-%d", type.toString(), id), type); + } + + HyperwalletItemLinkLocator aHyperwalletItemLinkLocator(int id, HyperwalletItemTypes type) { + return new HyperwalletItemLinkLocator(String.format("H-%s-%d", type.toString(), id), type); + } + + } + +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTestContext.java b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTestContext.java new file mode 100644 index 00000000..be9d4e36 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/ItemLinksITTestContext.java @@ -0,0 +1,25 @@ +package com.paypal.infrastructure.itemlinks; + +import com.paypal.infrastructure.configuration.InfrastructureDatasourceConfig; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@ComponentScan( + basePackages = { "com.paypal.infrastructure.itemlinks", "com.paypal.infrastructure.itemlinks.repository" }) +//@formatter:off +@Import({ + InfrastructureDatasourceConfig.class, + ItemLinksConfiguration.class, + HibernateJpaAutoConfiguration.class +}) +//@formatter:on +@EnableAspectJAutoProxy +@EnableTransactionManagement +public class ItemLinksITTestContext { + +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverterTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverterTest.java new file mode 100644 index 00000000..6e59e047 --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/converters/ItemLinksModelEntityConverterTest.java @@ -0,0 +1,54 @@ +package com.paypal.infrastructure.itemlinks.converters; + +import com.paypal.infrastructure.itemlinks.entities.ItemLinkEntity; +import com.paypal.infrastructure.itemlinks.model.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class ItemLinksModelEntityConverterTest { + + @Spy + private ItemLinksModelEntityConverter testObj = Mappers.getMapper(ItemLinksModelEntityConverter.class); + + @Test + void hyperwalletLocatorFromLinkTarget_shouldCreateHyperwalletItemLocator() { + //@formatter:off + ItemLinkEntity itemLinkEntity = ItemLinkEntity.builder() + .sourceSystem(ItemLinkExternalSystem.MIRAKL.toString()) + .sourceType(MiraklItemTypes.SHOP.toString()) + .sourceId("M1") + .targetSystem(ItemLinkExternalSystem.HYPERWALLET.toString()) + .targetType(HyperwalletItemTypes.BANK_ACCOUNT.toString()) + .targetId("H1") + .build(); + //@formatter:on + + HyperwalletItemLinkLocator result = testObj.hyperwalletLocatorFromLinkTarget(itemLinkEntity); + + assertThat(result.getId()).isEqualTo("H1"); + assertThat(result.getType()).isEqualTo(HyperwalletItemTypes.BANK_ACCOUNT); + } + + @Test + void from_shouldCreateEntityFromLocators() { + HyperwalletItemLinkLocator hyperwalletItemLocator = new HyperwalletItemLinkLocator("H1", + HyperwalletItemTypes.PAYMENT); + MiraklItemLinkLocator miraklItemLocator = new MiraklItemLinkLocator("M1", MiraklItemTypes.SHOP); + + ItemLinkEntity itemLinkEntity = testObj.from(miraklItemLocator, hyperwalletItemLocator); + + assertThat(itemLinkEntity.getSourceId()).isEqualTo("M1"); + assertThat(itemLinkEntity.getSourceType()).isEqualTo(MiraklItemTypes.SHOP.toString()); + assertThat(itemLinkEntity.getSourceSystem()).isEqualTo(ItemLinkExternalSystem.MIRAKL.toString()); + assertThat(itemLinkEntity.getTargetId()).isEqualTo("H1"); + assertThat(itemLinkEntity.getTargetType()).isEqualTo(HyperwalletItemTypes.PAYMENT.toString()); + assertThat(itemLinkEntity.getTargetSystem()).isEqualTo(ItemLinkExternalSystem.HYPERWALLET.toString()); + } + +} diff --git a/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImplTest.java b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImplTest.java new file mode 100644 index 00000000..de056cee --- /dev/null +++ b/infrastructure/src/test/java/com/paypal/infrastructure/itemlinks/services/ItemLinksServiceImplTest.java @@ -0,0 +1,95 @@ +package com.paypal.infrastructure.itemlinks.services; + +import com.paypal.infrastructure.itemlinks.converters.ItemLinksModelEntityConverter; +import com.paypal.infrastructure.itemlinks.entities.ItemLinkEntity; +import com.paypal.infrastructure.itemlinks.model.*; +import com.paypal.infrastructure.itemlinks.repository.ItemLinkRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; +import org.springframework.boot.test.autoconfigure.webservices.server.AutoConfigureMockWebServiceClient; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ItemLinksServiceImplTest { + + @InjectMocks + private ItemLinksServiceImpl testObj; + + @Mock + private ItemLinkRepository itemLinkRepositoryMock; + + @Mock + private ItemLinksModelEntityConverter itemLinksModelEntityConverterMock; + + @Mock + private HyperwalletItemLinkLocator hyperwalletItemLinkLocator1Mock, hyperwalletItemLinkLocator2Mock; + + @Mock + private MiraklItemLinkLocator miraklItemLinkLocator1Mock, miraklItemLinkLocator2Mock; + + @Mock + private ItemLinkEntity itemLinkEntity1Mock, itemLinkEntity2Mock; + + @Test + void createLinks() { + when(itemLinksModelEntityConverterMock.from(miraklItemLinkLocator1Mock, hyperwalletItemLinkLocator1Mock)) + .thenReturn(itemLinkEntity1Mock); + when(itemLinksModelEntityConverterMock.from(miraklItemLinkLocator1Mock, hyperwalletItemLinkLocator2Mock)) + .thenReturn(itemLinkEntity2Mock); + + testObj.createLinks(miraklItemLinkLocator1Mock, + List.of(hyperwalletItemLinkLocator1Mock, hyperwalletItemLinkLocator2Mock)); + + verify(itemLinkRepositoryMock).saveAll(Set.of(itemLinkEntity1Mock, itemLinkEntity2Mock)); + } + + @Test + void findLinks() { + when(miraklItemLinkLocator1Mock.getId()).thenReturn("H1"); + when(miraklItemLinkLocator1Mock.getType()).thenReturn(MiraklItemTypes.SHOP); + when(miraklItemLinkLocator1Mock.getSystem()).thenReturn(ItemLinkExternalSystem.MIRAKL); + when(miraklItemLinkLocator2Mock.getId()).thenReturn("H2"); + when(miraklItemLinkLocator2Mock.getType()).thenReturn(MiraklItemTypes.SHOP); + when(miraklItemLinkLocator2Mock.getSystem()).thenReturn(ItemLinkExternalSystem.MIRAKL); + + Set targetItemTypes = Set.of(HyperwalletItemTypes.BANK_ACCOUNT.toString(), + HyperwalletItemTypes.PROGRAM.toString()); + // formatter:off + when(itemLinkRepositoryMock.findBySourceSystemAndSourceIdAndSourceTypeAndTargetSystemAndTargetTypeIn( + eq("MIRAKL"), eq("H1"), eq("SHOP"), eq("HYPERWALLET"), argThat(x -> x.containsAll(targetItemTypes)))) + .thenReturn(List.of(itemLinkEntity1Mock, itemLinkEntity2Mock)); + + when(itemLinkRepositoryMock.findBySourceSystemAndSourceIdAndSourceTypeAndTargetSystemAndTargetTypeIn( + eq("MIRAKL"), eq("H2"), eq("SHOP"), eq("HYPERWALLET"), argThat(x -> x.containsAll(targetItemTypes)))) + .thenReturn(List.of()); + // formatter:on + + when(itemLinksModelEntityConverterMock.hyperwalletLocatorFromLinkTarget(itemLinkEntity1Mock)) + .thenReturn(hyperwalletItemLinkLocator1Mock); + when(itemLinksModelEntityConverterMock.hyperwalletLocatorFromLinkTarget(itemLinkEntity2Mock)) + .thenReturn(hyperwalletItemLinkLocator2Mock); + + Map> result = testObj.findLinks( + Set.of(miraklItemLinkLocator1Mock, miraklItemLinkLocator2Mock), + Set.of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM)); + + assertThat(result).containsKeys(miraklItemLinkLocator1Mock, miraklItemLinkLocator1Mock); + assertThat(result.get(miraklItemLinkLocator1Mock)).containsExactlyInAnyOrder(hyperwalletItemLinkLocator1Mock, + hyperwalletItemLinkLocator2Mock); + assertThat(result.get(miraklItemLinkLocator2Mock)).isEmpty(); + } + +} diff --git a/infrastructure/src/test/resources/infrastructure-test.properties b/infrastructure/src/test/resources/infrastructure-test.properties index ddeabc8d..65711e16 100644 --- a/infrastructure/src/test/resources/infrastructure-test.properties +++ b/infrastructure/src/test/resources/infrastructure-test.properties @@ -10,3 +10,4 @@ spring.mail.properties.mail.smtp.starttls.enable = true spring.mail.properties.mail.smtp.connectiontimeout = 5000 spring.mail.properties.mail.smtp.timeout = 3000 spring.mail.properties.mail.smtp.writetimeout = 5000 +retry.maxFailedItemsToProcessed = 5 diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJob.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJob.java new file mode 100644 index 00000000..dc4eeb38 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJob.java @@ -0,0 +1,41 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.*; + +import java.util.Optional; + +@SuppressWarnings("unchecked") +public abstract class AbstractAccountingDocumentBatchJob> + extends AbstractBatchJob { + + private final AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher; + + private final AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor; + + private final AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator; + + protected AbstractAccountingDocumentBatchJob( + final AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher, + final AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor, + final AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator) { + this.accountingDocumentBatchJobItemEnricher = accountingDocumentBatchJobItemEnricher; + this.accountingDocumentBatchJobPreProcessor = accountingDocumentBatchJobPreProcessor; + this.accountingDocumentBatchJobItemValidator = accountingDocumentBatchJobItemValidator; + } + + @Override + protected Optional> getBatchJobItemValidator() { + return Optional.of((BatchJobItemValidator) accountingDocumentBatchJobItemValidator); + } + + @Override + protected Optional> getBatchJobPreProcessor() { + return Optional.of((BatchJobPreProcessor) accountingDocumentBatchJobPreProcessor); + } + + @Override + protected Optional> getBatchJobItemEnricher() { + return Optional.of((BatchJobItemEnricher) accountingDocumentBatchJobItemEnricher); + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobItem.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobItem.java new file mode 100644 index 00000000..20feb143 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobItem.java @@ -0,0 +1,21 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.AbstractBatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; + +/** + * Base class for all invoice related batch job items. + * + * @param the type of the invoice domain object model. + */ +public abstract class AbstractAccountingDocumentBatchJobItem + extends AbstractBatchJobItem implements BatchJobItem { + + protected AbstractAccountingDocumentBatchJobItem(T item) { + super(item); + } + + protected abstract AbstractAccountingDocumentBatchJobItem from(T item); + +} diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricher.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricher.java new file mode 100644 index 00000000..0ebc5112 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricher.java @@ -0,0 +1,66 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItemEnricher; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +/** + * Batch job item enricher for invoice items that enrichs their information adding the + * bank account and the program token managed by the item links service. + */ +@Service +public class AccountingDocumentBatchJobItemEnricher implements + BatchJobItemEnricher> { + + private final AccountingDocumentsLinksService accountingDocumentsLinksService; + + public AccountingDocumentBatchJobItemEnricher(AccountingDocumentsLinksService accountingDocumentsLinksService) { + this.accountingDocumentsLinksService = accountingDocumentsLinksService; + } + + @Override + public AbstractAccountingDocumentBatchJobItem enrichItem(BatchJobContext ctx, + AbstractAccountingDocumentBatchJobItem jobItem) { + return jobItem.from(enrichedItem((AccountingDocumentModel) jobItem.getItem())); + } + + private AccountingDocumentModel enrichedItem(AccountingDocumentModel item) { + Collection accountingDocumentLinks = accountingDocumentsLinksService + .findRequiredLinks(item); + + String destinationToken = getDestinationToken(accountingDocumentLinks); + String hyperwalletProgram = getHyperwalletProgram(accountingDocumentLinks); + + return buildEnriched(item, destinationToken, hyperwalletProgram); + } + + protected AccountingDocumentModel buildEnriched(AccountingDocumentModel item, String destinationToken, + String hyperwalletProgram) { + return item.toBuilder().destinationToken(destinationToken).hyperwalletProgram(hyperwalletProgram).build(); + } + + private String getHyperwalletProgram(Collection accountingDocumentLinks) { + return getLink(accountingDocumentLinks, HyperwalletItemTypes.PROGRAM); + } + + private String getDestinationToken(Collection accountingDocumentLinks) { + return getLink(accountingDocumentLinks, HyperwalletItemTypes.BANK_ACCOUNT); + } + + private String getLink(Collection accountingDocumentLinks, HyperwalletItemTypes type) { + //@formatter:off + return accountingDocumentLinks.stream() + .filter(locator -> locator.getType().equals(type)) + .map(HyperwalletItemLinkLocator::getId) + .findAny() + .orElse(null); + //@formatter:on + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidator.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidator.java new file mode 100644 index 00000000..e4f6d14c --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidator.java @@ -0,0 +1,40 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.*; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * Batch job validator for invoice items that checks if destination and program tokens are + * fulfiled. + */ +@Service +public class AccountingDocumentBatchJobItemValidator + implements BatchJobItemValidator> { + + @Override + public BatchJobItemValidationResult validateItem(BatchJobContext ctx, + AbstractAccountingDocumentBatchJobItem jobItem) { + // formatter:off + if (hasAllRequiredTokens(jobItem)) { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.VALID).build(); + } + else { + return BatchJobItemValidationResult.builder().status(BatchJobItemValidationStatus.INVALID) + .reason(Optional.of(String.format( + "Invoice documents with id [%s] should be skipped because are lacking hw-program or bank account token", + jobItem.getItemId()))) + .build(); + } + // formatter:on + } + + private boolean hasAllRequiredTokens(BatchJobItem jobItem) { + return StringUtils.isNotEmpty(jobItem.getItem().getHyperwalletProgram()) + && StringUtils.isNotEmpty(jobItem.getItem().getDestinationToken()); + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessor.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessor.java new file mode 100644 index 00000000..93e520d1 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessor.java @@ -0,0 +1,41 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.infrastructure.batchjob.BatchJobPreProcessor; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Batch job preprocessor for invoice job items that ensures that all required + * relationships between Mirakl and Hyperwallet items are stored in the item links service + * before starting the invoice items processing. + */ +@Service +public class AccountingDocumentBatchJobPreProcessor + implements BatchJobPreProcessor> { + + private final AccountingDocumentsLinksService accountingDocumentsLinksService; + + public AccountingDocumentBatchJobPreProcessor(AccountingDocumentsLinksService accountingDocumentsLinksService) { + this.accountingDocumentsLinksService = accountingDocumentsLinksService; + } + + @Override + public void prepareForProcessing(BatchJobContext ctx, + Collection> itemsToBeProcessed) { + //@formatter:off + Set accountingDocumentModels = itemsToBeProcessed.stream() + .map(BatchJobItem::getItem) + .collect(Collectors.toSet()); + //@formatter:on + + accountingDocumentsLinksService.storeRequiredLinks(accountingDocumentModels); + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItem.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItem.java index b0cb46f8..4952b76c 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItem.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItem.java @@ -1,13 +1,13 @@ package com.paypal.invoices.batchjobs.creditnotes; -import com.paypal.infrastructure.batchjob.AbstractBatchJobItem; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJobItem; import com.paypal.invoices.invoicesextract.model.CreditNoteModel; /** * Class that holds the needed information for batch processing {@link CreditNoteModel} */ -public class CreditNoteExtractJobItem extends AbstractBatchJobItem { +public class CreditNoteExtractJobItem extends AbstractAccountingDocumentBatchJobItem { public static final String ITEM_TYPE = "CreditNote"; @@ -33,4 +33,9 @@ public String getItemType() { return ITEM_TYPE; } + @Override + protected CreditNoteExtractJobItem from(CreditNoteModel item) { + return new CreditNoteExtractJobItem(item); + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJob.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJob.java index 81fbd3e7..e4ca4cb0 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJob.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJob.java @@ -1,6 +1,13 @@ package com.paypal.invoices.batchjobs.creditnotes; -import com.paypal.infrastructure.batchjob.*; +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; +import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJob; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemEnricher; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemValidator; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobPreProcessor; import org.springframework.stereotype.Service; /** @@ -8,7 +15,7 @@ * HyperWallet. */ @Service -public class CreditNotesExtractBatchJob extends AbstractBatchJob { +public class CreditNotesExtractBatchJob extends AbstractAccountingDocumentBatchJob { private final CreditNotesExtractBatchJobItemsExtractor creditNotesExtractBatchJobItemsExtractor; @@ -16,7 +23,12 @@ public class CreditNotesExtractBatchJob extends AbstractBatchJob getBa return this.creditNotesExtractBatchJobItemProcessor; } + @Override + public BatchJobType getType() { + return BatchJobType.EXTRACT; + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJob.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJob.java index a8f86320..33b3a1db 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJob.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJob.java @@ -1,23 +1,33 @@ package com.paypal.invoices.batchjobs.creditnotes; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJob; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemEnricher; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemValidator; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobPreProcessor; import org.springframework.stereotype.Service; /** * Retries items that have failed during the credit notes extract job */ @Service -public class CreditNotesRetryBatchJob extends AbstractBatchJob { +public class CreditNotesRetryBatchJob extends AbstractAccountingDocumentBatchJob { private final CreditNotesExtractBatchJobItemProcessor creditNotesExtractBatchJobItemProcessor; private final CreditNotesRetryBatchJobItemsExtractor creditNotesRetryBatchJobItemsExtractor; - public CreditNotesRetryBatchJob(CreditNotesExtractBatchJobItemProcessor creditNotesExtractBatchJobItemProcessor, - CreditNotesRetryBatchJobItemsExtractor creditNotesRetryBatchJobItemsExtractor) { + public CreditNotesRetryBatchJob( + final CreditNotesExtractBatchJobItemProcessor creditNotesExtractBatchJobItemProcessor, + final CreditNotesRetryBatchJobItemsExtractor creditNotesRetryBatchJobItemsExtractor, + final AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher, + final AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor, + final AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator) { + super(accountingDocumentBatchJobItemEnricher, accountingDocumentBatchJobPreProcessor, + accountingDocumentBatchJobItemValidator); this.creditNotesExtractBatchJobItemProcessor = creditNotesExtractBatchJobItemProcessor; this.creditNotesRetryBatchJobItemsExtractor = creditNotesRetryBatchJobItemsExtractor; } @@ -32,4 +42,9 @@ protected BatchJobItemsExtractor getB return creditNotesRetryBatchJobItemsExtractor; } + @Override + public BatchJobType getType() { + return BatchJobType.RETRY; + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItem.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItem.java index b49c1b05..c9b566d8 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItem.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItem.java @@ -1,13 +1,13 @@ package com.paypal.invoices.batchjobs.invoices; -import com.paypal.infrastructure.batchjob.AbstractBatchJobItem; import com.paypal.infrastructure.batchjob.BatchJobItem; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJobItem; import com.paypal.invoices.invoicesextract.model.InvoiceModel; /** * Class that holds the needed information for batch processing {@link InvoiceModel} */ -public class InvoiceExtractJobItem extends AbstractBatchJobItem { +public class InvoiceExtractJobItem extends AbstractAccountingDocumentBatchJobItem { public static final String ITEM_TYPE = "Invoice"; @@ -33,4 +33,9 @@ public String getItemType() { return ITEM_TYPE; } + @Override + protected InvoiceExtractJobItem from(InvoiceModel item) { + return new InvoiceExtractJobItem(item); + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJob.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJob.java index 194e8a3c..feb23a59 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJob.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJob.java @@ -1,6 +1,13 @@ package com.paypal.invoices.batchjobs.invoices; -import com.paypal.infrastructure.batchjob.*; +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; +import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJob; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemEnricher; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemValidator; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobPreProcessor; import org.springframework.stereotype.Service; /** @@ -8,14 +15,19 @@ * HyperWallet. */ @Service -public class InvoicesExtractBatchJob extends AbstractBatchJob { +public class InvoicesExtractBatchJob extends AbstractAccountingDocumentBatchJob { private final InvoicesExtractBatchJobItemsExtractor invoicesExtractBatchJobItemsExtractor; private final InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor; public InvoicesExtractBatchJob(final InvoicesExtractBatchJobItemsExtractor invoicesExtractBatchJobItemsExtractor, - final InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor) { + final InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor, + final AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher, + final AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor, + final AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator) { + super(accountingDocumentBatchJobItemEnricher, accountingDocumentBatchJobPreProcessor, + accountingDocumentBatchJobItemValidator); this.invoicesExtractBatchJobItemsExtractor = invoicesExtractBatchJobItemsExtractor; this.invoicesExtractBatchJobItemProcessor = invoicesExtractBatchJobItemProcessor; } @@ -38,4 +50,9 @@ protected BatchJobItemProcessor getBatch return this.invoicesExtractBatchJobItemProcessor; } + @Override + public BatchJobType getType() { + return BatchJobType.EXTRACT; + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJob.java b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJob.java index 1c9d7541..f7908782 100644 --- a/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJob.java +++ b/invoices/src/main/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJob.java @@ -1,23 +1,32 @@ package com.paypal.invoices.batchjobs.invoices; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; +import com.paypal.invoices.batchjobs.common.AbstractAccountingDocumentBatchJob; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemEnricher; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobItemValidator; +import com.paypal.invoices.batchjobs.common.AccountingDocumentBatchJobPreProcessor; import org.springframework.stereotype.Service; /** * Retries items that have failed during the invoices extract job */ @Service -public class InvoicesRetryBatchJob extends AbstractBatchJob { +public class InvoicesRetryBatchJob extends AbstractAccountingDocumentBatchJob { private final InvoicesRetryBatchJobItemsExtractor invoicesRetryBatchJobItemsExtractor; private final InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor; - public InvoicesRetryBatchJob(InvoicesRetryBatchJobItemsExtractor invoicesRetryBatchJobItemsExtractor, - InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor) { + public InvoicesRetryBatchJob(final InvoicesRetryBatchJobItemsExtractor invoicesRetryBatchJobItemsExtractor, + final InvoicesExtractBatchJobItemProcessor invoicesExtractBatchJobItemProcessor, + final AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher, + final AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor, + final AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator) { + super(accountingDocumentBatchJobItemEnricher, accountingDocumentBatchJobPreProcessor, + accountingDocumentBatchJobItemValidator); this.invoicesRetryBatchJobItemsExtractor = invoicesRetryBatchJobItemsExtractor; this.invoicesExtractBatchJobItemProcessor = invoicesExtractBatchJobItemProcessor; } @@ -32,4 +41,9 @@ protected BatchJobItemsExtractor getBatc return invoicesRetryBatchJobItemsExtractor; } + @Override + public BatchJobType getType() { + return BatchJobType.RETRY; + } + } diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/AccountingDocumentsLinksService.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/AccountingDocumentsLinksService.java new file mode 100644 index 00000000..b198c204 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/AccountingDocumentsLinksService.java @@ -0,0 +1,45 @@ +package com.paypal.invoices.invoicesextract.service.hmc; + +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.mirakl.MiraklInvoiceLinksService; + +import java.util.Collection; + +/** + * This service manages the relationships between Mirakl and Hyperwallet items that are + * needed for the invoice creation process. + * + * It ensures that all the relationships required for the processing are stored in the HMC + * and tries to recover from Mirakl the additional relationships that weren't found on the + * HMC + * + * @see {@link MiraklInvoiceLinksService} + */ +public interface AccountingDocumentsLinksService { + + /** + * Stores the required relationships to be used during invoice processing for the + * collection of invoices provided. + * + * It checks if the relationships are already stored in HMC and if not found it tries + * to recovers the information from Mirakl using {@link MiraklInvoiceLinksService} + * @param accountingDocumentModels A collection of {@link AccountingDocumentModel}. + * @param Type of the invoice + */ + void storeRequiredLinks(Collection accountingDocumentModels); + + /** + * Retrieves the required relationships between Mirakl and Hyperwallet needed for the + * processing of the provided invoice. + * + * These relationships are retrieved from HMC local storage, so not found + * relationships are not requested to Mirakl API. + * @param accountingDocumentModel The invoice. + * @param Type of the invoice + * @return + */ + Collection findRequiredLinks( + T accountingDocumentModel); + +} diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImpl.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImpl.java new file mode 100644 index 00000000..51e518d0 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImpl.java @@ -0,0 +1,89 @@ +package com.paypal.invoices.invoicesextract.service.hmc.impl; + +import com.paypal.infrastructure.itemlinks.model.*; +import com.paypal.infrastructure.itemlinks.services.ItemLinksService; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; +import com.paypal.invoices.invoicesextract.service.mirakl.MiraklInvoiceLinksService; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class AccountingDocumentsLinksServiceImpl implements AccountingDocumentsLinksService { + + private static final Set REQUIRED_HYPERWALLET_TYPES = Set + .of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM); + + private final ItemLinksService itemLinksService; + + private final MiraklInvoiceLinksService miraklInvoiceLinksService; + + public AccountingDocumentsLinksServiceImpl(ItemLinksService itemLinksService, + MiraklInvoiceLinksService miraklInvoiceLinksService) { + this.itemLinksService = itemLinksService; + this.miraklInvoiceLinksService = miraklInvoiceLinksService; + } + + @Override + public void storeRequiredLinks(Collection accountingDocumentModels) { + Map> retrievedFromHmcShopLinks = itemLinksService + .findLinks(getMiraklItemLocators(getShopIdsSet(accountingDocumentModels)), REQUIRED_HYPERWALLET_TYPES); + + //@formatter:off + Set notFoundShopLinks = retrievedFromHmcShopLinks.entrySet().stream() + .filter(e -> !hasAllRequiredShopLinks(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + //@formatter:on + + Map> retrievedFromMiraklShopLinks = miraklInvoiceLinksService + .getInvoiceRelatedShopLinks(getShopIdsSet(notFoundShopLinks)); + + retrievedFromMiraklShopLinks.forEach(itemLinksService::createLinks); + } + + @Override + public Collection findRequiredLinks( + T accountingDocumentModel) { + return itemLinksService.findLinks( + new MiraklItemLinkLocator(accountingDocumentModel.getShopId(), MiraklItemTypes.SHOP), + REQUIRED_HYPERWALLET_TYPES); + } + + private boolean hasAllRequiredShopLinks(Collection hyperwalletItemLinkLocators) { + if (hyperwalletItemLinkLocators.isEmpty()) { + return false; + } + return hyperwalletItemLinkLocators.stream().map(HyperwalletItemLinkLocator::getType).collect(Collectors.toSet()) + .containsAll(REQUIRED_HYPERWALLET_TYPES); + } + + @NotNull + private Set getShopIdsSet(Collection accountingDocumentModels) { + //@formatter:off + return accountingDocumentModels.stream() + .map(AccountingDocumentModel::getShopId) + .collect(Collectors.toSet()); + //@formatter:on + } + + @NotNull + private Set getShopIdsSet(Set notFoundShopLinks) { + return notFoundShopLinks.stream().map(ItemLinkLocator::getId).collect(Collectors.toSet()); + } + + @NotNull + private Set getMiraklItemLocators(Set invoiceShopIds) { + return invoiceShopIds.stream().map(this::createMiraklItemLocator).collect(Collectors.toSet()); + } + + private MiraklItemLinkLocator createMiraklItemLocator(String shopId) { + return new MiraklItemLinkLocator(shopId, MiraklItemTypes.SHOP); + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/MiraklInvoiceLinksService.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/MiraklInvoiceLinksService.java new file mode 100644 index 00000000..e7f23f03 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/MiraklInvoiceLinksService.java @@ -0,0 +1,32 @@ +package com.paypal.invoices.invoicesextract.service.mirakl; + +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * This service retrieves from Mirakl the relationships with Hyperwallet items that are + * required for invoice processing. This relationships are: + *
    + *
  • {@link com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes#BANK_ACCOUNT}
  • + *
  • {@link com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes#PROGRAM}
  • + *
+ * + * This relationships are obtained from the custom fields of the Mirakl shop, so S20 will + * be used to retrieve this information. + */ +public interface MiraklInvoiceLinksService { + + /** + * Retrieves from Mirakl the relationships with Hyperwallet items required for invoice + * processing for the provided shop ids. + * @param shopIds List of shop ids. + * @return A Map with the references to a Mirakl item of type Shop and the list of + * related Hyperwallet items references. + */ + Map> getInvoiceRelatedShopLinks(Set shopIds); + +} diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImpl.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImpl.java index 2e2ec16b..b0585113 100644 --- a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImpl.java +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImpl.java @@ -1,35 +1,32 @@ package com.paypal.invoices.invoicesextract.service.mirakl.impl; -import com.mirakl.client.core.exception.MiraklApiException; import com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentPaymentStatus; import com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentType; import com.mirakl.client.mmp.domain.shop.MiraklShop; -import com.mirakl.client.mmp.domain.shop.MiraklShops; import com.mirakl.client.mmp.operator.request.payment.invoice.MiraklGetInvoicesRequest; import com.mirakl.client.mmp.request.payment.invoice.MiraklAccountingDocumentState; -import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; import com.paypal.infrastructure.converter.Converter; import com.paypal.infrastructure.mail.MailNotificationUtil; import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoice; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoices; -import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; import com.paypal.invoices.invoicesextract.service.mirakl.MiraklAccountingDocumentExtractService; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.EnumUtils; -import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; import java.util.stream.Collectors; -import java.util.stream.Stream; import static com.paypal.infrastructure.constants.HyperWalletConstants.MIRAKL_MAX_RESULTS_PER_PAGE; @@ -42,6 +39,8 @@ public abstract class AbstractAccountingDocumentsExtractServiceImpl miraklShopToAccountingModelConverter, final MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient, + AccountingDocumentsLinksService accountingDocumentsLinksService, final MailNotificationUtil invoicesMailNotificationUtil) { this.miraklShopToAccountingModelConverter = miraklShopToAccountingModelConverter; this.miraklMarketplacePlatformOperatorApiClient = miraklMarketplacePlatformOperatorApiClient; + this.accountingDocumentsLinksService = accountingDocumentsLinksService; this.invoicesMailNotificationUtil = invoicesMailNotificationUtil; } @Override public List extractAccountingDocuments(final Date delta) { - final List invoices = getAccountingDocuments(delta); - final Map> mapShopDestinationToken = getMapDestinationTokens(invoices); - - return associateBillingDocumentsWithTokens(invoices, mapShopDestinationToken); - } - - private Map> getMapDestinationTokens(final List creditNotes) { - final List shops = getMiraklShops(creditNotes); - return mapShopsWithDestinationToken(shops); - } - - protected List filterOnlyMappableDocuments(final List invoices, final Set shopIds) { - - //@formatter:off - log.warn("Credit notes documents with ids [{}] should be skipped because are lacking hw-program or bank account token", invoices.stream() - .filter(invoice -> !shopIds.contains(invoice.getShopId())) - .map(AccountingDocumentModel::getInvoiceNumber) - .collect(Collectors.joining(","))); - //@formatter:on + final List invoices = getInvoicesForDateAndType(delta, getInvoiceType()); //@formatter:off return invoices.stream() - .filter(invoice -> shopIds.contains(invoice.getShopId())) - .collect(Collectors.toList()); - //@formatter:on - } - - @NonNull - protected List getMiraklShops(final List invoices) { - //@formatter:off - final Set shopIds = invoices.stream() - .map(AccountingDocumentModel::getShopId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - log.info("Retrieving information of shops [{}] for invoices [{}]", String.join(",", shopIds), - invoices.stream() - .map(AccountingDocumentModel::getInvoiceNumber) - .collect(Collectors.joining(","))); - //@formatter:on - - return getAllShops(shopIds); - } - - protected List getAllShops(final Set shopIds) { - if (shopIds.isEmpty()) { - return Collections.emptyList(); - } - - List> partitionedShopIds = ListUtils.partition(shopIds.stream().collect(Collectors.toList()), 100); - //@formatter:off - return partitionedShopIds.stream() - .map(this::getAllShopsWithoutPartitioning) - .flatMap(List::stream) + .map(getMiraklInvoiceToAccountingModelConverter()::convert) .collect(Collectors.toList()); //@formatter:on } - protected List getAllShopsWithoutPartitioning(final List shopIds) { - if (shopIds.isEmpty()) { - return Collections.emptyList(); - } - - try { - final MiraklGetShopsRequest request = createShopRequest(shopIds.stream().collect(Collectors.toSet())); - final MiraklShops miraklShops = miraklMarketplacePlatformOperatorApiClient.getShops(request); - return miraklShops.getShops(); - } - catch (final MiraklApiException ex) { - List sortedShopIds = shopIds.stream().sorted().collect(Collectors.toList()); - log.error(String.format("Something went wrong getting information of shops [%s]%n%s", - String.join(",", sortedShopIds), MiraklLoggingErrorsUtil.stringify(ex)), ex); - invoicesMailNotificationUtil.sendPlainTextEmail("Issue detected getting shops in Mirakl", - String.format("Something went wrong getting information of shops [%s]%n%s", - String.join(",", sortedShopIds), MiraklLoggingErrorsUtil.stringify(ex))); - - return Collections.emptyList(); - } - } - - protected Map> mapShopsWithDestinationToken(final List shops) { - //@formatter:off - return Stream.ofNullable(shops) - .flatMap(Collection::stream) - .map(miraklShopToAccountingModelConverter::convert) - .filter(invoiceModel -> Objects.nonNull(invoiceModel.getDestinationToken()) && Objects.nonNull(invoiceModel.getHyperwalletProgram())) - .collect(Collectors.toMap(AccountingDocumentModel::getShopId, accountingDocument -> Pair.of(accountingDocument.getDestinationToken(), accountingDocument.getHyperwalletProgram()), (i1, i2) -> i1)); - //@formatter:on - } - @NonNull protected MiraklGetInvoicesRequest createAccountingDocumentRequest(final Date delta, final InvoiceTypeEnum invoiceType) { @@ -161,14 +81,6 @@ protected MiraklGetInvoicesRequest createAccountingDocumentRequest(final Date de return miraklGetInvoicesRequest; } - @NonNull - protected MiraklGetShopsRequest createShopRequest(final Set shopIds) { - final MiraklGetShopsRequest request = new MiraklGetShopsRequest(); - request.setShopIds(shopIds); - request.setPaginate(false); - return request; - } - protected List getInvoicesForDateAndType(final Date delta, final InvoiceTypeEnum invoiceType) { final List invoices = new ArrayList<>(); @@ -189,30 +101,6 @@ protected List getInvoicesForDateAndType(final Date delta, fin return invoices; } - protected List associateBillingDocumentsWithTokens(final List invoices, - final Map> mapShopDestinationToken) { - - final List filteredInvoices = filterOnlyMappableDocuments(invoices, mapShopDestinationToken.keySet()); - //@formatter:off - return filteredInvoices.stream() - .map(invoiceModel -> (T) invoiceModel.toBuilder() - .destinationToken(mapShopDestinationToken.get(invoiceModel.getShopId()).getLeft()) - .hyperwalletProgram(mapShopDestinationToken.get(invoiceModel.getShopId()).getRight()) - .build()) - .collect(Collectors.toList()); - //@formatter:on - } - - protected List getAccountingDocuments(final Date delta) { - final List invoices = getInvoicesForDateAndType(delta, getInvoiceType()); - - //@formatter:off - return invoices.stream() - .map(getMiraklInvoiceToAccountingModelConverter()::convert) - .collect(Collectors.toList()); - //@formatter:on - } - @Override public Collection extractAccountingDocuments(List ids) { final List invoices = getInvoicesForDateAndType(getTimeRangeForFindByIdInvoices(), diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImpl.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImpl.java index c7a7dfa6..1295d620 100644 --- a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImpl.java +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImpl.java @@ -8,6 +8,7 @@ import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.CreditNoteModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -22,9 +23,10 @@ public MiraklCreditNotesExtractServiceImpl( final MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient, final Converter miraklShopToAccountingModelConverter, final Converter miraklInvoiceToCreditNoteModelConverter, - final MailNotificationUtil invoicesMailNotificationUtil) { + final MailNotificationUtil invoicesMailNotificationUtil, + final AccountingDocumentsLinksService accountingDocumentsLinksService) { super(miraklShopToAccountingModelConverter, miraklMarketplacePlatformOperatorApiClient, - invoicesMailNotificationUtil); + accountingDocumentsLinksService, invoicesMailNotificationUtil); this.miraklInvoiceToCreditNoteModelConverter = miraklInvoiceToCreditNoteModelConverter; } diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImpl.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImpl.java new file mode 100644 index 00000000..e8a4e2a9 --- /dev/null +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImpl.java @@ -0,0 +1,120 @@ +package com.paypal.invoices.invoicesextract.service.mirakl.impl; + +import com.mirakl.client.core.exception.MiraklApiException; +import com.mirakl.client.mmp.domain.shop.MiraklShop; +import com.mirakl.client.mmp.domain.shop.MiraklShops; +import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; +import com.paypal.infrastructure.converter.Converter; +import com.paypal.infrastructure.itemlinks.model.*; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; +import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.mirakl.MiraklInvoiceLinksService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class MiraklInvoiceLinksServiceImpl implements MiraklInvoiceLinksService { + + private final MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient; + + private final Converter miraklShopToAccountingModelConverter; + + public MiraklInvoiceLinksServiceImpl( + MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient, + Converter miraklShopToAccountingModelConverter) { + this.miraklMarketplacePlatformOperatorApiClient = miraklMarketplacePlatformOperatorApiClient; + this.miraklShopToAccountingModelConverter = miraklShopToAccountingModelConverter; + } + + @Override + public Map> getInvoiceRelatedShopLinks( + Set shopIds) { + if (shopIds.isEmpty()) { + return Map.of(); + } + + Map miraklShops = getAllShops(shopIds); + + //@formatter:off + return shopIds.stream() + .map(this::createMiraklItemLocator) + .collect(Collectors.toMap(Function.identity(), miraklLocator -> getHyperwalletLinks(miraklShops.get(miraklLocator.getId())))); + //@formatter:on + } + + private Collection getHyperwalletLinks(MiraklShop miraklShop) { + if (miraklShop == null) { + return Collections.emptyList(); + } + return extractHyperwalletItemLocators(miraklShopToAccountingModelConverter.convert(miraklShop)); + } + + private Set extractHyperwalletItemLocators(AccountingDocumentModel invoiceModel) { + Set hyperwalletItemLinkLocators = new HashSet<>(); + + if (StringUtils.isNotEmpty(invoiceModel.getDestinationToken())) { + hyperwalletItemLinkLocators.add(new HyperwalletItemLinkLocator(invoiceModel.getDestinationToken(), + HyperwalletItemTypes.BANK_ACCOUNT)); + } + if (StringUtils.isNotEmpty(invoiceModel.getHyperwalletProgram())) { + hyperwalletItemLinkLocators.add( + new HyperwalletItemLinkLocator(invoiceModel.getHyperwalletProgram(), HyperwalletItemTypes.PROGRAM)); + } + + return hyperwalletItemLinkLocators; + } + + private MiraklItemLinkLocator createMiraklItemLocator(String shopId) { + return new MiraklItemLinkLocator(shopId, MiraklItemTypes.SHOP); + } + + protected Map getAllShops(final Set shopIds) { + List> partitionedShopIds = ListUtils.partition(new ArrayList<>(shopIds), 100); + //@formatter:off + List miraklShops = partitionedShopIds.stream() + .map(this::getAllShopsWithoutPartitioning) + .flatMap(List::stream) + .collect(Collectors.toList()); + //@formatter:on + + return miraklShops.stream().collect(Collectors.toMap(MiraklShop::getId, Function.identity())); + } + + private List getAllShopsWithoutPartitioning(final List shopIds) { + if (shopIds.isEmpty()) { + return Collections.emptyList(); + } + + try { + final MiraklGetShopsRequest request = createShopRequest(new HashSet<>(shopIds)); + final MiraklShops miraklShops = miraklMarketplacePlatformOperatorApiClient.getShops(request); + return miraklShops.getShops(); + } + catch (final MiraklApiException ex) { + log.warn(String.format( + "Error while recovering shop info during invoice processing. Failing shop ids[%s]. Failure reason: %s", + String.join(",", shopIds), MiraklLoggingErrorsUtil.stringify(ex))); + return Collections.emptyList(); + } + } + + @NonNull + private MiraklGetShopsRequest createShopRequest(final Set shopIds) { + final MiraklGetShopsRequest request = new MiraklGetShopsRequest(); + request.setShopIds(shopIds); + request.setPaginate(false); + return request; + } + +} diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImpl.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImpl.java index 8d9ed8ba..3cebb39f 100644 --- a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImpl.java +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImpl.java @@ -8,6 +8,7 @@ import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.InvoiceModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -23,9 +24,10 @@ public MiraklInvoicesExtractServiceImpl( final MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient, final Converter miraklShopAccountingDocumentModelConverter, final MailNotificationUtil invoicesMailNotificationUtil, - final Converter miraklInvoiceToInvoiceModelConverter) { + final Converter miraklInvoiceToInvoiceModelConverter, + final AccountingDocumentsLinksService accountingDocumentsLinksService) { super(miraklShopAccountingDocumentModelConverter, miraklMarketplacePlatformOperatorApiClient, - invoicesMailNotificationUtil); + accountingDocumentsLinksService, invoicesMailNotificationUtil); this.miraklInvoiceToInvoiceModelConverter = miraklInvoiceToInvoiceModelConverter; } diff --git a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceMock.java b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceMock.java index fd7e09d5..8261aa88 100644 --- a/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceMock.java +++ b/invoices/src/main/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceMock.java @@ -8,6 +8,7 @@ import com.paypal.invoices.infraestructure.testing.TestingInvoicesSessionDataHelper; import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.InvoiceModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -28,14 +29,15 @@ public MiraklInvoicesExtractServiceMock( final Converter miraklShopAccountingDocumentModelConverter, final MailNotificationUtil invoicesMailNotificationUtil, final Converter miraklInvoiceToInvoiceModelConverter, + final AccountingDocumentsLinksService accountingDocumentsLinksService, final TestingInvoicesSessionDataHelper testingInvoicesSessionDataHelper) { super(miraklMarketplacePlatformOperatorApiClient, miraklShopAccountingDocumentModelConverter, - invoicesMailNotificationUtil, miraklInvoiceToInvoiceModelConverter); + invoicesMailNotificationUtil, miraklInvoiceToInvoiceModelConverter, accountingDocumentsLinksService); this.testingInvoicesSessionDataHelper = testingInvoicesSessionDataHelper; } @Override - protected List getAccountingDocuments(final Date delta) { + public List extractAccountingDocuments(final Date delta) { //@formatter:off return Stream.ofNullable(testingInvoicesSessionDataHelper.getInvoices()) .flatMap(Collection::stream) diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobTest.java new file mode 100644 index 00000000..5a2d869b --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AbstractAccountingDocumentBatchJobTest.java @@ -0,0 +1,76 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.*; +import com.paypal.invoices.batchjobs.invoices.InvoiceExtractJobItem; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class AbstractAccountingDocumentBatchJobTest { + + @InjectMocks + private MyAccountingDocumentBatchJob testObj; + + @Mock + private AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricherMock; + + @Mock + private AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessorMock; + + @Mock + private AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidatorMock; + + @SuppressWarnings("unchecked") + @Test + void getBatchJobItemValidator_shouldReturnAccountingDocumenHandler() { + assertThat(testObj.getBatchJobItemValidator()) + .contains((BatchJobItemValidator) accountingDocumentBatchJobItemValidatorMock); + } + + @SuppressWarnings("unchecked") + @Test + void getBatchJobPreProcessor_shouldReturnAccountingDocumenHandler() { + assertThat(testObj.getBatchJobPreProcessor()) + .contains((BatchJobPreProcessor) accountingDocumentBatchJobPreProcessorMock); + } + + @SuppressWarnings("unchecked") + @Test + void getBatchJobItemEnricher_shouldReturnAccountingDocumenHandler() { + assertThat(testObj.getBatchJobItemEnricher()) + .contains((BatchJobItemEnricher) accountingDocumentBatchJobItemEnricherMock); + } + + static class MyAccountingDocumentBatchJob extends AbstractAccountingDocumentBatchJob { + + protected MyAccountingDocumentBatchJob( + AccountingDocumentBatchJobItemEnricher accountingDocumentBatchJobItemEnricher, + AccountingDocumentBatchJobPreProcessor accountingDocumentBatchJobPreProcessor, + AccountingDocumentBatchJobItemValidator accountingDocumentBatchJobItemValidator) { + super(accountingDocumentBatchJobItemEnricher, accountingDocumentBatchJobPreProcessor, + accountingDocumentBatchJobItemValidator); + } + + @Override + protected BatchJobItemProcessor getBatchJobItemProcessor() { + return null; + } + + @Override + protected BatchJobItemsExtractor getBatchJobItemsExtractor() { + return null; + } + + @Override + public BatchJobType getType() { + return null; + } + + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobHandlersTestSupport.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobHandlersTestSupport.java new file mode 100644 index 00000000..8ccfcfb8 --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobHandlersTestSupport.java @@ -0,0 +1,32 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.invoices.invoicesextract.model.InvoiceModel; + +public abstract class AccountingDocumentBatchJobHandlersTestSupport { + + static class TestAccountingDocumentBatchJobItem extends AbstractAccountingDocumentBatchJobItem { + + public static final String ITEM_TYPE = "Invoice"; + + public TestAccountingDocumentBatchJobItem(InvoiceModel item) { + super(item); + } + + @Override + public String getItemId() { + return getItem().getInvoiceNumber(); + } + + @Override + public String getItemType() { + return ITEM_TYPE; + } + + @Override + protected TestAccountingDocumentBatchJobItem from(InvoiceModel item) { + return new TestAccountingDocumentBatchJobItem(item); + } + + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricherTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricherTest.java new file mode 100644 index 00000000..8af44cf4 --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemEnricherTest.java @@ -0,0 +1,65 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.invoices.invoicesextract.model.InvoiceModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AccountingDocumentBatchJobItemEnricherTest extends AccountingDocumentBatchJobHandlersTestSupport { + + @InjectMocks + @Spy + private AccountingDocumentBatchJobItemEnricher testObj; + + @Mock + private AccountingDocumentsLinksService accountingDocumentsLinksServiceMock; + + @Mock + private BatchJobContext batchJobContextMock; + + @Mock + private InvoiceModel invoiceModelMock, enrichedInvoiceModelMock; + + @Mock + private TestAccountingDocumentBatchJobItem testAccountingDocumentBatchJobItemMock, + enrichedTestAccountingDocumentBatchJobItemMock; + + @Mock + private HyperwalletItemLinkLocator hyperwalletItemLinkLocator1Mock, hyperwalletItemLinkLocator2Mock; + + @Test + void enrichItem_shouldAddBankAccountAndProgramToken() { + when(testAccountingDocumentBatchJobItemMock.getItem()).thenReturn(invoiceModelMock); + + when(accountingDocumentsLinksServiceMock.findRequiredLinks(invoiceModelMock)) + .thenReturn(List.of(hyperwalletItemLinkLocator1Mock, hyperwalletItemLinkLocator2Mock)); + when(hyperwalletItemLinkLocator1Mock.getType()).thenReturn(HyperwalletItemTypes.BANK_ACCOUNT); + when(hyperwalletItemLinkLocator1Mock.getId()).thenReturn("B"); + when(hyperwalletItemLinkLocator2Mock.getType()).thenReturn(HyperwalletItemTypes.PROGRAM); + when(hyperwalletItemLinkLocator2Mock.getId()).thenReturn("P"); + + when(testAccountingDocumentBatchJobItemMock.from(enrichedInvoiceModelMock)) + .thenReturn(enrichedTestAccountingDocumentBatchJobItemMock); + doReturn(enrichedInvoiceModelMock).when(testObj).buildEnriched(invoiceModelMock, "B", "P"); + + TestAccountingDocumentBatchJobItem result = (TestAccountingDocumentBatchJobItem) testObj + .enrichItem(batchJobContextMock, testAccountingDocumentBatchJobItemMock); + + assertThat(result).isEqualTo(enrichedTestAccountingDocumentBatchJobItemMock); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidatorTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidatorTest.java new file mode 100644 index 00000000..0f97371e --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobItemValidatorTest.java @@ -0,0 +1,68 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItemValidationResult; +import com.paypal.infrastructure.batchjob.BatchJobItemValidationStatus; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.model.InvoiceModel; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AccountingDocumentBatchJobItemValidatorTest extends AccountingDocumentBatchJobHandlersTestSupport { + + @InjectMocks + private AccountingDocumentBatchJobItemValidator testObj; + + @Mock + private BatchJobContext batchJobContextMock; + + @Mock + private InvoiceModel invoiceModelMock; + + @Mock + private TestAccountingDocumentBatchJobItem testAccountingDocumentBatchJobItemMock; + + @Test + void validateItem_shouldReturnValidIfBankAccountAndProgramAreSet() { + when(testAccountingDocumentBatchJobItemMock.getItem()).thenReturn(invoiceModelMock); + when(invoiceModelMock.getHyperwalletProgram()).thenReturn("SOMETHING"); + when(invoiceModelMock.getDestinationToken()).thenReturn("SOMETHING"); + + BatchJobItemValidationResult result = testObj.validateItem(batchJobContextMock, + testAccountingDocumentBatchJobItemMock); + + assertThat(result.getStatus()).isEqualTo(BatchJobItemValidationStatus.VALID); + } + + @Test + void validateItem_shouldReturnInvalidIfBankAccountIsNotSet() { + when(testAccountingDocumentBatchJobItemMock.getItem()).thenReturn(invoiceModelMock); + when(testAccountingDocumentBatchJobItemMock.getItemId()).thenReturn("ITEMID-1"); + when(invoiceModelMock.getHyperwalletProgram()).thenReturn("SOMETHING"); + + BatchJobItemValidationResult result = testObj.validateItem(batchJobContextMock, + testAccountingDocumentBatchJobItemMock); + + assertThat(result.getStatus()).isEqualTo(BatchJobItemValidationStatus.INVALID); + assertThat(result.getReason()).contains( + "Invoice documents with id [ITEMID-1] should be skipped because are lacking hw-program or bank account token"); + } + + @Test + void validateItem_shouldReturnInvalidIfProgramIsNotSet() { + when(testAccountingDocumentBatchJobItemMock.getItem()).thenReturn(invoiceModelMock); + + BatchJobItemValidationResult result = testObj.validateItem(batchJobContextMock, + testAccountingDocumentBatchJobItemMock); + + assertThat(result.getStatus()).isEqualTo(BatchJobItemValidationStatus.INVALID); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessorTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessorTest.java new file mode 100644 index 00000000..a1e00851 --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/common/AccountingDocumentBatchJobPreProcessorTest.java @@ -0,0 +1,50 @@ +package com.paypal.invoices.batchjobs.common; + +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.invoices.invoicesextract.model.InvoiceModel; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AccountingDocumentBatchJobPreProcessorTest extends AccountingDocumentBatchJobHandlersTestSupport { + + @InjectMocks + private AccountingDocumentBatchJobPreProcessor testObj; + + @Mock + private AccountingDocumentsLinksService accountingDocumentsLinksServiceMock; + + @Mock + private BatchJobContext batchJobContextMock; + + @Mock + private InvoiceModel invoiceModel1Mock, invoiceModel2Mock; + + @Mock + private TestAccountingDocumentBatchJobItem testAccountingDocumentBatchJobItem1Mock, + testAccountingDocumentBatchJobItem2Mock; + + @Test + void prepareForProcessing_shouldStoreAllLinks() { + when(testAccountingDocumentBatchJobItem1Mock.getItem()).thenReturn(invoiceModel1Mock); + when(testAccountingDocumentBatchJobItem2Mock.getItem()).thenReturn(invoiceModel2Mock); + + testObj.prepareForProcessing(batchJobContextMock, + List.of(testAccountingDocumentBatchJobItem1Mock, testAccountingDocumentBatchJobItem2Mock)); + + verify(accountingDocumentsLinksServiceMock) + .storeRequiredLinks(argThat(x -> x.containsAll(List.of(invoiceModel1Mock, invoiceModel2Mock)))); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItemTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItemTest.java index 1b9fcfe5..b90548b2 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItemTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNoteExtractJobItemTest.java @@ -7,7 +7,9 @@ class CreditNoteExtractJobItemTest { - private static final String INVOICE_NUMBER = "002"; + private static final String INVOICE_NUMBER = "001"; + + private static final String INVOICE_NUMBER_2 = "002"; private static final String ITEM_TYPE = "CreditNote"; @@ -29,4 +31,17 @@ void getItemType_ShouldReturnCreditNote() { assertThat(testObj.getItemType()).isEqualTo(ITEM_TYPE); } + @Test + void from_ShouldReturnANewJobItemWithTheItemProvided() { + final CreditNoteModel creditNoteModel = CreditNoteModel.builder().invoiceNumber(INVOICE_NUMBER).build(); + final CreditNoteModel creditNoteModel2 = CreditNoteModel.builder().invoiceNumber(INVOICE_NUMBER_2).build(); + + final CreditNoteExtractJobItem testObj = new CreditNoteExtractJobItem(creditNoteModel); + + CreditNoteExtractJobItem result = testObj.from(creditNoteModel2); + + assertThat(result).isNotEqualTo(testObj); + assertThat(result.getItem().getInvoiceNumber()).isEqualTo(INVOICE_NUMBER_2); + } + } diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJobTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJobTest.java index 9fb28a00..e74016a4 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJobTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesExtractBatchJobTest.java @@ -3,6 +3,7 @@ import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -41,4 +42,9 @@ void getBatchJobItemProcessor_ShouldReturnCreditNotesExtractBatchJobItemProcesso assertThat(result).isEqualTo(creditNotesExtractBatchJobItemProcessorMock); } + @Test + void getBatchJobType_shouldReturnExtractType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.EXTRACT); + } + } diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJobTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJobTest.java index f8a1ba7a..e254042f 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJobTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/creditnotes/CreditNotesRetryBatchJobTest.java @@ -1,5 +1,6 @@ package com.paypal.invoices.batchjobs.creditnotes; +import com.paypal.infrastructure.batchjob.BatchJobType; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -30,4 +31,9 @@ void getBatchJobItemsExtractor_shouldReturnRetryBatchJobItemExtractor() { assertThat(testObj.getBatchJobItemsExtractor()).isEqualTo(creditNotesRetryBatchJobItemsExtractorMock); } -} \ No newline at end of file + @Test + void getBatchJobType_shouldReturnRetryType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.RETRY); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItemTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItemTest.java index 43c60676..49183ada 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItemTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoiceExtractJobItemTest.java @@ -7,7 +7,9 @@ class InvoiceExtractJobItemTest { - private static final String INVOICE_NUMBER = "002"; + private static final String INVOICE_NUMBER = "001"; + + private static final String INVOICE_NUMBER_2 = "002"; private static final String ITEM_TYPE = "Invoice"; @@ -29,4 +31,17 @@ void getItemType_ShouldReturnInvoice() { assertThat(testObj.getItemType()).isEqualTo(ITEM_TYPE); } + @Test + void from_ShouldReturnANewJobItemWithTheItemProvided() { + final InvoiceModel invoiceModel = InvoiceModel.builder().invoiceNumber(INVOICE_NUMBER).build(); + final InvoiceModel invoiceModel2 = InvoiceModel.builder().invoiceNumber(INVOICE_NUMBER_2).build(); + + final InvoiceExtractJobItem testObj = new InvoiceExtractJobItem(invoiceModel); + + InvoiceExtractJobItem result = testObj.from(invoiceModel2); + + assertThat(result).isNotEqualTo(testObj); + assertThat(result.getItem().getInvoiceNumber()).isEqualTo(INVOICE_NUMBER_2); + } + } diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJobTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJobTest.java index 31e2a88a..9c513a9c 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJobTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesExtractBatchJobTest.java @@ -3,6 +3,7 @@ import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.BatchJobType; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -40,4 +41,9 @@ void getBatchJobItemProcessor_ShouldReturnInvoicesExtractBatchJobItemProcessor() assertThat(result).isEqualTo(invoicesExtractBatchJobItemProcessorMock); } + @Test + void getBatchJobType_shouldReturnExtractType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.EXTRACT); + } + } diff --git a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJobTest.java b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJobTest.java index c0be7e21..178f0238 100644 --- a/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJobTest.java +++ b/invoices/src/test/java/com/paypal/invoices/batchjobs/invoices/InvoicesRetryBatchJobTest.java @@ -1,5 +1,6 @@ package com.paypal.invoices.batchjobs.invoices; +import com.paypal.infrastructure.batchjob.BatchJobType; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -30,4 +31,9 @@ void getBatchJobItemsExtractor_shouldReturnRetryBatchJobItemExtractor() { assertThat(testObj.getBatchJobItemsExtractor()).isEqualTo(invoicesRetryBatchJobItemsExtractorMock); } -} \ No newline at end of file + @Test + void getBatchJobType_shouldReturnRetryType() { + assertThat(testObj.getType()).isEqualTo(BatchJobType.RETRY); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImplTest.java b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImplTest.java new file mode 100644 index 00000000..e35e0738 --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/hmc/impl/AccountingDocumentsLinksServiceImplTest.java @@ -0,0 +1,101 @@ +package com.paypal.invoices.invoicesextract.service.hmc.impl; + +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import com.paypal.infrastructure.itemlinks.services.ItemLinksService; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.service.mirakl.MiraklInvoiceLinksService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AccountingDocumentsLinksServiceImplTest { + + private static final String SHOP_ID_1 = "1"; + + private static final String SHOP_ID_2 = "2"; + + @InjectMocks + private AccountingDocumentsLinksServiceImpl testObj; + + @Mock + private ItemLinksService itemLinksServiceMock; + + @Mock + private MiraklInvoiceLinksService miraklInvoiceLinksServiceMock; + + @Mock + private AccountingDocumentModel accountingDocumentModel1Mock, accountingDocumentModel2Mock; + + @Mock + private MiraklItemLinkLocator miraklItemLinkLocator1Mock, miraklItemLinkLocator2Mock; + + @Mock + private HyperwalletItemLinkLocator hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock; + + @Test + void storeRequiredLinks_shouldCreateNoneExistantLinksAndReturnAllLinks() { + when(accountingDocumentModel1Mock.getShopId()).thenReturn(SHOP_ID_1); + when(accountingDocumentModel2Mock.getShopId()).thenReturn(SHOP_ID_2); + when(miraklItemLinkLocator1Mock.getId()).thenReturn(SHOP_ID_1); + when(miraklItemLinkLocator2Mock.getId()).thenReturn(SHOP_ID_2); + + //@formatter:off + when(itemLinksServiceMock.findLinks(argThat(new ArgThatContainsAllShops()), + eq(Set.of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM)))) + .thenReturn(Map.of(miraklItemLinkLocator1Mock, List.of(hyperwalletItemLink1LocatorMock), + miraklItemLinkLocator2Mock, List.of())); + + when(miraklInvoiceLinksServiceMock.getInvoiceRelatedShopLinks(Set.of(SHOP_ID_1, SHOP_ID_2))) + .thenReturn(Map.of(miraklItemLinkLocator1Mock, List.of(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock), + miraklItemLinkLocator2Mock, List.of(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock))); + //@formatter:on + + testObj.storeRequiredLinks(List.of(accountingDocumentModel1Mock, accountingDocumentModel2Mock)); + + verify(itemLinksServiceMock, times(1)).createLinks(miraklItemLinkLocator1Mock, + List.of(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock)); + verify(itemLinksServiceMock, times(1)).createLinks(miraklItemLinkLocator2Mock, + List.of(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock)); + } + + @Test + void findRequiredLinks_shouldRecoverAllLocalStoredLinks() { + when(accountingDocumentModel1Mock.getShopId()).thenReturn("SHOP-1"); + when(itemLinksServiceMock.findLinks( + (MiraklItemLinkLocator) argThat(x -> ((MiraklItemLinkLocator) x).getId().equals("SHOP-1")), + eq(Set.of(HyperwalletItemTypes.BANK_ACCOUNT, HyperwalletItemTypes.PROGRAM)))) + .thenReturn(List.of(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock)); + + Collection result = testObj.findRequiredLinks(accountingDocumentModel1Mock); + + assertThat(result).containsExactlyInAnyOrder(hyperwalletItemLink1LocatorMock, hyperwalletItemLink2LocatorMock); + } + + static class ArgThatContainsAllShops implements ArgumentMatcher> { + + @Override + public boolean matches(Collection argument) { + // formatter:off + return argument.stream().map(MiraklItemLinkLocator::getId).collect(Collectors.toSet()) + .containsAll(Set.of(SHOP_ID_1, SHOP_ID_2)); + // formatter:on + } + + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImplTest.java b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImplTest.java index 1c08502b..34150dde 100644 --- a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImplTest.java +++ b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/AbstractAccountingDocumentsExtractServiceImplTest.java @@ -1,20 +1,19 @@ package com.paypal.invoices.invoicesextract.service.mirakl.impl; -import com.mirakl.client.core.error.MiraklErrorResponseBean; -import com.mirakl.client.core.exception.MiraklApiException; import com.mirakl.client.mmp.domain.shop.MiraklShop; -import com.mirakl.client.mmp.domain.shop.MiraklShops; import com.mirakl.client.mmp.operator.request.payment.invoice.MiraklGetInvoicesRequest; -import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; import com.paypal.infrastructure.converter.Converter; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; import com.paypal.infrastructure.mail.MailNotificationUtil; import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoice; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoices; -import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; +import com.paypal.infrastructure.util.DateUtil; import com.paypal.infrastructure.util.TimeMachine; import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; +import com.paypal.invoices.invoicesextract.service.hmc.AccountingDocumentsLinksService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -22,14 +21,17 @@ import org.springframework.beans.factory.annotation.Value; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; -import java.util.Collection; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentPaymentStatus.PENDING; +import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentType.MANUAL_CREDIT; +import static com.mirakl.client.mmp.request.payment.invoice.MiraklAccountingDocumentState.COMPLETE; +import static com.paypal.infrastructure.constants.HyperWalletConstants.MIRAKL_MAX_RESULTS_PER_PAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -48,6 +50,7 @@ class AbstractAccountingDocumentsExtractServiceImplTest { protected int maxNumberOfDaysForInvoiceIdSearch; @InjectMocks + @Spy private MyAccountingDocumentsExtractServiceImplTest testObj; @Mock @@ -59,27 +62,15 @@ class AbstractAccountingDocumentsExtractServiceImplTest { @Mock private MyAccountingDocumentModel myAccountingDocumentModel1Mock, myAccountingDocumentModel2Mock; - @Mock - private MailNotificationUtil mailNotificationUtilMock; - @Mock private HMCMiraklInvoice hmcMiraklInvoice1Mock, hmcMiraklInvoice2Mock, hmcMiraklInvoice3Mock; @Mock - private HMCMiraklInvoices hmcMiraklInvoicesMock; - - @Mock - private MiraklShops miraklShops1Mock, miraklShops2Mock, miraklShops3Mock; - - @Mock - private MiraklShop miraklShop1Mock, miraklShop2Mock, miraklShop3Mock; + private HMCMiraklInvoices hmcMiraklInvoicesMock, hmcMiraklInvoices2Mock; @Captor private ArgumentCaptor miraklGetInvoicesRequestArgumentCaptor; - @Captor - private ArgumentCaptor miraklGetShopsRequestArgumentCaptor; - @Test void extractAccountingDocumentsById_shouldReturnDocuments_whenTheyAreInsideSearchWindow() { TimeMachine.useFixedClockAt(LocalDateTime.now()); @@ -105,68 +96,97 @@ void extractAccountingDocumentsById_shouldReturnDocuments_whenTheyAreInsideSearc } @Test - void getMiraklShops_shouldSplitRequestInBatchesOfFixedSize() { - //@formatter:off - List invoicesMocks = Stream.iterate(1, n -> n + 1) - .limit(250) - .map(this::createMyAccountingDocumentModelMock) - .collect(Collectors.toList()); + void extractAccountingDocument_whenNoInvoicesAreReturned_shouldReturnEmptyList() { + final LocalDateTime now = LocalDateTime.now(); + TimeMachine.useFixedClockAt(now); + final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); + + doReturn(Collections.emptyList()).when(testObj).extractAccountingDocuments(nowAsDate); - when(miraklMarketplacePlatformOperatorApiClient.getShops(argThat(request -> request.getShopIds().size() <= 100))) - .thenReturn(miraklShops1Mock) - .thenReturn(miraklShops2Mock) - .thenReturn(miraklShops3Mock); - //@formatter:on - when(miraklShops1Mock.getShops()).thenReturn(List.of(miraklShop1Mock)); - when(miraklShops2Mock.getShops()).thenReturn(List.of(miraklShop2Mock)); - when(miraklShops3Mock.getShops()).thenReturn(List.of(miraklShop3Mock)); + final List result = testObj.extractAccountingDocuments(nowAsDate); - List result = testObj.getMiraklShops(invoicesMocks); + assertThat(result).isEmpty(); + } - assertThat(result).containsExactlyInAnyOrder(miraklShop1Mock, miraklShop2Mock, miraklShop3Mock); + @Test + void extractAccountingDocuments_shouldReturnListOfAccountingDocuments() { + TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); + final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - verify(miraklMarketplacePlatformOperatorApiClient, times(2)) - .getShops(argThat(request -> request.getShopIds().size() == 100)); - verify(miraklMarketplacePlatformOperatorApiClient, times(1)) - .getShops(argThat(request -> request.getShopIds().size() == 50)); + when(miraklMarketplacePlatformOperatorApiClient.getInvoices(any())).thenReturn(hmcMiraklInvoicesMock); + when(hmcMiraklInvoicesMock.getHmcInvoices()).thenReturn(List.of(hmcMiraklInvoice1Mock, hmcMiraklInvoice2Mock)); + when(invoiceConverterMock.convert(hmcMiraklInvoice1Mock)).thenReturn(myAccountingDocumentModel1Mock); + when(invoiceConverterMock.convert(hmcMiraklInvoice2Mock)).thenReturn(myAccountingDocumentModel2Mock); + + final List creditNoteList = testObj.extractAccountingDocuments(now); + + verify(miraklMarketplacePlatformOperatorApiClient) + .getInvoices(miraklGetInvoicesRequestArgumentCaptor.capture()); + + final MiraklGetInvoicesRequest miraklGetInvoicesRequest = miraklGetInvoicesRequestArgumentCaptor.getValue(); + + assertThat(miraklGetInvoicesRequest.getStartDate()).isEqualTo(now); + assertThat(miraklGetInvoicesRequest.getStates()).isEqualTo(List.of(COMPLETE)); + assertThat(miraklGetInvoicesRequest.getPaymentStatus()).isEqualTo(PENDING); + + assertThat(creditNoteList).containsExactlyInAnyOrder(myAccountingDocumentModel1Mock, + myAccountingDocumentModel2Mock); } @Test - void getMiraklShops_shouldSplitRequestInBatchesOfFixedSize_AndContinueOnErrorsInIndividualBatches() { - //@formatter:off - List invoicesMocks = Stream.iterate(1, n -> n + 1) - .limit(250) - .map(this::createMyAccountingDocumentModelMock) - .collect(Collectors.toList()); + void createAccountingDocumentRequest_shouldReturnRequestWithTargetInvoiceType() { + final Date date = new Date(); + + final MiraklGetInvoicesRequest result = testObj.createAccountingDocumentRequest(date, + InvoiceTypeEnum.MANUAL_CREDIT); - final MiraklApiException miraklApiException = new MiraklApiException( - new MiraklErrorResponseBean(1, "Something went wrong")); - when(miraklMarketplacePlatformOperatorApiClient.getShops(argThat(request -> request.getShopIds().size() <= 100))) - .thenReturn(miraklShops1Mock) - .thenThrow(miraklApiException) - .thenReturn(miraklShops3Mock); - //@formatter:on - when(miraklShops1Mock.getShops()).thenReturn(List.of(miraklShop1Mock)); - when(miraklShops3Mock.getShops()).thenReturn(List.of(miraklShop3Mock)); - - List result = testObj.getMiraklShops(invoicesMocks); - - assertThat(result).containsExactlyInAnyOrder(miraklShop1Mock, miraklShop3Mock); - - verify(miraklMarketplacePlatformOperatorApiClient, times(3)) - .getShops(miraklGetShopsRequestArgumentCaptor.capture()); - MiraklGetShopsRequest failedRequest = miraklGetShopsRequestArgumentCaptor.getAllValues().get(1); - verify(mailNotificationUtilMock).sendPlainTextEmail("Issue detected getting shops in Mirakl", - String.format("Something went wrong getting information of " + "shops" + " [%s]%n%s", - failedRequest.getShopIds().stream().sorted().collect(Collectors.joining(",")), - MiraklLoggingErrorsUtil.stringify(miraklApiException))); + assertThat(result.getMax()).isEqualTo(100); + assertThat(result.getStartDate()).isEqualTo(date); + assertThat(result.getType()).isEqualTo(MANUAL_CREDIT); + assertThat(result.getPaymentStatus()).isEqualTo(PENDING); + assertThat(result.getStates()).containsExactly(COMPLETE); } - private MyAccountingDocumentModel createMyAccountingDocumentModelMock(int id) { - MyAccountingDocumentModel myAccountingDocumentModelMock = Mockito.mock(MyAccountingDocumentModel.class); - when(myAccountingDocumentModelMock.getShopId()).thenReturn(String.valueOf(id)); + @Test + void getAccountingDocuments_whenRequestNeedsPagination_shouldRepeatRequestAndReturnAllInvoices() { + TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); + final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); + + final List firstPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( + MIRAKL_MAX_RESULTS_PER_PAGE); + final List secondPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( + MIRAKL_MAX_RESULTS_PER_PAGE / 2); + final long totalResponseInvoices = firstPageResponseInvoices.size() + secondPageResponseInvoices.size(); + + when(miraklMarketplacePlatformOperatorApiClient + .getInvoices(argThat(request -> request != null && request.getOffset() == 0))) + .thenReturn(hmcMiraklInvoicesMock); + when(miraklMarketplacePlatformOperatorApiClient + .getInvoices(argThat(request -> request != null && request.getOffset() == MIRAKL_MAX_RESULTS_PER_PAGE))) + .thenReturn(hmcMiraklInvoices2Mock); + when(hmcMiraklInvoicesMock.getTotalCount()).thenReturn(totalResponseInvoices); + when(hmcMiraklInvoicesMock.getHmcInvoices()).thenReturn(firstPageResponseInvoices); + when(hmcMiraklInvoices2Mock.getTotalCount()).thenReturn(totalResponseInvoices); + when(hmcMiraklInvoices2Mock.getHmcInvoices()).thenReturn(secondPageResponseInvoices); + + final List expectedAccountingDocuments = Stream + .concat(firstPageResponseInvoices.stream(), secondPageResponseInvoices.stream()) + .map(invoice -> mockAndReturn(invoice)).collect(Collectors.toList()); + + final List result = testObj.extractAccountingDocuments(now); + + assertThat(result).containsExactlyElementsOf(expectedAccountingDocuments); + } - return myAccountingDocumentModelMock; + private MyAccountingDocumentModel mockAndReturn(final HMCMiraklInvoice invoice) { + final MyAccountingDocumentModel myAccountingDocumentModel = mock(MyAccountingDocumentModel.class); + when(invoiceConverterMock.convert(invoice)).thenReturn(myAccountingDocumentModel); + return myAccountingDocumentModel; + } + + private List getListOfHMCMiraklInvoiceMocks(final int numberOfMocks) { + return IntStream.range(0, numberOfMocks).mapToObj(i -> mock(HMCMiraklInvoice.class)) + .collect(Collectors.toList()); } private Date searchWindow() { @@ -181,10 +201,11 @@ static class MyAccountingDocumentsExtractServiceImplTest protected MyAccountingDocumentsExtractServiceImplTest( Converter miraklShopToAccountingModelConverter, MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClient, + AccountingDocumentsLinksService accountingDocumentsLinksService, MailNotificationUtil invoicesMailNotificationUtil, Converter miraklInvoiceToInvoiceModelConverter) { super(miraklShopToAccountingModelConverter, miraklMarketplacePlatformOperatorApiClient, - invoicesMailNotificationUtil); + accountingDocumentsLinksService, invoicesMailNotificationUtil); this.miraklInvoiceToInvoiceModelConverter = miraklInvoiceToInvoiceModelConverter; } diff --git a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImplTest.java b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImplTest.java index e1936a2d..090eaf32 100644 --- a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImplTest.java +++ b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklCreditNotesExtractServiceImplTest.java @@ -1,324 +1,35 @@ package com.paypal.invoices.invoicesextract.service.mirakl.impl; -import com.mirakl.client.core.error.MiraklErrorResponseBean; -import com.mirakl.client.core.exception.MiraklApiException; -import com.mirakl.client.mmp.domain.shop.MiraklShop; -import com.mirakl.client.mmp.domain.shop.MiraklShops; -import com.mirakl.client.mmp.operator.request.payment.invoice.MiraklGetInvoicesRequest; -import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; import com.paypal.infrastructure.converter.Converter; -import com.paypal.infrastructure.exceptions.HMCMiraklAPIException; -import com.paypal.infrastructure.mail.MailNotificationUtil; -import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoice; -import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoices; -import com.paypal.infrastructure.util.DateUtil; -import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; -import com.paypal.infrastructure.util.TimeMachine; -import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.CreditNoteModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; -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.Captor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentPaymentStatus.PENDING; -import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentType.MANUAL_CREDIT; -import static com.mirakl.client.mmp.request.payment.invoice.MiraklAccountingDocumentState.COMPLETE; -import static com.paypal.infrastructure.constants.HyperWalletConstants.MIRAKL_MAX_RESULTS_PER_PAGE; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class MiraklCreditNotesExtractServiceImplTest { - private static final String SHOP_ID_ONE = "2000"; - - private static final String SHOP_ID_TWO = "2001"; - - private static final String TOKEN_1 = "token1"; - - private static final String TOKEN_2 = "token2"; - - private static final String SHOP_ID_ATTRIBUTE = "shopId"; - - private static final String DESTINATION_TOKEN_ATTRIBUTE = "destinationToken"; - - private static final String HYPERWALLET_PROGRAM = "hwProgram"; - + @InjectMocks private MiraklCreditNotesExtractServiceImpl testObj; @Mock - private MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClientMock; - - @Mock - private MiraklShops miraklShopsMock; - - @Mock - private MiraklShop miraklShopOneMock, miraklShopTwoMock; - - @Mock - private Converter miraklShopToAccountingModelConverter; - - @Mock - private CreditNoteModel creditNoteModelModelConvertedFromShopOneMock, creditNoteModelConvertedFromShopTwoMock, - creditNoteModelConvertedFromShopThreeMock; - - @Captor - private ArgumentCaptor miraklGetShopsRequestArgumentCaptor; - - @Mock - private MiraklShop miraklShopThreeMock; - - @Mock - private HMCMiraklInvoices miraklInvoicesOneMock, miraklInvoicesTwoMock; - - @Mock - private HMCMiraklInvoice miraklInvoiceOneMock, miraklInvoiceTwoMock; - - @Mock - private MailNotificationUtil mailNotificationUtilMock; - - @Mock - private Converter miraklInvoiceToCreditNoteModelConverter; - - @Mock - private CreditNoteModel creditNoteOneMock, creditNoteTwoMock; - - @Mock - private MiraklGetInvoicesRequest miraklGetInvoicesRequestMock; - - @Captor - private ArgumentCaptor miraklGetInvoicesRequestArgumentCaptor; - - @BeforeEach - void setUp() { - testObj = new MiraklCreditNotesExtractServiceImpl(miraklMarketplacePlatformOperatorApiClientMock, - miraklShopToAccountingModelConverter, miraklInvoiceToCreditNoteModelConverter, - mailNotificationUtilMock); - - testObj = spy(testObj); - } - - @Test - void extractAccountingDocument_whenRequestRequiresPagination_shouldRequestAllShops_AndPopulateCreditNotesModelWithTheTokensStoredInMirakl() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - final CreditNoteModel creditNoteOne = CreditNoteModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1) - .build(); - final CreditNoteModel creditNoteTwo = CreditNoteModel.builder().shopId(SHOP_ID_TWO).destinationToken(TOKEN_2) - .build(); - doReturn(List.of(creditNoteOne, creditNoteTwo)).when(testObj).getAccountingDocuments(nowAsDate); - - when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShopOneMock, miraklShopTwoMock)); - when(miraklMarketplacePlatformOperatorApiClientMock.getShops(any(MiraklGetShopsRequest.class))) - .thenReturn(miraklShopsMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopOneMock)) - .thenReturn(creditNoteModelModelConvertedFromShopOneMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopTwoMock)) - .thenReturn(creditNoteModelConvertedFromShopTwoMock); - when(creditNoteModelModelConvertedFromShopOneMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(creditNoteModelConvertedFromShopTwoMock.getShopId()).thenReturn(SHOP_ID_TWO); - when(creditNoteModelModelConvertedFromShopOneMock.getDestinationToken()).thenReturn(TOKEN_1); - when(creditNoteModelConvertedFromShopTwoMock.getDestinationToken()).thenReturn(TOKEN_2); - when(creditNoteModelModelConvertedFromShopOneMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(creditNoteModelConvertedFromShopTwoMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - verify(miraklMarketplacePlatformOperatorApiClientMock).getShops(miraklGetShopsRequestArgumentCaptor.capture()); - - assertThat(miraklGetShopsRequestArgumentCaptor.getValue().getShopIds()).containsExactlyInAnyOrder(SHOP_ID_ONE, - SHOP_ID_TWO); - - assertThat(result.get(0)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); - assertThat(result.get(1)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_TWO) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_2); - } - - @Test - void extractAccountingDocument_whenNoInvoicesAreReturned_shouldReturnEmptyList() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - doReturn(Collections.emptyList()).when(testObj).getAccountingDocuments(nowAsDate); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - assertThat(result).isEmpty(); - } - - @Test - void extractAccountingDocument_whenSeveralCreditNotesPerShopAreExtractedFromMirakl_shouldPopulateTheCreditNotes() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - final CreditNoteModel creditNoteOne = CreditNoteModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1) - .build(); - final CreditNoteModel creditNoteTwo = CreditNoteModel.builder().shopId(SHOP_ID_TWO).destinationToken(TOKEN_2) - .build(); - final CreditNoteModel creditNoteThree = CreditNoteModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1) - .build(); - - doReturn(List.of(creditNoteOne, creditNoteTwo, creditNoteThree)).when(testObj) - .getAccountingDocuments(nowAsDate); - - when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShopOneMock, miraklShopTwoMock, miraklShopThreeMock)); - when(miraklMarketplacePlatformOperatorApiClientMock.getShops(any(MiraklGetShopsRequest.class))) - .thenReturn(miraklShopsMock); - - when(miraklShopToAccountingModelConverter.convert(miraklShopOneMock)) - .thenReturn(creditNoteModelModelConvertedFromShopOneMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopTwoMock)) - .thenReturn(creditNoteModelConvertedFromShopTwoMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopThreeMock)) - .thenReturn(creditNoteModelConvertedFromShopThreeMock); - when(creditNoteModelModelConvertedFromShopOneMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(creditNoteModelConvertedFromShopTwoMock.getShopId()).thenReturn(SHOP_ID_TWO); - when(creditNoteModelConvertedFromShopThreeMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(creditNoteModelModelConvertedFromShopOneMock.getDestinationToken()).thenReturn(TOKEN_1); - when(creditNoteModelConvertedFromShopTwoMock.getDestinationToken()).thenReturn(TOKEN_2); - when(creditNoteModelConvertedFromShopThreeMock.getDestinationToken()).thenReturn(TOKEN_1); - when(creditNoteModelModelConvertedFromShopOneMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(creditNoteModelConvertedFromShopTwoMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(creditNoteModelConvertedFromShopThreeMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - verify(miraklMarketplacePlatformOperatorApiClientMock).getShops(miraklGetShopsRequestArgumentCaptor.capture()); - - assertThat(miraklGetShopsRequestArgumentCaptor.getValue().getShopIds()).containsExactlyInAnyOrder(SHOP_ID_ONE, - SHOP_ID_TWO); - - assertThat(result.get(0)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); - assertThat(result.get(1)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_TWO) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_2); - assertThat(result.get(2)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); - } - - @Test - void getAccountingDocuments_shouldReturnListOfCreditNoteModels() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - when(miraklMarketplacePlatformOperatorApiClientMock.getInvoices(any())).thenReturn(miraklInvoicesOneMock); - when(miraklInvoicesOneMock.getHmcInvoices()).thenReturn(List.of(miraklInvoiceOneMock, miraklInvoiceTwoMock)); - when(miraklInvoiceToCreditNoteModelConverter.convert(miraklInvoiceOneMock)).thenReturn(creditNoteOneMock); - when(miraklInvoiceToCreditNoteModelConverter.convert(miraklInvoiceTwoMock)).thenReturn(creditNoteTwoMock); - - final List creditNoteList = testObj.getAccountingDocuments(now); - - verify(miraklMarketplacePlatformOperatorApiClientMock) - .getInvoices(miraklGetInvoicesRequestArgumentCaptor.capture()); - - final MiraklGetInvoicesRequest miraklGetInvoicesRequest = miraklGetInvoicesRequestArgumentCaptor.getValue(); - - assertThat(miraklGetInvoicesRequest.getStartDate()).isEqualTo(now); - assertThat(miraklGetInvoicesRequest.getStates()).isEqualTo(List.of(COMPLETE)); - assertThat(miraklGetInvoicesRequest.getPaymentStatus()).isEqualTo(PENDING); - - assertThat(creditNoteList).containsExactlyInAnyOrder(creditNoteOneMock, creditNoteTwoMock); - } - - @Test - void getAccountingDocuments_whenNoMiraklCreditNotesAreReceived_shouldReturnEmptyList() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - when(miraklMarketplacePlatformOperatorApiClientMock.getInvoices(any())).thenReturn(miraklInvoicesOneMock); - - final List creditNoteList = testObj.getAccountingDocuments(now); - - assertThat(creditNoteList).isEqualTo(Collections.emptyList()); - } + private Converter miraklInvoiceToCreditNoteModelConverterMock; @Test - void createAccountingDocumentRequest_shouldReturnRequestWithInvoiceType() { - final Date date = new Date(); - - final MiraklGetInvoicesRequest result = testObj.createAccountingDocumentRequest(date, - InvoiceTypeEnum.MANUAL_CREDIT); - - assertThat(result.getMax()).isEqualTo(100); - assertThat(result.getStartDate()).isEqualTo(date); - assertThat(result.getType()).isEqualTo(MANUAL_CREDIT); - assertThat(result.getPaymentStatus()).isEqualTo(PENDING); - assertThat(result.getStates()).containsExactly(COMPLETE); + void shouldReturnCreditNoteType() { + assertThat(testObj.getInvoiceType()).isEqualTo(InvoiceTypeEnum.MANUAL_CREDIT); } @Test - void getAccountingDocuments_whenRequestNeedsPagination_shouldRepeatRequestAndReturnAllInvoices() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - final List firstPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( - MIRAKL_MAX_RESULTS_PER_PAGE); - final List secondPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( - MIRAKL_MAX_RESULTS_PER_PAGE / 2); - final long totalResponseInvoices = firstPageResponseInvoices.size() + secondPageResponseInvoices.size(); - - when(miraklMarketplacePlatformOperatorApiClientMock - .getInvoices(argThat(request -> request != null && request.getOffset() == 0))) - .thenReturn(miraklInvoicesOneMock); - when(miraklMarketplacePlatformOperatorApiClientMock - .getInvoices(argThat(request -> request != null && request.getOffset() == MIRAKL_MAX_RESULTS_PER_PAGE))) - .thenReturn(miraklInvoicesTwoMock); - when(miraklInvoicesOneMock.getTotalCount()).thenReturn(totalResponseInvoices); - when(miraklInvoicesOneMock.getHmcInvoices()).thenReturn(firstPageResponseInvoices); - when(miraklInvoicesTwoMock.getTotalCount()).thenReturn(totalResponseInvoices); - when(miraklInvoicesTwoMock.getHmcInvoices()).thenReturn(secondPageResponseInvoices); - - final List expectedCreditNotes = Stream - .concat(firstPageResponseInvoices.stream(), secondPageResponseInvoices.stream()) - .map(invoice -> mockAndReturn(invoice)).collect(Collectors.toList()); - - final List result = testObj.getAccountingDocuments(now); - - assertThat(result).containsExactlyElementsOf(expectedCreditNotes); - } - - private CreditNoteModel mockAndReturn(final HMCMiraklInvoice invoice) { - final CreditNoteModel creditNoteModelMock = mock(CreditNoteModel.class); - when(miraklInvoiceToCreditNoteModelConverter.convert(invoice)).thenReturn(creditNoteModelMock); - return creditNoteModelMock; - } - - private List getListOfHMCMiraklInvoiceMocks(final int numberOfMocks) { - return IntStream.range(0, numberOfMocks).mapToObj(i -> mock(HMCMiraklInvoice.class)) - .collect(Collectors.toList()); - } - - @Test - void createShopRequest_shouldCreateShopRequestWithGivenIds() { - final Set shopIds = Set.of(SHOP_ID_ONE, SHOP_ID_TWO); - - final MiraklGetShopsRequest result = testObj.createShopRequest(shopIds); - - assertThat(result.getShopIds()).isEqualTo(shopIds); - assertThat(result.isPaginate()).isFalse(); + void shouldReturnCreditNoteConverter() { + assertThat(testObj.getMiraklInvoiceToAccountingModelConverter()) + .isEqualTo(miraklInvoiceToCreditNoteModelConverterMock); } } diff --git a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImplTest.java b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImplTest.java new file mode 100644 index 00000000..5568a125 --- /dev/null +++ b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoiceLinksServiceImplTest.java @@ -0,0 +1,192 @@ +package com.paypal.invoices.invoicesextract.service.mirakl.impl; + +import com.mirakl.client.core.error.MiraklErrorResponseBean; +import com.mirakl.client.core.exception.MiraklApiException; +import com.mirakl.client.core.exception.MiraklException; +import com.mirakl.client.mmp.domain.shop.MiraklShop; +import com.mirakl.client.mmp.domain.shop.MiraklShops; +import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; +import com.paypal.infrastructure.converter.Converter; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.HyperwalletItemTypes; +import com.paypal.infrastructure.itemlinks.model.MiraklItemLinkLocator; +import com.paypal.infrastructure.itemlinks.model.MiraklItemTypes; +import com.paypal.infrastructure.mail.MailNotificationUtil; +import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; +import com.paypal.infrastructure.util.DateUtil; +import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; +import com.paypal.infrastructure.util.TimeMachine; +import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; +import com.paypal.invoices.invoicesextract.model.CreditNoteModel; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MiraklInvoiceLinksServiceImplTest { + + private static final String SHOP_ID_1 = "1"; + + private static final String SHOP_ID_2 = "2"; + + private static final String PROGRAM_TOKEN_1 = "PROG-1"; + + private static final String BANK_TOKEN_1 = "BANK-1"; + + private static final String BANK_TOKEN_2 = "BANK-2"; + + @InjectMocks + private MiraklInvoiceLinksServiceImpl testObj; + + @Mock + private MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClientMock; + + @Mock + private Converter miraklShopToAccountingModelConverterMock; + + @Mock + private MiraklShop miraklShop1Mock, miraklShop2Mock; + + @Mock + private AccountingDocumentModel accountingDocumentModel1Mock, accountingDocumentModel2Mock; + + @Mock + private MiraklShops miraklShopsMock, miraklShops2Mock; + + @Captor + private ArgumentCaptor miraklGetShopsRequestArgumentCaptor; + + @Test + void getShopLinks_shouldReturnProgramAndBankAccountLinks() { + when(miraklShop1Mock.getId()).thenReturn(SHOP_ID_1); + when(miraklShop2Mock.getId()).thenReturn(SHOP_ID_2); + + when(miraklShopToAccountingModelConverterMock.convert(miraklShop1Mock)) + .thenReturn(accountingDocumentModel1Mock); + when(miraklShopToAccountingModelConverterMock.convert(miraklShop2Mock)) + .thenReturn(accountingDocumentModel2Mock); + + when(accountingDocumentModel1Mock.getHyperwalletProgram()).thenReturn(PROGRAM_TOKEN_1); + when(accountingDocumentModel1Mock.getDestinationToken()).thenReturn(BANK_TOKEN_1); + when(accountingDocumentModel2Mock.getHyperwalletProgram()).thenReturn(PROGRAM_TOKEN_1); + when(accountingDocumentModel2Mock.getDestinationToken()).thenReturn(BANK_TOKEN_2); + + when(miraklMarketplacePlatformOperatorApiClientMock + .getShops(argThat(req -> req.getShopIds().containsAll(Set.of(SHOP_ID_1, SHOP_ID_2))))) + .thenReturn(miraklShopsMock); + when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShop1Mock, miraklShop2Mock)); + + Map> result = testObj + .getInvoiceRelatedShopLinks(Set.of("1", "2")); + + //@formatter:off + MiraklItemLinkLocator miraklItemLinkLocator1 = new MiraklItemLinkLocator(SHOP_ID_1, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator2 = new MiraklItemLinkLocator(SHOP_ID_2, MiraklItemTypes.SHOP); + assertThat(result.entrySet()).hasSize(2); + assertThat(result.keySet()).containsExactlyInAnyOrder(miraklItemLinkLocator1,miraklItemLinkLocator2); + assertThat(result.get(miraklItemLinkLocator1)).containsExactlyInAnyOrder( + new HyperwalletItemLinkLocator(PROGRAM_TOKEN_1, HyperwalletItemTypes.PROGRAM), + new HyperwalletItemLinkLocator(BANK_TOKEN_1, HyperwalletItemTypes.BANK_ACCOUNT)); + assertThat(result.get(miraklItemLinkLocator2)).containsExactlyInAnyOrder( + new HyperwalletItemLinkLocator(PROGRAM_TOKEN_1, HyperwalletItemTypes.PROGRAM), + new HyperwalletItemLinkLocator(BANK_TOKEN_2, HyperwalletItemTypes.BANK_ACCOUNT)); + //@formatter:on + } + + @Test + void getShopLinks_shouldNotReturnProgramAndBankAccountLinks_WhenShopFieldsAreEmpty() { + when(miraklShop1Mock.getId()).thenReturn(SHOP_ID_1); + when(miraklShop2Mock.getId()).thenReturn(SHOP_ID_2); + + when(miraklShopToAccountingModelConverterMock.convert(miraklShop1Mock)) + .thenReturn(accountingDocumentModel1Mock); + when(miraklShopToAccountingModelConverterMock.convert(miraklShop2Mock)) + .thenReturn(accountingDocumentModel2Mock); + + when(accountingDocumentModel1Mock.getHyperwalletProgram()).thenReturn(PROGRAM_TOKEN_1); + when(accountingDocumentModel1Mock.getDestinationToken()).thenReturn(null); + when(accountingDocumentModel2Mock.getHyperwalletProgram()).thenReturn(null); + when(accountingDocumentModel2Mock.getDestinationToken()).thenReturn(BANK_TOKEN_2); + + when(miraklMarketplacePlatformOperatorApiClientMock + .getShops(argThat(req -> req.getShopIds().containsAll(Set.of(SHOP_ID_1, SHOP_ID_2))))) + .thenReturn(miraklShopsMock); + when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShop1Mock, miraklShop2Mock)); + + Map> result = testObj + .getInvoiceRelatedShopLinks(Set.of("1", "2")); + + //@formatter:off + MiraklItemLinkLocator miraklItemLinkLocator1 = new MiraklItemLinkLocator(SHOP_ID_1, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator2 = new MiraklItemLinkLocator(SHOP_ID_2, MiraklItemTypes.SHOP); + assertThat(result.entrySet()).hasSize(2); + assertThat(result.keySet()).containsExactlyInAnyOrder(miraklItemLinkLocator1,miraklItemLinkLocator2); + assertThat(result.get(miraklItemLinkLocator1)).containsExactlyInAnyOrder( + new HyperwalletItemLinkLocator(PROGRAM_TOKEN_1, HyperwalletItemTypes.PROGRAM)); + assertThat(result.get(miraklItemLinkLocator2)).containsExactlyInAnyOrder( + new HyperwalletItemLinkLocator(BANK_TOKEN_2, HyperwalletItemTypes.BANK_ACCOUNT)); + //@formatter:on + } + + @Test + void getShopLinks_shouldIgnoreWhenMiraklHttpRequestFailAndReturnEmptyShopList() { + when(miraklMarketplacePlatformOperatorApiClientMock + .getShops(argThat(req -> req.getShopIds().containsAll(Set.of(SHOP_ID_1, SHOP_ID_2))))) + .thenThrow(new MiraklApiException(new MiraklErrorResponseBean(1, "Error"))); + + Map> result = testObj + .getInvoiceRelatedShopLinks(Set.of("1", "2")); + + MiraklItemLinkLocator miraklItemLinkLocator1 = new MiraklItemLinkLocator(SHOP_ID_1, MiraklItemTypes.SHOP); + MiraklItemLinkLocator miraklItemLinkLocator2 = new MiraklItemLinkLocator(SHOP_ID_2, MiraklItemTypes.SHOP); + assertThat(result.entrySet()).hasSize(2); + assertThat(result.keySet()).containsExactlyInAnyOrder(miraklItemLinkLocator1, miraklItemLinkLocator2); + assertThat(result.get(miraklItemLinkLocator1)).isEmpty(); + assertThat(result.get(miraklItemLinkLocator2)).isEmpty(); + } + + @Test + void getAllShops_shouldSplitRequestInBatchesOfFixedSize_AndContinueOnErrorsInIndividualBatches() { + final MiraklApiException miraklApiException = new MiraklApiException( + new MiraklErrorResponseBean(1, "Something went wrong")); + //@formatter:on + when(miraklMarketplacePlatformOperatorApiClientMock + .getShops(argThat(request -> request.getShopIds().size() <= 100))).thenReturn(miraklShopsMock) + .thenThrow(miraklApiException).thenReturn(miraklShops2Mock); + //@formatter:off + when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShop1Mock)); + when(miraklShops2Mock.getShops()).thenReturn(List.of(miraklShop2Mock)); + when(miraklShop1Mock.getId()).thenReturn("1"); + when(miraklShop2Mock.getId()).thenReturn("2"); + + Set invoiceIds = Stream.iterate(1, n -> n + 1) + .limit(250) + .map(String::valueOf) + .collect(Collectors.toSet()); + + Map result = testObj.getAllShops(invoiceIds); + + assertThat(result).hasSize(2); + assertThat(result.values()).containsExactlyInAnyOrder(miraklShop1Mock, miraklShop2Mock); + + verify(miraklMarketplacePlatformOperatorApiClientMock, times(3)) + .getShops(miraklGetShopsRequestArgumentCaptor.capture()); + MiraklGetShopsRequest failedRequest = miraklGetShopsRequestArgumentCaptor.getAllValues().get(1); + } + +} diff --git a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImplTest.java b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImplTest.java index 8950b412..06f9ca6b 100644 --- a/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImplTest.java +++ b/invoices/src/test/java/com/paypal/invoices/invoicesextract/service/mirakl/impl/MiraklInvoicesExtractServiceImplTest.java @@ -1,319 +1,35 @@ package com.paypal.invoices.invoicesextract.service.mirakl.impl; -import com.mirakl.client.core.error.MiraklErrorResponseBean; -import com.mirakl.client.core.exception.MiraklApiException; -import com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentPaymentStatus; -import com.mirakl.client.mmp.domain.shop.MiraklShop; -import com.mirakl.client.mmp.domain.shop.MiraklShops; -import com.mirakl.client.mmp.operator.request.payment.invoice.MiraklGetInvoicesRequest; -import com.mirakl.client.mmp.request.payment.invoice.MiraklAccountingDocumentState; -import com.mirakl.client.mmp.request.shop.MiraklGetShopsRequest; import com.paypal.infrastructure.converter.Converter; -import com.paypal.infrastructure.exceptions.HMCMiraklAPIException; -import com.paypal.infrastructure.mail.MailNotificationUtil; -import com.paypal.infrastructure.sdk.mirakl.MiraklMarketplacePlatformOperatorApiWrapper; import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoice; -import com.paypal.infrastructure.sdk.mirakl.domain.invoice.HMCMiraklInvoices; -import com.paypal.infrastructure.util.DateUtil; -import com.paypal.infrastructure.util.MiraklLoggingErrorsUtil; -import com.paypal.infrastructure.util.TimeMachine; -import com.paypal.invoices.invoicesextract.model.AccountingDocumentModel; import com.paypal.invoices.invoicesextract.model.InvoiceModel; import com.paypal.invoices.invoicesextract.model.InvoiceTypeEnum; -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.Captor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentPaymentStatus.PENDING; -import static com.mirakl.client.mmp.domain.accounting.document.MiraklAccountingDocumentType.AUTO_INVOICE; -import static com.mirakl.client.mmp.request.payment.invoice.MiraklAccountingDocumentState.COMPLETE; -import static com.paypal.infrastructure.constants.HyperWalletConstants.MIRAKL_MAX_RESULTS_PER_PAGE; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class MiraklInvoicesExtractServiceImplTest { - private static final String SHOP_ID_ONE = "2000"; - - private static final String SHOP_ID_TWO = "2001"; - - private static final String TOKEN_1 = "token1"; - - private static final String TOKEN_2 = "token2"; - - private static final String SHOP_ID_ATTRIBUTE = "shopId"; - - private static final String DESTINATION_TOKEN_ATTRIBUTE = "destinationToken"; - - private static final String HYPERWALLET_PROGRAM = "hwProgram"; - + @InjectMocks private MiraklInvoicesExtractServiceImpl testObj; - @Mock - private MiraklMarketplacePlatformOperatorApiWrapper miraklMarketplacePlatformOperatorApiClientMock; - - @Mock - private MiraklShops miraklShopsMock; - - @Mock - private MiraklShop miraklShopOneMock, miraklShopTwoMock, miraklShopThreeMock; - - @Mock - private Converter miraklShopToAccountingModelConverter; - - @Mock - private InvoiceModel invoiceModelConvertedFromShopOneMock, invoiceModelConvertedFromShopTwoMock, - invoiceModelConvertedFromShopThreeMock; - - @Mock - private HMCMiraklInvoices miraklInvoicesOneMock, miraklInvoicesTwoMock; - - @Mock - private HMCMiraklInvoice miraklInvoiceOneMock, miraklInvoiceTwoMock; - - @Mock - private MailNotificationUtil mailNotificationUtilMock; - @Mock private Converter miraklInvoiceToInvoiceModelConverterMock; - @Mock - private InvoiceModel invoiceModelOneMock, invoiceModelTwoMock; - - @Mock - private MiraklGetInvoicesRequest miraklGetInvoicesRequestMock; - - @Captor - private ArgumentCaptor miraklGetShopsRequestArgumentCaptor; - - @Captor - private ArgumentCaptor miraklGetInvoicesRequestArgumentCaptor; - - @BeforeEach - void setUp() { - testObj = new MiraklInvoicesExtractServiceImpl(miraklMarketplacePlatformOperatorApiClientMock, - miraklShopToAccountingModelConverter, mailNotificationUtilMock, - miraklInvoiceToInvoiceModelConverterMock); - - testObj = spy(testObj); - } - - @Test - void extractAccountingDocument_whenRequestRequiresPagination_shouldRequestAllShops_andPopulateInvoiceModelsWithTheTokensStoredInMirakl() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - final InvoiceModel invoiceOne = InvoiceModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1).build(); - final InvoiceModel invoiceTwo = InvoiceModel.builder().shopId(SHOP_ID_TWO).destinationToken(TOKEN_2).build(); - doReturn(List.of(invoiceOne, invoiceTwo)).when(testObj).getAccountingDocuments(nowAsDate); - - when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShopOneMock, miraklShopTwoMock)); - when(miraklMarketplacePlatformOperatorApiClientMock.getShops(any(MiraklGetShopsRequest.class))) - .thenReturn(miraklShopsMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopOneMock)) - .thenReturn(invoiceModelConvertedFromShopOneMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopTwoMock)) - .thenReturn(invoiceModelConvertedFromShopTwoMock); - when(invoiceModelConvertedFromShopOneMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(invoiceModelConvertedFromShopTwoMock.getShopId()).thenReturn(SHOP_ID_TWO); - when(invoiceModelConvertedFromShopOneMock.getDestinationToken()).thenReturn(TOKEN_1); - when(invoiceModelConvertedFromShopTwoMock.getDestinationToken()).thenReturn(TOKEN_2); - when(invoiceModelConvertedFromShopOneMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(invoiceModelConvertedFromShopTwoMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - verify(miraklMarketplacePlatformOperatorApiClientMock).getShops(miraklGetShopsRequestArgumentCaptor.capture()); - - assertThat(miraklGetShopsRequestArgumentCaptor.getValue().getShopIds()).containsExactlyInAnyOrder(SHOP_ID_ONE, - SHOP_ID_TWO); - - assertThat(result.get(0)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); - assertThat(result.get(1)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_TWO) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_2); - } - - @Test - void extractAccountingDocument_whenNoInvoicesAreReturned_shouldReturnEmptyList() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - doReturn(Collections.emptyList()).when(testObj).getAccountingDocuments(nowAsDate); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - assertThat(result).isEmpty(); - } - @Test - void extractAccountingDocument_whenSeveralInvoicesPerShopAreExtractedFromMirakl_shouldPopulateInvoicesExcludingInvoicesWithoutDestinationTokenAndHyperwalletProgram() { - final LocalDateTime now = LocalDateTime.now(); - TimeMachine.useFixedClockAt(now); - final Date nowAsDate = DateUtil.convertToDate(now, ZoneId.systemDefault()); - - final InvoiceModel invoiceOne = InvoiceModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1).build(); - final InvoiceModel invoiceTwo = InvoiceModel.builder().shopId(SHOP_ID_TWO).destinationToken(TOKEN_2).build(); - final InvoiceModel invoiceThree = InvoiceModel.builder().shopId(SHOP_ID_ONE).destinationToken(TOKEN_1).build(); - - doReturn(List.of(invoiceOne, invoiceTwo, invoiceThree)).when(testObj).getAccountingDocuments(nowAsDate); - - when(miraklShopsMock.getShops()).thenReturn(List.of(miraklShopOneMock, miraklShopTwoMock, miraklShopThreeMock)); - when(miraklMarketplacePlatformOperatorApiClientMock.getShops(any(MiraklGetShopsRequest.class))) - .thenReturn(miraklShopsMock); - - when(miraklShopToAccountingModelConverter.convert(miraklShopOneMock)) - .thenReturn(invoiceModelConvertedFromShopOneMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopTwoMock)) - .thenReturn(invoiceModelConvertedFromShopTwoMock); - when(miraklShopToAccountingModelConverter.convert(miraklShopThreeMock)) - .thenReturn(invoiceModelConvertedFromShopThreeMock); - when(invoiceModelConvertedFromShopOneMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(invoiceModelConvertedFromShopTwoMock.getShopId()).thenReturn(SHOP_ID_TWO); - when(invoiceModelConvertedFromShopThreeMock.getShopId()).thenReturn(SHOP_ID_ONE); - when(invoiceModelConvertedFromShopOneMock.getDestinationToken()).thenReturn(TOKEN_1); - when(invoiceModelConvertedFromShopTwoMock.getDestinationToken()).thenReturn(TOKEN_2); - when(invoiceModelConvertedFromShopThreeMock.getDestinationToken()).thenReturn(TOKEN_1); - when(invoiceModelConvertedFromShopOneMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(invoiceModelConvertedFromShopTwoMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - when(invoiceModelConvertedFromShopThreeMock.getHyperwalletProgram()).thenReturn(HYPERWALLET_PROGRAM); - - final List result = testObj.extractAccountingDocuments(nowAsDate); - - verify(miraklMarketplacePlatformOperatorApiClientMock).getShops(miraklGetShopsRequestArgumentCaptor.capture()); - - assertThat(miraklGetShopsRequestArgumentCaptor.getValue().getShopIds()).containsExactlyInAnyOrder(SHOP_ID_ONE, - SHOP_ID_TWO); - - assertThat(result.get(0)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); - assertThat(result.get(1)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_TWO) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_2); - assertThat(result.get(2)).hasFieldOrPropertyWithValue(SHOP_ID_ATTRIBUTE, SHOP_ID_ONE) - .hasFieldOrPropertyWithValue(DESTINATION_TOKEN_ATTRIBUTE, TOKEN_1); + void shouldReturnCreditNoteType() { + assertThat(testObj.getInvoiceType()).isEqualTo(InvoiceTypeEnum.AUTO_INVOICE); } @Test - void getInvoices_shouldReturnListOfInvoiceModels() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - when(miraklMarketplacePlatformOperatorApiClientMock.getInvoices(any())).thenReturn(miraklInvoicesOneMock); - when(miraklInvoicesOneMock.getHmcInvoices()).thenReturn(List.of(miraklInvoiceOneMock, miraklInvoiceTwoMock)); - when(miraklInvoicesOneMock.getTotalCount()).thenReturn(2L); - when(miraklInvoiceToInvoiceModelConverterMock.convert(miraklInvoiceOneMock)).thenReturn(invoiceModelOneMock); - when(miraklInvoiceToInvoiceModelConverterMock.convert(miraklInvoiceTwoMock)).thenReturn(invoiceModelTwoMock); - - final List invoices = testObj.getAccountingDocuments(now); - - verify(miraklMarketplacePlatformOperatorApiClientMock) - .getInvoices(miraklGetInvoicesRequestArgumentCaptor.capture()); - - final MiraklGetInvoicesRequest miraklGetInvoicesRequest = miraklGetInvoicesRequestArgumentCaptor.getValue(); - - assertThat(miraklGetInvoicesRequest.getStartDate()).isEqualTo(now); - assertThat(miraklGetInvoicesRequest.getStates()).isEqualTo(List.of(MiraklAccountingDocumentState.COMPLETE)); - assertThat(miraklGetInvoicesRequest.getPaymentStatus()) - .isEqualTo(MiraklAccountingDocumentPaymentStatus.PENDING); - - assertThat(invoices).containsExactlyInAnyOrder(invoiceModelOneMock, invoiceModelTwoMock); - } - - @Test - void getInvoices_whenNoMiraklInvoicesAreReceived_shouldReturnEmptyList() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - when(miraklMarketplacePlatformOperatorApiClientMock.getInvoices(any())).thenReturn(miraklInvoicesOneMock); - - final List invoices = testObj.getAccountingDocuments(now); - - assertThat(invoices).isEqualTo(Collections.emptyList()); - } - - @Test - void createAccountingDocumentRequest_shouldReturnRequestWithInvoiceType() { - final Date date = new Date(); - - final MiraklGetInvoicesRequest result = testObj.createAccountingDocumentRequest(date, - InvoiceTypeEnum.AUTO_INVOICE); - - assertThat(result.getMax()).isEqualTo(100); - assertThat(result.getStartDate()).isEqualTo(date); - assertThat(result.getType()).isEqualTo(AUTO_INVOICE); - assertThat(result.getPaymentStatus()).isEqualTo(PENDING); - assertThat(result.getStates()).containsExactly(COMPLETE); - } - - @Test - void getAccountingDocument_whenRequestNeedsPagination_shouldRepeatRequestAndReturnAllInvoices() { - TimeMachine.useFixedClockAt(LocalDateTime.of(2020, 11, 10, 20, 0, 55)); - final Date now = DateUtil.convertToDate(TimeMachine.now(), ZoneId.systemDefault()); - - final List firstPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( - MIRAKL_MAX_RESULTS_PER_PAGE); - final List secondPageResponseInvoices = getListOfHMCMiraklInvoiceMocks( - MIRAKL_MAX_RESULTS_PER_PAGE / 2); - final long totalResponseInvoices = firstPageResponseInvoices.size() + secondPageResponseInvoices.size(); - - when(miraklMarketplacePlatformOperatorApiClientMock - .getInvoices(argThat(request -> request != null && request.getOffset() == 0))) - .thenReturn(miraklInvoicesOneMock); - when(miraklMarketplacePlatformOperatorApiClientMock - .getInvoices(argThat(request -> request != null && request.getOffset() == MIRAKL_MAX_RESULTS_PER_PAGE))) - .thenReturn(miraklInvoicesTwoMock); - when(miraklInvoicesOneMock.getTotalCount()).thenReturn(totalResponseInvoices); - when(miraklInvoicesOneMock.getHmcInvoices()).thenReturn(firstPageResponseInvoices); - when(miraklInvoicesTwoMock.getTotalCount()).thenReturn(totalResponseInvoices); - when(miraklInvoicesTwoMock.getHmcInvoices()).thenReturn(secondPageResponseInvoices); - - final List expectedCreditNotes = Stream - .concat(firstPageResponseInvoices.stream(), secondPageResponseInvoices.stream()) - .map(invoice -> mockAndReturn(invoice)).collect(Collectors.toList()); - - final List result = testObj.getAccountingDocuments(now); - - assertThat(result).containsExactlyElementsOf(expectedCreditNotes); - } - - private InvoiceModel mockAndReturn(final HMCMiraklInvoice invoice) { - final InvoiceModel invoiceModelMock = mock(InvoiceModel.class); - when(miraklInvoiceToInvoiceModelConverterMock.convert(invoice)).thenReturn(invoiceModelMock); - return invoiceModelMock; - } - - private List getListOfHMCMiraklInvoiceMocks(final int numberOfMocks) { - return IntStream.range(0, numberOfMocks).mapToObj(i -> mock(HMCMiraklInvoice.class)) - .collect(Collectors.toList()); - } - - @Test - void createShopRequest_shouldCreateShopRequestWithGivenIds() { - final Set shopIds = Set.of(SHOP_ID_ONE, SHOP_ID_TWO); - - final MiraklGetShopsRequest result = testObj.createShopRequest(shopIds); - - assertThat(result.getShopIds()).isEqualTo(shopIds); - assertThat(result.isPaginate()).isFalse(); + void shouldReturnCreditNoteConverter() { + assertThat(testObj.getMiraklInvoiceToAccountingModelConverter()) + .isEqualTo(miraklInvoiceToInvoiceModelConverterMock); } } diff --git a/kyc/src/main/java/com/paypal/kyc/batchjobs/businessstakeholders/BusinessStakeholdersDocumentsExtractBatchJob.java b/kyc/src/main/java/com/paypal/kyc/batchjobs/businessstakeholders/BusinessStakeholdersDocumentsExtractBatchJob.java index 29df4155..fce1ca86 100644 --- a/kyc/src/main/java/com/paypal/kyc/batchjobs/businessstakeholders/BusinessStakeholdersDocumentsExtractBatchJob.java +++ b/kyc/src/main/java/com/paypal/kyc/batchjobs/businessstakeholders/BusinessStakeholdersDocumentsExtractBatchJob.java @@ -9,7 +9,7 @@ */ @Service public class BusinessStakeholdersDocumentsExtractBatchJob - extends AbstractBatchJob { + extends AbstractExtractBatchJob { private final BusinessStakeholdersDocumentsExtractBatchJobItemProcessor businessStakeholdersDocumentsExtractBatchJobItemProcessor; diff --git a/kyc/src/main/java/com/paypal/kyc/batchjobs/sellers/SellersDocumentsExtractBatchJob.java b/kyc/src/main/java/com/paypal/kyc/batchjobs/sellers/SellersDocumentsExtractBatchJob.java index c76b1799..ec47d352 100644 --- a/kyc/src/main/java/com/paypal/kyc/batchjobs/sellers/SellersDocumentsExtractBatchJob.java +++ b/kyc/src/main/java/com/paypal/kyc/batchjobs/sellers/SellersDocumentsExtractBatchJob.java @@ -9,7 +9,7 @@ */ @Service public class SellersDocumentsExtractBatchJob - extends AbstractBatchJob { + extends AbstractExtractBatchJob { private final SellersDocumentsExtractBatchJobItemProcessor sellersDocumentsExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountExtractBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountExtractBatchJob.java index 89372836..87af5835 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountExtractBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountExtractBatchJob.java @@ -8,7 +8,7 @@ * HyperWallet as users */ @Service -public class BankAccountExtractBatchJob extends AbstractBatchJob { +public class BankAccountExtractBatchJob extends AbstractExtractBatchJob { private final BankAccountExtractBatchJobItemProcessor bankAccountExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountRetryBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountRetryBatchJob.java index ff4ac02b..c8e44428 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountRetryBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/bankaccount/BankAccountRetryBatchJob.java @@ -1,16 +1,13 @@ package com.paypal.sellers.batchjobs.bankaccount; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; -import com.paypal.infrastructure.batchjob.BatchJobContext; -import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; -import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.*; import org.springframework.stereotype.Service; /** * Retries items that have failed during the bank account extract job */ @Service -public class BankAccountRetryBatchJob extends AbstractBatchJob { +public class BankAccountRetryBatchJob extends AbstractRetryBatchJob { private final BankAccountExtractBatchJobItemProcessor bankAccountExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersExtractBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersExtractBatchJob.java index d1160a9f..3c3b3ae2 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersExtractBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersExtractBatchJob.java @@ -9,7 +9,7 @@ */ @Service public class BusinessStakeholdersExtractBatchJob - extends AbstractBatchJob { + extends AbstractExtractBatchJob { private final BusinessStakeholdersExtractBatchJobItemsExtractor professionalSellersExtractBatchJobItemsExtractor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJob.java index 6585ecbd..00edf992 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJob.java @@ -1,9 +1,6 @@ package com.paypal.sellers.batchjobs.bstk; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; -import com.paypal.infrastructure.batchjob.BatchJobContext; -import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; -import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.*; import org.springframework.stereotype.Service; /** @@ -11,7 +8,7 @@ */ @Service public class BusinessStakeholdersRetryBatchJob - extends AbstractBatchJob { + extends AbstractRetryBatchJob { private final BusinessStakeholdersExtractBatchJobItemProcessor businessStakeholdersExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractor.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractor.java index 9f9a2c13..7530f610 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractor.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractor.java @@ -1,22 +1,49 @@ package com.paypal.sellers.batchjobs.bstk; -import com.paypal.infrastructure.batchjob.AbstractOnlyCachedFailedItemsBatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.AbstractCachingFailedItemsBatchJobItemsExtractor; import com.paypal.infrastructure.batchjob.BatchJobContext; import com.paypal.infrastructure.batchjob.BatchJobFailedItemService; import com.paypal.infrastructure.batchjob.cache.BatchJobFailedItemCacheService; +import com.paypal.sellers.sellersextract.model.BusinessStakeHolderModel; +import com.paypal.sellers.sellersextract.model.SellerModel; +import com.paypal.sellers.sellersextract.service.BusinessStakeholderExtractService; +import com.paypal.sellers.sellersextract.service.MiraklSellersExtractService; import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + /** * Extract business stakeholders for retry from the failed items cache. */ @Service -public class BusinessStakeholdersRetryBatchJobItemsExtractor extends - AbstractOnlyCachedFailedItemsBatchJobItemsExtractor { +public class BusinessStakeholdersRetryBatchJobItemsExtractor + extends AbstractCachingFailedItemsBatchJobItemsExtractor { + + private final MiraklSellersExtractService miraklSellersExtractService; + + private final BusinessStakeholderExtractService businessStakeholderExtractService; protected BusinessStakeholdersRetryBatchJobItemsExtractor(BatchJobFailedItemService batchJobFailedItemService, - BatchJobFailedItemCacheService batchJobFailedItemCacheService) { + BatchJobFailedItemCacheService batchJobFailedItemCacheService, + MiraklSellersExtractService miraklSellersExtractService, + BusinessStakeholderExtractService businessStakeholderExtractService) { super(BusinessStakeholderExtractJobItem.class, BusinessStakeholderExtractJobItem.ITEM_TYPE, batchJobFailedItemService, batchJobFailedItemCacheService); + this.miraklSellersExtractService = miraklSellersExtractService; + this.businessStakeholderExtractService = businessStakeholderExtractService; + } + + @Override + protected Collection getItems(final List ids) { + + final List miraklProfessionalSellers = miraklSellersExtractService.extractProfessionals(ids); + final List businessStakeHolderModels = businessStakeholderExtractService + .extractBusinessStakeHolders(miraklProfessionalSellers); + + return businessStakeHolderModels.stream().map(BusinessStakeholderExtractJobItem::new) + .collect(Collectors.toList()); } } diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersExtractBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersExtractBatchJob.java index 6ff3eae7..d986394a 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersExtractBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersExtractBatchJob.java @@ -1,6 +1,9 @@ package com.paypal.sellers.batchjobs.individuals; -import com.paypal.infrastructure.batchjob.*; +import com.paypal.infrastructure.batchjob.AbstractExtractBatchJob; +import com.paypal.infrastructure.batchjob.BatchJobContext; +import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; +import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -11,7 +14,7 @@ */ @Service public class IndividualSellersExtractBatchJob - extends AbstractBatchJob { + extends AbstractExtractBatchJob { @Resource private IndividualSellersExtractBatchJobItemProcessor individualSellersExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersRetryBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersRetryBatchJob.java index 300b3b2c..25a62f18 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersRetryBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/individuals/IndividualSellersRetryBatchJob.java @@ -1,16 +1,14 @@ package com.paypal.sellers.batchjobs.individuals; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; -import com.paypal.infrastructure.batchjob.BatchJobContext; -import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; -import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.*; import org.springframework.stereotype.Service; /** * Retries items that have failed during the individual sellers extract job */ @Service -public class IndividualSellersRetryBatchJob extends AbstractBatchJob { +public class IndividualSellersRetryBatchJob + extends AbstractRetryBatchJob { private final IndividualSellersRetryBatchJobItemExtractor individualSellersRetryBatchJobItemExtractor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersExtractBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersExtractBatchJob.java index 45ec7732..cfd4e41b 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersExtractBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersExtractBatchJob.java @@ -9,7 +9,7 @@ */ @Service public class ProfessionalSellersExtractBatchJob - extends AbstractBatchJob { + extends AbstractExtractBatchJob { private final ProfessionalSellersExtractBatchJobItemProcessor professionalSellersExtractBatchJobItemProcessor; diff --git a/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersRetryBatchJob.java b/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersRetryBatchJob.java index 04c0f940..39661840 100644 --- a/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersRetryBatchJob.java +++ b/sellers/src/main/java/com/paypal/sellers/batchjobs/professionals/ProfessionalSellersRetryBatchJob.java @@ -1,9 +1,6 @@ package com.paypal.sellers.batchjobs.professionals; -import com.paypal.infrastructure.batchjob.AbstractBatchJob; -import com.paypal.infrastructure.batchjob.BatchJobContext; -import com.paypal.infrastructure.batchjob.BatchJobItemProcessor; -import com.paypal.infrastructure.batchjob.BatchJobItemsExtractor; +import com.paypal.infrastructure.batchjob.*; import org.springframework.stereotype.Service; /** @@ -11,7 +8,7 @@ */ @Service public class ProfessionalSellersRetryBatchJob - extends AbstractBatchJob { + extends AbstractRetryBatchJob { private final ProfessionalSellersExtractBatchJobItemProcessor professionalSellersExtractBatchJobItemProcessor; diff --git a/sellers/src/test/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractorTest.java b/sellers/src/test/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractorTest.java index a6a744fc..b6dee155 100644 --- a/sellers/src/test/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractorTest.java +++ b/sellers/src/test/java/com/paypal/sellers/batchjobs/bstk/BusinessStakeholdersRetryBatchJobItemsExtractorTest.java @@ -1,23 +1,67 @@ package com.paypal.sellers.batchjobs.bstk; +import com.paypal.sellers.sellersextract.model.BusinessStakeHolderModel; +import com.paypal.sellers.sellersextract.model.SellerModel; +import com.paypal.sellers.sellersextract.service.BusinessStakeholderExtractService; +import com.paypal.sellers.sellersextract.service.MiraklSellersExtractService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class BusinessStakeholdersRetryBatchJobItemsExtractorTest { + public static final String PROFESSIONAL_SELLER_1 = "1"; + + public static final String PROFESSIONAL_SELLER_2 = "2"; + @InjectMocks private BusinessStakeholdersRetryBatchJobItemsExtractor testObj; + @Mock + private MiraklSellersExtractService miraklSellersExtractServiceMock; + + @Mock + private BusinessStakeholderExtractService businessStakeholderExtractServiceMock; + + @Mock + private SellerModel sellerModelMock1, sellerModelMock2; + + @Mock + private BusinessStakeHolderModel businessStakeHolderModelMock1, businessStakeHolderModelMock2; + @Test - void getItem_shouldReturnBusinessStakeholderType() { + void getItemType_shouldReturnBusinessStakeholderType() { String result = testObj.getItemType(); assertThat(result).isEqualTo(BusinessStakeholderExtractJobItem.ITEM_TYPE); } + @Test + void getItems_ShouldReturnBusinessStakeholderExtractJobItems() { + + when(miraklSellersExtractServiceMock + .extractProfessionals(List.of(PROFESSIONAL_SELLER_1, PROFESSIONAL_SELLER_2))) + .thenReturn(List.of(sellerModelMock1, sellerModelMock2)); + + when(businessStakeholderExtractServiceMock + .extractBusinessStakeHolders(List.of(sellerModelMock1, sellerModelMock2))) + .thenReturn(List.of(businessStakeHolderModelMock1, businessStakeHolderModelMock2)); + + final Collection result = testObj + .getItems(List.of(PROFESSIONAL_SELLER_1, PROFESSIONAL_SELLER_2)); + + assertThat(result.stream().map(BusinessStakeholderExtractJobItem::getItem).collect(Collectors.toList())) + .containsExactlyInAnyOrder(businessStakeHolderModelMock1, businessStakeHolderModelMock2); + } + }