- Prefijo v2: MCH - TRACEABILITY-MASTER.yml creado - Listo para integracion como submodulo Workspace: v2.0.0 | SIMCO: v4.0.0
11 KiB
| id | type | title | status | created_at | updated_at | simco_version | author | tags | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SPEC-CALCULADORA-CAMBIO | Specification | Especificacion: Calculadora de Cambio | Published | 2026-01-04 | 2026-01-10 | 3.8.0 | Equipo MiChangarrito |
|
Especificacion: Calculadora de Cambio
Proyecto: MiChangarrito Fecha: 2026-01-10 Estado: ESPECIFICACION Version: 1.0.0
1. Proposito
Definir la logica de calculo de cambio para transacciones en efectivo, optimizando la cantidad de billetes y monedas a devolver al cliente.
2. Denominaciones Mexico (MXN)
2.1 Billetes
| Denominacion | Valor | Color/Identificador |
|---|---|---|
| $1000 | 1000.00 | Azul - Hidalgo |
| $500 | 500.00 | Cafe - Benito Juarez |
| $200 | 200.00 | Verde - Sor Juana |
| $100 | 100.00 | Rojo - Nezahualcoyotl |
| $50 | 50.00 | Rosa - Jose Maria Morelos |
| $20 | 20.00 | Azul - Benito Juarez |
2.2 Monedas
| Denominacion | Valor | Caracteristica |
|---|---|---|
| $10 | 10.00 | Bimetalica |
| $5 | 5.00 | Acero inoxidable |
| $2 | 2.00 | Acero inoxidable |
| $1 | 1.00 | Acero inoxidable |
| $0.50 | 0.50 | Acero inoxidable |
Nota: Monedas de $0.10 y $0.20 estan en desuso para comercio general.
3. Algoritmo de Calculo
3.1 Entrada
interface CalculoCambioInput {
totalVenta: number; // Monto total de la venta
montoRecibido: number; // Efectivo recibido del cliente
}
3.2 Salida
interface CalculoCambioOutput {
cambioTotal: number; // Monto total a devolver
desglose: Denominacion[]; // Lista de billetes/monedas
esExacto: boolean; // true si pago fue exacto
error?: string; // Mensaje si monto insuficiente
}
interface Denominacion {
valor: number;
cantidad: number;
tipo: 'billete' | 'moneda';
}
3.3 Proceso
1. Validar entrada
- Si montoRecibido < totalVenta: Error "Monto insuficiente"
- Si montoRecibido == totalVenta: Retornar { cambioTotal: 0, esExacto: true }
2. Calcular cambio
- cambio = montoRecibido - totalVenta
- Redondear a 2 decimales
3. Aplicar algoritmo greedy
- Para cada denominacion (de mayor a menor):
- Calcular cuantas unidades caben en el cambio restante
- Agregar al desglose
- Restar del cambio restante
4. Retornar resultado
3.4 Implementacion TypeScript
const DENOMINACIONES = [
{ valor: 1000, tipo: 'billete' as const },
{ valor: 500, tipo: 'billete' as const },
{ valor: 200, tipo: 'billete' as const },
{ valor: 100, tipo: 'billete' as const },
{ valor: 50, tipo: 'billete' as const },
{ valor: 20, tipo: 'billete' as const },
{ valor: 10, tipo: 'moneda' as const },
{ valor: 5, tipo: 'moneda' as const },
{ valor: 2, tipo: 'moneda' as const },
{ valor: 1, tipo: 'moneda' as const },
{ valor: 0.5, tipo: 'moneda' as const },
];
function calcularCambio(
totalVenta: number,
montoRecibido: number
): CalculoCambioOutput {
// Validacion
if (montoRecibido < totalVenta) {
return {
cambioTotal: 0,
desglose: [],
esExacto: false,
error: `Monto insuficiente. Faltan $${(totalVenta - montoRecibido).toFixed(2)}`,
};
}
// Pago exacto
if (montoRecibido === totalVenta) {
return {
cambioTotal: 0,
desglose: [],
esExacto: true,
};
}
// Calcular cambio
let cambioRestante = Math.round((montoRecibido - totalVenta) * 100) / 100;
const cambioTotal = cambioRestante;
const desglose: Denominacion[] = [];
// Algoritmo greedy
for (const denom of DENOMINACIONES) {
if (cambioRestante >= denom.valor) {
const cantidad = Math.floor(cambioRestante / denom.valor);
if (cantidad > 0) {
desglose.push({
valor: denom.valor,
cantidad,
tipo: denom.tipo,
});
cambioRestante = Math.round((cambioRestante - cantidad * denom.valor) * 100) / 100;
}
}
}
return {
cambioTotal,
desglose,
esExacto: false,
};
}
4. Casos Especiales
4.1 Redondeo
| Escenario | Accion |
|---|---|
| Cambio < $0.50 | Redondear a $0.00 (favor cliente) |
| Cambio entre $0.50 y $0.99 | Dar $0.50 o $1.00 segun disponibilidad |
4.2 Sugerencia de Monto
Para facilitar el cobro, sugerir montos redondos al cliente:
function sugerirMontoRedondo(total: number): number[] {
const sugerencias: number[] = [];
// Redondear hacia arriba a multiplos de 10, 20, 50, 100
const multiplos = [10, 20, 50, 100, 200, 500];
for (const multiplo of multiplos) {
const sugerido = Math.ceil(total / multiplo) * multiplo;
if (sugerido > total && !sugerencias.includes(sugerido)) {
sugerencias.push(sugerido);
}
if (sugerencias.length >= 3) break;
}
return sugerencias;
}
// Ejemplo: total = $87.50
// Sugerencias: [$90, $100, $200]
4.3 Sin Cambio Disponible
Si el negocio no tiene cambio suficiente:
- Alertar al usuario antes de confirmar venta
- Sugerir pago con tarjeta
- Sugerir monto exacto
- Permitir "dejar propina" si cliente acepta
5. Integracion
5.1 Frontend Mobile (React Native)
Componente: ChangeCalculatorModal
Ubicacion: apps/mobile/src/components/sales/ChangeCalculatorModal.tsx
Props:
interface ChangeCalculatorProps {
total: number;
onConfirm: (montoRecibido: number, cambio: number) => void;
onCancel: () => void;
}
Uso:
<ChangeCalculatorModal
total={87.50}
onConfirm={(recibido, cambio) => {
// Registrar venta con efectivo
completarVenta({
paymentMethod: 'cash',
cashReceived: recibido,
changeGiven: cambio,
});
}}
onCancel={() => setShowCalculator(false)}
/>
5.2 Frontend Web (React)
Componente: CashPaymentForm
Ubicacion: apps/frontend/src/components/pos/CashPaymentForm.tsx
5.3 Backend
El calculo se realiza en el cliente (frontend) por performance. El backend solo recibe y valida:
// DTO de creacion de venta
interface CreateSaleDto {
items: SaleItemDto[];
paymentMethod: 'cash' | 'card' | 'transfer' | 'fiado';
// Solo para efectivo
cashReceived?: number;
changeGiven?: number;
}
// Validacion en backend
if (dto.paymentMethod === 'cash') {
if (!dto.cashReceived || dto.cashReceived < total) {
throw new BadRequestException('Monto recibido insuficiente');
}
const expectedChange = dto.cashReceived - total;
if (Math.abs(expectedChange - dto.changeGiven) > 0.01) {
throw new BadRequestException('Cambio calculado incorrectamente');
}
}
5.4 Base de Datos
Tabla: sales.sales
Campos relevantes:
cash_received DECIMAL(10,2), -- Monto recibido en efectivo
change_amount DECIMAL(10,2), -- Cambio entregado
6. UI/UX
6.1 Pantalla de Cobro en Efectivo
┌────────────────────────────────────────┐
│ COBRO EN EFECTIVO │
├────────────────────────────────────────┤
│ │
│ Total a cobrar: $87.50 │
│ │
│ ───────────────────────────────── │
│ │
│ Monto recibido: │
│ ┌────────────────────────────────┐ │
│ │ $100.00 [X] │ │
│ └────────────────────────────────┘ │
│ │
│ Sugerencias rapidas: │
│ [$90] [$100] [$200] [Exacto] │
│ │
│ ───────────────────────────────── │
│ │
│ CAMBIO A ENTREGAR: $12.50 │
│ │
│ Desglose: │
│ ┌────────────────────────────────┐ │
│ │ 1 x $10.00 (billete) │ │
│ │ 1 x $2.00 (moneda) │ │
│ │ 1 x $0.50 (moneda) │ │
│ └────────────────────────────────┘ │
│ │
│ [Cancelar] [Confirmar Venta] │
│ │
└────────────────────────────────────────┘
6.2 Teclado Numerico Rapido
┌─────────────────────────┐
│ [7] [8] [9] [←] │
│ [4] [5] [6] [C] │
│ [1] [2] [3] │
│ [0] [00] [.] [OK] │
└─────────────────────────┘
6.3 Iconografia
| Elemento | Icono Sugerido |
|---|---|
| Billete | Currency-dollar o banknote |
| Moneda | Coin |
| Cambio exacto | Check-circle |
| Error | Alert-triangle |
7. Pruebas
7.1 Casos de Prueba
| Total | Recibido | Cambio Esperado | Desglose |
|---|---|---|---|
| $87.50 | $100.00 | $12.50 | 1x$10, 1x$2, 1x$0.50 |
| $123.00 | $150.00 | $27.00 | 1x$20, 1x$5, 1x$2 |
| $50.00 | $50.00 | $0.00 | (exacto) |
| $99.50 | $100.00 | $0.50 | 1x$0.50 |
| $1.00 | $1000.00 | $999.00 | 1x$500, 2x$200, 1x$50, 2x$20, 1x$5, 2x$2 |
| $100.00 | $50.00 | Error | "Monto insuficiente" |
7.2 Test Unitarios
describe('calcularCambio', () => {
it('debe calcular cambio correcto', () => {
const result = calcularCambio(87.50, 100);
expect(result.cambioTotal).toBe(12.50);
expect(result.desglose).toHaveLength(3);
});
it('debe retornar exacto si no hay cambio', () => {
const result = calcularCambio(50, 50);
expect(result.esExacto).toBe(true);
expect(result.cambioTotal).toBe(0);
});
it('debe retornar error si monto insuficiente', () => {
const result = calcularCambio(100, 50);
expect(result.error).toBeDefined();
});
});
8. Consideraciones de Performance
- El calculo es O(n) donde n = numero de denominaciones (11)
- Se ejecuta en el cliente para respuesta inmediata
- No requiere llamada al servidor
9. Referencias
- Especificacion Punto de Venta
- Arquitectura Database - Schema sales
- Especificacion Componentes - SalesModule
Ultima actualizacion: 2026-01-10 Version: 1.0.0