Mariana escribe por WhatsApp: «quiero 500 libretas con mi logo». Hoy eso va a un papel. Pedro lo dijo sin rodeos: «me llamaron cinco personas, tú lo anotas en un papel de momento». La propuesta de búsquedas ya decidió dónde nace la fila —del mensaje, con sourceMessageId— y quién la trabaja —assignedAgentId, validado por permiso. Lo que faltaba es qué se escribe en la tarjeta, y eso lo dictó Pedro en la reunión, campo por campo.
Hoy
Dos campos, y nace sin encargado a propósito
El diálogo pide cliente y texto. No manda correo ni aviso —eso solo pasa al «enviar al cliente»— y
no guarda encargado por una razón escrita en el código: sellar al creador escondería la solicitud del
resto del equipo. Es una buena razón para el caso general. No es el caso de Pedro: los clientes primero
pasan por mí, hacemos un filtro y se la pongo a Jesús
.
nueva-busqueda-dialog.tsx:45 · use-create-busqueda.ts:41-69 · sendToClient en service-quotes.service.ts:1266
Propuesto
El diálogo crea la búsqueda y su primer producto
Lo que Pedro dictó son campos de producto, y el producto solicitado ya tiene quantity. Se le agregan tres columnas anulables: isMoq —«cotízame el mínimo del proveedor» es una cantidad válida—, isCustomized —con logo o sin logo— y category, que reusa las 33 categorías de órdenes y productos. El encargado es opcional al crear: vacío, la búsqueda cae en «Nuevas» para que alguien la tome, como hoy; puesto, cae en «Mías» de Jesús. Las dos razones sobreviven.
ServiceQuoteRequestedProduct · schema.prisma:2524 (quantity :2530) · MerchandiseCategoryEnum ya en SourcedProduct :2721
La búsqueda cuesta 100 dólares. Es un servicio: ubicar el producto con tres proveedores y comparar precio, calidad y tiempo. Hoy ese cobro no tiene forma: «ahorita los pagos simplemente son un campo abierto, él va pagando lo que le da la gana». Y como no tiene forma, no se puede contar: ni cuántos pagaron, ni cuántos desaparecieron cuando supieron que se cobraba.
La búsqueda nace con su cotización. Al pulsar «Crear y cobrar», la plataforma crea la búsqueda con una sola línea —«Búsqueda de productos», USD 100, o 200 o 300: el monto se edita antes de crear— y le manda a Mariana un mensaje con el enlace de pago. Nada de esto se inventa: el tablero ya tiene la acción «Cobrar», que genera un enlace firmado por lo que falta pagar y lo entrega por WhatsApp. Lo que cambia es que se dispara sola al crear, en vez de esperar a que alguien se acuerde.
WhatsApp · al crear y cobrar
Una acción por mensaje, el monto exacto, firma con nombre y rol, cero emojis porque toca plata. Es la guía de voz aplicada, no un texto nuevo. El botón es el enlace de pago que ya existe —firmado, vence en 24 horas, cobra por Stripe, PayPal o Binance— y que hoy solo sale cuando alguien pulsa «Cobrar».
WhatsApp · al no cobrar
«No cobrar la búsqueda» crea la misma cotización en cero y la marca exonerada. No se manda mensaje ni correo. Es para el amigo de Samuel, para el cliente grande al que los 100 se le meten en un costo — casos que existen y que hoy se registran como si hubieran pagado, lo que ensucia el número.
Hoy
El «descuento búsqueda −100» se escribe a mano
Cuando el cliente pagaba la búsqueda y luego compraba, Pedro agregaba un renglón de descuento por 100
en la cotización grande. Ya no: ya lo tiene dentro de los costos
. Pero el renglón sigue siendo
posible —«Búsqueda de productos» y «Descuento» son los dos primeros del selector de cargos— y cada vez que
alguien lo escribe hay dos verdades sobre los mismos 100.
charge-type-combobox.tsx:32-37 · additional-charge.dto.ts:38 (negativos OK para descuentos)
Propuesto
Exonerada es un estado, no un cero
Una cotización de búsqueda en cero sin la marca es un error; con la marca es una decisión. Se
guarda quién la exoneró y cuándo (waivedAt, waivedById):
dos columnas anulables, ninguna tabla. El embudo de la sección 07 las separa de las pagadas, que es todo
lo que Pedro pidió: lo que te importa es si pagaron los 100 o no
.
ServiceQuote · dos columnas anulables · sin migración de datos
La condición sin la cual «Cobrar» cobraría cero
Aquí está el «monto total cero» que Pedro vio en pantalla, y no es un dato migrado mal: es una regla. La cotización deriva su propio total solo de los productos y sus opciones; una búsqueda recién creada no tiene productos con precio, así que su total es cero aunque la factura tenga la línea de 100. Las dos cifras divergen a propósito, y el código lo documenta. Para que «Cobrar» genere un enlace de 100 y no de cero, el total de una búsqueda tiene que leerse de la factura, que es la que tiene la línea. Es un cambio de lectura, no de modelo — y arregla de paso el cero de las migradas, por la misma razón.
recalculateTotals · service-quotes.service.ts:3007-3046 · divergencia declarada en :2771-2775
Aquí estaba la confusión de la reunión, y se resolvió en la reunión. Pedro venía pensando la gestión de compra como un caso dentro de la búsqueda: «yo pensaba que era dentro de búsqueda de productos, había algunos que eran gestiones de compra». David lo separó: la búsqueda es abrir la vaina y cuesta 100; la gestión es lo que viene después. Son dos documentos distintos para el mismo cliente, y el esquema ya lo sabía: ServiceQuoteTypeEnum tiene los dos valores desde hace meses.
por defecto · editable antes de crear
producto + flete doméstico + gestión 5 % — Alibaba cobra 3 %, la gestión lo cubre
Hoy
Las dos se llaman «búsqueda de producto»
La cotización de 127 de un cliente que ya compró y la de 100 de uno que apenas empieza tienen el mismo
tipo de servicio. Pedro lo vio en pantalla: si coloco búsqueda de producto ahí y después monto otra
búsqueda de producto que son 100 dólares…
. Filtrar por tipo no dice nada.
service-quote.constants.ts:13 · service-quote-schema.ts:104 (etiquetas) · 11-quotations.ts:645 (toda migrada = PRODUCT_SEARCH)
Propuesto
El tipo manda el precio y el documento
PRODUCT_SEARCH siempre nace con la línea de servicio de 100 y un recibo.
CUSTOM_SOURCING nace con la tabla de productos y el asistente de la pieza
anterior. Y como dijo David, así lo diferencia de facilista: filtras el servicio y ya sabes todas las
que fueron
. Cero valores nuevos en el enum; cambia lo que cada valor hace.
ServiceQuoteTypeEnum · schema.prisma:3636 · PRODUCT_SEARCH · CONSOLIDATION · QUALITY_CONTROL · CUSTOM_SOURCING
Mariana paga los 100 por Zelle. Hoy Pedro se entera porque Jesús le avisa: «yo sé que ya pagó, ya lo notificó Jesús». Y el tablero no se mueve, porque nadie volvió a escribirle. La propuesta de búsquedas ya dijo que el estatus se lee, no se marca: es cruzar el estado del trato con el del cobro. Lo que se agrega aquí es qué pasa en ese cruce.
| Cuando… | La plataforma… |
|---|---|
| Se registra el pago de la búsqueda | La cotización de 100 pasa a PAID y la búsqueda aparece en «En búsqueda»
del tablero de su encargado, sin que nadie la toque. Pedro: cuando esté paga salta automáticamente y se ponga aquí en búsqueda. |
| El pago llega a nombre de otro | Pasa mucho: el Zelle lo hace un tercero y el cliente no avisa. El formulario de registrar pago ya pide la referencia —«Referencia de transacción (opcional)», única, y si se deja vacía Mogos inventa una—. Lo que falta es que ese número se vea: en la lista de pagos y en el documento. Hoy se guarda en Transaction.transactionRef y no se imprime en ningún lado. |
| No pagó y no volvió a escribir | La búsqueda queda en NEW con la cotización de 100 sin cobrar. No se borra:
es un lead frío, y contarlos es la mitad del embudo —así también podemos ver cuánta gente perdimos—. |
| Se exoneró | Salta a «En búsqueda» igual que si hubiera pagado, porque el trabajo empieza igual. La diferencia vive en la marca, no en el flujo. |
El salto no es un cron ni un servicio nuevo: es una condición sobre dos columnas que la lista ya devuelve juntas, status y paymentStatus. La pieza de búsquedas lo demostró para «En seguimiento»; aquí es la misma regla con otro par de valores.
La cotización de la compra se pagó. El proveedor termina la producción y los tubos pesan más: se venden por peso, y el sobrante se manda y se cobra. El flete doméstico, en cambio, salió más barato. Pedro necesita tocar dos líneas de una cotización pagada, y hoy no puede: «hay que hacer lo que dijo David de yo poder modificar los precios después». Lo resolvió con un descuento de 40 dólares. Funciona, y miente: el documento dice «descuento» donde hubo un ajuste de peso y uno de flete.
Hoy
«Pagada» no es el candado. El candado es de qué está hecha la línea
Una factura pagada sí se edita: el candado por cobro se quitó en julio, con una explicación escrita
en el código, y solo CLOSED y CANCELLED siguen cerradas.
Lo que no se edita es la línea de producto, porque es derivada: se reconstruye desde el proveedor
elegido en cada escritura, y el API responde cámbiale el precio al proveedor elegido y el total se
actualiza
. Ese camino no tiene guardia de estado: funciona en una cotización pagada. Cuando existe.
service-quotes.service.ts:2010-2038 assertQuotationNotClosed · :2436 assertChargeIsEditable · :1788 updateProductOption sin guardia
Hoy · peor
Las migradas no tienen proveedor al que cambiarle el precio
La migración escribió la cotización y sus líneas de factura, y ningún producto solicitado: 591 cotizaciones sin una sola fila detrás. Por eso «Productos solicitados» sale vacío, por eso el total del resumen es cero, y por eso lo único editable es el cargo manual —las líneas «como otro» que Pedro sí pudo tocar—. Ya costó caro una vez: el PDF imprimió 283.720 en una factura de 140.920, desde una migrada sin productos.
11-quotations.ts:640-660 (el INSERT, sin requested_products) · service-quote-document.service.ts:48-50
Propuesto · corregir, no descontar
El camino ya está abierto; lo que falta es que exista en todas y que deje rastro. Se corrige el precio del proveedor elegido y el monto del flete doméstico, y la línea se recalcula sola, como ya lo hace. Se agregan tres cosas: que las migradas tengan ese proveedor, que cada corrección deje bitácora, y que la diferencia contra lo pagado se vuelva saldo —a favor o por cobrar— en vez de un descuento inventado.
| Línea | Cant | Precio U. | Monto |
|---|---|---|---|
| Tubos de acero galvanizado · 2"Shenzhen Hengli · vendido por peso · 2,10 t finales | 1 | 6,520.006,465.00 | 6,520.006,465.00 |
| Flete doméstico · fábrica → almacén Guangzhou | 1 | 245.00340.00 | 245.00340.00 |
| Gestión de compra (5 %) | — | — | 338.25340.25 |
Bitácora · quién, cuándo, cuánto, por qué
6,465.00 → 6,520.00«producción terminó con 55 kg de más; los tubos se venden por peso, la clienta lo sabía»340.00 → 245.00«el transportista cobró menos de lo cotizado»saldo a favor USD 42.00 — sin descuento, sin línea a mano.Por qué bitácora y no solo permiso
Cuando en julio se quitó el candado por cobro, se dejó a cambio un aviso en el log: cada edición sobre una factura con pagos emite service_quote_charges_edited_with_payments y marca si quedó sobrepagada. Es la bitácora correcta en el sitio equivocado: nadie lee el log. Se guarda donde Pedro y el cliente la ven: el par before/after que el endpoint ya calcula en su respuesta, más el motivo, obligatorio. El monto pagado nunca se toca — cambia el saldo.
service-quotes.service.ts:2334-2340 (before/after) · :2390-2424 (el warn-log con overpaid)
Y las migradas
Para que el camino exista también en ellas, se les siembra el producto que les falta: una fila de ServiceQuoteRequestedProduct con su opción elegida, tomada de la línea que ya tienen. Es un script de una pasada, con conteo antes y después. El día 1, «Productos solicitados» deja de salir vacío y el total deja de ser cero — sin tocar ningún monto.
- El saldo a favor es un abono: el goal de proveedores de julio ya lo pedía para flete futuro.
- El «descuento» sigue existiendo para lo que es un descuento. Deja de ser el comodín.
Pedro escribe las especificaciones dentro del nombre del producto «para que pueda salir aquí». La columna existe —y se esconde sola cuando ninguna línea trae specs, que es siempre en una migrada, porque no hay producto del que leerlas—. Y los pagos salen con método, fecha y monto, sin la referencia, así que cuando un Zelle llega a nombre de otro no hay cómo casarlo con el documento. Es una columna que se destapa, un dato que se imprime, y una cabecera más compacta, con el cuadro a la derecha, que Pedro pidió y llamó estética — y tiene razón: es lo primero que ve el cliente.
Mogos C.A. · J-500832109
Quinta Orión, La Castellana · Caracas
0424-1856592 · info@mogosgroup.com
Facturar a
Mariana Ferrer
V-00.000.000+58 412 ··· 4471
mariana@ejemplo.com
Ruta
China → Venezuela · marítimo
Almacén GuangzhouCBM y flete marítimo se cotizan aparte
| # | Producto | Especificaciones columna propia | Cantidad | Precio U. | Monto |
|---|---|---|---|---|---|
| 1 | Libretas A5 tapa dura | Portada con logo a 1 tinta · 120 hojas · papel 80 g · con logo | 500 | 4.20 | 2,100.00 |
| 2 | Flete doméstico | Fábrica Yiwu → almacén Guangzhou | 1 | 85.00 | 85.00 |
| Fecha | Método | N.º de confirmación | Monto |
|---|---|---|---|
| 22 ago 2026 | Zelle · a nombre de un tercero | ZL-8827-4410 | 688.28 |
| 30 ago 2026 | Transferencia · Bank of America | BOA-2026-0830-117 | 1,605.97 |
Firma autorizada
Hoy
La columna de specs se esconde; la referencia no se imprime
La plantilla viva imprime «Especificaciones» solo si alguna línea las tiene, y las busca en el producto solicitado —por id, o por descripción normalizada cuando la línea es migrada y no trae id—. Sin producto detrás, la columna desaparece y Pedro las mete en el nombre. Los pagos se imprimen con tres columnas: método, fecha, monto. La referencia se guarda y no sale.
service-quote.template.ts:75-78 (columna condicional) · :134-159 (pagos) · service-quote-document.composition.ts:203-214
Propuesto
Una columna que se destapa, un dato que se imprime, y depende de Gaby
Con las migradas sembradas (sección 05) la columna aparece sola: no hay que tocar la plantilla para eso. Lo
que se agrega es la referencia en la tabla de pagos, leída de donde ya está. Si Gaby o Andreína la
dejan vacía, Mogos genera una — se imprime esa, que también sirve para buscar. David lo pidió por una razón
concreta: para poder buscar en caso de que no consiga
el pago entre tantos Zelle.
Transaction.transactionRef · schema.prisma:1296 · register-payment-form.tsx:814-830
Pedro lo dijo con números inventados y con la pregunta exacta: «una estadística de 100 clientes que pagaron la búsqueda, 80 le dieron play, los otros 20 no». Nada de esto se puede contar hoy porque las dos cotizaciones tienen el mismo tipo y no se apuntan entre sí. Con el tipo bien puesto, la marca de exonerada y searchQuoteId, el embudo es una consulta sobre columnas que ya existen.
Cifras de ejemplo. Cada barra es un count sobre serviceType,
paymentStatus, waivedAt y searchQuoteId. Sin tabla de métricas, sin
cron. El corte por rubro y por encargado sale de las mismas columnas — y de ahí la decisión que Pedro dejó
abierta: si los 100 espantan, «después vemos, a 60». Con el número en la mano, no a ojo.
Un tercer tipo de servicio «gestión de compra»
Estuvo sobre la mesa y se descartó en la reunión. Gestión de compra es lo que termina siendo el sourcing personalizado cuando Mogos paga en China. Ya tiene valor en el enum; darle otro nombre crea dos filtros para el mismo trabajo.
Un renglón de −100 automático en la cotización grande
Pedro ya lo dejó de hacer: los 100 van dentro de los costos. Automatizar un renglón que se abandonó es construir el pasado. El vínculo entre las dos cotizaciones es searchQuoteId, no un descuento.
Otra pasarela ni otro botón de cobro
El enlace de pago firmado, Stripe, PayPal, Binance y la entrega por WhatsApp ya existen detrás de «Cobrar». Esta pieza solo decide cuándo se dispara. El portal donde el cliente paga es otro proyecto, con su propio ritmo.
Cuentas por pagar al proveedor y la ganancia real
El cuadro de «control de gestiones» de Lark, el 3 % de Alibaba, lo que se le pagó al chino. Es el goal de proveedores de julio, con su propio alcance. Esta pieza le deja el vínculo listo: cada sourcing sabe de qué búsqueda vino.
Tocar los permisos de Jesús
Pedro lo decidió en vivo: «lo puedes dejar así, en verdad». Jesús monta cotizaciones y sabe cuánto paga el cliente y cuánto se ganó. Esconderle montos a quien cotiza es esconderle su trabajo.
Sincronizar con Notion y rehacer el centro de acciones
Los dos salieron al final, y los dos se pospusieron: «no vamos a entrar en esas aguas todavía». El centro de acciones ya existe y ya sabe de «Tomar»; cuando se mejore, la búsqueda pagada será una acción más.
| Riesgo | Arnés |
|---|---|
| Editar después de pagado | Es la invariante más vieja del módulo. Se abre solo con motivo obligatorio y bitácora; el monto pagado es inmutable y lo que cambia es el saldo. Prueba: ninguna corrección puede alterar Payment.amount; si lo hace, el endpoint está mal. |
| Sembrar productos en las migradas | Un script de una pasada que crea ServiceQuoteRequestedProduct desde la línea existente. Se corre con conteo antes y después, y con la regla de que ningún total cambia: si un total se mueve un centavo, se revierte. |
| El mensaje de los 100 espanta | Va a pasar: «no sabía que me iba a cobrar 100 dólares, no te responden más». No es un bug, es el dato que hoy no se tiene. El texto lo aprueba Pedro antes de salir, y «No cobrar» existe desde el día 1 para quien Pedro decida. |
| Encargado al crear esconde la cola | El código lo evitó a propósito: una búsqueda con dueño desde el inicio no aparece en «Nuevas» ni como TAKE_SEARCH en el centro de acciones. Por eso el campo es opcional y nace vacío: solo Pedro lo llena cuando ya decidió a quién va. La cola compartida sigue existiendo para todo lo demás. |
| El total de la búsqueda leído de la factura | Hoy las dos cifras divergen a propósito y el código lo declara. Cambiar de dónde se lee el total de una PRODUCT_SEARCH toca el resumen, la lista y «Cobrar». Prueba: una búsqueda con la línea de 100 y cero productos genera un enlace de 100, no de cero; y ninguna cotización con productos cambia de total. |
| El embudo con historia vieja | Las búsquedas anteriores a esto no tienen searchQuoteId ni marca de exonerada. El embudo las muestra como «sin vínculo», aparte, en vez de contarlas como perdidas. Un número honesto desde el primer día, no uno inflado hacia abajo. |
| Los índices de búsqueda por trigrama | Ya se borraron cinco veces en migraciones anteriores. Cualquier migración de esta tanda pasa por la guarda de check:search-indexes antes de compilar. |
Que el ciclo entero viva en la plataforma, no en el papel de Pedro.
Siete estaciones, y en ninguna hay una tabla nueva: dos valores de enum que ya existían, seis columnas anulables, un script de siembra, un total que se lee de otro sitio y una bitácora donde hoy hay un log. Lo que cambia es que cada cosa que pasa entre el primer mensaje y el último pago deja rastro donde se puede contar.
Antes de esto
La pieza multiempresa: el membrete, el motor de documentos y que «enviar» envíe.
Queda fuera · 01
Cuentas por pagar al proveedor y la ganancia real. El goal de proveedores.
Queda fuera · 02
La pasarela detrás del botón «Pagar USD 100».
Queda fuera · 03
El centro de acciones mejorado, y Notion. Pedro los dejó para después, a propósito.