mogos propuesta · v1.0 · jul 2026

Histórico de precios · sourcing

El navegador tira el costo a la basura.

Pedro escribe el costo que le dio la fábrica. El frontend lo multiplica por la tarifa Mogos y manda el resultado con el mismo nombre de campo. El costo nunca sale del navegador. Cada precio que una fábrica china le ha dado a Mogos —meses de sourcing— se ha borrado en el momento de escribirlo. Y se puede recuperar casi todo.

Se pierde hoy
El costo, en cada fila
Tablas nuevas
1 · espejo de Supplier
El histórico
Consulta, no tabla
Arranca
Con la historia rescatada
01Dónde se pierde
tres líneas de frontend
// apps/admin/schemas/service-quotes/create-service-quote-schema.ts
const finalUnitPrice = computeUnitPriceWithFee(supplier.unitPriceUsd, mode, value);
…
unitPriceUsd: finalUnitPrice,     // ← se manda ESTE. el costo no viaja.

El comentario del propio archivo lo confirma: «the backend receives a single unitPriceUsd on each proposed option». Un solo número. El API no tiene forma de saber si es un costo o un precio con margen adentro — y ServiceQuoteProductOption tampoco: tiene un solo campo de precio, y no existe supplierCostUsd.

Buscamos margin, markup, utilidad en todo el módulo de cotizaciones: cero coincidencias. El margen no se puede calcular no porque falte la resta — falta el minuendo.

Y el producto tampoco tiene identidad

productName es texto libre VarChar(255) por cotización. «Jacuzzy» escrito en enero y «Jacuzzi» en junio son dos cadenas sin relación.

Aunque tuvieras los precios, no habría contra qué agruparlos. Son dos pérdidas distintas y hacen falta las dos.

Pero el dato ya se está grabando

Cada ServiceQuoteProductOption guarda proveedor, marca, precio, moneda, días de entrega, garantía, CBM, dimensiones, fotos, enlace y createdAt.

Ya es un punto de dato de sourcing. Guarda el número equivocado y cuelga de un texto libre. Eso es todo lo que está mal.

02El modelo
espejo de Supplier
// 1 · el costo deja de destruirse. la tarifa vive aparte, el precio se calcula.
model ServiceQuoteProductOption {
  supplierCostUsd  Float     // lo que pidió la fábrica ← NUEVO
  unitPriceUsd     Float     // lo que se le cobra al cliente (derivado)
}
model ServiceQuote {
  serviceFeeMode   ServiceFeeModeEnum?   // PERCENT | USD_PER_UNIT ← NUEVO
  serviceFeeValue  Float?                // hoy solo vive en el navegador
}

// 2 · identidad del producto. mismo molde que Supplier, a diez líneas de aquí.
model SourcedProduct {
  name             String
  normalizedName   String   @unique   // minúsculas, sin acentos, espacios colapsados
  category         MerchandiseCategoryEnum
  catalogProductId String?  // si algún día se vuelve ficha de venta
  @@index([normalizedName(ops: raw("gin_trgm_ops"))], type: Gin)
}
model ServiceQuoteRequestedProduct { sourcedProductId String? }

Por qué no cuelga del catálogo

Era lo natural, y el esquema lo prohíbe con argumento escrito: «una ServiceQuote es un SERVICIO y su precio se descubre; un Product ya tiene monto conocido. Mezclarlos hacía imposible sacar estadísticas de ventas comparables contra el año pasado, que es justo lo que Andreína pidió».

Por eso Product.price es obligatorio. Colgar de ahí los productos que se están buscando llenaría el catálogo de fichas sin precio que nunca se vendieron.

Por qué sí una tabla nueva, esta vez

En el tablero de búsquedas rechazamos una tabla porque todo lo que necesitaba ya vivía en ServiceQuote. Aquí no hay candidato: Product es el catálogo de ventas y exige precio; productName es texto por cotización.

Y el molde está a diez líneas: Supplier resolvió exactamente este problema — texto libre convertido en entidad con normalizedName @unique.

Y la relación producto ↔ proveedor no se declara

Un producto tiene uno o más proveedores, claro. Pero cada opción ya graba (producto, proveedor, costo, fecha), así que «¿quién nos ha vendido jacuzzis?» es un DISTINCT sobre esas filas. Una tabla de unión crearía una segunda verdad que puede desfasarse de la historia real.

Y hay confirmación de que ese es el camino de la casa: las métricas de proveedor —timesQuoted, timesSelected, avgDeliveryDays, lastUsedAtno están persistidas: se computan al leer con dos groupBy sobre las opciones. Le estamos agregando un eje a un patrón que ya existe.

03El histórico no arranca vacío
el sufijo guarda la evidencia

Hay una casualidad afortunada: el frontend, además de inflar el precio, pega la tarifa a la descripción de la opción"Laptop Lenovo (Tarifa servicio: 5%)". Y ya existe un regex que lo lee de vuelta, escrito para otra cosa. La procedencia quedó grabada.

Lo que dice la filaEl costo original era
(Tarifa servicio: 5%)precio ÷ 1.05
(Tarifa servicio: USD 1.80/u)precio − 1.80
sin sufijoel precio guardado ya es el costo

Esa última línea cubre dos poblaciones de golpe: las del asistente con tarifa en cero, y todas las opciones agregadas desde el panel de detalle, que nunca pasan por la transformación. La regla es completa y no deja zona gris.

El histórico arranca con todo lo que Mogos ha cotizado, con un margen de céntimos por el ida y vuelta del flotante. La alternativa era empezar de cero.

04Los componentes
el dato va donde se decide

La regla que ordena todo esto: el histórico no vive en una pantalla de análisis. Vive debajo del campo donde Verónica está a punto de escribir un precio. Un tablero de precios sería un sitio más al que acordarse de ir; una línea bajo el campo es algo que no se puede no ver.

01 · La fila que se acuerda

Qué le cotizassección 2 del asistente
Jacuzzi
20
Shenzhen Yitai
820.00
cotizado 4 veces con 3 proveedores · mejor $820 · Shenzhen Yitai · marzo · último $910 ↑ 11%
Producto…

Elige el producto del catálogo y la fila se rellena con el mejor trato que Mogos ha conseguido — proveedor y costo, no el último precio. Es el ancla correcta: partir del mejor histórico y ver que hoy está 11% más caro le dice a Verónica exactamente qué negociar. La franja de abajo es la memoria, y el chispograma le da la tendencia sin abrir nada.

Y resuelve el favor que Pedro y Jesús se hacen por WhatsApp —«recomiéndame un proveedor de bicicletas»— sin que nadie pregunte.

02 · La aritmética, en una línea

Costo 820.00 + 12% 918.40 el cliente ve 918.40

Hoy la tarifa es una tira ámbar por producto, con un selector de modo y un campo. Pasa a ser un número por cotización —como dijo Pedro, «eso siempre va a ser un porcentaje»— y cada fila muestra la cuenta en una línea. Tres números y una flecha.

Lo importante es lo que ya no pasa: el costo deja de ser pisado. Se guarda, se muestra a quien cotiza, y nunca entra en lo que se le manda al cliente — que ve un solo número, como hoy.

03 · La ficha del producto buscado

Jacuzzi
Hogar y cocina · 4 cotizaciones · 3 proveedores
Mejor precio
$820marzo
Último
$910↑ 11%
Entrega típica
28días
ProveedorCuándoCostoEntregaQuién preguntó
Guangzhou Sun12 jun 2026910.0025 dVerónica
Shenzhen Yitai02 may 2026865.0030 dVerónica
Ningbo Aqua18 mar 2026890.0035 dVictoria
Shenzhen Yitai04 mar 2026820.0028 dVictoria

Cinco columnas y tres cifras arriba. «Quién preguntó» no es decoración: cuando un precio se ve raro, lo primero que quieres es a quién preguntarle. Y de aquí sale «cotizar esto otra vez», que abre una cotización nueva con el producto y el último proveedor puestos.

05Qué desaparece del trabajo
y qué se gana
HoyDespués
Empezar de cero cada vez que llega el mismo producto La fila parte del mejor trato conseguido. Para un producto repetido, elegirlo es cotizarlo.
Preguntarle a Pedro por WhatsApp quién vende bicicletas Es un DISTINCT. La respuesta está bajo el campo.
La tira ámbar de tarifa por producto Un número por cotización, y la cuenta en una línea por fila.
Calcular el margen a mano en otra herramienta Es una resta: precio − costo. Ambos guardados.
No saber si un proveedor te subió el precio El chispograma, sin abrir nada.

