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

  1. Configure an off-site gateway that supports stored payment methods (for example, Accept Hosted from commerce_authnet).
  2. As an authenticated customer, place order A and save the card.
  3. Place order B and select the saved card on the Payment information pane.
  4. Compare the billing_profile target ID of order A, order B and the payment method: they are all the same profile.
  5. 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

Command icon 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

vmarchuk created an issue. See original summary.

vmarchuk’s picture

Assigned: vmarchuk » Unassigned
Status: Active » Needs review

jsacksick made their first commit to this issue’s fork.

  • jsacksick committed d90a25eb on 3.x authored by vmarchuk
    fix: #3631059 Order reuses the stored payment method's billing profile...
jsacksick’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.