diff --git a/config/services.yaml b/config/services.yaml index aa782d27..5f67d873 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,6 +14,7 @@ parameters: env(MAIL_ON_NOTIFICATION_ENDPOINT_DOWN_COOLDOWN): 10 env(STRIPE_PREFILL_ONBOARDING): false env(MIRAKL_CUSTOM_FIELD_CODE): "stripe-url" + env(MIRAKL_IGNORED_SHOP_FIELD_CODE): "stripe-ignored" env(ENABLE_SERVICE_PAYMENT_SPLIT): false env(ENABLE_SERVICE_PAYMENT_REFUND): false env(ENABLE_SELLER_ONBOARDING): true @@ -45,6 +46,7 @@ parameters: app.mirakl.api_key: "%env(MIRAKL_API_KEY)%" app.mirakl.host_name: "%env(MIRAKL_HOST_NAME)%" app.mirakl.stripe_custom_field_code: "%env(MIRAKL_CUSTOM_FIELD_CODE)%" + app.mirakl.stripe_ignored_shop_field_code: "%env(MIRAKL_IGNORED_SHOP_FIELD_CODE)%" app.redirect.onboarding: "%env(default:default_redirect_onboarding:REDIRECT_ONBOARDING)%" app.operator.notification_url: "%env(OPERATOR_NOTIFICATION_URL)%" app.mailer.technical: "%env(TECHNICAL_ALERT_EMAIL)%" @@ -66,6 +68,7 @@ services: $miraklApiKey: "%app.mirakl.api_key%" $miraklHostName: "%app.mirakl.host_name%" $customFieldCode: "%app.mirakl.stripe_custom_field_code%" + $ignoredShopFieldCode: "%app.mirakl.stripe_ignored_shop_field_code%" $enableProductPaymentSplit: "%app.workflow.enable_product_payment_split%" $enableServicePaymentSplit: "%app.workflow.enable_service_payment_split%" $enableProductPaymentRefund: "%app.workflow.enable_product_payment_refund%" diff --git a/config/services_test.yaml b/config/services_test.yaml index 56ef56c8..3d50ad4a 100644 --- a/config/services_test.yaml +++ b/config/services_test.yaml @@ -17,6 +17,7 @@ services: public: true bind: $customFieldCode: "%app.mirakl.stripe_custom_field_code%" + $ignoredShopFieldCode: "%app.mirakl.stripe_ignored_shop_field_code%" App\Service\MiraklClient: class: App\Service\MiraklClient diff --git a/src/Command/SellerOnboardingCommand.php b/src/Command/SellerOnboardingCommand.php index ba23a836..e95e584c 100644 --- a/src/Command/SellerOnboardingCommand.php +++ b/src/Command/SellerOnboardingCommand.php @@ -99,6 +99,12 @@ private function processUpdatedShops() continue; } + $ignoredShop = $this->sellerOnboardingService->isShopIgnored($shop); + if ($accountMapping->getIgnored() !== $ignoredShop) { + $this->logger->info("Shop $shopId is now ignored=" . var_export($ignoredShop, true)); + $this->sellerOnboardingService->updateAccountMappingIgnored($accountMapping, $ignoredShop); + } + try { // Ignore if custom field already has a value other than the oauth URL (for backward compatibility) $customFieldValue = $this->sellerOnboardingService->getCustomFieldValue($shop); diff --git a/src/Entity/AccountMapping.php b/src/Entity/AccountMapping.php index edcf2249..afab74a7 100644 --- a/src/Entity/AccountMapping.php +++ b/src/Entity/AccountMapping.php @@ -52,6 +52,11 @@ class AccountMapping */ private $payinEnabled = false; + /** + * @ORM\Column(type="boolean", options={"default" : false}) + */ + private $ignored = false; + /** * @ORM\Column(type="string", length=255, nullable=true) */ @@ -134,6 +139,18 @@ public function setPayinEnabled(bool $payinEnabled): self return $this; } + public function getIgnored(): ?bool + { + return $this->ignored; + } + + public function setIgnored(bool $ignored): self + { + $this->ignored = $ignored; + + return $this; + } + public function getDisabledReason(): ?string { return $this->disabledReason; diff --git a/src/Entity/StripeTransfer.php b/src/Entity/StripeTransfer.php index 1edde24f..ded78afe 100644 --- a/src/Entity/StripeTransfer.php +++ b/src/Entity/StripeTransfer.php @@ -36,6 +36,7 @@ class StripeTransfer public const TRANSFER_PENDING = 'TRANSFER_PENDING'; public const TRANSFER_FAILED = 'TRANSFER_FAILED'; public const TRANSFER_CREATED = 'TRANSFER_CREATED'; + public const TRANSFER_IGNORED = 'TRANSFER_IGNORED'; // Transfer status reasons: on hold public const TRANSFER_STATUS_REASON_SHOP_NOT_READY = 'Cannot find Stripe account for shop ID %s'; @@ -139,6 +140,7 @@ public static function getAvailableStatus(): array self::TRANSFER_FAILED, self::TRANSFER_ON_HOLD, self::TRANSFER_ABORTED, + self::TRANSFER_IGNORED, ]; } diff --git a/src/Factory/StripeTransferFactory.php b/src/Factory/StripeTransferFactory.php index 4c031a4f..a987ad09 100644 --- a/src/Factory/StripeTransferFactory.php +++ b/src/Factory/StripeTransferFactory.php @@ -113,6 +113,11 @@ public function updateFromOrder(StripeTransfer $transfer, MiraklOrder $order, Mi return $this->putTransferOnHold($transfer, $e->getMessage()); } + if ($accountMapping->getIgnored()) { + $shopId = $accountMapping->getMiraklShopId(); + return $this->ignoreTransfer($transfer, "Shop $shopId is ignored"); + } + // Order must not be refused or canceled if ($order->isAborted()) { return $this->abortTransfer( @@ -538,6 +543,22 @@ private function abortTransfer(StripeTransfer $transfer, string $reason): Stripe ->setStatusReason(substr($reason, 0, 1024)); } + /** + * @param StripeTransfer $transfer + * @return StripeTransfer + */ + private function ignoreTransfer(StripeTransfer $transfer, string $reason): StripeTransfer + { + $this->logger->info( + 'Transfer ignored: ' . $reason, + ['order_id' => $transfer->getMiraklId()] + ); + + return $transfer + ->setStatus(StripeTransfer::TRANSFER_IGNORED) + ->setStatusReason(substr($reason, 0, 1024)); + } + /** * @param StripeTransfer $transfer * @return StripeTransfer diff --git a/src/Migrations/Version20221031140741.php b/src/Migrations/Version20221031140741.php new file mode 100644 index 00000000..30e592ce --- /dev/null +++ b/src/Migrations/Version20221031140741.php @@ -0,0 +1,31 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE account_mapping ADD ignored BOOLEAN DEFAULT \'false\' NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE account_mapping DROP ignored'); + } +} diff --git a/src/Service/SellerOnboardingService.php b/src/Service/SellerOnboardingService.php index 973aa72b..79f537b8 100644 --- a/src/Service/SellerOnboardingService.php +++ b/src/Service/SellerOnboardingService.php @@ -48,6 +48,11 @@ class SellerOnboardingService */ private $customFieldCode; + /** + * @var string + */ + private $ignoredShopFieldCode; + public function __construct( AccountMappingRepository $accountMappingRepository, MiraklClient $miraklClient, @@ -55,7 +60,8 @@ public function __construct( RouterInterface $router, string $redirectOnboarding, bool $stripePrefillOnboarding, - string $customFieldCode + string $customFieldCode, + string $ignoredShopFieldCode ) { $this->accountMappingRepository = $accountMappingRepository; $this->miraklClient = $miraklClient; @@ -64,6 +70,7 @@ public function __construct( $this->redirectOnboarding = $redirectOnboarding; $this->stripePrefillOnboarding = $stripePrefillOnboarding; $this->customFieldCode = $customFieldCode; + $this->ignoredShopFieldCode = $ignoredShopFieldCode; } /** @@ -92,6 +99,16 @@ public function getAccountMappingFromShop(MiraklShop $shop): AccountMapping return $accountMapping; } + /** + * @param AccountMapping $accountMapping + * @param bool $ignored + */ + public function updateAccountMappingIgnored(AccountMapping $accountMapping, bool $ignored): void + { + $accountMapping->setIgnored($ignored); + $this->accountMappingRepository->persistAndFlush($accountMapping); + } + /** * @param MiraklShop $shop * @return Account @@ -125,6 +142,15 @@ public function getCustomFieldValue(MiraklShop $shop): ?string return $shop->getCustomFieldValue($this->customFieldCode); } + /** + * @param MiraklShop $shop + * @return bool True if the field is set and the shop ignored, false otherwise. + */ + public function isShopIgnored(MiraklShop $shop): bool + { + return $shop->getCustomFieldValue($this->ignoredShopFieldCode) === "true"; + } + /** * @param int $shopId * @param AccountMapping $accountMapping diff --git a/tests/Command/SellerOnboardingCommandTest.php b/tests/Command/SellerOnboardingCommandTest.php index 8ba8dcb0..f4db068c 100644 --- a/tests/Command/SellerOnboardingCommandTest.php +++ b/tests/Command/SellerOnboardingCommandTest.php @@ -159,4 +159,23 @@ public function testShopUpdateDateCheckpoint() $this->executeCommand(); $this->assertEquals(MiraklMock::SHOP_DATE_1_EXISTING_WITH_OAUTH_URL, $this->configService->getSellerOnboardingCheckpoint()); } + + public function testIgnoredNewShop() + { + $this->deleteAllAccountMappingsFromRepository(); + $this->configService->setSellerOnboardingCheckpoint(MiraklMock::SHOP_DATE_1_NEW_IGNORED); + $this->executeCommand(); + $this->assertCount(1, $this->getAccountMappingsFromRepository()); + $this->assertEquals(true, current($this->getAccountMappingsFromRepository())->getIgnored()); + } + + public function testIgnoredExistingShop() + { + $this->deleteAllAccountMappingsFromRepository(); + $this->mockAccountMapping(MiraklMock::SHOP_EXISTING_IGNORED); + $this->configService->setSellerOnboardingCheckpoint(MiraklMock::SHOP_DATE_1_EXISTING_IGNORED); + $this->executeCommand(); + $this->assertCount(1, $this->getAccountMappingsFromRepository()); + $this->assertEquals(true, current($this->getAccountMappingsFromRepository())->getIgnored()); + } } diff --git a/tests/Factory/StripeTransferFactoryTest.php b/tests/Factory/StripeTransferFactoryTest.php index b4839477..fc197d31 100644 --- a/tests/Factory/StripeTransferFactoryTest.php +++ b/tests/Factory/StripeTransferFactoryTest.php @@ -53,9 +53,11 @@ protected function setUp(): void ->getRepository(StripeRefund::class); $this->stripeTransferRepository = $container->get('doctrine') ->getRepository(StripeTransfer::class); + $this->accountMappingRepository = $container->get('doctrine') + ->getRepository(AccountMapping::class); $this->stripeTransferFactory = new StripeTransferFactory( - $container->get('doctrine')->getRepository(AccountMapping::class), + $this->accountMappingRepository, $this->paymentMappingRepository, $this->stripeRefundRepository, $this->stripeTransferRepository, @@ -262,6 +264,22 @@ public function testProductOrderInvalidShop() $this->assertNotNull($transfer->getStatusReason()); } + public function testProductOrderIgnoredShop() + { + $accountMapping = new AccountMapping(); + $accountMapping->setMiraklShopId(MiraklMock::SHOP_EXISTING_IGNORED); + $accountMapping->setStripeAccountId(StripeMock::ACCOUNT_NEW); + $accountMapping->setIgnored(true); + $this->accountMappingRepository->persistAndFlush($accountMapping); + + $transfer = $this->stripeTransferFactory->createFromOrder( + current($this->miraklClient->listProductOrdersById([ + MiraklMock::ORDER_IGNORED_SHOP + ])) + ); + $this->assertEquals(StripeTransfer::TRANSFER_IGNORED, $transfer->getStatus()); + } + public function testProductOrderInvalidAmount() { $transfer = $this->stripeTransferFactory->createFromOrder( @@ -468,6 +486,22 @@ public function testServiceOrderInvalidShop() $this->assertNotNull($transfer->getStatusReason()); } + public function testServiceOrderIgnoredShop() + { + $accountMapping = new AccountMapping(); + $accountMapping->setMiraklShopId(MiraklMock::SHOP_EXISTING_IGNORED); + $accountMapping->setStripeAccountId(StripeMock::ACCOUNT_NEW); + $accountMapping->setIgnored(true); + $this->accountMappingRepository->persistAndFlush($accountMapping); + + $transfer = $this->stripeTransferFactory->createFromOrder( + current($this->miraklClient->listServiceOrdersById([ + MiraklMock::ORDER_IGNORED_SHOP + ])) + ); + $this->assertEquals(StripeTransfer::TRANSFER_IGNORED, $transfer->getStatus()); + } + public function testServiceOrderInvalidAmount() { $transfer = $this->stripeTransferFactory->createFromOrder( diff --git a/tests/MiraklMockedHttpClient.php b/tests/MiraklMockedHttpClient.php index 83063590..26c1ebe0 100644 --- a/tests/MiraklMockedHttpClient.php +++ b/tests/MiraklMockedHttpClient.php @@ -36,6 +36,7 @@ class MiraklMockedHttpClient extends MockHttpClient public const ORDER_WITH_TRANSACTION_NUMBER = 'order_with_transaction_number'; public const ORDER_INVALID_AMOUNT = 'order_invalid_amount'; public const ORDER_INVALID_SHOP = 'order_invalid_shop'; + public const ORDER_IGNORED_SHOP = 'order_ignored_shop'; public const ORDER_AMOUNT_NO_COMMISSION = 'order_no_commission'; public const ORDER_AMOUNT_NO_TAX = 'order_no_tax'; public const ORDER_AMOUNT_PARTIAL_TAX = 'order_partial_tax'; @@ -73,6 +74,8 @@ class MiraklMockedHttpClient extends MockHttpClient public const SHOP_NEW = 299; public const SHOP_STRIPE_ERROR = 399; public const SHOP_MIRAKL_ERROR = 499; + public const SHOP_NEW_IGNORED = 500; + public const SHOP_EXISTING_IGNORED = 501; public const SHOP_DATE_1_NEW = '2019-01-01T00:00:00+0100'; public const SHOP_DATE_1_STRIPE_ERROR = '2019-01-02T00:00:00+0100'; public const SHOP_DATE_1_MIRAKL_ERROR = '2019-01-03T00:00:00+0100'; @@ -80,6 +83,8 @@ class MiraklMockedHttpClient extends MockHttpClient public const SHOP_DATE_1_EXISTING_WITH_URL = '2019-01-05T00:00:00+0100'; public const SHOP_DATE_1_EXISTING_WITH_OAUTH_URL = '2019-01-06T00:00:00+0100'; public const SHOP_DATE_MULTIPLE_UNSORTED = '2019-01-07T00:00:00+0100'; + public const SHOP_DATE_1_NEW_IGNORED = '2019-01-08T00:00:00+0100'; + public const SHOP_DATE_1_EXISTING_IGNORED = '2019-01-09T00:00:00+0100'; public const INVOICE_BASIC = 1; public const INVOICE_INVALID_AMOUNT = 2; @@ -110,10 +115,12 @@ class MiraklMockedHttpClient extends MockHttpClient public const INVOICE_DATE_14_NEW_INVOICES_ALL_READY_END_ID = 1099; private $customFieldCode; + private $ignoredShopFieldCode; - public function __construct(string $customFieldCode) + public function __construct(string $customFieldCode, string $ignoredShopFieldCode) { $this->customFieldCode = $customFieldCode; + $this->ignoredShopFieldCode = $ignoredShopFieldCode; $responseFactory = function ($method, $url, $options) { $path = parse_url($url, PHP_URL_PATH); @@ -520,6 +527,13 @@ private function mockOrdersById($isService, $orderIds, $startDate = null) $order['total_commission'] = 100; } break; + case self::ORDER_IGNORED_SHOP: + if ($isService) { + $order['shop']['id'] = self::SHOP_EXISTING_IGNORED; + } else { + $order['shop_id'] = self::SHOP_EXISTING_IGNORED; + } + break; case self::ORDER_WITH_TRANSACTION_NUMBER: if (!$isService) { $order['transaction_number'] = StripeMock::CHARGE_BASIC; @@ -875,6 +889,12 @@ private function mockShopsByDate($date, $page) break; case self::SHOP_DATE_MULTIPLE_UNSORTED: $shops = $this->mockShopsById([self::SHOP_WITH_OAUTH_URL, self::SHOP_WITH_URL]); + break; + case self::SHOP_DATE_1_NEW_IGNORED: + $shops = $this->mockShopsById([self::SHOP_NEW_IGNORED]); + break; + case self::SHOP_DATE_1_EXISTING_IGNORED: + $shops = $this->mockShopsById([self::SHOP_EXISTING_IGNORED]); break; default: $shops = []; @@ -911,6 +931,14 @@ private function mockShopsById($shopIds) $shop['last_updated_date'] = self::SHOP_DATE_1_EXISTING_WITH_OAUTH_URL; $shops[] = $shop; break; + case self::SHOP_NEW_IGNORED: + case self::SHOP_EXISTING_IGNORED: + $shop['shop_additional_fields'][] = [ + 'code' => $this->ignoredShopFieldCode, + 'value' => 'true', + ]; + $shops[] = $shop; + break; case self::SHOP_INVALID: default: // Don't return anything