# US-MGN-004-003-003: Cancelar Asiento con Reversing Entry **RF Asociado:** [RF-MGN-004-003](../../02-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-003-registro-de-asientos-contables.md) **Módulo:** MGN-004 - Financiero Básico **Epic:** Asientos Contables **Prioridad:** P0 (MVP) **Story Points:** 3 **Sprint:** Sprint 8 **Estado:** Ready for Development **Fecha:** 2025-11-24 --- ## User Story **Como** contador, **Quiero** cancelar un asiento contable publicado creando un asiento de reversión, **Para** corregir errores contables sin eliminar el registro original, manteniendo auditoría completa. --- ## Descripción Detallada En contabilidad, los asientos publicados NO se pueden editar ni eliminar. Para corregirlos, se crea un **asiento de reversión** (reversing entry) que invierte los débitos y créditos del asiento original. Ejemplo: **Asiento Original:** - 1.1.01.001 (Caja) | Debe: 1000 | Haber: 0 - 4.1.01.001 (Ingresos) | Debe: 0 | Haber: 1000 **Asiento de Reversión:** - 1.1.01.001 (Caja) | Debe: 0 | Haber: 1000 - 4.1.01.001 (Ingresos) | Debe: 1000 | Haber: 0 El efecto neto es que el asiento original queda cancelado. Ambos asientos quedan registrados para auditoría. --- ## Criterios de Aceptación ### Escenario 1: Crear asiento de reversión exitosamente **Dado que** tengo un asiento posted con id=10, **Cuando** ejecuto POST /api/v1/financial/journal-entries/10/reverse con reason="Error en monto", **Entonces** el sistema: - Crea un nuevo asiento con líneas invertidas (débitos ↔ créditos) - Asigna state='posted' automáticamente - Vincula el asiento de reversión al original (reversed_entry_id) - Marca el asiento original como reversed=true - Retorna el asiento de reversión creado ### Escenario 2: No se puede revertir asiento draft **Dado que** tengo un asiento con state='draft', **Cuando** intento revertirlo, **Entonces** el sistema retorna error 400 "Solo se pueden revertir asientos publicados". ### Escenario 3: No se puede revertir asiento ya revertido **Dado que** tengo un asiento con reversed=true, **Cuando** intento revertirlo nuevamente, **Entonces** el sistema retorna error 409 "Este asiento ya fue revertido". ### Escenario 4: Asiento de reversión invierte correctamente débitos y créditos **Dado que** el asiento original tiene línea: account_id=1, debit=1000, credit=0, **Cuando** creo la reversión, **Entonces** el asiento de reversión tiene línea: account_id=1, debit=0, credit=1000. ### Escenario 5: Referencia del asiento de reversión **Dado que** reverso el asiento "VEN/2024/0001", **Cuando** se crea el asiento de reversión, **Entonces** su referencia es "Reversión de VEN/2024/0001: [reason]". --- ## Reglas de Negocio - **RN-1:** Solo asientos con state='posted' pueden revertirse. - **RN-2:** Un asiento solo puede revertirse una vez (prevenir reversiones múltiples). - **RN-3:** El asiento de reversión se crea automáticamente en state='posted' (no pasa por draft). - **RN-4:** Las líneas del asiento de reversión son idénticas pero con débitos y créditos invertidos. - **RN-5:** El asiento de reversión tiene la fecha contable actual (no la del asiento original). - **RN-6:** Se requiere motivo de reversión (reason) para auditoría. - **RN-7:** Los balances de cuentas se actualizan automáticamente al postear la reversión. - **RN-8:** Ambos asientos (original + reversión) quedan registrados permanentemente. --- ## Tareas Técnicas ### Backend - [ ] Endpoint: `POST /api/v1/financial/journal-entries/:id/reverse` - [ ] DTO: `ReverseJournalEntryDto` (reason: string required) - [ ] Service: `JournalEntryService.reverse(id, reason, userId)` - [ ] Service: `JournalEntryService.createReversingEntry(originalEntry, reason)` - [ ] Validar que entry.state = 'posted' - [ ] Validar que entry.reversed = false - [ ] Crear nuevo asiento con líneas invertidas - [ ] Marcar asiento original reversed=true, reversed_by=reversing_entry_id - [ ] Asignar reversing_entry.reversed_entry_id = original_entry_id - [ ] Postear asiento de reversión automáticamente (actualizar balances) - [ ] Unit tests (8 test cases) - [ ] Integration tests (6 test cases) - [ ] Swagger docs ### Frontend - [ ] Botón: "Cancelar Asiento" (visible solo si state='posted' y reversed=false) - [ ] Modal: `ReverseEntryModal.tsx` (solicitar motivo) - [ ] Campo: Motivo de reversión (textarea, required, min 10 chars) - [ ] Badge: Mostrar "Revertido" si reversed=true - [ ] Link: "Ver Asiento de Reversión" (navegar al reversing entry) - [ ] API client: `journalEntryApi.reverse(id, reason)` - [ ] Toast: "Asiento revertido exitosamente" - [ ] Component test: ReverseEntryModal.test.tsx - [ ] E2E test: "should reverse posted entry successfully" ### Database - [ ] Campo: `journal_entries.reversed` (boolean, default false) - [ ] Campo: `journal_entries.reversed_entry_id` (uuid nullable, FK a journal_entries) - [ ] Campo: `journal_entries.reverse_reason` (text nullable) - [ ] Índice: idx_journal_entries_reversed - [ ] Check constraint: reversed_entry_id solo puede asignarse si reversed=true --- ## Mockups / Wireframes **Botón Cancelar en Asiento Posted:** ``` ┌──────────────────────────────────────────┐ │ Asiento VEN/2024/0001 [Publicado ✓] │ │ [Ver PDF] [Cancelar Asiento] │ ├──────────────────────────────────────────┤ │ ...detalles del asiento... │ └──────────────────────────────────────────┘ ``` **Modal Cancelar Asiento:** ``` ┌──────────────────────────────────────────┐ │ ⚠️ Cancelar Asiento │ ├──────────────────────────────────────────┤ │ Está a punto de cancelar el asiento │ │ VEN/2024/0001. Esto creará un asiento │ │ de reversión que anulará los efectos │ │ contables del asiento original. │ │ │ │ Motivo de cancelación: (requerido) │ │ ┌────────────────────────────────────┐ │ │ │ Error en monto facturado │ │ │ │ │ │ │ └────────────────────────────────────┘ │ │ │ │ [Volver] [Sí, Cancelar] │ └──────────────────────────────────────────┘ ``` **Asiento Revertido (Vista Original):** ``` ┌──────────────────────────────────────────┐ │ Asiento VEN/2024/0001 [Revertido ⚠️] │ │ [Ver Asiento de Reversión →] │ ├──────────────────────────────────────────┤ │ Este asiento fue revertido el │ │ 2024-01-20 por Juan Pérez │ │ Motivo: Error en monto facturado │ └──────────────────────────────────────────┘ ``` --- ## Casos de Prueba ### Funcionales 1. **TC-001:** Reversar asiento posted exitosamente 2. **TC-002:** Error por asiento draft 3. **TC-003:** Error por asiento ya revertido 4. **TC-004:** Líneas invertidas correctamente (debit ↔ credit) 5. **TC-005:** Referencia incluye motivo de reversión 6. **TC-006:** Asiento original marcado reversed=true 7. **TC-007:** Balances actualizados correctamente 8. **TC-008:** Vincular asientos (reversed_entry_id, reversed_by) ### No Funcionales 1. **Performance:** < 500ms para crear reversión 2. **Atomicidad:** Rollback si falla al crear reversión 3. **Seguridad:** Solo accounting_manager puede reversar --- ## Dependencias - **US bloqueantes:** - US-MGN-004-003-002 (Postear Asiento) - **Módulos:** MGN-004 --- ## Notas de Implementación - **Auditoría completa:** Ambos asientos (original + reversión) quedan registrados permanentemente - **Fecha de reversión:** Usar fecha actual del sistema, NO la fecha del asiento original - **Número secuencial:** El asiento de reversión obtiene el siguiente número del journal - **Journal:** La reversión usa el mismo journal que el asiento original - **Validación de motivo:** Mínimo 10 caracteres para asegurar descripción clara - **Frontend:** Prevenir doble-click en botón "Cancelar" (loading state) --- ## Estimación Detallada | Tarea | Estimación | |-------|------------| | Backend | 2 horas | | Frontend | 2 horas | | Testing | 1.5 horas | | Code Review | 0.5 hora | | **TOTAL** | **6 horas = 3 SP** | --- ## Definition of Done - [ ] Código backend implementado - [ ] Código frontend implementado - [ ] Unit tests pasando (>80%) - [ ] Integration tests pasando - [ ] E2E tests pasando - [ ] Validaciones funcionan (solo posted, no revertidos 2 veces) - [ ] Líneas invertidas correctamente - [ ] Balances actualizados - [ ] Motivo de reversión requerido - [ ] Code review aprobado - [ ] Swagger docs actualizado - [ ] Merge a develop - [ ] QA validado - [ ] PO aprobado --- ## Referencias - [RF-MGN-004-003](../../02-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-003-registro-de-asientos-contables.md) - [ET-BACKEND-MGN-004-003](../../02-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-003-registro-de-asientos-contables.md) - [ET-FRONTEND-MGN-004-003](../../02-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-003-registro-de-asientos-contables.md) - [TRACEABILITY-MGN-004.yaml](../../02-modelado/trazabilidad/TRACEABILITY-MGN-004.yaml) - [Financial Schema DDL](../../02-modelado/database-design/schemas/financial-schema-ddl.sql)