Saltar a contenido

Sincronización con Medusa — línea base técnica (as-is)

Documento as-is: describe la sincronización Integrador → MedusaJS tal como está implementada hoy en staging, no como debería ser. Cada afirmación apunta al archivo y línea que la sostiene. Es la línea base (baseline) sobre la que se construye la propuesta de reingeniería del RFC 009.

Cómo está organizado. Se sigue el motor de ingeniería de sistemas del NASA Systems Engineering Handbook (SP-2016-6105 Rev. 2), aplicado en reversa: en vez de diseñar un sistema nuevo, se recupera de un sistema existente lo que cada proceso habría producido si se hubiera seguido. Cada sección abre con una nota corta de qué proceso del handbook cubre y qué se hizo para aplicarlo. Esto sirve para dos cosas: evaluar el sistema con criterios explícitos y dejar el "antes" medido, para poder comparar el "después".

Sección Proceso del handbook (NPR 7123.1)
§1 Contexto y stakeholders 1. Stakeholder Expectations Definition
§2 Concepto de operaciones as-is 1. (ConOps)
§3 Requisitos recuperados 2. Technical Requirements Definition
§4 Descomposición funcional 3. Logical Decomposition
§5 Solución de diseño implementada 4. Design Solution Definition
§6 Interfaces 12. Interface Management
§7 Verificación y validación existentes 7. Verification / 8. Validation
§8 Medidas técnicas de rendimiento 15. Technical Assessment (TPM)
§9 Hallazgos técnicos 15. Technical Assessment
§10 Riesgos 13. Technical Risk Management
§11 Gestión de configuración y datos 14. Configuration Mgmt / 15. Data Mgmt
§12 Cómo operar (Product Transition, operación)

1. Contexto de misión y stakeholders

Handbook: el primer proceso identifica a quién sirve el sistema y qué espera de él, antes de hablar de requisitos. Acá se recuperó desde el código, los ADR y los incidentes: no hay un documento previo de expectativas.

Misión del subsistema. Mantener el catálogo de Medusa (la tienda online propia) como un espejo fiel del catálogo canónico del Integrador: productos, variantes, precio base, stock, categoría, imágenes y visibilidad en el canal de venta.

Stakeholders y expectativa que cada uno tiene del subsistema:

Stakeholder Qué espera Dónde se ve
Comercial / e-commerce Que un cambio de precio, stock o publicación aparezca en la tienda "enseguida" y sin intervención Observer updated → job por evento
TI (operación) Poder ver qué falló y reintentarlo; que las colas no se atasquen product_sync_logs, sync:process-failed, Horizon
Otros canales de salida (Woo, TN, Contimarket, UMarket, PCM) Que el producto exista en Medusa antes de publicar oferta, porque le piden a Medusa el precio de la price list ProductPriceService::getMedusaPriceInfo
Algolia medusa_handle + id_medusa_variant para construir la URL del índice ProductItemView.php:267-361
Medusa (el backend) Recibir operaciones válidas para su modelo v2 (opciones consistentes, SKU único, inventory item por variante) Reintentos heurísticos en createVariantForItem

Restricciones heredadas que condicionan cualquier rediseño: Laravel 11 + Horizon; Medusa v2 (auth JWT por /auth/user/emailpass, módulos de inventario, stock locations, sales channels; evidencia en §6); la vista product_item_view con GROUP BY (ADR-0002); la política de reintentos del ADR-0003; el equipo controla también el backend de Medusa (existen endpoints custom /admin/custom/price-lists/current y /store/custom/products/{id}).


2. Concepto de operaciones as-is

Handbook: el ConOps describe cómo se usa el sistema en escenarios concretos, desde el punto de vista del operador. Estos son los escenarios que el código soporta hoy.

