Skip to content

Incorrect contact creation when a susbcribed customer changes their email #646

@digitalpianism

Description

@digitalpianism

Replicated on 4.29.0

When a customer changes their email address Magento fires two events that both feed into Dotdigital via separate, independent message queues:
Flow 1 — customer_save_after event:
The CreateUpdateContact observer detects the email change and publishes a message to the ddg.email_update.queue topic

Flow 2 — newsletter_subscriber_save_after event:
When Magento core updates the subscriber record with the new email, the ChangeContactSubscription observer publishes a message to the ddg.subscription.queue topic

This causes a new contact creation and a failure in the old email contact update with the following log entries:

dotdigital.INFO: Newsletter subscribe success {"email":"newemail@address.com"} []
dotdigital.ERROR: Contact email update error: {"emailBefore":"oldemail@address.com","emailAfter":"oldemail@address.com","exception":"[object] (Dotdigital\\Exception\\ResponseValidationException(code: 500): cont...

To fix that I had to implement a custom plugin in a custom module with the following:

di.xml


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Dotdigitalgroup\Email\Observer\Newsletter\ChangeContactSubscription">
        <plugin name="vendor_dotdigitalemail_skip_subscriptions_after_email_change"
                type="Vendor\DotdigitalEmail\Plugin\Observer\Newsletter\ChangeContactSubscriptionPlugin"
                sortOrder="10" />
    </type>
</config>

ChangeContactSubscriptionPlugin.php

<?php
declare(strict_types=1);

namespace Vendor\DotdigitalEmail\Plugin\Observer\Newsletter;

use Dotdigitalgroup\Email\Logger\Logger;
use Dotdigitalgroup\Email\Observer\Newsletter\ChangeContactSubscription;
use Magento\Framework\Event\Observer;

/**
 * Skip Dotdigital subscription queue publish when a customer email change is in progress.
 *
 * When a customer changes their email address, two independent Dotdigital queue messages
 * are published from separate observers:
 *   1. ddg.contact.email_update — renames the existing contact (old email → new email)
 *   2. ddg.newsletter.subscription — syncs the subscriber with the new email
 *
 * Because these messages are consumed by separate queue consumers with no ordering
 * guarantee, the subscription consumer can run first and create a new contact with the
 * new email. When the email-update consumer then tries to rename the old contact, it
 * fails with a duplicate / validation error, leaving the old contact orphaned.
 *
 * This plugin detects the email-change scenario by comparing the subscriber's original
 * email (before save) with the current email. If they differ, it skips the observer
 * entirely, allowing the email-update consumer to handle the rename without conflict.
 * The subscriber data will be synced during the next scheduled batch sync.
 */
class ChangeContactSubscriptionPlugin
{
    /**
     * @param Logger $logger
     */
    public function __construct(
        protected Logger $logger
    ) {
    }

    /**
     * Conditionally skip the observer when a customer email change is detected.
     *
     * @param ChangeContactSubscription $subject
     * @param callable $proceed
     * @param Observer $observer
     * @return ChangeContactSubscription
     */
    public function aroundExecute(
        ChangeContactSubscription $subject,
        callable $proceed,
        Observer $observer
    ) {
        $subscriber = $observer->getEvent()->getSubscriber();
        $origEmail = $subscriber->getOrigData('subscriber_email');
        $currentEmail = $subscriber->getEmail();

        if ($origEmail && $origEmail !== $currentEmail) {
            $this->logger->info(
                'Vendor_DotdigitalEmail: Skipping ChangeContactSubscription observer — '
                . 'email change in progress, deferring to email-update consumer',
                [
                    'emailBefore' => $origEmail,
                    'emailAfter' => $currentEmail,
                ]
            );

            return $subject;
        }

        return $proceed($observer);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions