michangarrito/docs/02-especificaciones/CALCULADORA-CAMBIO.md
rckrdmrd 97f407c661 [MIGRATION-V2] feat: Migrar michangarrito a estructura v2
- Prefijo v2: MCH
- TRACEABILITY-MASTER.yml creado
- Listo para integracion como submodulo

Workspace: v2.0.0 | SIMCO: v4.0.0
2026-01-10 11:28:54 -06:00

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
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

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:

  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:

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


Ultima actualizacion: 2026-01-10 Version: 1.0.0