- Prefijo v2: MCH - TRACEABILITY-MASTER.yml creado - Listo para integracion como submodulo Workspace: v2.0.0 | SIMCO: v4.0.0
417 lines
11 KiB
Markdown
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
|