Hace un tiempo me tocó encarar un proceso que originalmente debía ser un upgrade, pero al final se convirtió en un cambio de plataforma. Si bien se estaba en Magento y hoy se sigue estando en Magento, el paso fue de la versión 2.3 a la actual 2.4.
Esta opción se eligió porque fue más fácil deshacerse así de todos los vicios y problemas existentes, pero tuvimos que plantear qué cosas se perdían con este enfoque y cómo se iba a lidiar con eso.
Cuando tocó analizar cómo sería la migración de clientes/usuarios, mi primer planteo fue que haría todo lo posible para no usar el Data Migration Tool.
Eso llevó a que el pedido mutara por algo parecido a: «¿No podemos hacer alguna magia para que los usuarios se migren solos?» (no es 100% literal, pero iba por ahí).
Luego de validar algunas ideas y los elementos disponibles, la idea fue hacer que en la nueva instancia, cada vez que un usuario se logueara (o intentara loguearse) se haría una validación local y cuando no existiera el usuario en el sitio nuevo, se haría una petición a la tienda vieja (remota) y se validaría que el usuario existiera allí. Si el usuario no existe en la tienda vieja, no pasa nada y se devuelve el mensaje de rutina (usuario no existe, registrese, etc).
El truco aquí se da cuando el usuario no existe en la instancia nueva pero sí existe en la instancia vieja. Si existe, en ese momento obtengo todos los datos el customer de la instancia remota, y antes de completar la validación del login, creo al usuario en la instancia nueva con los datos de la instancia vieja.
Ahora si, permito que el flujo continúe y logueo al usuario. Es algo como esto:

