# 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)
