Skip to content

PRE-3761: Stop requiring the phone region to match the address countr… - #340

Open
ilajili wants to merge 1 commit into
developfrom
feature/PRE-3761_relax-phone-region-match
Open

ilajili wants to merge 1 commit into
developfrom
feature/PRE-3761_relax-phone-region-match

Conversation

@ilajili

@ilajili ilajili commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Phone numbers are no longer dropped from the billing and shipping details sent to PayPlug in two cases:

  • Hosted Fields (new card and saved card): a valid number that belongs to another country than the address (e.g. +33… on a DE address) used to be rejected, because OrderAddressDtoCreator called UPC's
    PhoneHelper in strict mode.
  • Every payment method: a metropolitan French mobile entered in national format on an overseas address (e.g. 07 81 23 45 67 on MQ) used to be read as a local number, judged invalid and sent as null.

Motivation: since UPC 1.2.2 (PRE-3751, payplug/unified-plugin-core#42), PhoneHelper::toE164() and isMobile() accept an optional bool $requireRegionMatch = true. OrderAddressDtoCreator::splitPhone() called them with two arguments, so it stayed strict: a number such as
+33612345678 on a DE address became [null, null] and the payment was created with no phone. This is the same regression PrestaShop hit in
5.1.0 (PRE-3704, fixed by PRE-3752 / payplug-prestashop#141). Fixing it here before the next release from develop.

What changes

  • composer.json / composer.lock: payplug/unified-plugin-core ^1.2.1 → ^1.2.2. The lock diff is the UPC entry and the content-hash only (giggsey/libphonenumber-for-php stays at 8.13.45).

  • OrderAddressDtoCreator::createBilling() / createShipping(): new optional last parameter bool $requireRegionMatch = true. Constructor and DI are unchanged, and the default keeps the previous strict
    behaviour for any other caller.

  • PaymentCaptureContextBuilder: passes requireRegionMatch: false for billing and shipping.

  • New Upc\PhoneRegionResolver: the single source of the overseas rule, shared by both paths. For GP, MQ, RE, GF, YT, PM, BL, MF, NC, PF and WF it returns the address region first, then the regions sharing its
    numbering (BL ↔ MF ↔ GP), then FR; any other country returns only itself. Same rule as getPhoneNumberRegions() in payplug-prestashop#141. A genuine local number keeps its territory (0696… on MQ → +596…).

  • OrderAddressDtoCreator retries through that rule in strict and non-strict mode, with the same winning region for toE164 and isMobile. An unusable number gives no phone, never ''.

  • Legacy PayPlugPaymentDataCreator::formatNumber() (libphonenumber) applies the same retry. Signature and return shape are unchanged, and foreign numbers are still accepted as before.

  • CHANGELOG.md: "Requires UPC 1.2.2 or later" under ### Changed, and one entry under ### Fixed, both in the 2.0.0 section

    Which code path serves which payment method

  • OrderAddressDtoCreator (new path): the payplug method with Hosted Fields enabled, new card (CaptureHostedPaymentRequestHandler) and saved card (CaptureAliasPaymentRequestHandler).

  • PayPlugPaymentDataCreator::formatNumber() (legacy path): Oney, Bancontact, Wero, Scalapay, Apple Pay, American Express, and payplug without Hosted Fields.

  • Foreign numbers were already accepted on the legacy path (verified by running formatNumber()), so only Hosted Fields had that bug. The overseas case affected both paths.

Scope, please read

  • Oney is not changed: no Oney file is edited and no strict-method list is added. The ticket asks for Oney to stay strict ("a foreign number is rejected"), but Oney does not go through
    OrderAddressDtoCreator on Sylius, so that criterion is not covered by this PR.
  • What Oney does today (executed against the real OneyInvalidDataRetriever and OneyCompleteInfoDTO): they require a valid mobile number but do not check the region, so +33612345678 on a DE address passes.
    A 07 81 23 45 67 on MQ is refused by them.
  • formatNumber() is shared, so Oney's payload goes through the new retry. In the checkout-form flow it changes nothing for Oney, because the validators refuse such a number before the payload is built (the
    validators were run; the checkout guard in PaymentTypeExtension was read, not run). A payment that reaches create() without that form was not traced.
  • Whether Oney (and Bizum, if added) should become strict, and whether the legacy path should move to UPC's PhoneHelper(false), is pending the ticket author. Strict mode exists and is tested through the
    parameter, ready for that.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) [x]
  • ✨ New ffeature (non-breaking change that adds functionality) [ ]
  • 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs) [ ]
  • 🔧 Refactor (no functional changes, code improvement only) [ ]
  • 📦 Dependency update [x]
  • 🔒 Security fix [ ]
  • 📝 Documentation update [ ]

BC: none. PHP floor: unchanged (^8.2). Behaviour: more valid phone numbers are now sent instead of being dropped.

Checklist

Code Quality

  • Code is linted and formatted (ECS and PHPStan pass)
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config)

Testing

  • Unit tests added / updated
  • New/changed code is covered by tests — SonarCloud Quality Gate (coverage on new code) passes on the sonarcloud CI job (to confirm on the
    CI)

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

Testing notes