En el gráfico puede verse que el truco o la trampa está en usar el endpoint /customers/me
.
Al usar ese endpoint, previa autenticación exitosa, el response son todos los datos del usuario, incluyendo direcciones (y todo en un único response).
¿Qué pasa la próxima vez que ese usuario vuelva a loguearse en la tienda nueva?. Como ya existe, no hay validaciones extra y todo funciona como de costumbre en la tienda nueva.
¿Cómo sería, más o menos, la implementación de código para resolver este tema? (sobre esto volvemos al final del post)
Lo primero es crear un plugin que va a actuar antes del …
<type name="Magento\Customer\Model\AccountManagement\Authenticate">
<plugin name="barbanet_customer_migration_authenticate" type="Barbanet\CustomerMigration\Plugin\MigrateCustomer" sortOrder="1" disabled="false" />
</type>
Y el plugin es (o ha de ser) algo más o menos así:
<?php
declare(strict_types=1);
namespace Barbanet\CustomerMigration\Plugin;
use Barbanet\CustomerMigration\Api\Data\ConfigInterface;
use Barbanet\CustomerMigration\Service\CreateCustomer;
use Barbanet\CustomerMigration\Service\GetCustomer;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Model\AccountManagement\Authenticate;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Psr\Log\LoggerInterface;
class Migrate
{
public function __construct(
private readonly ConfigInterface $config,
private CreateCustomer $createCustomer,
private readonly CustomerRepositoryInterface $customerRepository,
private readonly GetCustomer $sourceDataService,
private readonly LoggerInterface $logger
) { }
public function beforeExecute(
Authenticate $subject,
string $email,
string $password
) {
if ($this->config->isEnabled()) {
try {
$customer = $this->customerRepository->get($email);
// El usuario existe en la instancia nueva, no hay que hacer nada
} catch (NoSuchEntityException | LocalizedException $exception) {
// El email no existente en la base local, vamos a ver si existe en la instancia remota
// Voy a enviarle los datos ingresados en el formulario a mi service class que hará la petición la instancia vieja de la tienda
$data = $this->sourceDataService->readData(
$email,
$password
);
// Si mi service class devuelve datos, quiere decir que tengo información para crear el nuevo Customer en la instancia nueva
if (!empty($data)) {
$this->createCustomer->createCustomer(
\json_decode($data, true), // No usen json_decode, siempre es mejor usar el serializer de Magento
$password
);
}
}
}
}
}
Lo primero es entrar en la validación sólo si el módulo está habilitado (luego veremos cómo es la configuración).
Una vez dentro del try
, intentamos leer el Customer en según el email ingresado en el formulario de login. Si el usuario existe, todo sigue el flujo natural. Si el usuario no existe, el repositorio nos arrojará una excepción.
Aquí es donde aplicamos nuestra nueva lógica, en el catch. Usamos objeto instanciado en $this->sourceDataService
y le pasamos al método readData
las variables/parámetros $email
y $password
que ingresó el usuario en el login.
Ese método lo que hará es intentar loguearse en la instancia vieja usando el endpoint /rest/V1/integration/customer/token/
, el cual devolverá una excepción si los datos son incorrectos o devolverá un token válido.
Si devuelve un token, entonces se hace, ahora si, una segunda llamada a /rest/V1/customers/me
, lo que devolverá algo como esto (el response es del sample data).
{
"id": 1,
"group_id": 1,
"default_billing": "1",
"default_shipping": "1",
"created_at": "2025-06-06 04:19:54",
"updated_at": "2025-06-18 06:45:30",
"created_in": "Default Store View",
"dob": "1973-12-15",
"email": "roni_cost@example.com",
"firstname": "Veronica",
"lastname": "Costello",
"gender": 2,
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 1,
"customer_id": 1,
"region": {
"region_code": "MI",
"region": "Michigan",
"region_id": 33
},
"region_id": 33,
"country_id": "US",
"street": [
"6146 Honey Bluff Parkway"
],
"telephone": "(555) 229-3326",
"postcode": "49628-7978",
"city": "Calder",
"firstname": "Veronica",
"lastname": "Costello",
"default_shipping": true,
"default_billing": true
}
],
"disable_auto_group_change": 0,
"extension_attributes": {
"is_subscribed": false
}
}
Volvamos ahora al plugin, en donde ahora validamos si la variable $data
que contiene datos. Si hay datos, se los enviamos a un segundo objeto, $this->createCustomer
, que usé como wrapper para crear un nuevo usuario a través del método createCustomer
que recibirá dos parámetros: el array con los datos obtenidos del endpoint remoto y la password (aún plana) que fue ingresada en el formulario.
Dentro de ese método se mapearán los datos del customer, se mapearán las direcciones y se hasheará la password para guardar al nuevo cliente en la nueva base de datos.
Ahora si, el flujo sigue su curso y tratará de loguear al usuario que ahora sí existe en esta nueva instancia, por lo que el login será exitoso.
Antes de avanzar con algunos detalles y conclusiones, ¿cómo se configura este módulo en la tienda?

Si se lo deshabilita, el plugin no hará nada.
Vamos ahora con algunas notas y conclusiones:
- Mantener las dos tiendas funcionando puede ser un costo extra que no se quiera tener, pero como la operación es bastante rápida y no se hace siempre. Una vez agotado el tiempo planeado para esta migración automática, sólo es necesario apagar el módulo y apagar la instancia vieja (y no se necesita deploy).
- Puede parecer contraintuitivo que no se migren todos los usuarios. En este caso se usó el proceso para realizar una especie de saneado de la lista de Customers. Pero al no haber perdido los datos, luego se podrá proceder a un cruce de datos para ver qué hacer con esa posible diferencia de datos.
- No se migraron los Pedidos, por lo que un usuario frecuente entró un día y tenía sus pedidos y al día siguiente volvió a ingresar y no tenía más Pedidos. Aquí siempre suelen haber estrategias diferentes, por lo que no lo consideré un problema de este proceso.
- Este desarrollo no fue costoso en tiempo. No pude encontrar ahora el worklog de esa tarea, pero fue muchísimo (pero muchísimo) menor al proceso de migración usando el toolkit.
Con respecto al fuente completo, dado que había algunas particularidades en el mapeo del Customer (y además sospecho que la mayoría de las instancias han de tener diferentes atributos para esa entidad), decidí no publicarlo. Pero si algo de esto resulta útil para alguien más, a pesar que creo que con lo explicado en el plugin podría ser suficiente, no tienen más que contactarme para que lo veamos.