RFC 009 — Reingeniería de la sincronización con Medusa¶
Estado: Borrador para discusión del equipo
Fecha: 2026-09-11
Autores: TI Compulandia + Claude
Relacionado con: línea base as-is (léase primero),
ADR-0002, ADR-0003, RFC 004 (compuestos), incidente 2026-06, CatalogPort
(app/Services/Medusa/CatalogPort.php)
0. Cómo leer este documento¶
Sigue el motor del NASA Systems Engineering Handbook (SP-2016-6105 Rev. 2) en su orden natural: primero qué se espera (§1–§2), después qué debe cumplir (§3), qué funciones tiene (§4), qué alternativas hay y cuál se elige (§5), cómo se diseña (§6–§7), qué riesgos trae (§8), cómo se construye y transiciona (§9) y cómo se verifica que funciona (§10). Cada sección abre con una nota corta del proceso del handbook que aplica. Las decisiones que el equipo tiene que tomar están juntas en §11.
Regla del documento: cada número del "después" tiene su par en el "antes" de la línea base, para poder medir si la reingeniería valió la pena.
1. Problema y expectativas de los stakeholders¶
Handbook, proceso 1 (Stakeholder Expectations Definition): necesidades → metas → objetivos, y las medidas de efectividad (MOE) con las que el stakeholder va a juzgar el resultado.
1.1 Por qué rehacer y no parchar¶
El módulo actual (6 000 líneas, 64 commits, oct-2025 a jul-2026) se construyó de forma incremental, con asistencia de IA sin acceso al repositorio completo y sin línea base de requisitos ni tests. El resultado, medido en la línea base, es un sistema que funciona por fuerza bruta: cada job re-asegura todo el estado remoto (≈ 26 llamadas HTTP para no cambiar nada), traga errores, no expone fallos a Horizon y acopla a todos los demás canales de salida a la latencia de Medusa. Parchar los síntomas (§5, alternativa A) reduce el costo pero no cambia la propiedad estructural que los genera: no hay una función "decidir qué cambió" (línea base §4).
1.2 Necesidades, metas y objetivos¶
| Stakeholder | Necesidad | Meta | Objetivo medible (MOE) |
|---|---|---|---|
| Comercial | Que la tienda refleje el catálogo | Cambios visibles en minutos, sin intervención | p95 evento→Medusa < 5 min en operación normal |
| TI operación | Saber qué falló y por qué | Cero fallos silenciosos | 100 % de fallos en failed_jobs o product_sync_logs, con causa clasificada por código |
| TI desarrollo | Poder cambiar el módulo sin miedo | Módulo testeable sin Medusa | Planificador con cobertura 100 % de ramas; contrato HTTP verificable con fake |
| Otros canales | No depender de Medusa para publicar | Cero HTTP a Medusa en jobs de Woo/TN/Conti/UMarket/PCM | 0 llamadas (hoy: 2 por producto) |
| Infra | Que Medusa no se sature | Mínimas llamadas por cambio; batch en masivos | ≤ 2 llamadas por cambio de precio+stock (hoy 26); ≥ 10× throughput masivo |
1.3 Fuera de alcance¶
- Cambiar el modelo canónico del Integrador (
products,product_items, atributos). - Rehacer los demás canales de salida (sólo se les quita la dependencia de Medusa).
- Migrar la versión de Medusa. Se asume v2 ≥ 2.11 (línea base §6).
- Sincronizar pedidos o clientes desde Medusa.
2. Concepto de operaciones (to-be)¶
Handbook: el ConOps describe el sistema en uso, escenario por escenario. Los mismos escenarios de la línea base §2, con el comportamiento nuevo.
2.1 Cambio en un ítem (camino principal)¶
- Alguien modifica un
ProductItem(o el workflow de proveedor, o la edición masiva). - El observer traduce las columnas cambiadas a aspectos (no a "tipos de sync"):
product,options,variant,price,stock,images,category,channel. Marca esos aspectos como sucios enmedusa_sync_statey despacha un job por ítem. Si ya hay un job en espera para ese ítem, sólo se suman los aspectos sucios (coalescencia natural). - El job lee los aspectos sucios, construye el estado deseado de cada uno desde la base local, calcula su hash y lo compara con el hash del último envío exitoso. Sólo los que difieren generan operaciones.
- Ejecuta las operaciones con el mínimo de llamadas (una por aspecto, batch cuando
aplica), sin
GETprevios. Si Medusa responde con un conflicto estructurado (type: invalid_data,code: ...), entra el reconciliador, que sí consulta el estado remoto y repara el mapeo local (ids perdidos, variante en otro producto). - Registra en
medusa_sync_stateel hash enviado por aspecto y enproduct_sync_logsun resultado resumido. Emite una sola líneaJOB:DONEcon duración, aspectos y llamadas.
Diferencia de comportamiento visible para el operador: un cambio de descripción hace una llamada; un cambio de stock hace una; una alta completa hace unas seis. Un job que no tiene nada que enviar termina en milisegundos y lo dice.
2.2 Alta y baja¶
created y deleted pasan a estar observados. El alta marca todos los aspectos
sucios. La baja (soft delete) quita del sales channel y marca la variante para borrado
diferido; el borrado físico en Medusa es una operación explícita del reconciliador,
nunca un efecto lateral de otro job (elimina el riesgo RK-4 de la línea base).
2.3 Despublicación¶
publish=0 sí llega a Medusa y se traduce a "fuera del sales channel". active=0
se traduce a stock 0 y fuera del canal. La semántica exacta es una decisión abierta
(§11, D-4).
2.4 Ofertas (price lists)¶
Medusa sigue siendo la fuente de las price lists. La diferencia: el Integrador
mantiene un espejo local (medusa_price_list_prices) alimentado por un webhook
de Medusa al crear/editar/borrar price lists, más una reconciliación diaria por pull.
ProductPriceService lee el espejo. Ningún job de salida hace HTTP a Medusa. Las
ventanas de vigencia se agendan desde el espejo, no desde un daily() que puede
fallar.
2.5 Cambio de padre¶
Un aspecto más (parent), que pasa por el despachador como cualquier otro: los demás
canales se enteran. El reconciliador mueve la variante (borra en el viejo, crea en el
nuevo) como una operación atómica desde el punto de vista del estado local.
2.6 Corridas masivas¶
Los comandos medusa:* no ejecutan lógica propia: marcan aspectos sucios en lotes y
despachan MedusaBatchSyncJob (100 ítems por job) que usa los endpoints batch de v2.
Tienen --dry-run que imprime el plan (qué operaciones haría), y un circuit
breaker compartido con los jobs por evento: tras N errores de conexión seguidos, el
comando aborta y los jobs se liberan con backoff sin quemar intentos.
2.7 Categorías¶
Un job asincrónico con product_sync_logs propio, disparado por el observer de
Category, que envía nombre, handle, parent_category_id y is_active. El árbol
completo se puede reconstruir con un comando idempotente.
Este punto se adelantó como entregable independiente: ver análisis de sincronización de categorías y ADR-0006 (2026-09-15). Por eso la numeración de ADRs propuesta en §12 corre un lugar.
3. Requisitos técnicos¶
Handbook, proceso 2: cada requisito es una oración con "debe", verificable, con método asignado: I inspección, A análisis, D demostración, T test. Los IDs enlazan con la matriz de verificación de §10.
3.1 Funcionales¶
| ID | Requisito | Método |
|---|---|---|
| F-01 | El sistema debe detectar cambios en ProductItem (created, updated, deleted, restored), Product, SupplierProduct seleccionado, imágenes, componentes y categoría, y traducirlos a aspectos sucios |
T |
| F-02 | El sistema debe calcular el estado deseado de cada aspecto sólo desde la base local, sin llamadas a Medusa | T |
| F-03 | El sistema debe enviar únicamente los aspectos cuyo hash difiere del último envío exitoso | T |
| F-04 | El sistema debe coalescer múltiples eventos del mismo ítem en un solo job mientras el anterior no haya empezado | T |
| F-05 | El sistema debe reconciliar el mapeo local (ids de producto, variante, inventory item, categoría) ante conflictos estructurados de Medusa, sin borrar entidades ajenas | T, D |
| F-06 | El sistema debe soportar variantes simples, compuestas (kit de inventory items) y familias con opciones derivadas de atributos, con un modelo de opciones determinista (§11, D-1) | T |
| F-07 | El sistema debe propagar publish y active a Medusa con la semántica de §11, D-4 |
T |
| F-08 | El sistema debe mantener un espejo local de las price lists de Medusa, actualizado por webhook y reconciliado a diario | T, D |
| F-09 | Ningún job de otro canal de salida debe llamar a Medusa | I, T |
| F-10 | Las corridas masivas deben usar endpoints batch y admitir --dry-run con el plan |
D |
| F-11 | Toda operación fallida debe quedar en product_sync_logs con código de clasificación y, si agota reintentos, en failed_jobs |
T |
| F-12 | Las categorías deben sincronizarse en cola, con árbol, y nunca dentro del request HTTP | I, T |
3.2 Rendimiento (TPM objetivo)¶
| ID | TPM | As-is | Objetivo | Método |
|---|---|---|---|---|
| P-01 | Llamadas HTTP por job sin cambios efectivos | ≈ 26 | 0 | T |
| P-02 | Llamadas por cambio de precio + stock | ≈ 26 | ≤ 2 | T |
| P-03 | Llamadas por alta completa de variante simple con imagen | ≈ 30 | ≤ 6 | T |
| P-04 | Consultas DB por job (aparte de las del estado deseado) | ≈ 6 redundantes | 0 redundantes; forProduct() para la vista |
T, I |
| P-05 | Throughput masivo, 1 proceso, Medusa local | 5,6 ítems/s | ≥ 50 ítems/s (batch de 100) — a validar en TRR | D |
| P-06 | p95 de duración de job por evento | no medible | < 5 s | D |
| P-07 | Líneas de log por job feliz | 40–60 | ≤ 5 | I |
| P-08 | HTTP a Medusa desde otros canales | 2 por producto | 0 | I |
| P-09 | Tiempo de un job ante Medusa caído | hasta 25 s × N | < 1 s (circuit abierto) | T |
3.3 Interfaz, operación y calidad¶
| ID | Requisito | Método |
|---|---|---|
| I-01 | El cliente HTTP debe leer timeouts de config/medusa.php con las claves que ese archivo define, y .env-example debe documentar todas las variables MEDUSA_* |
I |
| I-02 | El cliente debe reintentar 429/5xx/conexión con backoff y respetar Retry-After; debe abrir un circuit breaker tras N fallos de conexión consecutivos |
T |
| I-03 | Los errores de Medusa deben clasificarse por type/code del cuerpo JSON, nunca por str_contains sobre texto |
I, T |
| O-01 | El job debe declarar $timeout=120, usar RetriesWithBackoff (ADR-0003) y relanzar excepciones no recuperables |
I |
| O-02 | Cada job debe emitir una línea JOB:DONE con duration_ms, http_calls, aspects, ops en formato [MEDUSA:{id}:{sku}] |
I |
| O-03 | Los logs deben rotar (daily, ≤ 14 días) en todos los canales del módulo |
I |
| Q-01 | El planificador debe ser una función pura testeable sin base ni red | T |
| Q-02 | Debe existir un FakeCatalogPort en memoria que emule las reglas de Medusa necesarias (SKU único, opciones consistentes) |
T |
| Q-03 | Ninguna clase del módulo debe superar 400 líneas ni ningún método 60 | I |
| Q-04 | Los tests no deben correrse en el servidor de desarrollo compartido (restricción del repo); se ejecutan en local o CI | I |
4. Descomposición funcional (to-be)¶
Handbook, proceso 3: la misma jerarquía de funciones de la línea base §4, con la función que faltaba y la reasignación a componentes nuevos.
F0 Propagar cambios del catálogo a Medusa
├── F1 Detectar el cambio y marcar aspectos sucios ......... ChangeDetector (observers + estrategias)
├── F2 Encolar con coalescencia y trazabilidad ............. SyncStateRepository + SyncToMedusaJob
├── F3 DECIDIR QUÉ CAMBIÓ (nueva) .......................... DesiredStateBuilder + SyncPlanner
│ ├── F3.1 Construir estado deseado por aspecto ............ DesiredStateBuilder (puro, sólo DB)
│ ├── F3.2 Comparar hashes y producir operaciones .......... SyncPlanner (puro)
│ └── F3.3 Ordenar operaciones por dependencia ............. SyncPlanner
├── F4 Ejecutar operaciones con mínimas llamadas ........... SyncExecutor + MedusaClient
├── F5 Reconciliar ante conflicto / deriva ................. Reconciler (único que hace GET)
├── F6 Mover variante entre padres ......................... Reconciler (operación explícita)
├── F7 Espejar price lists y agendar ventanas .............. PriceListMirror + webhook + reconciliación diaria
├── F8 Registrar resultado y exponer fallos ................ SyncStateRepository + product_sync_logs + failed_jobs
├── F9 Corridas masivas por lotes .......................... MedusaBatchSyncJob + comandos
└── F10 Observar (duración, llamadas, circuito) ............. SyncMetrics + JOB:DONE
5. Análisis de alternativas¶
Handbook, proceso 17 (Decision Analysis): alternativas, criterios ponderados, puntuación y recomendación. Escala 1 (peor) a 5 (mejor).
5.1 Alternativas¶
- A. Parche incremental. Corregir los defectos puntuales de la línea base §9
(bloque duplicado, caché, claves de config,
$timeout, doble categoría, thumbnails,stock-locationspor variante). Misma arquitectura. - B. Reescritura del módulo en el Integrador. Estado local por aspecto, planificador puro, ejecutor con batch, reconciliador separado, espejo de price lists. Es lo que describe §6.
- C. Lógica de diff en Medusa. Un endpoint custom en el backend de Medusa
(
POST /admin/custom/catalog/upsert) que recibe el payload canónico del Integrador y hace el diff y las escrituras en un workflow de Medusa, en una sola ida y vuelta. El Integrador queda como emisor "tonto". - D. Pull desde Medusa. Medusa consulta al Integrador. Descartada en el análisis inicial: el catálogo de Medusa vive en su propia base y esto requiere un plugin que reimplementa todo lo anterior del lado de Medusa, con menos herramientas de cola y observabilidad que Laravel.
5.2 Criterios y puntuación¶
| Criterio (peso) | A Parche | B Reescritura | C Diff en Medusa |
|---|---|---|---|
| Reducción de llamadas por job (5) | 3 (≈ 26 → ≈ 12) | 5 (→ 0–2) | 5 (→ 1) |
| Elimina el acople de precio de otros canales (5) | 1 | 5 | 2 (sigue habiendo HTTP salvo que se agregue el espejo igual) |
| Testabilidad sin Medusa (4) | 1 | 5 | 2 (tests en TS, otro repo) |
| Riesgo de transición (4) | 5 | 3 (mitigable con modo sombra, §9) | 2 (dos repos, dos despliegues acoplados) |
| Esfuerzo (3) | 5 | 2 | 2 |
| Fallos visibles y clasificados (4) | 2 | 5 | 4 |
| Mantiene el conocimiento en un solo repo/lenguaje (2) | 5 | 5 | 1 |
| Total ponderado (máx. 135) | 77 | 117 | 73 |
5.3 Recomendación¶
B, con dos préstamos de C para más adelante, si la verificación de la fase 2 los
justifica: (1) un endpoint custom en Medusa para crear producto + opciones + variante +
inventory item en una transacción, si los endpoints batch de v2 no alcanzan para P-03;
(2) el suscriptor de eventos en Medusa que alimenta el webhook de price lists, que es
código mínimo del lado de Medusa y ya hay precedente (/admin/custom/price-lists/current).
Los ítems de A que son correcciones de configuración puras (claves de timeout,
$timeout, .env-example, rotación de logs, JOB:DONE) se hacen primero, en la
fase 0, porque cuestan horas, no cambian la arquitectura y permiten medir la línea
base con precisión antes de tocarla.
6. Solución de diseño¶
Handbook, proceso 4 (Design Solution Definition): componentes, responsabilidades, datos y comportamiento. Todo lo de esta sección vive en
app/Services/Medusa/salvo que se indique.
6.1 Componentes¶
| Componente | Responsabilidad | Regla de oro |
|---|---|---|
ChangeDetector |
Traduce eventos de modelo a aspectos sucios | No decide nada más; no toca Medusa |
SyncStateRepository |
Lee/escribe medusa_sync_state (aspectos sucios, hashes, ids remotos) |
Única puerta a la tabla |
DesiredStateBuilder |
Construye el DesiredState de un ítem por aspecto desde la base |
Sólo lecturas locales; usa ProductItemView::forProduct() |
SyncPlanner |
plan(DesiredState, SyncState): SyncPlan |
Función pura; sin IO |
SyncExecutor |
Ejecuta el plan con CatalogPort; actualiza hashes por operación exitosa |
Nunca hace GET "por las dudas" |
Reconciler |
Ante conflicto o deriva, consulta Medusa y repara el mapeo local | Único que hace GET; nunca borra sin plan explícito |
MedusaClient |
Guzzle con base_uri, retry middleware, circuit breaker, errores tipados |
Errores por type/code |
CatalogPort / MedusaCatalogAdapter |
Contrato completo (productos, variantes, inventario, categorías, imágenes, canal, batch) | FakeCatalogPort lo implementa entero |
PriceListMirror |
Recibe webhook y pull diario; escribe medusa_price_list_prices; agenda ventanas |
ProductPriceService lee de acá |
SyncToMedusaJob |
Un ítem: lee sucios → build → plan → execute → registrar | $timeout=120, RetriesWithBackoff |
MedusaBatchSyncJob |
Hasta 100 ítems con endpoints batch | Mismo planner, otro executor |
SyncMetrics |
JOB:DONE, contadores, estado del circuito |
Una línea por job |
6.2 Datos nuevos¶
medusa_sync_state (una fila por product_item_id):
| Columna | Tipo | Uso |
|---|---|---|
product_item_id |
FK, PK | |
dirty_aspects |
set/JSON | Aspectos pendientes; el observer agrega, el job vacía |
hash_product, hash_options, hash_variant, hash_price, hash_stock, hash_images, hash_category, hash_channel |
char(64) nullable | Hash del último envío exitoso por aspecto |
remote_inventory_item_id |
varchar nullable | Hoy se busca por q=sku en cada job |
last_plan_at, last_success_at, last_error_code |
timestamps / varchar | Observabilidad |
Los ids id_medusa_product, id_medusa_variant y medusa_handle se quedan donde
están (los consume Algolia y la vista; ADR-0002 obliga a mantener la vista).
medusa_price_list_prices (espejo): price_list_id, variant_id, amount,
currency_code, starts_at, ends_at, synced_at. Índice por variant_id.
product_sync_logs no cambia de forma. Deja de guardar payload_sent y
response_body completos en éxitos (sólo en fallos), decisión D-5.
6.3 Aspectos y hash¶
El estado deseado de cada aspecto es un array normalizado (claves ordenadas,
valores canónicos) y su hash es sha256(json_encode(...)). Ejemplos:
price:{amount: 1250000, currency_code: 'pyg'}stock:{location_id, quantity, manage_inventory, allow_backorder, kit: [{sku, qty}]}images:[{cf_image_id, rank}]másthumbnail_cf_image_idoptions:{titles: [...], values: {title: value}}de toda la familia, calculado una vez por producto y compartido por los hermanos (evita el "reseteo total" de hoy)
Un aspecto se envía sólo si hash(desired) != stored_hash. Después de un éxito se
guarda el nuevo hash. Si Medusa devuelve conflicto, el hash no se guarda y se
delega al reconciliador. Esto hace al job idempotente por construcción y re-ejecutable
sin costo.
6.4 Plan y orden de operaciones¶
SyncPlan es una lista ordenada de SyncOperation {aspect, kind, payload} con el
orden fijo product → options → variant → inventory → price → images → category → channel.
El planner es una función pura: dado el mismo DesiredState y SyncState produce el
mismo plan; por eso --dry-run puede imprimirlo y los tests lo cubren sin IO.
6.5 Modelo de opciones determinista¶
Regla propuesta (decisión D-1): los títulos de opción son los nombres de atributo
presentes en cualquier hermano de la familia; el valor de un ítem para un título que
no tiene es la cadena — (no Default N); si dos ítems quedan con la misma
combinación, la familia agrega una opción Variante cuyo valor es el SKU. Nunca hay
"bump" ni reseteo: el estado deseado de las opciones es una función determinista de
la familia. La migración de familias ya existentes en Medusa se hace con el
reconciliador en la fase 4.
6.6 Cliente HTTP y circuit breaker¶
base_uri, keep-alive,timeout/connect_timeoutdesde las claves correctas (fase 0). Recomendación inicial: 10 s / 3 s.- Middleware de retry: 429, 502, 503, 504 y
ConnectException, conRetry-After. - Circuit breaker en
Cache(medusa:circuit): tras 5 errores de conexión en 60 s se abre 120 s; con el circuito abierto el job hacerelease()con el backoff del ADR-0003 sin consumir intento, y los comandos masivos abortan con mensaje claro. - Excepciones tipadas:
MedusaConnectionException,MedusaRateLimited,MedusaConflict {code},MedusaNotFound,MedusaServerError. El job relanza las no recuperables: Horizon las ve.
6.7 Espejo de price lists¶
- Endpoint
POST /api/medusa/webhooks/price-listsconStaticTokenAuth(patrón ya existente para syncs externos). Payload:price_list_id,event. El Integrador responde 202 y encolaRefreshPriceListJob, que hace elGET /admin/price-lists/{id}y escribe el espejo. - Suscriptor en Medusa (
price-list.created|updated|deleted) que llama al webhook. Es el único código nuevo del lado de Medusa. medusa:reconcile-price-listsdiario como red de seguridad (pull completo).- Las ventanas (
starts_at/ends_at) se agendan desde el espejo conDispatchProductItemSyncAt, ahora conWithoutOverlappingporprice_list_id, no por concatenación de ids. ProductPriceService::getMedusaPriceInfopasa a leer el espejo. Se eliminaMedusaPriceListServicey el Guzzle propio.
6.8 Observabilidad¶
JOB:DONEpor job:[MEDUSA:{id}:{sku}] done {duration_ms, http_calls, aspects, ops, skipped};JOB:SKIPcuando no hay nada que enviar.- Contadores en Redis (por hora): jobs, llamadas, fallos por código, circuito abierto. Suficiente para un panel simple; Prometheus queda como opción posterior.
- Canal
MEDUSAcon rotación diaria; el canalMEDUSA_PRICELISTdesaparece con el servicio.
7. Interfaces (to-be)¶
Handbook, proceso 12: cada frontera, con dirección y contrato.
| Interfaz | Dirección | Contrato |
|---|---|---|
| Admin API v2 | Integrador → Medusa | Los mismos recursos de la línea base §6.1 más POST /admin/products/batch, POST /admin/products/{id}/variants/batch, POST /admin/inventory-items/location-levels/batch |
| Webhook price lists | Medusa → Integrador | POST /api/medusa/webhooks/price-lists, token estático, 202 + job |
| (Opcional, fase 4) upsert atómico | Integrador → Medusa | POST /admin/custom/catalog/upsert si P-03 no se cumple con batch |
medusa_sync_state |
Integrador interno | Sólo vía SyncStateRepository |
medusa_price_list_prices |
Integrador interno | Leído por ProductPriceService; escrito por PriceListMirror |
Cola medusa_products |
Horizon | $timeout=120, retry_after=150, uniqueId = product_item_id |
| Algolia | Integrador interno | Sin cambio: medusa_handle + id_medusa_variant; el reindex sigue siendo el último paso |
| Cloudflare Images | Sólo URL | Un único constructor de URL (CloudflareImagesService::url(), corregida su config), consumido por todos |
Se elimina la interfaz Integrador → Store API (x-publishable-api-key) desde jobs de
salida.
8. Riesgos de la reingeniería¶
Handbook, proceso 13: riesgos del proyecto de cambio, no del sistema actual (esos están en la línea base §10). P y C en escala 1–5.
| ID | Riesgo | P | C | Mitigación |
|---|---|---|---|---|
| RR-1 | El estado deseado calculado localmente diverge del estado real en Medusa (deriva histórica: 53 % de ítems sin variante en dev) | 4 | 3 | Fase 2 en modo sombra; reconciliador con comando medusa:audit que compara hashes contra Medusa por muestreo |
| RR-2 | El modelo de opciones determinista rompe familias ya publicadas | 3 | 4 | Decisión D-1 antes de codificar; migración por familia con --dry-run; se mantiene el modelo legacy detrás de flag hasta migrar |
| RR-3 | Los endpoints batch de v2 no cubren opciones + variante + inventario en una transacción | 3 | 3 | Préstamo de C (endpoint custom) como plan B, decidido en la revisión de fase 2 |
| RR-4 | El equipo no puede validar con tests en el servidor compartido | 5 | 2 | Tests en local/CI; verificación en dev por --dry-run y medusa:audit (lectura) |
| RR-5 | El webhook exige tocar el repo de Medusa y coordinar despliegues | 3 | 2 | Pull diario funciona sin el webhook; el webhook sólo reduce latencia |
| RR-6 | Se rehace el módulo y se repiten los defectos por falta de línea base | 2 | 5 | Este RFC + matriz §10 + revisiones de fase con criterios de salida |
| RR-7 | Esfuerzo mayor al previsto y convivencia larga de dos motores | 3 | 3 | Corte por aspecto (§9), no "big bang"; el legacy sigue detrás de MEDUSA_SYNC_ENGINE=legacy |
9. Plan de implementación y transición¶
Handbook, procesos 5–9 y 10 (Technical Planning): fases con criterio de salida revisable, tomado de las revisiones estándar del ciclo de vida (SRR, PDR, CDR, TRR, ORR). Ninguna fase empieza sin cerrar la anterior. Los tiempos son estimaciones para discutir, no compromisos.
Fase 0 — Higiene y medición (≈ 1 semana)¶
Correcciones que no cambian la arquitectura y que hacen medible la línea base:
claves de timeout (I-01), $timeout + RetriesWithBackoff en el job (O-01),
JOB:DONE en el job actual (O-02), rotación de logs (O-03), .env-example, quitar
el GET stock-locations por variante y el doble ensureAndAttachCategory. No se
tocan las heurísticas de opciones.
Criterio de salida (SRR — revisión de requisitos): el equipo aprueba §3 con las
decisiones de §11 tomadas; hay una semana de JOB:DONE en producción para fijar
P-06 real.
Fase 1 — Diseño detallado (≈ 1 semana)¶
ADRs de §12, migración de medusa_sync_state y medusa_price_list_prices en el
subdirectorio numerado que corresponda, CatalogPort completo, FakeCatalogPort,
firma del SyncPlanner, formato canónico de cada aspecto (§6.3).
Criterio de salida (PDR): revisión de diseño con functional-analyst y
database-analyst; la interfaz del planner está congelada.
Fase 2 — Núcleo en modo sombra (≈ 3 semanas)¶
ChangeDetector, SyncStateRepository, DesiredStateBuilder, SyncPlanner,
SyncExecutor, MedusaClient nuevo con circuit breaker. Detrás de
MEDUSA_SYNC_ENGINE=shadow: el job legacy sigue escribiendo en Medusa y el motor nuevo
sólo calcula y loguea el plan que habría ejecutado. Un comando compara, por
muestreo, el plan con lo que el legacy efectivamente cambió.
Criterio de salida (CDR): tests del planner con 100 % de ramas; ≥ 95 % de
coincidencia plan vs legacy en una semana de sombra; el 5 % restante explicado.
Fase 3 — Espejo de price lists (≈ 1–2 semanas, en paralelo con la 2)¶
PriceListMirror, pull diario, webhook, ProductPriceService leyendo el espejo,
retiro de MedusaPriceListService. Es la fase de mayor impacto sobre el resto del
sistema (RK-1 de la línea base).
Criterio de salida: P-08 = 0 verificado por inspección; los jobs de Woo/TN/Conti
no muestran HTTP a Medusa en logs durante 48 h.
Fase 4 — Corte por aspecto y masivos (≈ 2–3 semanas)¶
Se activa el motor nuevo aspecto por aspecto (price, stock primero; options
último, tras D-1), Reconciler, MedusaBatchSyncJob, comandos reescritos, categorías
en cola, backfill de los ítems publicados sin variante.
Criterio de salida (TRR — revisión de aptitud para pruebas): P-01 a P-05 medidos
en dev con --dry-run y una corrida real controlada; medusa:audit sin deriva en
una muestra de 500 ítems.
Fase 5 — Operación y retiro del legacy (≈ 1 semana)¶
MEDUSA_SYNC_ENGINE=v2 en producción, una semana de observación, borrado del código
legacy, actualización de docs/integraciones/medusa.md a "as-is v2", runbook,
UNIQUE_JOB_LOCKS.md y ADR-0003 corregidos.
Criterio de salida (ORR — revisión de aptitud operacional): dos semanas sin
incidentes; todas las TPM de §3.2 con valor medido en producción.
Reglas de transición¶
- Patrón strangler fig sobre
CatalogPort: el motor nuevo y el legacy comparten el puerto; nada del legacy se modifica salvo la fase 0. - Cada fase entra en un PR con su ADR (regla del repo).
- Nunca se mutan
product_itemsreales para probar:--dry-runymedusa:auditson de sólo lectura. - Recordar
php artisan queue:restart+ Horizon tras editar jobs.
10. Verificación y validación¶
Handbook, procesos 7 y 8: matriz requisito → método → evidencia. La validación (¿es el sistema correcto?) la hacen los stakeholders de §1 con las MOE.
| Requisito | Método | Evidencia esperada |
|---|---|---|
| F-01, F-03, F-04 | T | Tests de ChangeDetector y SyncStateRepository con eventos simulados |
| F-02, Q-01 | T | Tests del SyncPlanner con DesiredState fijos; sin contenedor ni red |
| F-05, F-06 | T + D | FakeCatalogPort con reglas de conflicto; demostración en dev con --dry-run sobre familias reales |
| F-08, F-09, P-08 | T + I | Test del espejo; grep de medusa en logs de otros canales = 0 |
| F-10, P-05 | D | Corrida masiva controlada en dev con JOB:DONE agregados |
| F-11, O-01 | T + I | Test de que una excepción no recuperable llega a failed_jobs; inspección del job |
| P-01 a P-03 | T | El FakeCatalogPort cuenta llamadas; asserts por escenario |
| P-06, P-07, P-09 | D + I | Una semana de JOB:DONE en producción; conteo de líneas por job |
| I-01, O-03, Q-03 | I | Revisión con code-reviewer; Pint; conteo de líneas |
| Q-04 | I | Los tests se corren en local/CI; el guard de tests/CreatesApplication.php sigue |
| MOE comercial (p95 evento→Medusa < 5 min) | D | Medición en producción, fase 5 |
Validación final: los stakeholders de §1.2 confirman cada MOE con el valor medido. Si alguna no se alcanza, se registra como desviación aceptada o se abre una fase correctiva; no se declara "terminado" por inspección del código.
11. Decisiones abiertas para el equipo¶
Estas decisiones cambian el diseño y deben tomarse antes de la fase 1.
| ID | Decisión | Opciones | Recomendación |
|---|---|---|---|
| D-1 | Modelo de opciones para ítems sin atributos | (a) mantener Default N; (b) — + opción Variante=SKU (§6.5); (c) un producto Medusa por ítem sin atributos |
(b): determinista y sin migración de estructura de productos |
| D-2 | Webhook desde Medusa | (a) sólo pull diario; (b) pull + webhook | (b), en la fase 3 si el repo de Medusa está disponible; si no, (a) y se agrega después |
| D-3 | Qué hacer con los 9 295 ítems publicados sin variante en dev (verificar la cifra en producción) | (a) backfill masivo en la fase 4; (b) sólo bajo demanda | (a), con MedusaBatchSyncJob y --dry-run previo |
| D-4 | Semántica de publish=0 y active=0 en Medusa |
(a) ambos = fuera del canal; (b) publish=0 = status: draft, active=0 = stock 0 + fuera del canal |
(b): distingue "no vender" de "no mostrar" |
| D-5 | product_sync_logs en éxitos |
(a) seguir guardando payload y respuesta; (b) sólo resumen en éxito, completo en fallo | (b) |
| D-6 | Lenguaje del endpoint custom de Medusa (préstamo de C) | Decidir sólo si P-03 falla en la fase 2 | Postergar |
| D-7 | Timeouts iniciales del cliente | 10 s / 3 s propuestos | Confirmar con la latencia real de producción (hoy no medida) |
12. ADRs a producir¶
Cada uno en su PR, según la regla del repo:
- ADR-0006 Estado local de sincronización por aspecto con hash (reemplaza el "ensure por fuerza bruta").
- ADR-0007 Espejo local de price lists de Medusa; prohibición de HTTP a Medusa en jobs de otros canales.
- ADR-0008 Modelo determinista de opciones de variantes (D-1).
- ADR-0009 Política de errores del cliente Medusa: excepciones tipadas, clasificación por código, circuit breaker; corrige la afirmación del ADR-0003 sobre el job de Medusa.
Referencias¶
- Línea base as-is
- ADR-0002, ADR-0003
- Incidente 2026-06 — Colas atascadas
- RFC 004 — Productos compuestos
- Estándar de implementación de jobs
- NASA Systems Engineering Handbook, SP-2016-6105 Rev. 2: §4.1–4.4 (diseño), §5.1–5.5 (realización), §6.1–6.8 (gestión técnica), Apéndice C (ConOps) y G (revisiones del ciclo de vida).