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

417 lines
11 KiB
Markdown

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