--- id: SPEC-CALCULADORA-CAMBIO type: Specification title: "Especificacion: Calculadora de Cambio" status: Published created_at: 2026-01-04 updated_at: 2026-01-10 simco_version: "3.8.0" author: "Equipo MiChangarrito" tags: - calculadora - cambio - efectivo - pos - ventas --- # 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 ```typescript interface CalculoCambioInput { totalVenta: number; // Monto total de la venta montoRecibido: number; // Efectivo recibido del cliente } ``` ### 3.2 Salida ```typescript 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 ```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: ```typescript 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: 1. Alertar al usuario antes de confirmar venta 2. Sugerir pago con tarjeta 3. Sugerir monto exacto 4. 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:** ```typescript interface ChangeCalculatorProps { total: number; onConfirm: (montoRecibido: number, cambio: number) => void; onCancel: () => void; } ``` **Uso:** ```tsx { // 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: ```typescript // 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:** ```sql 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 ```typescript 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](../01-epicas/MCH-004-punto-venta.md) - [Arquitectura Database - Schema sales](./ARQUITECTURA-DATABASE.md) - [Especificacion Componentes - SalesModule](./ESPECIFICACION-COMPONENTES.md) --- **Ultima actualizacion:** 2026-01-10 **Version:** 1.0.0