2.1 Escenario principal: cambio en un ítem

  1. Alguien (ABM, workflow de proveedor, edición masiva) modifica un ProductItem.
  2. ProductItemObserver::updated (app/Observers/ProductItemObserver.php:22-25) delega en ProductEvaluationStrategy, que traduce columnas cambiadas a un tipo de sync: seo_name/short_description/ean/upc/part_number → product_basic; active, publish, selected_supplier_product_id nulo → product_status; product_id → variant_reparent (app/Services/Sync/Evaluation/ProductEvaluationStrategy.php:28-94).
  3. ProductSyncDispatcherService (app/Services/Sync/ProductSyncDispatcherService.php:27-89) recorre config/product_sync_channels.php, marca product_sync_logs en pending y encola SyncToMedusaJob en medusa_products.
  4. El job ejecuta syncVariantBasics, syncProductStatus o reparentVariantToNewProduct (app/Jobs/SyncToMedusaJob.php:116-125). Cualquier tipo desconocido cae en syncVariantBasics.
  5. El servicio "asegura" todo el estado remoto por fuerza bruta (§5.3) y registra éxito o fallo en product_sync_logs.

Sólo el evento updated está observado. created, deleted, restored y forceDeleted están vacíos: no hay flujo de alta por evento ni de baja.

2.2 Cambio de precio, stock u oferta de proveedor

SupplierProductChangeWorkflow detecta el cambio en SupplierProduct y termina en DispatchSyncActivity con product_basic (app/Workflows/SupplierProductChange/Activities/DispatchSyncActivity.php:58-60). Un cambio de stock recorre exactamente el mismo camino que un cambio de descripción.

2.3 Oferta programada (price list de Medusa)

La dirección de este flujo es inversa: Medusa es la fuente. medusa:schedule-price-list-syncs corre a diario (routes/console.php:65), lee las price lists que empiezan o terminan hoy y agenda un DispatchProductItemSyncAt con delay a esa hora (app/Console/Commands/ScheduleMedusaPriceListSyncs.php:137-152). Cuando se ejecuta, despacha product_basic a todos los canales, y cada canal le pregunta a Medusa el precio calculado por HTTP (app/Services/ProductPriceService.php:183-272).

2.4 Despublicación

Un publish=0 no llega a Medusa: el despachador sólo encola canales con sync_unpublished y Medusa no lo declara (ProductSyncDispatcherService.php:33-45). Lo único que quita un producto del canal es active=false, vía product_status → ensureSalesChannel(remove) (MedusaProductSyncService.php:1718, 2199-2230). El status del producto en Medusa es siempre published (computePublishStatus, L227-230).

2.5 Cambio de padre (reparent)

Al cambiar product_id, la estrategia despacha variant_reparent directo al job de Medusa, sin pasar por el despachador y con return inmediato (ProductEvaluationStrategy.php:34-54): los demás canales no se enteran, y si en el mismo save cambió active, ese product_status se pierde.

2.6 Corridas masivas y reparación

medusa:sync-products (padres) y medusa:sync-variants (ítems) en chunks de 200, sincrónicos, sin cola; medusa:sync-handles, medusa:sync-variant-images, sync:process-failed (clasifica y reintenta 20 fallidos) y sync:retry-pending (re-despacha 100 a todos los canales, RetryPendingSyncs.php:125). Ninguna corrida masiva corta ante fallos consecutivos de conexión.

2.7 Categorías

CategoryObserver y ProductCategoryObserver llaman a MedusaCategoryService de forma sincrónica dentro del request HTTP del usuario, tragan errores y no escriben product_sync_logs (app/Observers/CategoryObserver.php:29-65, app/Observers/ProductCategoryObserver.php:22-66). No se envía árbol (parent_category_id), sólo categorías planas por nombre.


3. Requisitos recuperados

Handbook: los requisitos técnicos se escriben como "el sistema debe…" con un método de verificación. Como no existían, se recuperan del comportamiento del código. La columna "estado" dice si hoy se cumple, según la evidencia de §8 y §9.

