Files
brahman/crates/modules/nakui/core/tests/sales.rs
T
sergio 78fbde12b4 feat(nakui): módulo crm — clientes, pipeline de ventas, interacciones
Módulo CRM declarativo (schema.ncl + nsmc.json + morfismos Rhai) con
tres entities (Cliente, Oportunidad, Interaccion) y tres morfismos:
abrir_oportunidad, mover_oportunidad (pipeline con validación de
transiciones) y registrar_interaccion.

crm_demo: demo realista de 18 eventos que —a diferencia de los otros
demos— conserva el event log e imprime el comando de nakui-explorer,
así el explorador muestra un CRM con cuerpo. tests/crm.rs: 8 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:21:09 +00:00

162 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Cross-module integration tests. The `sales` module references entities
//! defined in `treasury` and `inventory` via its manifest's `schemas` list.
//! These tests assert:
//! - The kernel correctly bundles multiple .k files at module load.
//! - Per-entity KCL post-checks fire against the right schema even when
//! three are concatenated.
//! - A non-conserving morphism (sale = stock1, caja+price) passes the
//! kernel cleanly because no `invariants.conserve` was declared.
use std::path::{Path, PathBuf};
use nakui_core::executor::{ExecError, Executor};
use nakui_core::store::{MemoryStore, Store};
use serde_json::{json, Value};
use uuid::Uuid;
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root above core/")
.to_path_buf()
}
fn sales_module() -> PathBuf {
workspace_root().join("modules/sales")
}
fn caja_saldo(store: &MemoryStore, id: Uuid) -> i64 {
store
.load("Caja", id)
.and_then(|v| v.get("saldo").and_then(Value::as_i64))
.expect("caja with saldo")
}
fn stock_cantidad(store: &MemoryStore, id: Uuid) -> i64 {
store
.load("Stock", id)
.and_then(|v| v.get("cantidad").and_then(Value::as_i64))
.expect("stock with cantidad")
}
fn seed(store: &mut MemoryStore) -> (Uuid, Uuid) {
let stock = Uuid::new_v4();
let caja = Uuid::new_v4();
store.seed(
"Stock",
stock,
json!({
"id": stock.to_string(),
"sku_id": "sku-test",
"ubicacion": "test-loc",
"cantidad": 500_i64,
}),
);
store.seed(
"Caja",
caja,
json!({
"id": caja.to_string(),
"name": "Caja Test",
"saldo": 1_000_000_i64,
"currency": "USD",
}),
);
(stock, caja)
}
#[test]
fn sale_decreases_stock_and_increases_caja() {
let exec = Executor::load_module(sales_module()).expect("load module");
let mut store = MemoryStore::new();
let (stock, caja) = seed(&mut store);
let venta_id = Uuid::new_v4();
let ops = exec
.run(
&mut store,
"vender",
&[("stock", stock), ("caja", caja)],
json!({
"cantidad": 100_i64,
"precio_unitario": 5_000_i64,
"timestamp": "2026-05-04T10:00:00Z",
"venta_id": venta_id.to_string(),
}),
)
.expect("sale must succeed");
assert_eq!(ops.len(), 3, "2 sets + 1 create");
assert_eq!(stock_cantidad(&store, stock), 400);
assert_eq!(caja_saldo(&store, caja), 1_500_000);
let venta = store
.load("Venta", venta_id)
.expect("venta must be persisted");
assert_eq!(venta.get("total").and_then(Value::as_i64), Some(500_000));
assert_eq!(venta.get("cantidad").and_then(Value::as_i64), Some(100));
assert_eq!(venta.get("currency").and_then(Value::as_str), Some("USD"));
}
#[test]
fn overdraw_stock_rejected_by_inventory_post_check() {
let exec = Executor::load_module(sales_module()).expect("load module");
let mut store = MemoryStore::new();
let (stock, caja) = seed(&mut store);
let result = exec.run(
&mut store,
"vender",
&[("stock", stock), ("caja", caja)],
json!({
"cantidad": 9999_i64,
"precio_unitario": 100_i64,
"timestamp": "2026-05-04T10:00:00Z",
"venta_id": Uuid::new_v4().to_string(),
}),
);
match result {
Err(ExecError::SchemaPost { role, entity, .. }) => {
assert_eq!(role, "stock");
assert_eq!(entity, "Stock");
}
other => panic!("expected SchemaPost on stock, got {:?}", other),
}
assert_eq!(stock_cantidad(&store, stock), 500);
assert_eq!(caja_saldo(&store, caja), 1_000_000);
}
#[test]
fn venta_total_invariant_caught_when_corrupted() {
// The Venta schema's check block enforces `total == cantidad * precio`.
// The production script always produces a consistent total. To prove
// the schema check fires, this test would need a buggy script — that's
// covered indirectly: if anyone breaks the script, this fails. For now
// we just confirm a clean sale's Venta passes its own invariant.
let exec = Executor::load_module(sales_module()).expect("load module");
let mut store = MemoryStore::new();
let (stock, caja) = seed(&mut store);
let venta_id = Uuid::new_v4();
exec.run(
&mut store,
"vender",
&[("stock", stock), ("caja", caja)],
json!({
"cantidad": 7_i64,
"precio_unitario": 13_i64,
"timestamp": "2026-05-04T10:00:00Z",
"venta_id": venta_id.to_string(),
}),
)
.expect("sale must pass");
let venta = store.load("Venta", venta_id).expect("venta");
assert_eq!(
venta.get("total").and_then(Value::as_i64),
Some(7 * 13),
"Venta.total must equal cantidad * precio"
);
}