Saltar a contenido

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)

  1. Alguien modifica un ProductItem (o el workflow de proveedor, o la edición masiva).
  2. 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 en medusa_sync_state y despacha un job por ítem. Si ya hay un job en espera para ese ítem, sólo se suman los aspectos sucios (coalescencia natural).
  3. 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.
  4. Ejecuta las operaciones con el mínimo de llamadas (una por aspecto, batch cuando aplica), sin GET previos. 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).
  5. Registra en medusa_sync_state el hash enviado por aspecto y en product_sync_logs un resultado resumido. Emite una sola línea JOB:DONE con 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-locations por 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ás thumbnail_cf_image_id
  • options: {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_timeout desde las claves correctas (fase 0). Recomendación inicial: 10 s / 3 s.
  • Middleware de retry: 429, 502, 503, 504 y ConnectException, con Retry-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 hace release() 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-lists con StaticTokenAuth (patrón ya existente para syncs externos). Payload: price_list_id, event. El Integrador responde 202 y encola RefreshPriceListJob, que hace el GET /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-lists diario como red de seguridad (pull completo).
  • Las ventanas (starts_at/ends_at) se agendan desde el espejo con DispatchProductItemSyncAt, ahora con WithoutOverlapping por price_list_id, no por concatenación de ids.
  • ProductPriceService::getMedusaPriceInfo pasa a leer el espejo. Se elimina MedusaPriceListService y el Guzzle propio.

6.8 Observabilidad

  • JOB:DONE por job: [MEDUSA:{id}:{sku}] done {duration_ms, http_calls, aspects, ops, skipped}; JOB:SKIP cuando 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 MEDUSA con rotación diaria; el canal MEDUSA_PRICELIST desaparece 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_items reales para probar: --dry-run y medusa:audit son 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