ID Requisito recuperado Evidencia en código Estado
R-01 Todo cambio de un ítem publicado debe reflejarse en Medusa sin intervención Observer + despachador Parcial: created/deleted no observados; publish=0 no llega
R-02 Un ítem debe existir en Medusa como variante de un producto cuyo id se guarda localmente products.id_medusa_product, product_items.id_medusa_variant Cumple en dev para padres (100 %), no para variantes (47 %)
R-03 Las opciones del producto Medusa deben derivarse de atributos/términos y ser consistentes entre hermanos MedusaOptionService Cumple con heurísticas frágiles (Default N, reseteo total)
R-04 El stock publicado debe ser el del proveedor seleccionado; 0 si inactivo computeInventoryQuantity, syncSimpleVariantInventoryLevel Cumple; puede pisar con 0 si la vista no devuelve fila
R-05 El precio de la variante debe ser el especial si existe, si no el regular computeVariantPriceAmount (L2181-2197) Cumple
R-06 Los compuestos deben publicarse como kit de inventory items de sus componentes orchestrateKitForVariant Cumple (RFC 004)
R-07 Las imágenes públicas del ítem deben verse en la variante MedusaVariantImageService Cumple; thumbnail del padre no determinista
R-08 El producto debe estar en la categoría principal y en el sales channel ensureAndAttachCategory, ensureSalesChannel Cumple (una sola categoría)
R-09 Un fallo debe quedar registrado y ser reintentable product_sync_logs, sync:process-failed Parcial: errores de categoría, opciones e inventario se tragan
R-10 El job no debe bloquear la cola más de N segundos ADR-0003 ($timeout=120) No cumple: el job no declara $timeout
R-11 Jobs duplicados en espera deben deduplicarse ShouldBeUniqueUntilProcessing Parcial: product_basic y product_status del mismo ítem no se deduplican
R-12 Los demás canales deben poder leer el precio de oferta vigente en Medusa MedusaPriceListService Cumple con costo alto (2 HTTP por producto por canal)
R-13 Los timeouts HTTP deben poder ajustarse por .env config/medusa.php:29-30 No cumple: el cliente lee otra clave (§9.1)

4. Descomposición funcional

Handbook: la descomposición lógica separa qué hace el sistema de cómo lo hace, para poder asignar funciones a componentes. Estas son las funciones que existen hoy, con el componente que las cumple.

F0  Propagar cambios del catálogo a Medusa
├── F1  Detectar el cambio y clasificarlo ............ ProductItemObserver + ProductEvaluationStrategy
├── F2  Encolar con deduplicación y trazabilidad ..... ProductSyncDispatcherService + SyncToMedusaJob
├── F3  Asegurar el producto padre ................... ensureProductId / updateProduct
│   ├── F3.1 Handle y categoría ....................... ensureAndAttachCategory
│   └── F3.2 Opciones consistentes en la familia ...... MedusaOptionService
├── F4  Asegurar la variante ......................... resolveVariantIdForItem / createVariantForItem
│   ├── F4.1 Flags y metadata ......................... safeVariantUpdate
│   ├── F4.2 Precio base .............................. upsertPrices
│   ├── F4.3 Inventario (simple o kit) ................ MedusaInventoryService
│   └── F4.4 Imágenes y thumbnails .................... MedusaVariantImageService
├── F5  Visibilidad en el canal ...................... ensureSalesChannel
├── F6  Mover variante entre padres .................. reparentVariantToNewProduct
├── F7  Leer ofertas de Medusa y programar re-sync ... MedusaPriceListService + ScheduleMedusaPriceListSyncs
├── F8  Registrar resultado y permitir reintento ..... product_sync_logs + FailedSyncClassifier
└── F9  Corridas masivas y reparación ................ comandos medusa:*

Observación estructural: no existe una función "decidir qué cambió". F3 a F5 se ejecutan completas en cada job, sea cual sea el disparador. Ésa es la raíz de la mayor parte de los hallazgos de §9.


5. Solución de diseño implementada

Handbook: la solución de diseño asigna las funciones a componentes físicos y define su comportamiento. Ésta es la arquitectura real.

5.1 Piezas

