diff --git a/composer.json b/composer.json index 43fd6812..4edd883c 100755 --- a/composer.json +++ b/composer.json @@ -85,7 +85,7 @@ "ecs": "ecs check -c ruleset/ecs.php --ansi --clear-cache", "fix-ecs": "@ecs --fix --memory-limit=4G", "phpmd": "phpmd src ansi ruleset/.php_md.xml", - "phpstan": "phpstan analyse src -c ruleset/phpstan.neon", + "phpstan": "phpstan analyse src -c ruleset/phpstan.neon --memory-limit=4G", "phpunit": "phpunit tests/PHPUnit --colors=always", "test-coverage": "phpunit tests/PHPUnit --colors=always --coverage-clover=build/logs/clover.xml", "tests": [ diff --git a/config/twig_hooks/admin.yaml b/config/twig_hooks/admin.yaml index a95b1f1e..f53f5c60 100644 --- a/config/twig_hooks/admin.yaml +++ b/config/twig_hooks/admin.yaml @@ -43,6 +43,9 @@ sylius_twig_hooks: 'sylius_admin.payment_method.create.content.form.sections.gateway_configuration.payplug_scalapay': &scalapayGateway live_checkbox: *liveCheckbox + amount_range: + template: '@PayPlugSyliusPayPlugPlugin/admin/payment_method/form/scalapay_amount_range.html.twig' + priority: 0 'sylius_admin.payment_method.create.content.form.sections.gateway_configuration.payplug_wero': &weroGateway live_checkbox: *liveCheckbox diff --git a/ruleset/phpstan-baseline.neon b/ruleset/phpstan-baseline.neon index abca980f..7736db9b 100644 --- a/ruleset/phpstan-baseline.neon +++ b/ruleset/phpstan-baseline.neon @@ -1252,18 +1252,6 @@ parameters: count: 1 path: ../src/Provider/PaymentTokenProvider.php - - - message: '#^Cannot access offset ''max_amount'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../src/Provider/SupportedMethodsProvider.php - - - - message: '#^Cannot access offset ''min_amount'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../src/Provider/SupportedMethodsProvider.php - - message: '#^PHPDoc tag @var with type Payum\\Core\\Model\\GatewayConfigInterface is not subtype of native type Sylius\\Component\\Payment\\Model\\GatewayConfigInterface\|null\.$#' identifier: varTag.nativeType diff --git a/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php b/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php new file mode 100644 index 00000000..4cecf182 --- /dev/null +++ b/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php @@ -0,0 +1,43 @@ +add(ScalapayGatewayFactory::MIN_AMOUNT, MoneyType::class, [ + 'label' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.min_amount', + 'help' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.amount_help', + 'currency' => 'EUR', + 'required' => false, + 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS, + ]) + ->add(ScalapayGatewayFactory::MAX_AMOUNT, MoneyType::class, [ + 'label' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.max_amount', + 'help' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.amount_help', + 'currency' => 'EUR', + 'required' => false, + 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS, + ]) + ; + } + + public static function getExtendedTypes(): iterable + { + return [ScalapayGatewayConfigurationType::class]; + } +} diff --git a/src/Gateway/ScalapayGatewayFactory.php b/src/Gateway/ScalapayGatewayFactory.php index 5e4b063a..e982ffaa 100644 --- a/src/Gateway/ScalapayGatewayFactory.php +++ b/src/Gateway/ScalapayGatewayFactory.php @@ -11,4 +11,8 @@ final class ScalapayGatewayFactory extends AbstractGatewayFactory public const FACTORY_TITLE = 'Scalapay by PayPlug'; public const PAYMENT_METHOD_SCALAPAY = 'scalapay'; + + public const MIN_AMOUNT = 'min_amount'; + + public const MAX_AMOUNT = 'max_amount'; } diff --git a/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php b/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php new file mode 100644 index 00000000..10c0d5ee --- /dev/null +++ b/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php @@ -0,0 +1,22 @@ +resolveApplicableConfiguredAmounts($value, $constraint); + if (null === $configuredAmounts) { + return; + } + + $authorizedRange = $this->resolveAuthorizedRange($value); + if (null === $authorizedRange) { + return; + } + + $this->applyRangeViolations($configuredAmounts, $authorizedRange, $constraint); + } + + /** + * Resolves the merchant-configured amounts, applying the early guards that don't need a + * live API call: the method must be enabled, amounts must be configured, and — when both + * sides are explicitly set — locally consistent. + * + * @return array{0: int|null, 1: int|null}|null + */ + private function resolveApplicableConfiguredAmounts( + PaymentMethodInterface $paymentMethod, + IsScalapayAmountRangeValid $constraint, + ): ?array { + $configuredAmounts = false !== $paymentMethod->isEnabled() ? $this->resolveConfiguredAmounts($paymentMethod) : null; + if (null === $configuredAmounts) { + return null; + } + + [$minAmount, $maxAmount] = $configuredAmounts; + + if (\is_int($minAmount) && \is_int($maxAmount) && $minAmount > $maxAmount) { + $this->context->buildViolation($constraint->minGreaterThanMaxMessage)->addViolation(); + + return null; + } + + return $configuredAmounts; + } + + /** + * @param array{0: int|null, 1: int|null} $configuredAmounts + * @param array{min_amount: int, max_amount: int} $authorizedRange + */ + private function applyRangeViolations( + array $configuredAmounts, + array $authorizedRange, + IsScalapayAmountRangeValid $constraint, + ): void { + [$minAmount, $maxAmount] = $configuredAmounts; + + // A merchant may configure only one side of the range; the other falls back to the + // API bound at checkout (see SupportedMethodsProvider), so the min>max check must + // compare against that same effective range, not just the explicitly configured side. + $effectiveMinAmount = $minAmount ?? $authorizedRange['min_amount']; + $effectiveMaxAmount = $maxAmount ?? $authorizedRange['max_amount']; + + if ($effectiveMinAmount > $effectiveMaxAmount) { + $this->context->buildViolation($constraint->minGreaterThanMaxMessage)->addViolation(); + + return; + } + + if ( + (\is_int($minAmount) && $minAmount < $authorizedRange['min_amount']) || + (\is_int($maxAmount) && $maxAmount > $authorizedRange['max_amount']) + ) { + $this->context->buildViolation($constraint->outOfRangeMessage) + ->setParameter('%min_amount%', self::formatAmount($authorizedRange['min_amount'])) + ->setParameter('%max_amount%', self::formatAmount($authorizedRange['max_amount'])) + ->addViolation() + ; + } + } + + /** + * The bounds are EUR cents (the form field is hardcoded to EUR), rendered with two decimals so + * 500 reads as "5.00" rather than "5". + */ + private static function formatAmount(int $amountInCents): string + { + return number_format($amountInCents / 100, 2, '.', ''); + } + + /** + * @return array{0: int|null, 1: int|null}|null + */ + private function resolveConfiguredAmounts(PaymentMethodInterface $paymentMethod): ?array + { + $gatewayConfig = $paymentMethod->getGatewayConfig(); + + if (!$gatewayConfig instanceof GatewayConfigInterface || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) { + return null; + } + + [$minAmount, $maxAmount] = $this->readConfiguredAmounts($gatewayConfig->getConfig()); + + return null === $minAmount && null === $maxAmount ? null : [$minAmount, $maxAmount]; + } + + /** + * The admin form only ever writes null or an int, but the gateway config is a plain serialized + * array that a direct DB edit, an import script or an admin API write can leave anything in. + * PaymentMethodValidator::process() has no surrounding try/catch, so a malformed value + * degrades to "not configured" — leaving the API bounds in force at checkout — rather than + * throwing an assertion error that would 500 the admin save. + * + * @param array $config + * + * @return array{0: int|null, 1: int|null} + */ + private function readConfiguredAmounts(array $config): array + { + $minAmount = $config[ScalapayGatewayFactory::MIN_AMOUNT] ?? null; + $maxAmount = $config[ScalapayGatewayFactory::MAX_AMOUNT] ?? null; + + try { + Assert::nullOrInteger($minAmount); + Assert::nullOrInteger($maxAmount); + } catch (InvalidArgumentException $exception) { + $this->logger->warning('Skipping Scalapay amount range validation: the stored range is malformed.', [ + 'min_amount' => $minAmount, + 'max_amount' => $maxAmount, + 'exception' => $exception->getMessage(), + ]); + + return [null, null]; + } + + return [$minAmount, $maxAmount]; + } + + /** + * Fails open: when the authorized range can't be established the config saves unvalidated, + * matching the plugin's convention of never blocking an admin save on an API hiccup. That is + * not free — a one-sided range that inverts against the live API bounds slips through and + * silently hides Scalapay at checkout — so the skip is logged rather than swallowed. + * + * @return array{min_amount: int, max_amount: int}|null + */ + private function resolveAuthorizedRange(PaymentMethodInterface $paymentMethod): ?array + { + try { + $authorizedRange = $this->resolveApiAuthorizedRange($paymentMethod); + } catch (GatewayConfigurationException | PayplugException | InvalidArgumentException $exception) { + $this->logger->warning('Skipping Scalapay amount range validation: the PayPlug account could not be read.', [ + 'payment_method' => $paymentMethod->getCode(), + 'exception' => $exception->getMessage(), + ]); + + return null; + } + + if (null === $authorizedRange) { + $this->logger->warning('Skipping Scalapay amount range validation: the PayPlug account authorizes no EUR range for Scalapay.', [ + 'payment_method' => $paymentMethod->getCode(), + ]); + } + + return $authorizedRange; + } + + /** + * @return array{min_amount: int, max_amount: int}|null + */ + private function resolveApiAuthorizedRange(PaymentMethodInterface $paymentMethod): ?array + { + $account = $this->apiClientFactory->createForPaymentMethod($paymentMethod)->getAccount(); + $currencies = $this->amountRangeResolver->resolve($account, ScalapayGatewayFactory::PAYMENT_METHOD_SCALAPAY); + + return $currencies['EUR'] ?? null; + } +} diff --git a/src/PaymentProcessing/PaymentTransitionApplier.php b/src/PaymentProcessing/PaymentTransitionApplier.php index a5af2e8b..1836a80f 100644 --- a/src/PaymentProcessing/PaymentTransitionApplier.php +++ b/src/PaymentProcessing/PaymentTransitionApplier.php @@ -24,11 +24,17 @@ public function apply(PaymentInterface $payment): bool $status = $details['status'] ?? ''; // These are known PayPlug statuses that do not map to a Sylius payment transition. - if (\in_array($status, [ - PayPlugApiClientInterface::STATUS_CREATED, - PayPlugApiClientInterface::REFUNDED, - PayPlugApiClientInterface::INTERNAL_STATUS_ONE_CLICK, - ], true)) { + if ( + \in_array( + $status, + [ + PayPlugApiClientInterface::STATUS_CREATED, + PayPlugApiClientInterface::REFUNDED, + PayPlugApiClientInterface::INTERNAL_STATUS_ONE_CLICK, + ], + true, + ) + ) { return false; } @@ -41,23 +47,29 @@ public function apply(PaymentInterface $payment): bool }; if (null === $transition) { - $this->logger->warning('[PayPlug] Cannot apply payment transition: unknown status.', [ + $this->logger->warning( + '[PayPlug] Cannot apply payment transition: unknown status.', + [ 'sylius_payment_id' => $payment->getId(), 'payplug_payment_id' => $details['payment_id'] ?? null, 'status' => $status, - ]); + ], + ); return false; } if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) { - $this->logger->warning('[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).', [ + $this->logger->warning( + '[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).', + [ 'sylius_payment_id' => $payment->getId(), 'payplug_payment_id' => $details['payment_id'] ?? null, 'current_state' => $payment->getState(), 'transition' => $transition, 'status' => $status, - ]); + ], + ); return false; } diff --git a/src/Provider/SupportedMethodsProvider.php b/src/Provider/SupportedMethodsProvider.php index 572d27e6..c9c0d4a4 100644 --- a/src/Provider/SupportedMethodsProvider.php +++ b/src/Provider/SupportedMethodsProvider.php @@ -6,16 +6,22 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; +use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory; +use PayPlug\SyliusPayPlugPlugin\Resolver\AccountAmountRangeResolver; +use Psr\Log\LoggerInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Currency\Context\CurrencyContextInterface; use Sylius\Component\Payment\Model\GatewayConfigInterface; use Webmozart\Assert\Assert; +use Webmozart\Assert\InvalidArgumentException; final class SupportedMethodsProvider { public function __construct( private CurrencyContextInterface $currencyContext, private PayPlugApiClientFactoryInterface $clientFactory, + private AccountAmountRangeResolver $amountRangeResolver, + private LoggerInterface $logger, ) { } @@ -83,54 +89,93 @@ public function provide( continue; } - if ( - $paymentAmount < $authorizedCurrencies[$activeCurrencyCode]['min_amount'] || - $paymentAmount > $authorizedCurrencies[$activeCurrencyCode]['max_amount'] - ) { - unset($supportedMethods[$key]); + [$minAmount, $maxAmount] = $this->resolveAmountBounds( + $gatewayConfig, + $activeCurrencyCode, + $authorizedCurrencies[$activeCurrencyCode], + ); - continue; + if ($paymentAmount < $minAmount || $paymentAmount > $maxAmount) { + unset($supportedMethods[$key]); } } return $supportedMethods; } - private function resolveAuthorizedCurrencies(string $factoryName): array - { - $account = $this->clientFactory->create($factoryName)->getAccount(); + /** + * ScalapayGatewayConfigurationTypeExtension lets the merchant tighten the API-provided bounds + * via the min_amount/max_amount config keys. The override is deliberately scoped to Scalapay: + * IsScalapayAmountRangeValidValidator — the save-time guardrail that keeps the configured + * range inside what PayPlug authorizes — is wired for Scalapay only, so honouring the same + * keys on another gateway would grant it a checkout override with no validation behind it. + * The values are entered in EUR, so the override also only applies to an EUR checkout; other + * currencies keep the raw API bounds. + * + * @param array{min_amount: int, max_amount: int} $authorizedRange + * + * @return array{0: int, 1: int} + */ + private function resolveAmountBounds( + GatewayConfigInterface $gatewayConfig, + string $activeCurrencyCode, + array $authorizedRange, + ): array { + if ('EUR' !== $activeCurrencyCode || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) { + return [$authorizedRange['min_amount'], $authorizedRange['max_amount']]; + } - $configuration = $account['configuration'] ?? []; - Assert::isArray($configuration); - $defaultMin = $configuration['min_amounts'] ?? []; - Assert::isArray($defaultMin); - $defaultMax = $configuration['max_amounts'] ?? []; - Assert::isArray($defaultMax); + [$minAmount, $maxAmount] = $this->readConfiguredAmounts($gatewayConfig->getConfig()); - $underscorePos = strpos($factoryName, '_'); - if ($underscorePos !== false) { - $pmKey = substr($factoryName, $underscorePos + 1); - $paymentMethods = $account['payment_methods'] ?? []; - Assert::isArray($paymentMethods); - $pmData = $paymentMethods[$pmKey] ?? []; - Assert::isArray($pmData); - $minAmounts = isset($pmData['min_amounts']) && \is_array($pmData['min_amounts']) ? $pmData['min_amounts'] : $defaultMin; - $maxAmounts = isset($pmData['max_amounts']) && \is_array($pmData['max_amounts']) ? $pmData['max_amounts'] : $defaultMax; - } else { - $minAmounts = $defaultMin; - $maxAmounts = $defaultMax; - } + return [ + $minAmount ?? $authorizedRange['min_amount'], + $maxAmount ?? $authorizedRange['max_amount'], + ]; + } - $currencies = []; - foreach ($minAmounts as $currency => $min) { - Assert::string($currency); - Assert::integer($min); - if (isset($maxAmounts[$currency]) && \is_int($maxAmounts[$currency])) { - $currencies[$currency] = ['min_amount' => $min, 'max_amount' => $maxAmounts[$currency]]; - } + /** + * The admin form only ever writes null or an int, but the gateway config is a plain serialized + * array that a direct DB edit, an import script or an admin API write can leave anything in. + * provide() runs unguarded on every checkout page (via the gateway resolver decorators), so a + * malformed value degrades to "not configured" — falling back to the API bounds — rather than + * throwing an assertion error that would break payment-method resolution for the whole + * checkout, not just hide Scalapay. + * + * @param array $config + * + * @return array{0: int|null, 1: int|null} + */ + private function readConfiguredAmounts(array $config): array + { + $minAmount = $config[ScalapayGatewayFactory::MIN_AMOUNT] ?? null; + $maxAmount = $config[ScalapayGatewayFactory::MAX_AMOUNT] ?? null; + + try { + Assert::nullOrInteger($minAmount); + Assert::nullOrInteger($maxAmount); + } catch (InvalidArgumentException $exception) { + $this->logger->warning('Ignoring malformed Scalapay amount range in gateway config; falling back to the PayPlug API bounds.', [ + 'min_amount' => $minAmount, + 'max_amount' => $maxAmount, + 'exception' => $exception->getMessage(), + ]); + + return [null, null]; } - return $currencies; + return [$minAmount, $maxAmount]; + } + + /** + * @return array + */ + private function resolveAuthorizedCurrencies(string $factoryName): array + { + $account = $this->clientFactory->create($factoryName)->getAccount(); + $underscorePos = strpos($factoryName, '_'); + $paymentMethodKey = false !== $underscorePos ? substr($factoryName, $underscorePos + 1) : null; + + return $this->amountRangeResolver->resolve($account, $paymentMethodKey); } private function resolveAllowedCountries(string $factoryName): array diff --git a/src/Resolver/AccountAmountRangeResolver.php b/src/Resolver/AccountAmountRangeResolver.php new file mode 100644 index 00000000..91271cab --- /dev/null +++ b/src/Resolver/AccountAmountRangeResolver.php @@ -0,0 +1,53 @@ + $account + * + * @return array + */ + public function resolve(array $account, ?string $paymentMethodKey): array + { + $configuration = $account['configuration'] ?? []; + Assert::isArray($configuration); + $defaultMinAmounts = $configuration['min_amounts'] ?? []; + Assert::isArray($defaultMinAmounts); + $defaultMaxAmounts = $configuration['max_amounts'] ?? []; + Assert::isArray($defaultMaxAmounts); + + if (null !== $paymentMethodKey) { + $paymentMethods = $account['payment_methods'] ?? []; + Assert::isArray($paymentMethods); + $pmData = $paymentMethods[$paymentMethodKey] ?? []; + Assert::isArray($pmData); + $minAmounts = isset($pmData['min_amounts']) && \is_array($pmData['min_amounts']) ? $pmData['min_amounts'] : $defaultMinAmounts; + $maxAmounts = isset($pmData['max_amounts']) && \is_array($pmData['max_amounts']) ? $pmData['max_amounts'] : $defaultMaxAmounts; + } else { + $minAmounts = $defaultMinAmounts; + $maxAmounts = $defaultMaxAmounts; + } + + $currencies = []; + foreach ($minAmounts as $currency => $min) { + Assert::string($currency); + Assert::integer($min); + if (isset($maxAmounts[$currency]) && \is_int($maxAmounts[$currency])) { + $currencies[$currency] = ['min_amount' => $min, 'max_amount' => $maxAmounts[$currency]]; + } + } + + return $currencies; + } +} diff --git a/src/Validator/PaymentMethodValidator.php b/src/Validator/PaymentMethodValidator.php index 2a9b0d49..ed2cf5ec 100644 --- a/src/Validator/PaymentMethodValidator.php +++ b/src/Validator/PaymentMethodValidator.php @@ -14,6 +14,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsCanSavePaymentMethod; use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsOneyEnabled; +use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsScalapayAmountRangeValid; use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\PayplugPermission; use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -48,7 +49,7 @@ public function process(PaymentMethodInterface $paymentMethod): void BancontactGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod), AmericanExpressGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod), ApplePayGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod), - ScalapayGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod), + ScalapayGatewayFactory::FACTORY_NAME => $this->processScalapay($paymentMethod), WeroGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod), default => throw new \InvalidArgumentException('Unsupported payment method'), }; @@ -94,4 +95,11 @@ private function processDefault(PaymentMethodInterface $paymentMethod): Constrai return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS); } + + private function processScalapay(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface + { + $constraintList = [new IsCanSavePaymentMethod(), new IsScalapayAmountRangeValid()]; + + return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS); + } } diff --git a/templates/admin/payment_method/form/scalapay_amount_range.html.twig b/templates/admin/payment_method/form/scalapay_amount_range.html.twig new file mode 100644 index 00000000..6a0c0b3b --- /dev/null +++ b/templates/admin/payment_method/form/scalapay_amount_range.html.twig @@ -0,0 +1,9 @@ +{% set min_amount_form = hookable_metadata.context.form.gatewayConfig.config.min_amount %} +{% set max_amount_form = hookable_metadata.context.form.gatewayConfig.config.max_amount %} + +
+ {{ form_row(min_amount_form) }} +
+
+ {{ form_row(max_amount_form) }} +
diff --git a/tests/PHPUnit/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtensionTest.php b/tests/PHPUnit/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtensionTest.php new file mode 100644 index 00000000..5e5f636d --- /dev/null +++ b/tests/PHPUnit/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtensionTest.php @@ -0,0 +1,71 @@ +extension = new ScalapayGatewayConfigurationTypeExtension(); + } + + public function testBuildForm_addsMinAmountMoneyField(): void + { + [, $addCalls] = $this->buildFormAndCollectAddCalls(); + + [$name, $type, $options] = $addCalls[0]; + self::assertSame(ScalapayGatewayFactory::MIN_AMOUNT, $name); + self::assertSame(MoneyType::class, $type); + self::assertSame('EUR', $options['currency']); + self::assertFalse($options['required']); + } + + public function testBuildForm_addsMaxAmountMoneyField(): void + { + [, $addCalls] = $this->buildFormAndCollectAddCalls(); + + [$name, $type, $options] = $addCalls[1]; + self::assertSame(ScalapayGatewayFactory::MAX_AMOUNT, $name); + self::assertSame(MoneyType::class, $type); + self::assertSame('EUR', $options['currency']); + self::assertFalse($options['required']); + } + + public function testGetExtendedTypes_returnsScalapayGatewayConfigurationType(): void + { + self::assertSame([ScalapayGatewayConfigurationType::class], ScalapayGatewayConfigurationTypeExtension::getExtendedTypes()); + } + + /** + * @return array{0: FormBuilderInterface, 1: array}>} + */ + private function buildFormAndCollectAddCalls(): array + { + $builder = $this->createMock(FormBuilderInterface::class); + + $addCalls = []; + $builder + ->method('add') + ->willReturnCallback(function ($name, $type = null, array $options = []) use (&$addCalls, $builder) { + $addCalls[] = [$name, $type, $options]; + + return $builder; + }) + ; + + $this->extension->buildForm($builder, []); + + return [$builder, $addCalls]; + } +} diff --git a/tests/PHPUnit/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidatorTest.php b/tests/PHPUnit/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidatorTest.php new file mode 100644 index 00000000..3574934f --- /dev/null +++ b/tests/PHPUnit/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidatorTest.php @@ -0,0 +1,255 @@ +apiClientFactory = $this->createMock(PayPlugApiClientFactoryInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); + + return new IsScalapayAmountRangeValidValidator($this->apiClientFactory, new AccountAmountRangeResolver(), $this->logger); + } + + public function testValidate_nonScalapayFactory_noViolationAndApiNeverCalled(): void + { + $this->apiClientFactory->expects(self::never())->method('createForPaymentMethod'); + + $paymentMethod = $this->buildPaymentMethod(OneyGatewayFactory::FACTORY_NAME, []); + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + public function testValidate_noAmountsConfigured_noViolationAndApiNeverCalled(): void + { + $this->apiClientFactory->expects(self::never())->method('createForPaymentMethod'); + + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, []); + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + public function testValidate_minGreaterThanMax_raisesViolationWithoutCallingApi(): void + { + $this->apiClientFactory->expects(self::never())->method('createForPaymentMethod'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 5000, ScalapayGatewayFactory::MAX_AMOUNT => 1000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $constraint = new IsScalapayAmountRangeValid(); + $this->validator->validate($paymentMethod, $constraint); + + $this->buildViolation($constraint->minGreaterThanMaxMessage)->assertRaised(); + } + + public function testValidate_minBelowApiMin_raisesOutOfRangeViolation(): void + { + $apiClient = $this->mockApiClientWithAccount(500, 200000); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 100]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $constraint = new IsScalapayAmountRangeValid(); + $this->validator->validate($paymentMethod, $constraint); + + $this->buildViolation($constraint->outOfRangeMessage) + ->setParameter('%min_amount%', '5.00') + ->setParameter('%max_amount%', '2000.00') + ->assertRaised() + ; + } + + public function testValidate_maxAboveApiMax_raisesOutOfRangeViolation(): void + { + $apiClient = $this->mockApiClientWithAccount(500, 200000); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $config = [ScalapayGatewayFactory::MAX_AMOUNT => 300000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $constraint = new IsScalapayAmountRangeValid(); + $this->validator->validate($paymentMethod, $constraint); + + $this->buildViolation($constraint->outOfRangeMessage) + ->setParameter('%min_amount%', '5.00') + ->setParameter('%max_amount%', '2000.00') + ->assertRaised() + ; + } + + public function testValidate_withinApiBounds_noViolation(): void + { + $apiClient = $this->mockApiClientWithAccount(500, 200000); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 1000, ScalapayGatewayFactory::MAX_AMOUNT => 100000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + public function testValidate_apiThrowsUnauthorizedException_noViolation(): void + { + $apiClient = $this->createMock(PayPlugApiClientInterface::class); + $apiClient->method('getAccount')->willThrowException(new UnauthorizedException('unauthorized')); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + // Failing open leaves the range unvalidated, so the skip must at least be traceable. + $this->logger->expects(self::once())->method('warning'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 1000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + public function testValidate_apiThrowsConnectionException_noViolation(): void + { + $apiClient = $this->createMock(PayPlugApiClientInterface::class); + $apiClient->method('getAccount')->willThrowException(new ConnectionException('network blip')); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $this->logger->expects(self::once())->method('warning'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 1000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + /** + * The account authorizes no EUR range for Scalapay at all, so there is nothing to check the + * configured range against. Same fail-open outcome as an API error, and logged for the same + * reason. + */ + public function testValidate_accountHasNoEurRange_noViolationButLogged(): void + { + $apiClient = $this->createMock(PayPlugApiClientInterface::class); + $apiClient->method('getAccount')->willReturn([ + 'configuration' => ['min_amounts' => ['USD' => 500], 'max_amounts' => ['USD' => 200000]], + 'payment_methods' => [], + ]); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $this->logger->expects(self::once())->method('warning'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 1000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + /** + * The gateway config is a plain serialized array, so a direct DB edit or an import script can + * leave a non-int in it. PaymentMethodValidator::process() has no try/catch: a malformed value + * must degrade to "not configured" rather than 500 the admin save with an assertion error. + */ + public function testValidate_malformedConfiguredAmounts_noViolationButLogged(): void + { + $this->apiClientFactory->expects(self::never())->method('createForPaymentMethod'); + $this->logger->expects(self::once())->method('warning'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => '1000', ScalapayGatewayFactory::MAX_AMOUNT => 'nonsense']; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + public function testValidate_disabledMethod_noViolationAndApiNeverCalled(): void + { + $this->apiClientFactory->expects(self::never())->method('createForPaymentMethod'); + + $config = [ScalapayGatewayFactory::MIN_AMOUNT => 1000, ScalapayGatewayFactory::MAX_AMOUNT => 100000]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config, false); + + $this->validator->validate($paymentMethod, new IsScalapayAmountRangeValid()); + + $this->assertNoViolation(); + } + + /** + * API range: min=500, max=200000 (cents). Merchant sets only max_amount=300, leaving + * min_amount blank. At checkout, the blank side falls back to the API bound (500), making + * the *effective* range inverted (500 > 300) even though neither configured value alone + * looks invalid against its own matching API bound. + */ + public function testValidate_onlyMaxConfiguredBelowEffectiveMin_raisesMinGreaterThanMaxViolation(): void + { + $apiClient = $this->mockApiClientWithAccount(500, 200000); + $this->apiClientFactory->method('createForPaymentMethod')->willReturn($apiClient); + + $config = [ScalapayGatewayFactory::MAX_AMOUNT => 300]; + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, $config); + + $constraint = new IsScalapayAmountRangeValid(); + $this->validator->validate($paymentMethod, $constraint); + + $this->buildViolation($constraint->minGreaterThanMaxMessage)->assertRaised(); + } + + private function mockApiClientWithAccount(int $minAmount, int $maxAmount): PayPlugApiClientInterface&MockObject + { + $apiClient = $this->createMock(PayPlugApiClientInterface::class); + $apiClient->method('getAccount')->willReturn([ + 'configuration' => [ + 'min_amounts' => ['EUR' => $minAmount], + 'max_amounts' => ['EUR' => $maxAmount], + ], + 'payment_methods' => [], + ]); + + return $apiClient; + } + + private function buildPaymentMethod( + string $factoryName, + array $config, + bool $enabled = true, + ): PaymentMethodInterface&MockObject + { + $gatewayConfig = $this->createMock(GatewayConfigInterface::class); + $gatewayConfig->method('getFactoryName')->willReturn($factoryName); + $gatewayConfig->method('getConfig')->willReturn($config); + + $paymentMethod = $this->createMock(PaymentMethodInterface::class); + $paymentMethod->method('getGatewayConfig')->willReturn($gatewayConfig); + $paymentMethod->method('isEnabled')->willReturn($enabled); + + return $paymentMethod; + } +} diff --git a/tests/PHPUnit/Provider/SupportedMethodsProviderTest.php b/tests/PHPUnit/Provider/SupportedMethodsProviderTest.php index 80277592..d208c8f3 100644 --- a/tests/PHPUnit/Provider/SupportedMethodsProviderTest.php +++ b/tests/PHPUnit/Provider/SupportedMethodsProviderTest.php @@ -8,9 +8,12 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\BancontactGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; +use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Provider\SupportedMethodsProvider; +use PayPlug\SyliusPayPlugPlugin\Resolver\AccountAmountRangeResolver; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Currency\Context\CurrencyContextInterface; use Sylius\Component\Payment\Model\GatewayConfigInterface; @@ -33,7 +36,7 @@ protected function setUp(): void $this->clientFactory->method('create')->willReturn($this->apiClient); - $this->provider = new SupportedMethodsProvider($this->currencyContext, $this->clientFactory); + $this->provider = new SupportedMethodsProvider($this->currencyContext, $this->clientFactory, new AccountAmountRangeResolver(), new NullLogger()); } // ------------------------------------------------------------------------- @@ -447,6 +450,128 @@ public function testProvide_fallsBackToConfigurationAmounts(): void self::assertEmpty($result2); } + // ------------------------------------------------------------------------- + // provide() — merchant-configured min/max override the API bounds + // ------------------------------------------------------------------------- + + /** + * The gateway config sets a min_amount (1000) tighter than the API min (99). + * Verifies amounts below the merchant's min are removed, and the merchant's own min boundary is kept. + */ + public function testProvide_withMerchantConfiguredMinAmount_overridesApiMin(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('EUR'); + $this->apiClient->method('getAccount')->willReturn($this->buildAccount(99, 2000000)); + + $method = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, ['min_amount' => 1000]); + + $result = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 500); + self::assertEmpty($result); + + $result2 = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 1000); + self::assertCount(1, $result2); + } + + /** + * The gateway config sets a max_amount (100000) tighter than the API max (2000000). + * Verifies amounts above the merchant's max are removed, and the merchant's own max boundary is kept. + */ + public function testProvide_withMerchantConfiguredMaxAmount_overridesApiMax(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('EUR'); + $this->apiClient->method('getAccount')->willReturn($this->buildAccount(99, 2000000)); + + $method = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, ['max_amount' => 100000]); + + $result = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 150000); + self::assertEmpty($result); + + $result2 = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 100000); + self::assertCount(1, $result2); + } + + /** + * No min_amount/max_amount set in the gateway config (merchant left the fields blank). + * Verifies the API bounds alone still apply, unchanged from today's behavior. + */ + public function testProvide_withoutMerchantConfiguredAmounts_fallsBackToApiBoundsOnly(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('EUR'); + $this->apiClient->method('getAccount')->willReturn($this->buildAccount(99, 2000000)); + + $method = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, []); + + $result = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 50); + self::assertEmpty($result); + + $result2 = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 99); + self::assertCount(1, $result2); + } + + /** + * The merchant's min_amount/max_amount override is entered as EUR (MoneyType field), but + * checkout is happening in USD. The EUR-denominated override must not be applied to a + * USD amount — only the API's own per-currency bounds apply. + */ + public function testProvide_merchantConfiguredAmountsIgnoredForNonEurCurrency(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('USD'); + + $account = [ + 'configuration' => [ + 'min_amounts' => ['USD' => 100], + 'max_amounts' => ['USD' => 200000], + ], + 'payment_methods' => [], + ]; + $this->apiClient->method('getAccount')->willReturn($account); + + // If wrongly applied to USD, this EUR-denominated max_amount would exclude the payment. + $method = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, ['max_amount' => 300]); + + $result = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 150000); + self::assertCount(1, $result); + } + + /** + * The min_amount/max_amount keys are Scalapay's own: only IsScalapayAmountRangeValidValidator + * keeps them inside the API-authorized range at save time, and it is wired for Scalapay only. + * Another gateway carrying the same keys must therefore be left on the raw API bounds rather + * than granted an unvalidated checkout override. + */ + public function testProvide_merchantConfiguredAmountsIgnoredForNonScalapayGateway(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('EUR'); + $this->apiClient->method('getAccount')->willReturn($this->buildAccount(99, 2000000)); + + // If wrongly honored, this max_amount would exclude the payment below. + $method = $this->buildPaymentMethod(PayPlugGatewayFactory::FACTORY_NAME, ['min_amount' => 1000, 'max_amount' => 300]); + + $result = $this->provider->provide([$method], PayPlugGatewayFactory::FACTORY_NAME, 150000); + self::assertCount(1, $result); + } + + /** + * The gateway config is a plain serialized array, so a direct DB edit or an import script can + * leave a non-int in it. provide() runs on every checkout page with no surrounding try/catch: + * a malformed override must degrade to the API bounds, not throw and break payment-method + * resolution for the whole checkout. + */ + public function testProvide_withMalformedMerchantConfiguredAmounts_fallsBackToApiBounds(): void + { + $this->currencyContext->method('getCurrencyCode')->willReturn('EUR'); + $this->apiClient->method('getAccount')->willReturn($this->buildAccount(99, 2000000)); + + $method = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, ['min_amount' => '1000', 'max_amount' => 'nonsense']); + + // API bounds are 99–2000000: an in-range amount is kept, an out-of-range one still removed. + $result = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 150000); + self::assertCount(1, $result); + + $result2 = $this->provider->provide([$method], ScalapayGatewayFactory::FACTORY_NAME, 50); + self::assertEmpty($result2); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/tests/PHPUnit/Resolver/AccountAmountRangeResolverTest.php b/tests/PHPUnit/Resolver/AccountAmountRangeResolverTest.php new file mode 100644 index 00000000..31dc7520 --- /dev/null +++ b/tests/PHPUnit/Resolver/AccountAmountRangeResolverTest.php @@ -0,0 +1,120 @@ +resolver = new AccountAmountRangeResolver(); + } + + public function testResolve_withoutPaymentMethodKey_usesConfigurationDefaults(): void + { + $account = [ + 'configuration' => [ + 'min_amounts' => ['EUR' => 100, 'USD' => 200], + 'max_amounts' => ['EUR' => 100000, 'USD' => 200000], + ], + ]; + + $result = $this->resolver->resolve($account, null); + + self::assertSame([ + 'EUR' => ['min_amount' => 100, 'max_amount' => 100000], + 'USD' => ['min_amount' => 200, 'max_amount' => 200000], + ], $result); + } + + public function testResolve_withPaymentMethodOverride_usesOverrideInsteadOfDefaults(): void + { + $account = [ + 'configuration' => [ + 'min_amounts' => ['EUR' => 30], + 'max_amounts' => ['EUR' => 2000000], + ], + 'payment_methods' => [ + 'scalapay' => [ + 'min_amounts' => ['EUR' => 500], + 'max_amounts' => ['EUR' => 200000], + ], + ], + ]; + + $result = $this->resolver->resolve($account, 'scalapay'); + + self::assertSame(['EUR' => ['min_amount' => 500, 'max_amount' => 200000]], $result); + } + + public function testResolve_withPaymentMethodKeyButNoOverride_fallsBackToConfigurationDefaults(): void + { + $account = [ + 'configuration' => [ + 'min_amounts' => ['EUR' => 30], + 'max_amounts' => ['EUR' => 2000000], + ], + 'payment_methods' => [ + 'apple_pay' => [ + 'enabled' => true, + // no min_amounts / max_amounts + ], + ], + ]; + + $result = $this->resolver->resolve($account, 'apple_pay'); + + self::assertSame(['EUR' => ['min_amount' => 30, 'max_amount' => 2000000]], $result); + } + + /** + * The per-payment-method override is present but the wrong shape (a string, not an array). + * Verifies this degrades gracefully to the configuration defaults instead of blowing up on + * a malformed API response — this is the divergence the two original, independent + * implementations of this parsing logic used to disagree on. + */ + public function testResolve_withMalformedOverride_fallsBackToConfigurationDefaults(): void + { + $account = [ + 'configuration' => [ + 'min_amounts' => ['EUR' => 30], + 'max_amounts' => ['EUR' => 2000000], + ], + 'payment_methods' => [ + 'scalapay' => [ + 'min_amounts' => 'not-an-array', + 'max_amounts' => 'not-an-array', + ], + ], + ]; + + $result = $this->resolver->resolve($account, 'scalapay'); + + self::assertSame(['EUR' => ['min_amount' => 30, 'max_amount' => 2000000]], $result); + } + + public function testResolve_currencyMissingFromMaxAmounts_isExcluded(): void + { + $account = [ + 'configuration' => [ + 'min_amounts' => ['EUR' => 100, 'USD' => 200], + 'max_amounts' => ['EUR' => 100000], + ], + ]; + + $result = $this->resolver->resolve($account, null); + + self::assertSame(['EUR' => ['min_amount' => 100, 'max_amount' => 100000]], $result); + } + + public function testResolve_missingConfiguration_returnsEmptyArray(): void + { + self::assertSame([], $this->resolver->resolve([], null)); + } +} diff --git a/tests/PHPUnit/Validator/PaymentMethodValidatorTest.php b/tests/PHPUnit/Validator/PaymentMethodValidatorTest.php index 2a33bae5..8ce05c0a 100644 --- a/tests/PHPUnit/Validator/PaymentMethodValidatorTest.php +++ b/tests/PHPUnit/Validator/PaymentMethodValidatorTest.php @@ -8,7 +8,9 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\BancontactGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; +use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsCanSavePaymentMethod; +use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsScalapayAmountRangeValid; use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\PayplugPermission; use PayPlug\SyliusPayPlugPlugin\Validator\PaymentMethodValidator; use PHPUnit\Framework\MockObject\MockObject; @@ -284,6 +286,38 @@ public function testProcess_payplugFactory_hostedFieldsTrueOneClickTrue_validate $this->paymentMethodValidator->process($paymentMethod); } + // ------------------------------------------------------------------------- + // process() — Scalapay factory → base constraint + amount range constraint + // ------------------------------------------------------------------------- + + /** + * Scalapay gateway config. Verifies both IsCanSavePaymentMethod and + * IsScalapayAmountRangeValid are passed to the validator (2 total). + */ + public function testProcess_scalapayFactory_validatesWithBaseAndAmountRangeConstraints(): void + { + $paymentMethod = $this->buildPaymentMethod(ScalapayGatewayFactory::FACTORY_NAME, []); + + $this->validator + ->expects(self::once()) + ->method('validate') + ->willReturnCallback(function ($subject, array $constraints) { + self::assertCount(2, $constraints); + self::assertInstanceOf(IsCanSavePaymentMethod::class, $constraints[0]); + self::assertInstanceOf(IsScalapayAmountRangeValid::class, $constraints[1]); + + return new ConstraintViolationList(); + }) + ; + + $flashBag = $this->createMock(FlashBagInterface::class); + $session = $this->createMock(Session::class); + $session->method('getFlashBag')->willReturn($flashBag); + $this->requestStack->method('getSession')->willReturn($session); + + $this->paymentMethodValidator->process($paymentMethod); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/translations/messages.en.yml b/translations/messages.en.yml index aa7d7102..a486cfb3 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -93,6 +93,10 @@ payplug_sylius_payplug_plugin: title: 'The fees are:' client: Split between you and your customers merchant: For you + scalapay_gateway_config: + min_amount: Minimum amount + max_amount: Maximum amount + amount_help: Leave empty to use the limits authorized by PayPlug. integrated_payment: card_holder.title: 'Cardholder name' card_holder.error: 'Invalid Name and/or Last Name.' diff --git a/translations/messages.fr.yml b/translations/messages.fr.yml index fa51f2ec..4c17f7ad 100644 --- a/translations/messages.fr.yml +++ b/translations/messages.fr.yml @@ -111,6 +111,10 @@ payplug_sylius_payplug_plugin: title: 'Les frais sont :' client: Répartis entre vous et vos clients merchant: À votre charge + scalapay_gateway_config: + min_amount: Montant minimum + max_amount: Montant maximum + amount_help: Laissez vide pour utiliser les limites autorisées par PayPlug. integrated_payment: card_holder.title: 'Nom du titulaire de la carte' card_holder.error: 'Nom et/ou prénom invalide(s).' diff --git a/translations/messages.it.yml b/translations/messages.it.yml index 1bfe7a6f..16e60374 100644 --- a/translations/messages.it.yml +++ b/translations/messages.it.yml @@ -93,6 +93,10 @@ payplug_sylius_payplug_plugin: title: 'Le spese sono:' client: Ripartite tra te e i tuoi clienti merchant: A tuo carico + scalapay_gateway_config: + min_amount: Importo minimo + max_amount: Importo massimo + amount_help: Lascia vuoto per usare i limiti autorizzati da PayPlug. integrated_payment: card_holder.title: 'Titolare della carta' card_holder.error: 'Nome e/o Cognome non valido(i).' diff --git a/translations/validators.en.yml b/translations/validators.en.yml index 78c1c1fe..cf4141c2 100644 --- a/translations/validators.en.yml +++ b/translations/validators.en.yml @@ -33,6 +33,8 @@ payplug_sylius_payplug_plugin: You don't have access to this feature yet. To activate Scalapay, please contact us at support@payplug.com and activate the LIVE mode. + min_amount_greater_than_max: The minimum amount must be lower than or equal to the maximum amount. + amount_out_of_authorized_range: The amount limits must be between %min_amount% and %max_amount%. payplug_wero: can_not_save_method_with_test_key: | The Wero payment method is not available for the TEST mode. diff --git a/translations/validators.fr.yml b/translations/validators.fr.yml index 6031253d..5f4baffc 100644 --- a/translations/validators.fr.yml +++ b/translations/validators.fr.yml @@ -32,6 +32,8 @@ payplug_sylius_payplug_plugin: Vous n'avez pas accès à cette fonctionnalité. Pour activer Scalapay, contactez-nous à support@payplug.com et activez le mode LIVE. + min_amount_greater_than_max: Le montant minimum doit être inférieur ou égal au montant maximum. + amount_out_of_authorized_range: Les limites de montant doivent être comprises entre %min_amount% et %max_amount%. payplug_wero: can_not_save_method_with_test_key: | Le paiement par Wero n'est pas disponible en mode TEST. diff --git a/translations/validators.it.yml b/translations/validators.it.yml index 7dcf67b4..7ebaea99 100644 --- a/translations/validators.it.yml +++ b/translations/validators.it.yml @@ -32,6 +32,8 @@ payplug_sylius_payplug_plugin: Non puoi ancora accedere a questa funzionalità. Per attivare Scalapay, contattaci a support@payplug.com e attiva la modalità LIVE. + min_amount_greater_than_max: L'importo minimo deve essere inferiore o uguale all'importo massimo. + amount_out_of_authorized_range: I limiti di importo devono essere compresi tra %min_amount% e %max_amount%. payplug_wero: can_not_save_method_with_test_key: | Il metodo di pagamento Wero non è disponibile in modalità TEST.