Problem/Motivation
When a customer selects a stored payment method of an off-site gateway (SupportsStoredPaymentMethodsInterface, but not SupportsCreatingPaymentMethodsInterface), PaymentInformation::submitPaneForm() assigns the payment method's billing profile entity directly to the order:
if ($payment_method_profile = $payment_method->getBillingProfile()) {
$this->order->setBillingProfile($payment_method_profile);
}
As a result, the payment method and every order paid with it reference the same profile entity. This has several side effects:
- Editing the billing address of one order (for example, on the order edit form) also changes the billing address of the payment method and of all other orders paid with it.
- Updating the payment method (for example, when a gateway syncs the billing address from the remote payment profile) overwrites the billing address of all past orders.
- The payment method's billing address in Drupal no longer matches the address stored at the gateway, because nothing sends the order edit to the gateway.
The on-site branch (SupportsCreatingPaymentMethodsInterface) right above already avoids this. It copies the payment method's profile into the order's own billing profile with populateFromProfile(). This was done in #3281185: Payment method profile overwrites order billing profile. The off-site branch should do the same.
Steps to reproduce
- Configure an off-site gateway that supports stored payment methods (for example, Accept Hosted from commerce_authnet).
- As an authenticated customer, place order A and save the card.
- Place order B and select the saved card on the Payment information pane.
- Compare the
billing_profiletarget ID of order A, order B and the payment method: they are all the same profile. - Edit the billing address of order B in the admin UI. The billing address of order A and of the payment method changes too.
Proposed resolution
In the off-site stored payment method branch, copy the payment method's profile into the order's own billing profile, the same way the on-site branch does. Create a new profile when the order has none, or when the order's current profile is the payment method's profile (orders that already share it).
if ($payment_method_profile = $payment_method->getBillingProfile()) {
$billing_profile = $this->order->getBillingProfile();
if (!$billing_profile || $billing_profile->id() == $payment_method_profile->id()) {
$billing_profile = $this->entityTypeManager->getStorage('profile')->create([
'type' => $payment_method_profile->bundle(),
'uid' => 0,
]);
}
$billing_profile->populateFromProfile($payment_method_profile);
// The data field is not copied by default but needs to be.
// For example, both profiles need to have an address_book_profile_id.
$billing_profile->populateFromProfile($payment_method_profile, ['data']);
$billing_profile->save();
$this->order->setBillingProfile($billing_profile);
}
Remaining tasks
- Create a merge request.
- Add test coverage: after checkout with a stored off-site payment method, the order's billing profile ID differs from the payment method's, and the address values match.
- Decide whether an update hook should separate existing orders that share a profile with a payment method, or whether that is left to sites.
User interface changes
None.
API changes
None.
Data model changes
None. New orders get their own billing profile entity instead of sharing the payment method's.
Issue fork commerce-3631059
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
vmarchukComment #6
jsacksick commented