erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-007-caja.md

540 lines
13 KiB
Markdown

# ANALISIS MODULO RT-007: CAJA (ARQUEOS Y CORTES)
**Fecha:** 2025-12-18
**Fase:** 2 - Analisis por Modulo
**Modulo:** RT-007 Caja
**Herencia:** 10%
**Story Points:** 28
**Prioridad:** P0
---
## 1. DESCRIPCION GENERAL
### 1.1 Proposito
Control de efectivo con apertura/cierre de caja, movimientos de efectivo, arqueos y cortes con declaracion por denominacion.
### 1.2 Funcionalidades Principales
| Funcionalidad | Descripcion | Criticidad |
|---------------|-------------|------------|
| Apertura caja | Con fondo inicial | Critica |
| Cierre caja | Con conteo | Critica |
| Movimientos | Entradas/salidas | Alta |
| Arqueos | Parciales | Media |
| Declaracion | Por denominacion | Alta |
| Diferencias | Control | Alta |
---
## 2. HERENCIA DEL CORE
### 2.1 Componentes Heredados (10%)
| Componente Core | % Uso | Accion |
|-----------------|-------|--------|
| financial.payments | 10% | Referencia |
### 2.2 Observacion
Este modulo es casi 100% nuevo. No existe gestion de caja en el core. Solo se reutilizan conceptos de pagos.
---
## 3. COMPONENTES NUEVOS
### 3.1 Entidades (TypeORM)
```typescript
// 1. CashRegister - Caja registradora (ya existe)
@Entity('cash_registers', { schema: 'retail' })
export class CashRegister {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
tenantId: string;
@ManyToOne(() => Branch)
branch: Branch;
@Column()
code: string;
@Column()
name: string;
@Column({ type: 'boolean', default: true })
isActive: boolean;
@Column({ type: 'enum', enum: PaymentMethod, nullable: true })
defaultPaymentMethod: PaymentMethod;
}
// 2. CashSession - Sesion de caja (extiende POSSession)
// Ya definida en RT-002, aqui se agregan campos de arqueo
// 3. CashMovement - Movimiento de efectivo
@Entity('cash_movements', { schema: 'retail' })
export class CashMovement {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
tenantId: string;
@ManyToOne(() => POSSession)
session: POSSession;
@Column({ type: 'enum', enum: CashMovementType })
movementType: CashMovementType; // in, out
@Column({ type: 'decimal', precision: 12, scale: 2 })
amount: number;
@Column()
reason: string;
@Column({ nullable: true })
notes: string;
@ManyToOne(() => User, { nullable: true })
authorizedBy: User;
@Column({ type: 'timestamptz' })
createdAt: Date;
}
// 4. CashClosing - Corte de caja
@Entity('cash_closings', { schema: 'retail' })
export class CashClosing {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
tenantId: string;
@ManyToOne(() => POSSession)
session: POSSession;
@Column({ type: 'timestamptz' })
closingDate: Date;
// Montos esperados (calculados)
@Column({ type: 'decimal', precision: 12, scale: 2 })
expectedCash: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
expectedCard: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
expectedTransfer: number;
// Montos declarados
@Column({ type: 'decimal', precision: 12, scale: 2 })
declaredCash: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
declaredCard: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
declaredTransfer: number;
// Diferencias
@Column({ type: 'decimal', precision: 12, scale: 2 })
cashDifference: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
cardDifference: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
transferDifference: number;
// Detalle de denominaciones
@Column({ type: 'jsonb', nullable: true })
denominationDetail: DenominationDetail;
@Column({ nullable: true })
notes: string;
@ManyToOne(() => User)
closedBy: User;
@ManyToOne(() => User, { nullable: true })
approvedBy: User;
@Column({ type: 'boolean', default: false })
isApproved: boolean;
}
// 5. CashCount - Arqueo parcial
@Entity('cash_counts', { schema: 'retail' })
export class CashCount {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('uuid')
tenantId: string;
@ManyToOne(() => POSSession)
session: POSSession;
@Column({ type: 'timestamptz' })
countDate: Date;
@Column({ type: 'decimal', precision: 12, scale: 2 })
expectedAmount: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
countedAmount: number;
@Column({ type: 'decimal', precision: 12, scale: 2 })
difference: number;
@Column({ type: 'jsonb', nullable: true })
denominationDetail: DenominationDetail;
@ManyToOne(() => User)
countedBy: User;
@Column({ nullable: true })
notes: string;
}
// Interfaz para denominaciones
interface DenominationDetail {
bills: {
'1000': number;
'500': number;
'200': number;
'100': number;
'50': number;
'20': number;
};
coins: {
'20': number;
'10': number;
'5': number;
'2': number;
'1': number;
'0.50': number;
};
total: number;
}
```
### 3.2 Servicios Backend
| Servicio | Metodos Principales |
|----------|-------------------|
| CashRegisterService | getAll(), getByBranch(), activate(), deactivate() |
| CashSessionService | open(), close(), getActive(), getSummary() |
| CashMovementService | createIn(), createOut(), getBySession() |
| CashClosingService | prepare(), declare(), approve(), reject() |
| CashCountService | create(), compare() |
### 3.3 Controladores
```typescript
@Controller('cash')
export class CashController {
// Cajas registradoras
@Get('registers')
getRegisters(@Query('branchId') branchId: string): Promise<CashRegister[]>;
// Sesiones
@Post('sessions/open')
openSession(@Body() dto: OpenSessionDto): Promise<POSSession>;
@Get('sessions/active')
getActiveSession(): Promise<POSSession>;
@Get('sessions/:id/summary')
getSessionSummary(@Param('id') id: string): Promise<SessionSummary>;
// Movimientos
@Post('movements')
createMovement(@Body() dto: CreateMovementDto): Promise<CashMovement>;
@Get('sessions/:id/movements')
getMovements(@Param('id') id: string): Promise<CashMovement[]>;
// Arqueos
@Post('sessions/:id/count')
createCount(@Param('id') id: string, @Body() dto: CountDto): Promise<CashCount>;
// Corte
@Get('sessions/:id/closing/prepare')
prepareClosing(@Param('id') id: string): Promise<ClosingPreparation>;
@Post('sessions/:id/closing')
closeSession(@Param('id') id: string, @Body() dto: ClosingDto): Promise<CashClosing>;
@Post('closings/:id/approve')
approveClosing(@Param('id') id: string): Promise<CashClosing>;
// Reportes
@Get('reports/daily')
getDailyReport(@Query('date') date: string): Promise<DailyReport>;
@Get('reports/differences')
getDifferencesReport(@Query() filters: DifferenceFilters): Promise<DifferenceReport>;
}
```
---
## 4. FLUJOS DE NEGOCIO
### 4.1 Flujo de Apertura
```
1. Cajero selecciona caja
2. Verificar caja disponible (no en uso)
3. Ingresar fondo inicial
4. Sistema crea sesion (status: OPENING)
5. Confirmar apertura
6. Estado: OPENING → OPEN
7. Cajero puede iniciar ventas
```
### 4.2 Flujo de Movimiento
```
1. Cajero solicita retiro/ingreso
2. Ingresar monto y motivo
3. Si retiro > limite:
a. Solicitar autorizacion supervisor
b. Supervisor aprueba
4. Registrar movimiento
5. Actualizar balance de sesion
```
### 4.3 Flujo de Corte
```
1. Cajero solicita cierre
2. Sistema prepara resumen:
- Ventas en efectivo
- Ventas en tarjeta
- Ventas en transferencia
- Movimientos de efectivo
- Fondo inicial
- Esperado en efectivo
3. Cajero cuenta efectivo
4. Declarar por denominacion:
- Billetes: $1000, $500, $200, $100, $50, $20
- Monedas: $20, $10, $5, $2, $1, $0.50
5. Sistema calcula total declarado
6. Calcular diferencia
7. Si diferencia > tolerancia:
a. Registrar motivo
b. Supervisor aprueba/rechaza
8. Estado: OPEN → CLOSED
9. Generar reporte de corte
```
---
## 5. CALCULOS
### 5.1 Efectivo Esperado
```typescript
function calculateExpectedCash(session: POSSession): number {
const openingBalance = session.openingBalance;
// Ventas en efectivo
const cashSales = session.orders
.filter(o => o.status === 'done')
.reduce((sum, o) => {
const cashPayments = o.payments.filter(p => p.method === 'cash');
return sum + cashPayments.reduce((s, p) => s + p.amount, 0);
}, 0);
// Cambio dado
const changeGiven = session.orders
.filter(o => o.status === 'done')
.reduce((sum, o) => sum + (o.changeAmount || 0), 0);
// Movimientos
const cashIn = session.movements
.filter(m => m.type === 'in')
.reduce((sum, m) => sum + m.amount, 0);
const cashOut = session.movements
.filter(m => m.type === 'out')
.reduce((sum, m) => sum + m.amount, 0);
return openingBalance + cashSales - changeGiven + cashIn - cashOut;
}
```
### 5.2 Declaracion por Denominacion
```typescript
interface DenominationCount {
denomination: string;
quantity: number;
subtotal: number;
}
function calculateDenominations(counts: DenominationCount[]): number {
return counts.reduce((total, c) => total + c.subtotal, 0);
}
// Ejemplo
const declaration = [
{ denomination: '1000', quantity: 2, subtotal: 2000 },
{ denomination: '500', quantity: 5, subtotal: 2500 },
{ denomination: '200', quantity: 3, subtotal: 600 },
{ denomination: '100', quantity: 10, subtotal: 1000 },
{ denomination: '50', quantity: 8, subtotal: 400 },
{ denomination: '20', quantity: 15, subtotal: 300 },
// Monedas
{ denomination: '10', quantity: 20, subtotal: 200 },
{ denomination: '5', quantity: 10, subtotal: 50 },
{ denomination: '2', quantity: 5, subtotal: 10 },
{ denomination: '1', quantity: 10, subtotal: 10 },
{ denomination: '0.50', quantity: 20, subtotal: 10 },
];
// Total: $7,080
```
---
## 6. TABLAS DDL
### 6.1 Tablas
```sql
-- Ya definidas parcialmente, agregar:
CREATE TABLE retail.cash_closings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
session_id UUID NOT NULL REFERENCES retail.pos_sessions(id),
closing_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Esperados
expected_cash DECIMAL(12,2) NOT NULL,
expected_card DECIMAL(12,2) NOT NULL DEFAULT 0,
expected_transfer DECIMAL(12,2) NOT NULL DEFAULT 0,
-- Declarados
declared_cash DECIMAL(12,2) NOT NULL,
declared_card DECIMAL(12,2) NOT NULL DEFAULT 0,
declared_transfer DECIMAL(12,2) NOT NULL DEFAULT 0,
-- Diferencias
cash_difference DECIMAL(12,2) GENERATED ALWAYS AS (declared_cash - expected_cash) STORED,
card_difference DECIMAL(12,2) GENERATED ALWAYS AS (declared_card - expected_card) STORED,
transfer_difference DECIMAL(12,2) GENERATED ALWAYS AS (declared_transfer - expected_transfer) STORED,
-- Detalle
denomination_detail JSONB,
notes TEXT,
-- Auditoria
closed_by UUID NOT NULL REFERENCES auth.users(id),
approved_by UUID REFERENCES auth.users(id),
is_approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE retail.cash_counts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
session_id UUID NOT NULL REFERENCES retail.pos_sessions(id),
count_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expected_amount DECIMAL(12,2) NOT NULL,
counted_amount DECIMAL(12,2) NOT NULL,
difference DECIMAL(12,2) GENERATED ALWAYS AS (counted_amount - expected_amount) STORED,
denomination_detail JSONB,
counted_by UUID NOT NULL REFERENCES auth.users(id),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
---
## 7. DEPENDENCIAS
### 7.1 Dependencias de Retail
| Modulo | Tipo |
|--------|------|
| RT-001 Fundamentos | Prerequisito |
| RT-002 POS | Sesiones de caja |
### 7.2 Bloquea a
| Modulo | Razon |
|--------|-------|
| RT-008 Reportes | Reportes de caja |
---
## 8. CRITERIOS DE ACEPTACION
### 8.1 Funcionales
- [ ] Abrir caja con fondo inicial
- [ ] Registrar movimiento de entrada
- [ ] Registrar movimiento de salida
- [ ] Requerir autorizacion para montos altos
- [ ] Realizar arqueo parcial
- [ ] Preparar cierre (mostrar esperados)
- [ ] Declarar por denominacion
- [ ] Calcular diferencias
- [ ] Aprobar cierre con diferencia
- [ ] Generar reporte de corte
- [ ] Ver historial de diferencias
### 8.2 Auditoria
- [ ] Registrar responsable de cada movimiento
- [ ] Registrar autorizador de retiros
- [ ] Registrar aprobador de diferencias
---
## 9. ESTIMACION DETALLADA
| Componente | SP Backend | SP Frontend | Total |
|------------|-----------|-------------|-------|
| Entities + Migrations | 3 | - | 3 |
| CashSessionService | 5 | - | 5 |
| CashMovementService | 3 | - | 3 |
| CashClosingService | 5 | - | 5 |
| CashCountService | 2 | - | 2 |
| Controllers | 3 | - | 3 |
| Opening UI | - | 2 | 2 |
| Movements UI | - | 2 | 2 |
| Closing UI | - | 3 | 3 |
| **TOTAL** | **21** | **7** | **28** |
---
**Estado:** ANALISIS COMPLETO
**Bloqueado por:** RT-001, RT-002