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¶
- Alguien (ABM, workflow de proveedor, edición masiva) modifica un
ProductItem. ProductItemObserver::updated(app/Observers/ProductItemObserver.php:22-25) delega enProductEvaluationStrategy, que traduce columnas cambiadas a un tipo de sync:seo_name/short_description/ean/upc/part_number→product_basic;active,publish,selected_supplier_product_idnulo →product_status;product_id→variant_reparent(app/Services/Sync/Evaluation/ProductEvaluationStrategy.php:28-94).ProductSyncDispatcherService(app/Services/Sync/ProductSyncDispatcherService.php:27-89) recorreconfig/product_sync_channels.php, marcaproduct_sync_logsenpendingy encolaSyncToMedusaJobenmedusa_products.- El job ejecuta
syncVariantBasics,syncProductStatusoreparentVariantToNewProduct(app/Jobs/SyncToMedusaJob.php:116-125). Cualquier tipo desconocido cae ensyncVariantBasics. - 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) yproducts.medusa_handle.product_items.id_medusa_variant(unique).categories→ tablametadatacon claveid_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)constatus ∈ {pending, success, failed, skipped, archived},payload_sentyresponse_bodycomo JSON completo en cada éxito.product_item_view: fuente deregular_price,special_pricey 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
Throwabley nunca relanza (SyncToMedusaJob.php:119-222):release(60)fijo enConnectException, 429 y 5xx; todo lo demás termina "OK" conproduct_sync_logs.status=failed. Consecuencia medida: 0 filas enfailed_jobspara la colamedusa_productsfrente a 45 fallos enproduct_sync_logs. Horizon no ve nada. tries=3,backoff=15(inútil, nunca se lanza),uniqueFor=600(SyncToMedusaJob.php:45-72). No usaRetriesWithBackoffni la escala[60,300,900]del ADR-0003, que afirma haberse aplicado a Medusa.- Sin
$timeoutpropio: heredatimeout=60del supervisor (config/horizon.php:226, 298-303) conretry_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:successcon 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ónredis_medusa_products(retry_after=150), supervisorsupervisor_medusa(3 procesos en producción,balance=auto). product_sync_logscomo contrato consync:process-failed,--pending-onlyy 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,SyncToMedusaJobni el despachador.tests/Unit/MedusaPriceSimulationTest.phpes aritmética con constantes.CatalogPortpermite un fake, peroMedusaVariantImageServiceexige 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-rundemedusa:sync-variantsexiste. - 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-17yMedusaPriceListService.php:139-140leenconfig('medusa.http_timeout')yhttp_connect_timeout;config/medusa.php:29-30definetimeoutyconnect_timeout.MEDUSA_HTTP_TIMEOUTen.envno tiene efecto. La mitigación 1 del incidente 2026-06 fue un no-op. - El job no tiene
$timeoutaunque el ADR-0003 lo declara aplicado. medusa.log_verbosityylog_payload_previewse leen conenv()en runtime (MedusaProductSyncService.php:37, 72): con config cacheada devuelvennull.MedusaPriceListServicepideconfig('medusa.default_region_id'), que no existe:region_id=nullen la Store API..env-exampleno documenta ninguna de las 16 variablesMEDUSA_*.
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_descriptioncuesta 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(unGET productcada vez) se invoca 4–5 veces por job;nextLabelForTitleyensureOptionValuesExisthacen 2 GET + 1 POST por iteración en un bucle de hasta 50 vueltas (L1998-2010).ensureAndAttachCategorycorre dos veces por sync (L807 y L1511): 35 029 "Category linked" para 17 573 ítems el 2026-09-10.resolveStockLocationIdhace unGET /admin/stock-locations/{id}por variante para un valor que viene del.envy que devuelve igual si falla (MedusaInventoryService.php:438-446).- Búsquedas por texto en vez de id: inventory item por
?q=skuconlimit=20y filtro en cliente (MedusaInventoryService.php:304-311), categoría porq=name, variante global porq=skuque 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);computeInventoryQuantityusaproductItemView()->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_sentyresponse_bodyguardan 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-failedno 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.
computeInventoryQuantityretorna 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).
ScheduleMedusaPriceListSyncslee$price['variant_id'](L99) aunque el comentario L74-75 dice que en Medusa 2.15 el id viene porprice_set.variant.id;daily()corre con el desfase horario del servidor; si falla ese día, la lista no se sincroniza nunca.sync:process-failedreintenta con'variant_basic'(ProcessFailedSyncsCommand.php:286) mientras la constante real es'product_basic'; eldefaultdel 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.
ensureProductOptionsCommonOnlytiene 430 líneas y seis copias de la misma resolución devariant.options;buildProductOptionsForCreateybuildCommonOptionsForProductson 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 (RetriesWithBackoffaplicado). 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¶
- RFC 009 — Reingeniería de la sincronización con Medusa
- ADR-0002 — Lookup scopeado de
product_item_view - ADR-0003 — Política de reintentos de jobs de salida
- Incidente 2026-06 — Colas de salida atascadas
- Análisis de confiabilidad de sincronización
- Plan de imágenes de variantes en Medusa
- NASA Systems Engineering Handbook, SP-2016-6105 Rev. 2, cap. 4 (System Design Processes) y 6 (Crosscutting Technical Management Processes).