Parte V · En la práctica
Un sistema de ventas
Hasta aquí, ejemplos de una línea. A partir de ahora, un negocio entero: un spa que vende servicios, emite comprobantes, paga impuestos y cobra en dos monedas. Es donde la teoría se vuelve código que puedes ejecutar.
Son las 18:05 de un jueves en Serena Termas, un spa de barrio. Una clienta (Lucía) acaba de salir de la cámara de vapor, se acerca al mostrador y pide pagar: un circuito termal y un masaje descontracturante. La recepcionista teclea, el sistema calcula el subtotal, le suma el impuesto al consumo, descuenta un cupón de fidelidad, cobra la mitad en efectivo y la mitad con una tarjeta extranjera en otra moneda, e imprime una boleta numerada que esa misma noche viajará al organismo tributario. Toda esa coreografía (servicio, precio, impuesto, comprobante, divisa, fidelidad) ocurre en menos de un minuto y deja un rastro que un contador, un auditor y el dueño tendrán que poder leer meses después. Ese minuto es el examen de este capítulo.
Hasta este punto, el libro te explicó cada decisión de diseño con escenas pequeñas y autocontenidas: una venta de camiseta que se registra, una jugada de fútbol con dos agentes, una ordenanza que entra en vigor a los treinta días. Eran lo bastante chicas para caber en una página. Pero ninguna arquitectura merece llamarse universal si no sostiene un negocio completo, con sus impuestos, sus comprobantes legales y sus monedas. Un spa parece el dominio más inofensivo del mundo (toallas, vapor, música suave) y es justo por eso que conviene empezar aquí: bajo esa superficie relajada se esconde un sistema de ventas con casi todas las exigencias de un comercio formal.
Lo que pone a prueba este capítulo
Un mismo negocio nos obliga a usar, a la vez, casi todas las herramientas de las Partes II y III: los siete ejes, los hechos atómicos, las situaciones reificadas, el catálogo de roles y el lexicon. Y le añade lo que un comercio real no perdona: autoridad tributaria, comprobantes numerados, impuesto al consumo y cobros en varias divisas.
Serena Termas en una página
Fijemos el negocio antes de modelarlo. Serena Termas opera en una sede con dos cámaras de vapor y una cámara seca a 58 °C, una sala de masajes y una pequeña barra de jugos. El catálogo es corto y claro:
♨️
Circuito termal
Veinte minutos por cámara y una ducha helada de cierre. Precio de lista: S/ 80. El producto estrella.
💆
Masaje descontracturante
Cincuenta minutos con una terapeuta. S/ 140. Se vende solo o como complemento del circuito.
🧃
Barra de jugos
Bebidas y fruta. Aquí lo vendido no es un servicio sino un bien: importa para el impuesto, como veremos.
Y, como todo comercio, tiene reglas de negocio que la contabilidad de juguete suele maltratar:
- Fidelidad. A la décima visita pagada, el circuito de la undécima sale gratis.
- Impuesto al consumo. Cada venta lleva un impuesto del 18 % que el spa cobra al cliente y luego entrega al fisco. No es ingreso del negocio: es dinero en tránsito.
- Comprobantes. Toda venta exige un documento numerado (boleta o factura) con una serie correlativa que la autoridad tributaria audita.
- Multi-divisa. La mitad de la clientela es de paso y paga con tarjetas en dólares; la contabilidad lleva todo en la moneda local.
Usaremos tres clientes para estresar el modelo: Lucía Rentería, fiel y a punto de ganar su sesión gratis; Martín Paredes, un turista que paga en dos monedas; y Noa Cifuentes, que pidió factura a nombre de su empresa. Tres personas bastan para tropezar con casi todos los bordes interesantes.
Paso 1 · Acomodar el negocio en los siete ejes
El primer trabajo de quien modela datos es repartir las entidades del negocio en las coordenadas correctas. Es la decisión que más se paga después: si una cosa cae en el eje equivocado, el sistema cojea para siempre. Así se reparte Serena Termas.
Serena Termas y el propio fisco viven en
Q, porque ambos son agentes que cobran y reciben dinero. Las cámaras viven en
L (son lugares donde ocurre la sesión) pero también en O (son
máquinas con temperatura y mantenimiento): una misma cosa puede habitar dos ejes según la
pregunta. La banda inferior, el eje M, no contiene entidades sino los cables
que las conectan.Repara en la barra de jugos. Un servicio y un jugo parecen lo mismo en la caja
registradora, pero el fisco los trata distinto: el servicio es prestación, el jugo
es bien, y de eso depende qué impuesto aplica. Modelarlos como clases distintas en
K (servicio_spa frente a bien_consumible) no es
pedantería: es lo que permite, más tarde, declarar el impuesto correcto sin un solo
if incrustado en el código de la caja.
El nudo central · la venta
Si tuviéramos que elegir el evento alrededor del cual orbita todo el negocio, sería la venta. Quién la hizo, qué incluyó, dónde, cuándo, cuánto sumó, qué impuesto gravó, qué comprobante la respalda y cómo se pagó: la venta es el punto donde los siete ejes se cruzan. Tal como vimos al tratar el hecho atómico (D3), no la guardamos como una fila gorda, sino como una constelación de tripletas tipadas que se apilan.
La venta de las 18:05 a Lucía (un circuito más un masaje) se reifica como una situación en
O (D4: lo merece, porque agrupa varias partes, tiene fecha y participa en otras
relaciones) y se describe con esta lista de hechos atómicos:
(venta_4471) ∈ O
instancia_de : transaccion_venta # K · qué clase de cosa es
cliente : lucia_renteria # Q · quién compró
vendido_por : serena_termas # Q · qué agente vendió
lugar_de : sede_central # L · dónde ocurrió
inicio : 2026-06-11T18:05-05:00 # T · cuándo
comprende : item_4471_a # O · una línea de la venta
comprende : item_4471_b # O · otra línea
moneda_base : soles # K · la moneda de la contabilidad
subtotal : 220.00 # N · antes de impuesto
impuesto_total : 39.60 # N · el 18 % del gravado
total : 259.60 # N · lo que se cobra
documentado_por : boleta_4471 # O · el comprobante legal
estatus_factual : pagada # K · su estado
Cada línea de la venta es a su vez un objeto en O, porque cada una tiene su
propio precio, su propia clase de producto y —esto es lo importante— su propio tratamiento
tributario. El masaje y el jugo no se gravan igual; meterlos en el mismo campo perdería esa
distinción para siempre.
(item_4471_a, instancia_de, circuito_termal) # un servicio
(item_4471_a, parte_de, venta_4471)
(item_4471_a, precio_unitario, 80.00) # N · soles
(item_4471_a, gravado_con, impuesto_consumo_18) # K · le aplica el 18 %
(item_4471_b, instancia_de, masaje_descontracturante)
(item_4471_b, parte_de, venta_4471)
(item_4471_b, precio_unitario, 140.00)
(item_4471_b, gravado_con, impuesto_consumo_18)
El cable parte_de hace de hilo: cada línea cuelga de su venta, y la venta de su
comprobante. Recorriendo ese hilo, el sistema reconstruye la jerarquía completa sin un solo
JOIN. Visto como tripleta, ese andamiaje se lee de un vistazo:
Y en el código real del prototipo, construir esa venta no exige redactar las tripletas a mano: una llamada de pocas líneas consulta el lexicon, desambigua el verbo, crea la situación y le enchufa los cables: validando que ninguno esté mal puesto.
venta = ingest_situation(u, lex, "vender",
roles={
"cliente": lucia,
"vendido_por": serena_termas,
"lugar_de": sede_central,
"inicio": t_venta,
},
complements=["circuito", "masaje"], # el lexicon arma las dos líneas
extra={
"moneda_base": soles,
"estatus_factual": pagada,
},
sit_id="venta_4471",
)
El sistema se niega a aceptar disparates
Si un programador distraído intentara poner a lucia en el rol
lugar_de, o un texto donde va una cantidad, ingest_situation
aborta de inmediato con un error de firma (SignatureError). El rol
lugar_de exige un habitante del eje L; una persona no lo es. La
validación no es opcional: es la frontera que impide que la basura entre al grafo.
El impuesto al consumo · dinero que solo está de paso
Aquí es donde un sistema de ventas se separa de un cuaderno de apuntes. Ese 18 % que aparece en la boleta no es ingreso del spa: es dinero que el negocio cobra por cuenta del fisco y que tendrá que entregarle. Confundir ambas cosas (tratar el impuesto como ganancia) es uno de los errores contables más caros que existen. El modelo lo mantiene separado por construcción.
El impuesto no se guarda como un número suelto pegado a la venta, sino como un objeto con identidad: sabe cuánto es, sobre qué base se calculó, qué tasa aplicó y a quién se le debe.
(impuesto_4471) ∈ O
instancia_de : impuesto_consumo_18 # K · qué tributo es
grava : venta_4471 # O · sobre qué venta recae
base_imponible : 220.00 # N · el subtotal gravado
tasa : 0.18 # N · la alícuota vigente
monto : 39.60 # N · lo que se debe
acreedor : fisco # Q · a quién se le entrega
estatus_factual: por_remitir # K · cobrado, aún no entregado
Modelarlo así tiene una consecuencia inmediata y deliciosa: la pregunta del contador
«¿cuánto impuesto debo entregar este mes?» deja de ser un cálculo que alguien
reprograma cada vez que cambia la ley, y pasa a ser una simple suma sobre los objetos cuyo
acreedor es el fisco y cuyo estado sigue en por_remitir. Y cuando
la tasa cambie del 18 % al 16 %, las ventas viejas conservan su tasa congelada: se
calcularon con la alícuota que regía ese día, y esa verdad histórica no se reescribe.
El comprobante · cuando la ley pide un número correlativo
Un comercio formal no puede limitarse a saber que vendió: debe emitir un documento numerado, de una serie correlativa que la autoridad tributaria audita. Si la boleta 4470 existe y la 4472 existe, la 4471 tiene que existir; un hueco en la numeración es, para un auditor, una venta sospechosamente desaparecida. El comprobante, por tanto, no es un adorno de la venta: es un objeto de pleno derecho, con su propia clase, su propio número y su propio destino regulatorio.
(boleta_4471) ∈ O
instancia_de : tipo_boleta_venta # K · boleta (consumidor final)
serie : B001 # K · la serie correlativa
numero : 4471 # N · correlativo dentro de la serie
respalda : venta_4471 # O · qué venta documenta
emitido_por : serena_termas # Q · el emisor
receptor : lucia_renteria # Q · a nombre de quién
emitido_el : 2026-06-11T18:06-05:00 # T
estatus_factual: emitida # K · emitida, anulada, etc.
La distinción entre boleta y factura (una para el consumidor final, otra
a nombre de una empresa que descontará el impuesto) es, en el modelo, nada más que dos clases
distintas en K: tipo_boleta_venta y tipo_factura_venta.
Cuando Noa pidió factura a nombre de su empresa, el sistema no necesitó otra tabla ni otro
flujo: la misma maquinaria emitió un objeto de otra clase, con un campo extra para el
identificador tributario del receptor.
Comprobante de venta
Documento legal, numerado en serie correlativa, que respalda una
transacción ante la autoridad tributaria. En WQuestions es un objeto en O con
su clase (tipo_boleta, tipo_factura), su número, su emisor, su
receptor y un cable respalda hacia la venta. La numeración correlativa es una
invariante auditable: el grafo permite verificar que no falta ninguno.
Multi-divisa · dos monedas, una contabilidad
Martín, el turista, pagó su circuito en dos partes: la mitad en efectivo, en moneda local,
y la mitad con una tarjeta en dólares. La trampa clásica es guardar «pagó 22.50» sin decir
de qué moneda hablamos (el pecado capital de los sistemas que asumen una sola divisa). En
WQuestions, un monto nunca viaja solo: siempre arrastra su unidad, que es un habitante de
K. Y un pago en otra moneda guarda, además, la tasa de cambio con la que se
convirtió, fechada en el día de la operación.
(cobro_4480_efectivo) ∈ O
instancia_de : pago_recibido
paga : venta_4480 # la venta de Martín
monto : 40.00
moneda : soles # K · moneda local
medio : efectivo
(cobro_4480_tarjeta) ∈ O
instancia_de : pago_recibido
paga : venta_4480
monto : 10.75 # N · lo que se cobró
moneda : dolares # K · ¡otra unidad!
tasa_cambio : 3.72 # N · soles por dólar, ese día
equiv_base : 40.00 # N · su valor en moneda local
medio : tarjeta_credito
Como cada cantidad declara su unidad, el sistema nunca suma peras con manzanas. Para totalizar la caja del día convierte cada cobro a la moneda base usando la tasa que ya quedó guardada con él —no la de hoy, sino la del momento del cobro—, y la contabilidad cierra al centavo aunque hayan entrado cuatro monedas distintas. La unidad como ciudadano de primera clase, que el capítulo del eje cuantitativo defendió en abstracto, aquí se cobra su sueldo.
El lexicon del spa · enseñarle a la máquina el idioma del negocio
Para que la recepcionista no tenga que aprender jerga de bases de datos, configuramos el lexicon con las palabras exactas del spa. El lexicon es el compilador del que habló la Parte IV: traduce el vocabulario humano a los roles canónicos del modelo, y de paso resuelve la polisemia. El verbo «tomar», por ejemplo, significa cosas distintas según lo que lo acompañe:
lex.register(LexiconEntry(
verb="tomar",
situation_type="servicio_spa",
obligatory=["cliente", "lugar_de"],
pattern=("sesion",), # "tomar una sesión" → activa esta regla
))
lex.register(LexiconEntry(
verb="tomar",
situation_type="accion_decidir",
obligatory=["agente", "objeto"],
pattern=("una_decision",), # "tomar una decisión" → otra regla
))
Y le instalamos el «dialecto corporativo» del spa (D8/D9): un mapa entre las palabras que el personal usa de verdad y las etiquetas canónicas que el catálogo conoce. El usuario final nunca toca esas etiquetas internas; el sistema traduce por debajo.
lex.register_domain_dialect("serena_termas", {
"cliente": "agente",
"sesion": "servicio_spa",
"boleta": "tipo_boleta_venta",
"factura": "tipo_factura_venta",
"sesion_gratis": "beneficio_fidelidad",
"igv": "impuesto_consumo_18",
})
Gracias a esto, la recepcionista puede escribirle al sistema «emítele una boleta a Lucía por una sesión, con su sesión gratis aplicada» y el grafo lo traduce a sus identificadores internos sin que ella sepa que existen.
La interfaz es el lexicon, no el catálogo
El catálogo canónico (impuesto_consumo_18,
tipo_boleta_venta) es invisible para quien usa el negocio. Lo que ve es su
propio idioma: «IGV», «boleta», «sesión gratis». Dos spas con vocabularios distintos pueden
compartir el mismo catálogo y aun así cada uno hablar como siempre habló.
Tres preguntas de negocio, resueltas como recorridos del grafo
El valor de un sistema se mide por la facilidad con que responde las preguntas que de verdad importan a fin de mes. Veamos tres clásicas de este spa y cómo el código las resuelve recorriendo tripletas en vez de encadenar tablas.
Pregunta 1 · ¿Cuántos circuitos pagados lleva Lucía? ¿Le toca el gratis?
No hay JOIN que escribir: el motor busca un patrón en el grafo (todas las ventas
de clase circuito_termal cuyo cliente sea Lucía y cuyo estado sea
pagada) y las cuenta.
n = count(u, Pattern(
fixed={"cliente": lucia, "estatus_factual": u.ind("pagada")},
type_constraint=u.ind("circuito_termal"),
))
le_toca_gratis = n >= 10 # la regla de fidelidad, en una línea
El modelo no ejecuta la regla de negocio por su cuenta (para eso hace falta un motor externo o una IA); lo que hace es entregar el dato prístino y estructurado para que ese motor decida sin equivocarse. La división del trabajo es deliberada: el grafo guarda la verdad, la lógica vive afuera.
Pregunta 2 · ¿Cuánto impuesto debo entregarle al fisco este mes?
Una suma sobre todos los objetos impuesto cuyo acreedor es el fisco y que siguen pendientes
de remitir. La ley puede cambiar la tasa: las ventas viejas no se tocan, cada una ya guardó
la suya.
deuda_fiscal = suma(u, "monto", Pattern(
fixed={"acreedor": fisco, "estatus_factual": u.ind("por_remitir")},
type_constraint=u.ind("impuesto_consumo_18"),
))
Pregunta 3 · ¿Quedó algún hueco en la numeración de boletas?
La auditoría más temida del negocio se vuelve trivial: pide al grafo todos los números de la
serie B001 y verifica que sean correlativos. Un salto delata una boleta
extraviada —o un fraude.
Q, estado en K)
y restringe por tipo; el motor recorre los objetos impuesto que encajan y proyecta su
campo monto sobre el eje N, donde los agrega. La misma mecánica
responde «cuántos circuitos» (un conteo) o «cuánto facturé» (otra suma): cambia la
proyección, no la maquinaria.Detente un segundo aquí, porque esa suma es la primera aparición de algo que volverá en cada dominio del libro. No preguntamos por una venta del spa: preguntamos por todo el impuesto del mes a la vez. Modelar con cuidado la venta de un circuito de aguas termales fue, sin proponérnoslo, dejar listo el reporte fiscal del negocio entero.
El «por qué» de una venta · los cuatro cables
En un negocio real, saber qué pasó no basta; a menudo hace falta saber por qué. Tal como argumentó el capítulo del «por qué» (D7), no existe un eje «por qué»: el porqué se reparte en cuatro cables distintos, y un sistema de ventas los necesita a casi todos. Dos ejemplos del spa lo dejan claro.
El descuento que tiene una causa normativa. A Lucía se le aplicó su circuito gratis
de la décima visita. Ese descuento no es un capricho de la recepcionista: está
justificado por el contrato de fidelidad, una regla del negocio. El cable
justificado_por ata el descuento a la norma que lo autoriza, de modo que
cualquier auditor pueda rastrear por qué esa venta cobró menos —y comprobar que fue
legítimo, no un robo.
El cliente que casi compra. Noa quiso contratar el plan mensual, pero no cerró ese
día. En una caja registradora común, ese dato se pierde. En WQuestions se guarda como una
intención: una situación con modalidad volitiva y estado
intencionado, no real. Cuando marketing pregunte «¿a quién
llamamos para ofrecer el plan?», Noa salta a la lista; cuando contabilidad pregunte «¿qué
planes están activos?», Noa no aparece y nadie le cobra por error.
(descuento_4471, aplica_a, venta_4471)
(descuento_4471, justificado_por, contrato_fidelidad_lucia) # ← regla normativa
(intencion_noa_01, instancia_de, contrato_plan_mensual)
(intencion_noa_01, cliente, noa_cifuentes)
(intencion_noa_01, modalidad, volitiva) # ← lo quería
(intencion_noa_01, estatus_factual, intencionado) # ← aún no es real
La distinción entre lo intencionado y lo real es la misma que separa, en banca, una solicitud de un préstamo desembolsado, o en una clínica un diagnóstico de sospecha de uno confirmado. Que un spa la necesite tanto como un banco es, en sí mismo, una pequeña confirmación de que el modelo capturó algo general.
El antes y el después · del esquema fragmentado al grafo único
Antes, en SQL. En un esquema relacional clásico, Serena Termas vive
repartido en media docena de tablas que no se hablan solas: clientes,
ventas, lineas_venta, pagos, comprobantes,
impuestos. Responder «¿cuánto IGV debo este mes, descontando las ventas anuladas
y convertido todo a moneda local?» obliga a encadenar cinco JOIN frágiles,
filtrar por estado, cruzar tasas de cambio y dejar la mitad de la lógica enterrada en código
de aplicación que nadie documenta y que se rompe cada vez que cambia una regla.
-- Antes: venta, línea, impuesto y comprobante viven en tablas distintas;
-- la pregunta tributaria obliga a encadenarlas con JOINs frágiles.
CREATE TABLE ventas (id INTEGER PRIMARY KEY, cliente_id INT, estado TEXT, moneda TEXT);
CREATE TABLE lineas_venta (id INTEGER PRIMARY KEY, venta_id INT, producto_id INT, precio NUMERIC);
CREATE TABLE impuestos (id INTEGER PRIMARY KEY, venta_id INT, tasa NUMERIC, monto NUMERIC);
CREATE TABLE comprobantes (id INTEGER PRIMARY KEY, venta_id INT, serie TEXT, numero INT);
SELECT SUM(i.monto) AS igv_por_pagar
FROM impuestos i
JOIN ventas v ON v.id = i.venta_id
JOIN comprobantes c ON c.venta_id = v.id
WHERE v.estado = 'pagada' -- ojo: ¿y las anuladas?
AND c.numero IS NOT NULL; -- la lógica de moneda… vive en otra parte
Después, en WQuestions. La misma información es un grafo único de hechos atómicos:
cada impuesto es un objeto con su cable acreedor → fisco y su
estatus_factual. La pregunta se vuelve una suma sobre un patrón (sin migrar
esquema, sin JOIN, sin lógica escondida en procedimientos almacenados). Y
cuando la tasa cambie, se edita un solo hecho en el catálogo; las ventas pasadas conservan la
suya y el resto del sistema ni se entera.
# Después: la misma pregunta, como un patrón sobre el grafo de hechos.
igv_por_pagar = suma(u, "monto", Pattern(
fixed={"acreedor": fisco, "estatus_factual": u.ind("por_remitir")},
type_constraint=u.ind("impuesto_consumo_18")))
En un sistema de ventas, el impuesto no es un número: es una deuda con un acreedor, una base y una fecha. Tratarlo como un objeto, y no como una columna, es lo que convierte la auditoría en una consulta.La lección del spa
Cuánto se vendió · el negocio en una barra
Un sistema de ventas que no sabe responder «¿qué se vendió más?» no merece el nombre. Como cada línea de venta es un objeto con su clase de producto, totalizar por servicio es —otra vez— una agregación sobre el grafo. Este fue el desglose de ingresos de un mes en Serena Termas:
K.Balance · el spa fue un cliente amable
Cerremos repasando qué demostró el prototipo al absorber este negocio de punta a punta:
El veredicto del primer dominio industrial
- Los siete ejes funcionan juntos. Clientes y fisco en
Q, ventas y comprobantes enO, cámaras enL, momentos y vigencias enT, precios, impuestos y tasas enN, tipos y estados enK, y los predicados deMcosiéndolo todo. - El sistema de ventas, completo. Comprobantes numerados auditables, impuesto al consumo como dinero en tránsito, cobros en dos divisas con su tasa fechada. Nada de esto pidió ingeniería nueva: cayó sobre la maquinaria existente.
- La historia se preserva (D6). Una tasa de impuesto que cambia no reescribe las boletas viejas; cada comprobante sigue siendo coherente con la ley que regía el día en que se emitió.
- Cero parches. Todo el negocio se modeló con los roles base de la arquitectura y un puñado de clases en el catálogo. Donde un sistema tradicional pediría media docena de tablas y procedimientos almacenados, aquí bastaron tripletas y un lexicon.
El spa parecía el dominio más inofensivo del libro y resultó ser un sistema de ventas completo, con su autoridad tributaria mirando por encima del hombro. El modelo lo absorbió sin doblarse. Pero un comercio de barrio opera a su propio ritmo: las ventas no llegan por miles ni cambian de estado cada segundo. En el próximo capítulo subimos la apuesta hacia la velocidad y la concurrencia: un servicio on-demand, donde un viaje en taxi cambia de estado una docena de veces en quince minutos y miles ocurren a la vez.