Los clics no bajan — el asistente ya lo dejamos en seis. Lo que cambia es que ahora cada fila que se escribe deja rastro, y la siguiente vez cuesta menos que la anterior. Eso es lo contrario de lo que pasa hoy, donde cada búsqueda empieza exactamente igual de vacía que la primera.

06Qué NO hacemos
y por qué
×

Un tablero de análisis de precios

Sería un sitio más al que acordarse de ir — y ya sabemos cómo termina eso: el cuadro de Pedro tiene dos columnas en cero desde hace seis meses. El dato va debajo del campo donde se decide, no en una pantalla aparte.

×

Un gráfico grande

La pregunta es «¿subió o bajó?», y eso cabe en 54 píxeles. Un gráfico con ejes y leyenda pide atención que esta decisión no merece.

×

Una tabla de unión producto ↔ proveedor

Cada opción ya graba el par con su fecha y su precio. Declararlo aparte sería una segunda verdad que puede desfasarse de la historia real.

×

Colgar el producto buscado del catálogo de ventas

El esquema lo prohíbe con argumento: rompería las estadísticas comparables que pidió Andreína. Se modela espejo de Supplier, con un enlace opcional al catálogo para cuando un producto sí se vuelva ficha de venta.

×

Un campo de margen que alguien teclee

Es una resta entre dos números que ya están guardados. Un tercer campo sería una tercera oportunidad de que no cuadre.

×

Mostrarle el costo al cliente

El costo se guarda en la fila, y la barrera es la misma que ya funciona: nunca entra en el objeto que arma el correo ni en la proyección del cliente. La plantilla, además, es posicional — físicamente no tiene dónde meterlo.

07Qué se puede romper
y cómo lo sabemos
RiesgoArnés
La moneda El campo se llama unitPriceUsd y convive con un currency que acepta USD · BS · EUR · USDT. Para un histórico eso es una mina: comparar bolívares contra dólares a lo largo del tiempo no significa nada. Hay que decidir —antes de la migración— si se normaliza a USD al guardar o si el histórico separa por moneda. No hay opción de dejarlo como está.
El relleno del costo histórico Un ida y vuelta de flotante deja céntimos de error. La migración registra qué regla aplicó a cada fila (sufijo de porcentaje, sufijo de monto, o sin sufijo) para que el resultado sea auditable y no un número que apareció.
El precio deja de ser lo que se escribe Hoy unitPriceUsd es lo que se teclea; pasa a ser derivado. Todo lo que lo lee —las líneas de la factura, los totales, el recálculo— tiene que seguir dando el mismo número. Prueba de archivo dorado sobre cotizaciones existentes: mismo total antes y después.
Duplicados en el catálogo de buscados Es el riesgo de darle identidad al producto. Lo ataja normalizedName @unique más el índice trigrama — el mismo par que ya evita proveedores duplicados. Aun así habrá «Jacuzzi» y «Jacuzzi 6 personas»: conviene poder fusionar dos fichas sin perder su historia.
El índice trigrama Se han borrado cinco veces en este repo. Se declara en el esquema y pasa por la guarda de check:search-indexes antes de compilar.

Cada precio que una fábrica dio, guardado en vez de borrado.

El dato ya se está capturando: alguien le pregunta a una fábrica, la fábrica responde un número, y ese número se escribe en la plataforma. Lo único que pasa es que el navegador lo pisa antes de guardarlo, y que no hay contra qué agruparlo. Arreglar esas dos cosas convierte seis meses de trabajo desechado en la única ventaja que un importador puede acumular.

Se guarda

El costo y la tarifa por separado. El precio se calcula.

Se agrupa

SourcedProduct, espejo de Supplier.

Se consulta

El histórico y el margen. Ninguna tabla de más.

Se decide antes

La moneda. Es lo único que no puede quedarse como está.

mogos · propuesta del histórico de precios · v1.0 Fuente: reunión con Pedro, 21 de julio de 2026 · mapeo de finanzas, proveedores y margen