Archivo Rol Líneas
config/medusa.php Credenciales, ids de canal/ubicación/perfil, timeouts —
config/product_sync_channels.php:39-46 Declara el canal (enabled, log_sync, sync_type_resolver) —
app/Services/Medusa/MedusaClient.php Guzzle + JWT cacheado 55 min 137
app/Services/Medusa/CatalogPort.php Interfaz de producto/variante (puerto) 25
app/Services/Medusa/MedusaCatalogAdapter.php Implementación del puerto + "caché" 340
app/Services/Medusa/MedusaProductSyncService.php Orquestador: todo el flujo 2 507
app/Services/Medusa/MedusaOptionService.php Opciones y valores 788
app/Services/Medusa/MedusaInventoryService.php Inventory items, niveles, kits 584
app/Services/Medusa/MedusaCategoryService.php Categorías 356
app/Services/Medusa/MedusaVariantImageService.php Imágenes y thumbnails 369
app/Services/Medusa/PriceList/MedusaPriceListService.php Lectura de precios (Store API) 453
app/Jobs/SyncToMedusaJob.php Job por ítem y tipo 323
app/Jobs/DispatchProductItemSyncAt.php Lote diferido para ofertas 116
Comandos medusa:*, sync:process-failed, sync:retry-pending Masivos y reparación —

Total del módulo: unas 6 000 líneas, 64 commits entre 2025-10-07 y 2026-07-16, dos autores. Sin cambios desde julio de 2026.

5.2 Modelo de datos

  • products.id_medusa_product (unique) y products.medusa_handle.
  • product_items.id_medusa_variant (unique).
  • categories → tabla metadata con clave id_medusa_category (app/Models/Category.php:415-429); product_categories.id_medusa_category (legacy, RFC 003 a medio migrar).
  • product_sync_logs: una fila viva por (product_id, channel) con status ∈ {pending, success, failed, skipped, archived}, payload_sent y response_body como JSON completo en cada éxito.
  • product_item_view: fuente de regular_price, special_price y stock de respaldo; expone los tres ids de Medusa para Algolia.
  • No existe estado de sincronización por recurso (qué hash de precio, stock, opciones o imágenes se envió por última vez). El único estado es "la última corrida del ítem terminó bien o mal".

5.3 Comportamiento del job típico

syncVariantBasics para una variante existente, sin cambios remotos, una imagen ya subida (MedusaProductSyncService.php:1187-1493 y :2352-2506):

Paso Qué hace HTTP
A1 refresh() + carga de toda la familia (product.productItems.terms.attribute) —
A2 Verifica que el padre exista (GET product) 1
A3 Actualiza el padre completo, siempre (POST product) 1
A4 Normaliza opciones de la familia: GET product ×2 + GET variants 3
A5 Resuelve la variante (GET variant) 1
A6 Precio (POST variant {prices}) 1
B1 Flags de la variante (POST variant) 1
B2 Arma payload de opciones y descarta el resultado (bloque vacío en L1264-1265): GET product ×2 + GET variants 3
B2' Bloque duplicado (L1278-1364): flags otra vez, payload otra vez, POST variant {options} 5
B3 Inventario: GET inventory-items?q=sku, GET variant inventory, GET stock-location, POST level (409 tragado), POST level 5
B4 Precio otra vez 1
B5 Imágenes: GET product images, GET variant images, POST thumbnail variante, POST thumbnail producto (los dos siempre) 4
— logSyncOk escribe product_sync_logs; el job lo vuelve a escribir (SyncToMedusaJob.php:239-260) —

≈ 26 llamadas HTTP para no cambiar nada. De ellas: 8 GET del mismo producto, 3 GET de la misma lista de variantes, 2 POST idénticos de precio, 2 de flags y 2 thumbnails redundantes. La "caché" del adaptador no ahorra ninguna: sólo escribe en productCache cuando recibe 404 y nunca escribe variantsCache (MedusaCatalogAdapter.php:36-65). Los logs CACHE_STATS reportan un ahorro que no existe.

syncProductStatus (L1693-1855) repite la fase A completa más ensureSalesChannel e inventario en línea (reimplementado, L1752-1811). reparentVariantToNewProduct (L1543-1691) borra la variante o el producto viejo y vuelve a crear todo.