Automated (run in Docker, PHP 8.2)

  • OrderAddressDtoCreatorTest: 49 new cases. They cover the ticket's examples, non-strict, strict through the parameter, strict by default, the
    11 territories in both modes, a null or lowercase country, and shipping.
  • PaymentCaptureContextBuilderTest: checks that a foreign number is sent for billing and shipping.
  • PHPUnit full suite (1071 tests), PHPStan and ECS pass. Mutation checks made by the reviewer (no FR retry, FR first, strict toE164 or
    isMobile, default flipped to false, no strtoupper) each make at least one test fail.

@ilajili ilajili self-assigned this Oct 27, 2026

@jhoaraupp jhoaraupp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary: approve once the CHANGELOG is fixed. Nothing here can lose or duplicate money.

Checks run locally (PR head on a scratch worktree, UPC 1.2.2 loaded ahead of a local 1.2.1 vendor):

  • PHPUnit: full suite OK, 1071 tests. OrderAddressDtoCreatorTest and PaymentCaptureContextBuilderTest: 60 tests OK.
  • The same tests/PHPUnit/Upc run against UPC 1.2.1 gives 28 errors and 9 failures, so the ^1.2.2 bump is required.
  • ECS: clean on both changed files.
  • PHPStan: not conclusive locally (static reflection still reads the 1.2.1 vendor); to confirm on CI.
  • Called splitPhone directly on edge cases: 0612345678 on MQ gives +33612345678, a BL 0690… mobile is kept in strict mode thanks to the GP retry, +33… on DE is kept in non-strict mode and dropped in strict mode, and 0639… on YT gives +262…. All as expected.
  • Checked: PaymentCaptureContextBuilder is only used by the Hosted Fields new-card and saved-card handlers, so the scope described is right and Oney is not affected.

PR title and description (no file to anchor this on):

  • The title is cut off: "…to match the address countr…".
  • The description has leftover pasted text: "A gen 1 new message (ctrl+End) ↓ territory (0696… on MQ → +596…)".
  • Typo carried over from the template: "ffeature".

Not for this PR, but worth knowing: a national-format number is still read in the address country. For example 0612345678 on a DE address becomes +49612345678, a valid German landline. This was already the case before the PR.

Worth keeping: the same region is used for toE164 and isMobile, an unusable number gives [null, null] and never '', the named argument reads clearly at the call site, and the tests cover all 11 territories in both modes.

Comment thread CHANGELOG.md Outdated
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.4.0] - Unreleased

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Add these entries under the existing ## [2.0.0] - Unreleased section instead of a new 2.4.0 one.

On develop the convention is to add to the 2.0.0 - Unreleased section (#333, #335 and #337 all did). This creates a second "Unreleased" heading with a new version number.

The ### Fixed entry also describes a bug in Hosted Fields, which merchants have never had: it is still listed under 2.0.0 / Added. Merchant release notes built from this would announce a fix for something that was never released.

Suggested fix: drop this section, add the UPC requirement under a ### Changed heading in 2.0.0, and either drop the Fixed line or fold it into the existing Hosted Fields entry:

-## [2.4.0] - Unreleased
-
-### Changed
-- Requires `payplug/unified-plugin-core` 1.2.2 or later
-
-### Fixed
-- Hosted Fields payments, ...
-
 ## [2.0.0] - Unreleased
+
+### Changed
+- Requires `payplug/unified-plugin-core` 1.2.2 or later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGE done

* @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
*/
public function createBilling(OrderInterface $order, bool $requireRegionMatch = true): ?BillingDto

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] The default of requireRegionMatch is the opposite of what the only caller uses.

The only caller in prod (PaymentCaptureContextBuilder) passes false. A future caller that forgets the argument gets strict mode, which drops the phone without any error: exactly the bug this PR fixes.

Either option is fine:

  • Keep true for backward compatibility, and add a line to the docblock saying that the Hosted Fields capture path uses false.
  • Or, if Oney stays out of this path (pending the ticket author), drop the parameter and always read in non-strict mode. That also removes about half of the new test cases.

Same for createShipping() at line 61.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, défaut conservé à true (compatibilité), docblock précisant que le chemin Hosted Fields passe false. Pas de suppression du paramètre : le ticket PRE-3761 demande Oney en strict, ce que le paramètre permet.

$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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The ticket's acceptance criterion "Oney: a foreign number is rejected" is still not covered.

The description already says so. I confirmed that this builder is only used by CaptureHostedPaymentRequestHandler and CaptureAliasPaymentRequestHandler, so Oney never goes through it. Let's leave the ticket open until the ticket author has decided on the Oney path (and so on whether the strict mode is worth keeping, see the comment on OrderAddressDtoCreator).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vu ce matin au daily avec le PM, le critère reste ouvert. Oney n'est pas strict aujourd'hui et on garde cet état

@ilajili
ilajili force-pushed the feature/PRE-3761_relax-phone-region-match branch from fc22bb2 to 64afcc3 Compare October 29, 2026 14:48
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.
@ilajili
ilajili force-pushed the feature/PRE-3761_relax-phone-region-match branch from 64afcc3 to 59fd320 Compare October 29, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants