From 59fd3201cf9a6491ade9ef25bd7fe05e8f295374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Im=C3=A8ne?= Date: Tue, 27 Oct 2026 17:21:20 +0100 Subject: [PATCH] PRE-3761: Stop requiring phone region to match address Require UPC ^1.2.2 and send a valid phone number even when it belongs to another country than the billing or shipping address. A metropolitan French number entered on an overseas address is sent as French, for Hosted Fields and for the other payment methods. --- CHANGELOG.md | 5 + composer.json | 2 +- composer.lock | 14 +- src/Creator/PayPlugPaymentDataCreator.php | 36 ++-- src/Upc/OrderAddressDtoCreator.php | 49 ++++-- src/Upc/PaymentCaptureContextBuilder.php | 6 +- src/Upc/PhoneRegionResolver.php | 48 ++++++ .../Creator/PayPlugPaymentDataCreatorTest.php | 76 +++++++- .../Upc/OrderAddressDtoCreatorTest.php | 163 +++++++++++++++++- .../Upc/PaymentCaptureContextBuilderTest.php | 49 ++++++ tests/PHPUnit/Upc/PhoneRegionResolverTest.php | 35 ++++ 11 files changed, 442 insertions(+), 41 deletions(-) create mode 100644 src/Upc/PhoneRegionResolver.php create mode 100644 tests/PHPUnit/Upc/PhoneRegionResolverTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a24170b..9efc5040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > Merchants will need to contact support to switch to the new authentication method. ### Changed +- Requires `payplug/unified-plugin-core` 1.2.2 or later - Plugin structure has been changed to follow the new Symfony bundle structure - Front assets have been migrated to use Stimulus - Gateway uniqueness is now enforced **per channel** instead of per installation: two gateways of @@ -111,6 +112,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 error is logged - Hosted Fields iframes are only mounted once the payment method's form is displayed, so they no longer render broken when the method is preselected or chosen before its form is shown +- A phone number valid in a country other than the address, or a metropolitan French number entered + on an overseas address (GP, MQ, RE, GF, YT, PM, BL, MF, NC, PF, WF), is no longer dropped from the + billing and shipping details sent to PayPlug (Hosted Fields), and the overseas case is also fixed + for the other payment methods; a local overseas number keeps its own territory ### Breaking changes for anyone extending the plugin diff --git a/composer.json b/composer.json index 3ea65550..e82d4580 100755 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "ext-json": "*", "giggsey/libphonenumber-for-php": "^8.12", "payplug/payplug-php": "^4.0", - "payplug/unified-plugin-core": "^1.2.1", + "payplug/unified-plugin-core": "^1.2.2", "php-http/message-factory": "^1.1", "sylius/refund-plugin": "^2.0", "sylius/sylius": "^2.0", diff --git a/composer.lock b/composer.lock index 15ccf32d..ef8d3706 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39a63c300688d00971a9a43114e99ec7", + "content-hash": "b501ab6e80635ff6a85bbfdf3e64f58a", "packages": [ { "name": "alcohol/iso4217", @@ -5748,16 +5748,16 @@ }, { "name": "payplug/unified-plugin-core", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/payplug/unified-plugin-core.git", - "reference": "05423ee4bae1bef5e1374ba1aeadf127c9bb0dca" + "reference": "d43d8cd45da031060189385fc40e0c8d407a7242" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/payplug/unified-plugin-core/zipball/05423ee4bae1bef5e1374ba1aeadf127c9bb0dca", - "reference": "05423ee4bae1bef5e1374ba1aeadf127c9bb0dca", + "url": "https://api.github.com/repos/payplug/unified-plugin-core/zipball/d43d8cd45da031060189385fc40e0c8d407a7242", + "reference": "d43d8cd45da031060189385fc40e0c8d407a7242", "shasum": "" }, "require": { @@ -5794,9 +5794,9 @@ "description": "Core foundations shared library for Payplug e-commerce plugins.", "support": { "issues": "https://github.com/payplug/unified-plugin-core/issues", - "source": "https://github.com/payplug/unified-plugin-core/tree/1.2.1" + "source": "https://github.com/payplug/unified-plugin-core/tree/1.2.2" }, - "time": "2026-10-08T07:20:41+00:00" + "time": "2026-10-19T13:45:18+00:00" }, { "name": "payum/core", diff --git a/src/Creator/PayPlugPaymentDataCreator.php b/src/Creator/PayPlugPaymentDataCreator.php index 7b7000df..12d7afc1 100644 --- a/src/Creator/PayPlugPaymentDataCreator.php +++ b/src/Creator/PayPlugPaymentDataCreator.php @@ -22,6 +22,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Upc\CustomerTitleResolver; +use PayPlug\SyliusPayPlugPlugin\Upc\PhoneRegionResolver; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; @@ -112,31 +113,36 @@ public function create( return $details; } + /** + * The number is read in each region of PhoneRegionResolver::resolve() in turn, the first one + * in which it is valid wins. + */ public function formatNumber(string $phoneNumber, ?string $isoCode): array { - try { - $phoneNumberUtil = PhoneNumberUtil::getInstance(); - $parsed = $phoneNumberUtil->parse($phoneNumber, $isoCode); + $phoneNumberUtil = PhoneNumberUtil::getInstance(); + $regions = null !== $isoCode ? PhoneRegionResolver::resolve($isoCode) : [null]; - if (!$phoneNumberUtil->isValidNumber($parsed)) { - return [ - 'phone' => null, - 'is_mobile' => null, - ]; + foreach ($regions as $region) { + try { + $parsed = $phoneNumberUtil->parse($phoneNumber, $region); + } catch (NumberParseException) { + continue; } - $formatted = $phoneNumberUtil->format($parsed, PhoneNumberFormat::E164); + if (!$phoneNumberUtil->isValidNumber($parsed)) { + continue; + } return [ - 'phone' => $formatted, + 'phone' => $phoneNumberUtil->format($parsed, PhoneNumberFormat::E164), 'is_mobile' => PhoneNumberType::MOBILE === $phoneNumberUtil->getNumberType($parsed), ]; - } catch (NumberParseException) { - return [ - 'phone' => null, - 'is_mobile' => null, - ]; } + + return [ + 'phone' => null, + 'is_mobile' => null, + ]; } private function formatTitle(CustomerInterface $customer): ?string diff --git a/src/Upc/OrderAddressDtoCreator.php b/src/Upc/OrderAddressDtoCreator.php index b466aa57..09c5103e 100644 --- a/src/Upc/OrderAddressDtoCreator.php +++ b/src/Upc/OrderAddressDtoCreator.php @@ -22,7 +22,13 @@ */ final class OrderAddressDtoCreator { - public function createBilling(OrderInterface $order): ?BillingDto + /** + * @param bool $requireRegionMatch true: the phone number must belong to one of the address's + * phoneRegions(); false: any valid number is sent, formatted in its own country. + * Defaults to true to keep the previous behaviour for any other caller: the Hosted + * Fields capture path (PaymentCaptureContextBuilder) passes false explicitly. + */ + public function createBilling(OrderInterface $order, bool $requireRegionMatch = true): ?BillingDto { $address = $order->getBillingAddress(); if (null === $address) { @@ -31,12 +37,15 @@ public function createBilling(OrderInterface $order): ?BillingDto return new BillingDto( $this->buildAddress($address), - $this->buildContact($address), + $this->buildContact($address, $requireRegionMatch), $this->title($order), ); } - public function createShipping(OrderInterface $order): ?ShippingDto + /** + * @param bool $requireRegionMatch same as createBilling() + */ + public function createShipping(OrderInterface $order, bool $requireRegionMatch = true): ?ShippingDto { $address = $order->getShippingAddress(); if (null === $address) { @@ -45,15 +54,15 @@ public function createShipping(OrderInterface $order): ?ShippingDto return new ShippingDto( $this->buildAddress($address), - $this->buildContact($address), + $this->buildContact($address, $requireRegionMatch), $order->getCustomer()?->getEmail(), $address->getCompany(), ); } - private function buildContact(AddressInterface $address): ContactDto + private function buildContact(AddressInterface $address, bool $requireRegionMatch): ContactDto { - [$phone, $mobilePhone] = $this->splitPhone($address); + [$phone, $mobilePhone] = $this->splitPhone($address, $requireRegionMatch); return new ContactDto($address->getFirstName(), $address->getLastName(), $phone, $mobilePhone); } @@ -86,8 +95,9 @@ private function shortProvinceCode(AddressInterface $address): ?string * phone number per address, so only one of the two ever comes back non-null here, matching * whichever type PhoneHelper::isMobile() detects it as; an unparseable/invalid number is * treated as absent rather than failing the whole payment over supplementary contact data. + * The number is read in each region of phoneRegions() in turn, the first one accepting it wins. */ - private function splitPhone(AddressInterface $address): array + private function splitPhone(AddressInterface $address, bool $requireRegionMatch): array { $rawPhone = $address->getPhoneNumber(); $countryCode = $address->getCountryCode(); @@ -95,14 +105,27 @@ private function splitPhone(AddressInterface $address): array return [null, null]; } - try { - $e164Phone = PhoneHelper::toE164($rawPhone, $countryCode); - $isMobile = PhoneHelper::isMobile($rawPhone, $countryCode); - } catch (InvalidPhoneNumberException) { - return [null, null]; + foreach ($this->phoneRegions($countryCode) as $region) { + try { + $e164Phone = PhoneHelper::toE164($rawPhone, $region, $requireRegionMatch); + $isMobile = PhoneHelper::isMobile($rawPhone, $region, $requireRegionMatch); + } catch (InvalidPhoneNumberException) { + continue; + } + + return $isMobile ? [null, $e164Phone] : [$e164Phone, null]; } - return $isMobile ? [null, $e164Phone] : [$e164Phone, null]; + return [null, null]; + } + + /** + * @return list the address country first, then the regions sharing its numbering, then + * FR for an overseas territory sharing the French numbering + */ + private function phoneRegions(string $countryCode): array + { + return PhoneRegionResolver::resolve($countryCode); } private function title(OrderInterface $order): ?string diff --git a/src/Upc/PaymentCaptureContextBuilder.php b/src/Upc/PaymentCaptureContextBuilder.php index cedea723..8fd645b2 100644 --- a/src/Upc/PaymentCaptureContextBuilder.php +++ b/src/Upc/PaymentCaptureContextBuilder.php @@ -71,8 +71,10 @@ public function buildCommonFields( $common->successUrl = $successUrl; $common->cancelUrl = $successUrl . '?' . http_build_query(['status' => 'canceled']); if (null !== $order) { - $common->billing = $this->orderAddressDtoCreator->createBilling($order); - $common->shipping = $this->orderAddressDtoCreator->createShipping($order); + // Any valid phone number is sent, even from another country than the address. The + // creator's strict mode (requireRegionMatch: true) is there for a method that needs it. + $common->billing = $this->orderAddressDtoCreator->createBilling($order, requireRegionMatch: false); + $common->shipping = $this->orderAddressDtoCreator->createShipping($order, requireRegionMatch: false); } return $common; diff --git a/src/Upc/PhoneRegionResolver.php b/src/Upc/PhoneRegionResolver.php new file mode 100644 index 00000000..6f3885e0 --- /dev/null +++ b/src/Upc/PhoneRegionResolver.php @@ -0,0 +1,48 @@ + ['GP', 'MF'], + 'MF' => ['GP', 'BL'], + ]; + + /** + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * @return list the address country first, then the regions sharing its numbering, then + * FR for an overseas territory sharing the French numbering + */ + public static function resolve(string $countryCode): array + { + $countryCode = \strtoupper($countryCode); + if (!\in_array($countryCode, self::FRENCH_NUMBERING_TERRITORIES, true)) { + return [$countryCode]; + } + + return [$countryCode, ...(self::SHARED_NUMBERING_REGIONS[$countryCode] ?? []), 'FR']; + } +} diff --git a/tests/PHPUnit/Creator/PayPlugPaymentDataCreatorTest.php b/tests/PHPUnit/Creator/PayPlugPaymentDataCreatorTest.php index 4b545215..9f6914ae 100644 --- a/tests/PHPUnit/Creator/PayPlugPaymentDataCreatorTest.php +++ b/tests/PHPUnit/Creator/PayPlugPaymentDataCreatorTest.php @@ -127,6 +127,65 @@ public function testFormatNumber_validBelgianNumber_returnsFormattedE164(): void self::assertFalse($result['is_mobile']); } + /** + * @dataProvider phoneRegionProvider + */ + public function testFormatNumber_readsTheNumberInTheAddressRegionsAndAcceptsForeignNumbers( + string $phoneNumber, + ?string $isoCode, + ?string $expectedPhone, + ?bool $expectedIsMobile, + ): void { + $result = $this->creator->formatNumber($phoneNumber, $isoCode); + + self::assertSame(['phone' => $expectedPhone, 'is_mobile' => $expectedIsMobile], $result); + } + + /** + * @return iterable + */ + public static function phoneRegionProvider(): iterable + { + yield 'metropolitan mobile on a MQ address' => ['07 81 23 45 67', 'MQ', '+33781234567', true]; + yield 'metropolitan mobile on a lowercase mq address' => ['07 81 23 45 67', 'mq', '+33781234567', true]; + yield 'MQ mobile stays in MQ' => ['0696 12 34 56', 'MQ', '+596696123456', true]; + yield 'YT landline also valid in FR stays in YT' => ['02 69 61 23 45', 'YT', '+262269612345', false]; + yield 'shared +590 mobile on a BL address' => ['0690 12 34 56', 'BL', '+590690123456', true]; + yield 'shared +590 mobile on a MF address' => ['0690123456', 'MF', '+590690123456', true]; + yield 'shared +590 landline on a MF address' => ['0590871234', 'MF', '+590590871234', false]; + yield 'FR mobile on a DE address' => ['+33612345678', 'DE', '+33612345678', true]; + yield 'FR landline on a DE address' => ['+33142345678', 'DE', '+33142345678', false]; + yield 'FR mobile with 00 prefix on a BE address' => ['0033609860479', 'BE', '+33609860479', true]; + yield 'BE mobile with 00 prefix on a FR address' => ['0032 470 12 34 56', 'FR', '+32470123456', true]; + yield 'FR mobile on a FR address' => ['06 12 34 56 78', 'FR', '+33612345678', true]; + yield 'national number read in DE, not retried as FR' => ['0612345678', 'DE', '+49612345678', false]; + yield 'national number invalid in BE, not retried as FR' => ['0612345678', 'BE', null, null]; + yield 'invalid number on a MQ address' => ['0612 34', 'MQ', null, null]; + yield 'national number without country' => ['0612345678', null, null, null]; + yield 'international number without country' => ['+33612345678', null, '+33612345678', true]; + } + + /** + * @dataProvider frenchNumberingTerritoryProvider + */ + public function testFormatNumber_withAMetropolitanMobileOnAnOverseasTerritory_returnsItAsFrench( + string $isoCode, + ): void { + $result = $this->creator->formatNumber('0612345678', $isoCode); + + self::assertSame(['phone' => '+33612345678', 'is_mobile' => true], $result); + } + + /** + * @return iterable + */ + public static function frenchNumberingTerritoryProvider(): iterable + { + foreach (['GP', 'MQ', 'RE', 'GF', 'YT', 'PM', 'BL', 'MF', 'NC', 'PF', 'WF'] as $isoCode) { + yield $isoCode => [$isoCode]; + } + } + // ------------------------------------------------------------------------- // create() — basic fields // ------------------------------------------------------------------------- @@ -503,6 +562,18 @@ public function testCreate_withNullPhone_doesNotPopulatePhoneFields(): void self::assertNull($details['billing']['landline_phone_number']); } + public function testCreate_withAMetropolitanMobileOnAnOverseasAddress_sendsItAsFrenchInBillingAndShipping(): void + { + $payment = $this->buildMinimalPayment(1000, 'EUR', 1, 'ORD-1', phone: '07 81 23 45 67', countryCode: 'MQ'); + + $details = $this->creator->create($payment); + + self::assertSame('+33781234567', $details['billing']['mobile_phone_number']); + self::assertNull($details['billing']['landline_phone_number']); + self::assertSame('+33781234567', $details['shipping']['mobile_phone_number']); + self::assertNull($details['shipping']['landline_phone_number']); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- @@ -516,14 +587,15 @@ private function buildMinimalPayment( string $locale = 'fr_FR', bool $sameAddress = false, ?string $phone = null, + string $countryCode = 'FR', ): PaymentInterface { $customer = $this->createMock(CustomerInterface::class); $customer->method('getId')->willReturn($customerId); $customer->method('getGender')->willReturn($gender); $customer->method('getEmail')->willReturn('test@example.com'); - $billingAddress = $this->buildAddress(1, 'FR', $phone); - $shippingAddress = $this->buildAddress($sameAddress ? 1 : 2, 'FR', $phone); + $billingAddress = $this->buildAddress(1, $countryCode, $phone); + $shippingAddress = $this->buildAddress($sameAddress ? 1 : 2, $countryCode, $phone); $order = $this->createMock(OrderInterface::class); $order->method('getCustomer')->willReturn($customer); diff --git a/tests/PHPUnit/Upc/OrderAddressDtoCreatorTest.php b/tests/PHPUnit/Upc/OrderAddressDtoCreatorTest.php index 60dcc2b1..a515599c 100644 --- a/tests/PHPUnit/Upc/OrderAddressDtoCreatorTest.php +++ b/tests/PHPUnit/Upc/OrderAddressDtoCreatorTest.php @@ -22,7 +22,7 @@ protected function setUp(): void private function address( ?string $phoneNumber = null, - string $countryCode = 'FR', + ?string $countryCode = 'FR', string $provinceCode = '75', ): AddressInterface&MockObject { $address = $this->createMock(AddressInterface::class); @@ -185,4 +185,165 @@ public function testCreateBillingAndCreateShipping_areIndependentOfEachOther(): self::assertNotNull($this->factory->createBilling($order)); self::assertNull($this->factory->createShipping($order)); } + + /** + * @dataProvider nonStrictPhoneProvider + */ + public function testCreateBilling_withoutRegionMatch_sendsEveryValidNumberInItsOwnCountry( + string $rawPhone, + string $countryCode, + ?string $expectedPhone, + ?string $expectedMobilePhone, + ): void { + $order = $this->orderWithAddresses($this->address($rawPhone, $countryCode), null); + + $billing = $this->factory->createBilling($order, false); + + self::assertNotNull($billing); + self::assertNotNull($billing->contact); + self::assertSame($expectedPhone, $billing->contact->phone); + self::assertSame($expectedMobilePhone, $billing->contact->mobilePhone); + } + + /** + * @return iterable + */ + public static function nonStrictPhoneProvider(): iterable + { + yield 'FR mobile on a DE address' => ['+33612345678', 'DE', null, '+33612345678']; + yield 'FR landline on a DE address' => ['+33142345678', 'DE', '+33142345678', null]; + yield 'FR mobile with 00 prefix on a BE address' => ['0033609860479', 'BE', null, '+33609860479']; + yield 'BE mobile with 00 prefix on a FR address' => ['0032 470 12 34 56', 'FR', null, '+32470123456']; + yield 'metropolitan mobile on a MQ address' => ['07 81 23 45 67', 'MQ', null, '+33781234567']; + yield 'metropolitan mobile on a lowercase mq address' => ['07 81 23 45 67', 'mq', null, '+33781234567']; + yield 'MQ mobile stays in MQ' => ['0696 12 34 56', 'MQ', null, '+596696123456']; + yield 'YT landline also valid in FR stays in YT' => ['02 69 61 23 45', 'YT', '+262269612345', null]; + yield 'shared +590 mobile on a BL address' => ['0690 12 34 56', 'BL', null, '+590690123456']; + yield 'shared +590 mobile on a MF address' => ['0690123456', 'MF', null, '+590690123456']; + yield 'shared +590 landline on a MF address' => ['0590871234', 'MF', '+590590871234', null]; + yield 'national number read in DE, not retried as FR' => ['0612345678', 'DE', '+49612345678', null]; + yield 'national number invalid in BE, not retried as FR' => ['0612345678', 'BE', null, null]; + yield 'invalid number on a MQ address' => ['0612 34', 'MQ', null, null]; + } + + /** + * @dataProvider strictPhoneProvider + */ + public function testCreateBilling_withRegionMatch_rejectsForeignNumbersButRetriesOverseasTerritories( + string $rawPhone, + string $countryCode, + ?string $expectedMobilePhone, + ): void { + $order = $this->orderWithAddresses($this->address($rawPhone, $countryCode), null); + + $billing = $this->factory->createBilling($order, true); + + self::assertNotNull($billing); + self::assertNotNull($billing->contact); + self::assertNull($billing->contact->phone); + self::assertSame($expectedMobilePhone, $billing->contact->mobilePhone); + } + + /** + * @return iterable + */ + public static function strictPhoneProvider(): iterable + { + yield 'FR mobile on a DE address' => ['+33612345678', 'DE', null]; + yield 'FR mobile with 00 prefix on a BE address' => ['0033609860479', 'BE', null]; + yield 'BE mobile with 00 prefix on a FR address' => ['0032 470 12 34 56', 'FR', null]; + yield 'metropolitan mobile on a MQ address' => ['07 81 23 45 67', 'MQ', '+33781234567']; + yield 'MQ mobile stays in MQ' => ['0696 12 34 56', 'MQ', '+596696123456']; + yield 'shared +590 mobile on a BL address' => ['0690 12 34 56', 'BL', '+590690123456']; + yield 'shared +590 mobile on a MF address' => ['0690123456', 'MF', '+590690123456']; + yield 'FR mobile on a FR address' => ['0612345678', 'FR', '+33612345678']; + yield 'invalid number on a MQ address' => ['0612 34', 'MQ', null]; + } + + public function testCreateBilling_withoutTheRegionMatchArgument_rejectsAForeignNumber(): void + { + $order = $this->orderWithAddresses($this->address('+33612345678', 'DE'), null); + + $billing = $this->factory->createBilling($order); + + self::assertNotNull($billing); + self::assertNotNull($billing->contact); + self::assertNull($billing->contact->phone); + self::assertNull($billing->contact->mobilePhone); + } + + /** + * @dataProvider frenchNumberingTerritoryProvider + */ + public function testCreateBilling_withAMetropolitanMobileOnAnOverseasTerritory_sendsItAsFrench( + string $countryCode, + bool $requireRegionMatch, + ): void { + $order = $this->orderWithAddresses($this->address('0612345678', $countryCode), null); + + $billing = $this->factory->createBilling($order, $requireRegionMatch); + + self::assertNotNull($billing); + self::assertNotNull($billing->contact); + self::assertNull($billing->contact->phone); + self::assertSame('+33612345678', $billing->contact->mobilePhone); + } + + /** + * @return iterable + */ + public static function frenchNumberingTerritoryProvider(): iterable + { + foreach (['GP', 'MQ', 'RE', 'GF', 'YT', 'PM', 'BL', 'MF', 'NC', 'PF', 'WF'] as $countryCode) { + yield $countryCode . ' strict' => [$countryCode, true]; + yield $countryCode . ' non-strict' => [$countryCode, false]; + } + } + + public function testCreateBilling_withNoCountryCode_leavesBothPhoneFieldsNull(): void + { + $order = $this->orderWithAddresses($this->address('+33612345678', null), null); + + $billing = $this->factory->createBilling($order, false); + + self::assertNotNull($billing); + self::assertNotNull($billing->contact); + self::assertNull($billing->contact->phone); + self::assertNull($billing->contact->mobilePhone); + } + + public function testCreateShipping_withoutRegionMatch_sendsAForeignNumber(): void + { + $order = $this->orderWithAddresses(null, $this->address('+33612345678', 'DE')); + + $shipping = $this->factory->createShipping($order, false); + + self::assertNotNull($shipping); + self::assertNotNull($shipping->contact); + self::assertSame('+33612345678', $shipping->contact->mobilePhone); + } + + public function testCreateShipping_withRegionMatch_rejectsAForeignNumber(): void + { + $order = $this->orderWithAddresses(null, $this->address('+33612345678', 'DE')); + + $shipping = $this->factory->createShipping($order, true); + + self::assertNotNull($shipping); + self::assertNotNull($shipping->contact); + self::assertNull($shipping->contact->phone); + self::assertNull($shipping->contact->mobilePhone); + } + + public function testCreateShipping_withoutTheRegionMatchArgument_rejectsAForeignNumber(): void + { + $order = $this->orderWithAddresses(null, $this->address('+33612345678', 'DE')); + + $shipping = $this->factory->createShipping($order); + + self::assertNotNull($shipping); + self::assertNotNull($shipping->contact); + self::assertNull($shipping->contact->phone); + self::assertNull($shipping->contact->mobilePhone); + } } diff --git a/tests/PHPUnit/Upc/PaymentCaptureContextBuilderTest.php b/tests/PHPUnit/Upc/PaymentCaptureContextBuilderTest.php index 8615f31e..724a1994 100644 --- a/tests/PHPUnit/Upc/PaymentCaptureContextBuilderTest.php +++ b/tests/PHPUnit/Upc/PaymentCaptureContextBuilderTest.php @@ -257,6 +257,55 @@ private function commonFieldsForGatewayConfig(array $config): CommonFieldsDto return $this->builder->buildCommonFields('acct_123', 1000, 'eur', $paymentRequest, null); } + /** + * @dataProvider paymentMethodFactoryProvider + */ + public function testBuildCommonFields_withAPhoneNumberFromAnotherCountry_sendsItOnBillingAndShipping( + ?string $factoryName, + ): void { + $this->urlGenerator->method('generate')->willReturn('https://shop.test/payplug/notify/abc'); + + $address = $this->createMock(AddressInterface::class); + $address->method('getCountryCode')->willReturn('DE'); + $address->method('getPhoneNumber')->willReturn('+33612345678'); + + $order = $this->createMock(OrderInterface::class); + $order->method('getNumber')->willReturn('00000042'); + $order->method('getItems')->willReturn(new ArrayCollection()); + $order->method('getBillingAddress')->willReturn($address); + $order->method('getShippingAddress')->willReturn($address); + + $method = null; + if (null !== $factoryName) { + $gatewayConfig = new GatewayConfig(); + $gatewayConfig->setFactoryName($factoryName); + $method = new PaymentMethod(); + $method->setGatewayConfig($gatewayConfig); + } + + $payment = $this->createMock(PaymentInterface::class); + $payment->method('getId')->willReturn(42); + $payment->method('getMethod')->willReturn($method); + + $paymentRequest = $this->createMock(PaymentRequestInterface::class); + $paymentRequest->method('getPayment')->willReturn($payment); + $paymentRequest->method('getHash')->willReturn(Uuid::v4()); + + $common = $this->builder->buildCommonFields('acct_123', 1000, 'eur', $paymentRequest, $order); + + self::assertSame('+33612345678', $common->billing?->contact?->mobilePhone); + self::assertSame('+33612345678', $common->shipping?->contact?->mobilePhone); + } + + /** + * @return iterable + */ + public static function paymentMethodFactoryProvider(): iterable + { + yield 'Hosted Fields card payment' => [PayPlugGatewayFactory::FACTORY_NAME]; + yield 'no payment method' => [null]; + } + public function testBuildCommonFields_withAnOrderItem_usesItsProductNameAsDescription(): void { $this->urlGenerator->method('generate')->willReturn('https://shop.test/payplug/notify/abc'); diff --git a/tests/PHPUnit/Upc/PhoneRegionResolverTest.php b/tests/PHPUnit/Upc/PhoneRegionResolverTest.php new file mode 100644 index 00000000..d998777f --- /dev/null +++ b/tests/PHPUnit/Upc/PhoneRegionResolverTest.php @@ -0,0 +1,35 @@ + $expectedRegions + */ + public function testResolve_returnsTheRegionsToReadAPhoneNumberIn(string $countryCode, array $expectedRegions): void + { + self::assertSame($expectedRegions, PhoneRegionResolver::resolve($countryCode)); + } + + /** + * @return iterable}> + */ + public static function countryCodeProvider(): iterable + { + yield 'metropolitan France' => ['FR', ['FR']]; + yield 'another country' => ['DE', ['DE']]; + yield 'overseas territory' => ['MQ', ['MQ', 'FR']]; + yield 'lowercase overseas territory' => ['mq', ['MQ', 'FR']]; + yield 'Guadeloupe' => ['GP', ['GP', 'FR']]; + yield 'Saint-Barthelemy' => ['BL', ['BL', 'GP', 'MF', 'FR']]; + yield 'Saint-Martin' => ['MF', ['MF', 'GP', 'BL', 'FR']]; + } +}