5.4 Manejo de errores y reintentos

  • El job captura Throwable y nunca relanza (SyncToMedusaJob.php:119-222): release(60) fijo en ConnectException, 429 y 5xx; todo lo demás termina "OK" con product_sync_logs.status=failed. Consecuencia medida: 0 filas en failed_jobs para la cola medusa_products frente a 45 fallos en product_sync_logs. Horizon no ve nada.
  • tries=3, backoff=15 (inútil, nunca se lanza), uniqueFor=600 (SyncToMedusaJob.php:45-72). No usa RetriesWithBackoff ni la escala [60,300,900] del ADR-0003, que afirma haberse aplicado a Medusa.
  • Sin $timeout propio: hereda timeout=60 del supervisor (config/horizon.php:226, 298-303) con retry_after=150 (config/queue.php:138-143). Con dos requests lentas de 25 s el job supera el timeout del supervisor.
  • Excepciones tragadas con catch (\Throwable) en inventario (L444-455, 1789-1791), precios (L1406-1411), categoría (MedusaCategoryService.php:200-213), getOptionTitleMap (MedusaOptionService.php:495, devuelve [] ante un 5xx y el resto del flujo trata al producto como "sin opciones"). Resultado: success con stock, precio o categoría no aplicados.
  • Detección de errores por texto del body: str_contains($body, 'already exists') y similares en seis sitios del orquestador (L180, 786, 937, 968, 1005, 1041).

5.5 Logging

mlog con muestreo 1/200 para mensajes "ruidosos" (L54-60) en el orquestador; los satélites loguean info en cada request sin muestreo y sin el formato [CANAL:product_id:sku] del proyecto. Un job feliz emite entre 40 y 60 líneas. No existe JOB:DONE con duración: no se puede medir p50/p95 de un job desde los logs. El canal MEDUSA_PRICELIST escribió 25 MB en un día (2026-09-10) con stack traces completos por cada producto no encontrado.


6. Interfaces

Handbook: la gestión de interfaces documenta cada frontera del sistema (ICD). Éstas son las interfaces reales, en la dirección en que se usan.

6.1 Integrador → Medusa Admin API (v2)

Autenticación POST /auth/user/emailpass → Bearer JWT cacheado 55 min (MedusaClient.php:44-83); alternativa MEDUSA_ADMIN_TOKEN. Guzzle sin base_uri, sin middleware de retry, sin keep-alive explícito.

Recurso Endpoints usados
Productos GET/POST /admin/products[/{id}], GET ?handle=, ?fields= selectivos
Variantes GET/POST/DELETE /admin/products/{id}/variants[/{vid}], GET /admin/product-variants?q=|id=
Opciones POST /admin/products/{id}/options/{oid}
Inventario GET/POST /admin/inventory-items, POST .../location-levels[/{loc}], POST/DELETE .../variants/{vid}/inventory-items[/{iid}], POST .../variants/inventory-items/batch, GET /admin/stock-locations/{id}
Imágenes POST /admin/products/{id}/variants/{vid}/images/batch, thumbnails vía POST product/variant
Categorías GET/POST/DELETE /admin/product-categories[/{id}[/products]]
Canal POST /admin/sales-channels/{id}/products
Price lists GET /admin/price-lists/{id}, GET /admin/custom/price-lists/current (custom)

Endpoints batch de v2 que existen y no se usan: POST /admin/products/batch, POST /admin/products/{id}/variants/batch, POST /admin/inventory-items/location-levels/batch.

6.2 Integrador → Medusa Store API

GET /store/custom/products/{id}?fields=...calculated_price con x-publishable-api-key, Guzzle propio (MedusaPriceListService.php:138-152). Usado por ProductPriceService desde todos los jobs de salida y desde el modelo ProductItemView (ProductItemView.php:153-163): cualquier consumidor de la vista puede disparar HTTP a Medusa.

6.3 Medusa → Integrador

No existe. No hay webhooks ni suscriptores; las ofertas se descubren por polling diario (§2.3).

6.4 Interfaces internas

  • Cola medusa_products / conexión redis_medusa_products (retry_after=150), supervisor supervisor_medusa (3 procesos en producción, balance=auto).
  • product_sync_logs como contrato con sync:process-failed, --pending-only y la UI de fallidos.
  • Cloudflare Images: sólo por URL construida (imagedelivery.net/{hash}/{cf_image_id}/public), sin llamadas al servicio. Tres constructores de URL distintos en el repo (MedusaVariantImageService.php:308, ProductItemImage::getImageUrlAttribute, CloudflareImagesService::url()), sólo el primero funciona sin config faltante.
  • Algolia: url = /{medusa_handle}/{id_medusa_variant}; si falta uno, url=null. Orden obligatorio Medusa → Algolia.

7. Verificación y validación existentes

Handbook: verificación = "¿construimos el producto bien?" (contra requisitos); validación = "¿construimos el producto correcto?" (contra expectativas).

  • Tests automatizados: ninguno sobre MedusaProductSyncService, MedusaCatalogAdapter, los satélites, SyncToMedusaJob ni el despachador. tests/Unit/MedusaPriceSimulationTest.php es aritmética con constantes. CatalogPort permite un fake, pero MedusaVariantImageService exige la clase concreta (MedusaVariantImageService.php:19-20), lo que rompe la sustituibilidad.
  • Verificación en operación: por inspección de logs y de product_sync_logs. El --dry-run de medusa:sync-variants existe.
  • Validación: implícita, por ausencia de quejas. El incidente 2026-06 fue la única validación formal y fue negativa (docs/incidentes/2026-06-colas-atascadas.md).

8. Medidas técnicas de rendimiento (TPM) — línea base

Handbook: una TPM es una medida cuantitativa que se sigue en el tiempo para saber si el sistema cumple. Éstos son los valores medidos hoy, para comparar después. Todos vienen de la base y los logs de desarrollo (2026-09-10/11, Medusa local); no hay métricas de producción en el repo.

TPM Valor as-is Fuente
Llamadas HTTP por sync de variante sin cambios ≈ 26 §5.3
Consultas DB redundantes por sync ≈ 6 (terms N+1 ×2 por hermano, refresh() ×3, metadata, vista sin scope) MedusaOptionService.php:684-761, MedusaInventoryService.php:65-67
Throughput masivo (creación de padres, 1 proceso) 5,6 productos/s; 52 min para 17 573 MEDUSA-2026-09-10.log
Latencia por request al Medusa local p50 9 ms, p90 44 ms, máx 617 ms elapsed_ms
Líneas de log por job feliz 40–60 §5.5
Duración de job no medible (sin JOB:DONE) —
Ítems publicados con id_medusa_variant (dev) 8 228 de 17 523 (47 %) tinker
product_sync_logs Medusa 8 195 success / 45 failed / 8 pending tinker
failed_jobs en cola medusa 0 (los fallos no llegan a Horizon) tinker
Tipo de error dominante en el histórico de logs cURL error 7: Connection refused (100 % de los ERROR) grep sobre 14 archivos, 55 MB
Efectividad del dedupe por uniqueId 4 saltados sobre 176 locks (2026-09-08) job_locks-2026-09-08.log
HTTP sincrónico a Medusa por producto en otros canales 2 (store + admin price-list), timeout 25 s + 10 s conexión ProductPriceService.php:183-272
Caché de precio / lock 60 s / 10 s (sin cambios desde el incidente 2026-06) ProductPriceService.php:19, 25

9. Hallazgos técnicos

Handbook: la evaluación técnica compara el estado real contra lo esperado y clasifica las desviaciones. Se agrupan por atributo de calidad. Cada uno tiene referencia para poder verificarlo.

9.1 Configuración que no hace lo que dice

  • Timeouts fijos en 25/10 s. MedusaClient.php:16-17 y MedusaPriceListService.php:139-140 leen config('medusa.http_timeout') y http_connect_timeout; config/medusa.php:29-30 define timeout y connect_timeout. MEDUSA_HTTP_TIMEOUT en .env no tiene efecto. La mitigación 1 del incidente 2026-06 fue un no-op.
  • El job no tiene $timeout aunque el ADR-0003 lo declara aplicado.
  • medusa.log_verbosity y log_payload_preview se leen con env() en runtime (MedusaProductSyncService.php:37, 72): con config cacheada devuelven null.
  • MedusaPriceListService pide config('medusa.default_region_id'), que no existe: region_id=null en la Store API.
  • .env-example no documenta ninguna de las 16 variables MEDUSA_*.

9.2 Rendimiento

  • Sin detección de cambio: cada job re-asegura padre, opciones de toda la familia, precio, stock, imágenes y thumbnails (§5.3). Un cambio de short_description cuesta lo mismo que un alta.
  • Bloque duplicado en syncVariantBasics (L1213-1276 vs L1278-1364) y precio enviado dos veces (fase A y STEP4).
  • Caché del adaptador inoperante (§5.3) y, además, invalidada por cada updateProduct (categoría ×2, thumbnail, opciones).
  • getOptionTitleMap (un GET product cada vez) se invoca 4–5 veces por job; nextLabelForTitle y ensureOptionValuesExist hacen 2 GET + 1 POST por iteración en un bucle de hasta 50 vueltas (L1998-2010).
  • ensureAndAttachCategory corre dos veces por sync (L807 y L1511): 35 029 "Category linked" para 17 573 ítems el 2026-09-10.
  • resolveStockLocationId hace un GET /admin/stock-locations/{id} por variante para un valor que viene del .env y que devuelve igual si falla (MedusaInventoryService.php:438-446).
  • Búsquedas por texto en vez de id: inventory item por ?q=sku con limit=20 y filtro en cliente (MedusaInventoryService.php:304-311), categoría por q=name, variante global por q=sku que borra la variante si está en otro producto (MedusaProductSyncService.php:835-846).
  • N+1 en DB: $pi->terms()->with('attribute')->get() dentro de bucles que ya tenían la relación cargada (MedusaOptionService.php:684, 704, 742, 761); computeInventoryQuantity usa productItemView()->whereRaw('LOWER(channel)=?'), el patrón no scopeado del incidente 2026-06 (MedusaInventoryService.php:65-67).
  • Sin uso de endpoints batch; un job por ítem también en lotes de oferta.
  • El job serializa el modelo completo y luego hace refresh(); payload_sent y response_body guardan JSON completo por cada éxito.
  • Precio de oferta para los demás canales: 2 HTTP sincrónicos por producto, dentro de cada job de salida, con caché de 60 s. Es la causa raíz 2 del incidente 2026-06 y ninguno de los siete pendientes de ese post-mortem se ejecutó.

9.3 Confiabilidad

  • Fallos invisibles para Horizon (§5.4) y errores tragados sin product_sync_logs (categorías, opciones, inventario): sync:process-failed no los ve.
  • Sin circuit breaker: 1 371 fallos consecutivos de conexión el 2026-09-11 sin abortar, con stack trace de 35 frames cada uno.
  • Efectos colaterales no idempotentes: borrado de variantes ajenas por SKU compartido, borrado de productos en reparent, vaciado de precios antes de borrar (L191-224). Creación de inventory items sin lock: dos jobs del mismo SKU pueden duplicar.
  • computeInventoryQuantity retorna 0 si la vista no devuelve fila, contradiciendo su propio docblock (MedusaInventoryService.php:83-88).
  • Thumbnail del producto lo pisa la última variante sincronizada (no determinista).
  • ScheduleMedusaPriceListSyncs lee $price['variant_id'] (L99) aunque el comentario L74-75 dice que en Medusa 2.15 el id viene por price_set.variant.id; daily() corre con el desfase horario del servidor; si falla ese día, la lista no se sincroniza nunca.
  • sync:process-failed reintenta con 'variant_basic' (ProcessFailedSyncsCommand.php:286) mientras la constante real es 'product_basic'; el default del switch lo salva.

9.4 Mantenibilidad

  • Un orquestador de 2 507 líneas con lista de "milestones" hard-coded (L81-170), heurísticas de opciones, inventario, precios, categoría y reparent mezclados.
  • ensureProductOptionsCommonOnly tiene 430 líneas y seis copias de la misma resolución de variant.options; buildProductOptionsForCreate y buildCommonOptionsForProduct son idénticos.
  • Inventario orquestado tres veces (orchestrateForVariant, finalizeVariantInventory, syncSimpleVariantInventoryLevel).
  • Servicios instanciados con ??= new (MedusaProductSyncService.php:17-26), sin binding en el contenedor: cada job crea su propio grafo y su propia caché.
  • Documentación contradictoria con el código: UNIQUE_JOB_LOCKS.md (uniqueFor 180), sync-architecture.md (backoff 30, timeout 300), ADR-0003 (RetriesWithBackoff aplicado). Ver §11.
  • Cero tests.

