# 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; // Sesiones @Post('sessions/open') openSession(@Body() dto: OpenSessionDto): Promise; @Get('sessions/active') getActiveSession(): Promise; @Get('sessions/:id/summary') getSessionSummary(@Param('id') id: string): Promise; // Movimientos @Post('movements') createMovement(@Body() dto: CreateMovementDto): Promise; @Get('sessions/:id/movements') getMovements(@Param('id') id: string): Promise; // Arqueos @Post('sessions/:id/count') createCount(@Param('id') id: string, @Body() dto: CountDto): Promise; // Corte @Get('sessions/:id/closing/prepare') prepareClosing(@Param('id') id: string): Promise; @Post('sessions/:id/closing') closeSession(@Param('id') id: string, @Body() dto: ClosingDto): Promise; @Post('closings/:id/approve') approveClosing(@Param('id') id: string): Promise; // Reportes @Get('reports/daily') getDailyReport(@Query('date') date: string): Promise; @Get('reports/differences') getDifferencesReport(@Query() filters: DifferenceFilters): Promise; } ``` --- ## 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