540 lines
13 KiB
Markdown
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
|