10. Riesgos técnicos

Handbook: riesgo = probabilidad × consecuencia, con una respuesta (mitigar, aceptar, vigilar). Escala 1–5.

ID Riesgo P C Respuesta actual
RK-1 Medusa lento o caído atasca todas las colas de salida por el acople de precio 4 5 Ninguna efectiva (timeouts no ajustables, caché 60 s)
RK-2 Ítem publicado sin variante en Medusa (53 % en dev) → sin URL en Algolia, sin venta 4 4 Corridas manuales
RK-3 Fallo silencioso: success con stock/precio/categoría no aplicados 3 4 Ninguna
RK-4 Un job borra una variante o producto ajeno (SKU compartido, reparent) 2 5 Ninguna
RK-5 Cambio de versión de Medusa rompe la detección por texto de errores 3 3 Ninguna
RK-6 Oferta programada no se propaga (cron fallido, desfase tz, campo variant_id) 3 4 Ninguna
RK-7 Volumen de logs sin rotación (laravel.log 165 MB, pricelist 25 MB/día) llena disco 3 3 days=14 sólo en canales daily
RK-8 Nadie puede modificar el módulo con seguridad (2 507 líneas, sin tests) 5 3 Ninguna

11. Gestión de configuración y datos

Handbook: la línea base debe ser una, y los documentos deben coincidir con ella. Estas discrepancias se detectaron al construir este documento y deben resolverse (corrigiendo el doc o el código) antes de cualquier rediseño.

Documento Dice Código
docs/UNIQUE_JOB_LOCKS.md uniqueFor=180, uniqueId=productItemId:syncType 600, syncType:productItemId:oldLocalProductId
docs/analisis/sync-architecture.md tries=3, backoff=30, timeout=300 tries=3, backoff=15, sin timeout
ADR-0003 RetriesWithBackoff y $timeout=120 aplicados a jobs de salida Medusa no lo usa; release(60) fijo
Incidente 2026-06, mitigación 1 Bajar MEDUSA_HTTP_TIMEOUT La variable no se lee
plan-medusa-variant-images.md Fase 3/4 Eliminar metadata.variant_images con asset() Sigue en buildCreateVariantPayload (L2028-2036)
docs/SYNC_CHANNELS_ARQUITECTURA.md Medusa "retry 5xx" Cierto, pero también 4xx terminan "OK"

Datos técnicos que faltan para gestionar el sistema: duración por job, tasa de éxito por tipo de sync, llamadas HTTP por job, tamaño de cola en el tiempo. La propuesta de Prometheus (docs/analisis/prometheus-monitoring-v1.md) no se implementó.


12. Cómo operar (hoy)

# padres (crea/normaliza opciones), luego variantes; sincrónico, sin cola
php artisan medusa:sync-products
php artisan medusa:sync-variants --pending-only --dry-run
php artisan medusa:sync-variants --pending-only

# reparación
php artisan medusa:sync-handles
php artisan medusa:sync-variant-images --only-missing
php artisan sync:process-failed --channel=medusa

# ofertas del día (ya en el scheduler)
php artisan medusa:schedule-price-list-syncs

# seguir un ítem
grep "\[MEDUSA:14275" storage/logs/jobs/medusa/MEDUSA.log

Variables de entorno leídas por el módulo (config/medusa.php): MEDUSA_BASE_URL, MEDUSA_ADMIN_EMAIL, MEDUSA_ADMIN_PASSWORD o MEDUSA_ADMIN_TOKEN, MEDUSA_PUBLISHABLE_KEY, MEDUSA_SALES_CHANNEL_ID, MEDUSA_STOCK_LOCATION_ID, MEDUSA_PRICE_LIST_ID, MEDUSA_SHIPPING_PROFILE_ID, MEDUSA_MANAGE_INVENTORY, MEDUSA_ALLOW_BACKORDER, MEDUSA_HTTP_TIMEOUT y MEDUSA_HTTP_CONNECT_TIMEOUT (estas dos últimas sin efecto, §9.1). Ninguna está en .env-example.


Referencias