Skip to content

Condición IVA - Documentación Técnica Backend

⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11

Metadata

Módulo: ventas Feature: Condición IVA (Condiciones frente al IVA) Tipo: Resource (CRUD) Fecha: 2026-02-11 Estado: Implementado (Backend Legacy)


Descripción General

Sistema de gestión de condiciones fiscales frente al IVA para clientes y proveedores. Permite mantener un catálogo de condiciones tributarias (Responsable Inscripto, Monotributo, Consumidor Final, etc.) que determinan el tratamiento impositivo en operaciones de compra/venta.

Propósito de Negocio: Cumplir con regulaciones de AFIP/ARCA sobre emisión de comprobantes fiscales según la condición tributaria de cada cliente/proveedor.


Arquitectura Implementada

🚨 Arquitectura Simplificada (Legacy)

El módulo NO sigue la arquitectura 5-layer DDD estándar de Bautista Backend:

Backend Legacy (ordiva.php)

Controller (CondicionIvaController)

Model (CondicionIva)

Database (ordiva table)

Capas ausentes:

  • ❌ Service Layer (no hay transacciones complejas)
  • ❌ Domain Layer (no hay lógica de negocio compleja)
  • ❌ Validator Middleware (validaciones inline en Model)
  • ❌ Slim Routes (usa backend legacy)

Ubicación de Archivos

ComponentePathObservaciones
Backend Legacybackend/ordiva.phpEndpoints REST implementados
Controllercontroller/modulo-venta/CondicionIvaController.phpThin wrapper sobre Model
Modelmodels/modulo-venta/CondicionIva.phpCRUD + batch loading
DomainDomain/Ventas/Facturacion/Strategy/Config/CondicionIvaFallback.phpEnum para estrategia fallback
Migrationmigrations/migrations/tenancy/20240823200738_new_table_ordiva.phpSchema definition
Seedmigrations/seeds/tenancy/Ordiva.php12 condiciones predefinidas AFIP

API Endpoints (Backend Legacy)

Base URL

/backend/ordiva.php

⚠️ Nota: Este recurso usa el sistema legacy de routing (archivos PHP directos), NO Slim Framework routes.

GET /backend/ordiva.php

Descripción: Listar todas las condiciones de IVA o consultar una específica por ID.

Query Parameters:

ParámetroTipoRequeridoDescripción
idintNoCódigo de condición IVA (CIVA). Si se omite, devuelve todas.

Responses:

200 OK - Lista de condiciones:

json
{
  "status": 200,
  "message": "Datos recibidos correctamente.",
  "data": [
    {
      "id": 1,
      "nombre": "RESP. INSCRIPTO",
      "registra_cuit": true,
      "discrimina_iva": true,
      "abreviacion": "RI",
      "defecto": false,
      "habilitado": true
    }
  ]
}

200 OK - Condición específica (?id=1):

json
{
  "status": 200,
  "message": "Datos recibidos correctamente.",
  "data": {
    "id": 1,
    "nombre": "RESP. INSCRIPTO",
    "registra_cuit": true,
    "discrimina_iva": true,
    "abreviacion": "RI",
    "defecto": false
  }
}

400 Bad Request - ID inválido:

json
{
  "error": "El código proporcionado es inválido"
}

PUT /backend/ordiva.php

Descripción: Actualización completa de una condición de IVA.

Request Body:

json
{
  "id": 1,
  "nombre": "RESPONSABLE INSCRIPTO",
  "registra_cuit": true,
  "discrimina_iva": true,
  "defecto": false
}

Validaciones:

  • id: requerido, entero
  • nombre: requerido, string
  • registra_cuit: requerido, boolean
  • discrimina_iva: requerido, boolean
  • defecto: requerido, boolean

Responses:

204 No Content - Actualización exitosa (sin body)

400 Bad Request - Validación fallida:

json
{
  "error": "Debe proporcionarse el código de condición de iva"
}

500 Internal Server Error - Error en actualización:

json
{
  "error": "Error al modificar la condición de IVA"
}

⚠️ Bug identificado: SQL query en línea 104 tiene falta de coma después de ivadiscri:

sql
defecto = :defecto  -- FALTA COMA aquí
WHERE civa = :id

PATCH /backend/ordiva.php

Descripción: Actualización parcial de una condición de IVA. Actualmente solo soporta actualización del campo defecto.

Request Body:

json
{
  "id": 5,
  "defecto": true
}

Lógica de Negocio Especial:

Si defecto: true:

  1. Primero desactiva defecto = FALSE en TODAS las condiciones
  2. Luego activa defecto = TRUE en la condición especificada

Garantía: Solo UNA condición puede tener defecto = TRUE simultáneamente.

Responses:

204 No Content - Actualización exitosa

400 Bad Request - ID inválido:

json
{
  "error": "El código proporcionado es inválido"
}

500 Internal Server Error - Error en actualización:

json
{
  "error": "Error al modificar la condición de IVA"
}

Capa de Modelo

CondicionIva Model

File: models/modulo-venta/CondicionIva.php

Responsabilidades:

  • CRUD completo sobre tabla ordiva
  • Validaciones de entrada
  • Transformación de datos (CASE WHEN para booleans)
  • Batch loading para prevenir N+1 queries

Métodos Públicos:

getById(int $id): array

Obtiene una condición de IVA por su código.

Validaciones:

  • ID requerido
  • ID debe ser entero

Query SQL:

sql
SELECT
    CIVA::int AS id,
    IDES AS nombre,
    ICUIT AS registra_cuit,
    CASE WHEN IVADISCRI = 'S' THEN true ELSE false END AS discrimina_iva,
    RDES AS abreviacion,
    DEFECTO AS defecto
FROM ordiva
WHERE civa = :id

Retorno: Array asociativo con datos o [] si no existe.


getAll(): array

Obtiene todas las condiciones de IVA (incluye campo habilitado).

Query SQL:

sql
SELECT
    CIVA::int AS id,
    IDES AS nombre,
    ICUIT AS registra_cuit,
    habilitado,
    CASE WHEN IVADISCRI = 'S' THEN true ELSE false END AS discrimina_iva,
    RDES AS abreviacion,
    DEFECTO AS defecto
FROM ordiva

Retorno: Array de arrays asociativos.


update(int $id, array $data): bool

Actualización completa de condición de IVA.

Parámetros esperados ($data):

  • nombre: string
  • registra_cuit: boolean
  • discrimina_iva: boolean (transformado a 'S'/'N')
  • defecto: boolean

Validaciones:

  • ID requerido y entero
  • Transforma discrimina_iva boolean → 'S'/'N'

⚠️ Bug: Falta coma en SQL query (línea 104).

Retorno: true si actualización exitosa, false en caso contrario.


partialUpdate(int $id, array $data): bool

Actualización parcial. Solo implementado para campo defecto.

Lógica Especial:

php
if (isset($data['defecto'])) {
    $defecto = (bool) $data['defecto'];

    if ($defecto) {
        // Desactivar todos los defectos
        $this->conn->prepare("UPDATE ordiva SET defecto = FALSE")->execute();
    }

    // Activar/desactivar el registro específico
    $sql = "UPDATE ordiva SET defecto = :defecto WHERE civa = :id";
    // ... execute

    return true;
}

Garantía de Exclusividad: Al activar un defecto, primero desactiva todos los demás.

Retorno: true si actualización exitosa, false si no hay cambios.


getByIds(array $ids): array

Batch loading para prevenir N+1 queries en consultas masivas.

Parámetros:

  • $ids: Array de enteros (códigos CIVA)

Proceso:

  1. Sanitiza IDs (solo enteros)
  2. Genera placeholders dinámicos
  3. Ejecuta query IN() con prepared statement
  4. Indexa resultados por ID para lookup O(1)

Query SQL:

sql
SELECT
    CIVA::int AS id,
    IDES AS nombre,
    ICUIT AS registra_cuit,
    CASE WHEN IVADISCRI = 'S' THEN true ELSE false END AS discrimina_iva,
    RDES AS abreviacion,
    DEFECTO AS defecto
FROM ordiva
WHERE CIVA IN (?, ?, ?)  -- Placeholders dinámicos

Retorno: Array asociativo indexado por ID:

php
[
    1 => ['id' => 1, 'nombre' => 'RI', ...],
    5 => ['id' => 5, 'nombre' => 'CF', ...],
]

Uso: Módulo Membresía usa este método para enriquecer miembros con sus condiciones IVA sin causar N+1 queries.


Capa de Dominio

CondicionIvaFallback Enum

File: Domain/Ventas/Facturacion/Strategy/Config/CondicionIvaFallback.php

Tipo: Enum PHP 8.1+ (backed enum con valores int)

Propósito: Estrategia fallback para determinar tipo de comprobante (letra A/B/C) en facturación electrónica según condición IVA del cliente.

Casos Definidos:

php
enum CondicionIvaFallback: int
{
    // Principales (más usadas)
    case RESPONSABLE_INSCRIPTO = 1;
    case RESP_NO_INSCRIPTO = 2;
    case EXENTO = 4;
    case CONSUMIDOR_FINAL = 5;
    case MONOTRIBUTO = 6;

    // Otras (menos comunes)
    case SUJETO_NO_CATEGORIZADO = 7;
    case PROVEEDOR_EXTERIOR = 8;
    case CLIENTE_EXTERIOR = 9;
    case IVA_LIBERADO = 10;
    case MONOTRIBUTISTA_SOCIAL = 13;
    case IVA_NO_ALCANZADO = 15;
    case MON_TRAB_IN_PROM = 16;
}

Métodos:

getFallbackLetra(): string

Determina letra de comprobante fallback (A/B/C) según condición IVA.

Lógica (delegada a TipoComprobanteCatalog):

  • Letra A: RI, RNI, MON_TRAB_IN_PROM (discriminan IVA)
  • Letra B: Monotributo, Exento, IVA Liberado, IVA No Alcanzado
  • Letra C: Consumidor Final, Sujeto No Categorizado, clientes/proveedores exterior

requiereCuitEnFallback(): bool

Determina si se requiere CUIT en estrategia fallback.

Retorno:

  • true: RI, RNI, Exento, Monotributo, MS, IVA Liberado, IVA No Alcanzado, MON_TRAB_IN_PROM
  • false: Consumidor Final, Sujeto No Categorizado, clientes/proveedores exterior

Esquema de Base de Datos

Tabla: ordiva

Nivel: EMPRESA ⚠️ (Compartida por todas las sucursales y cajas)

Primary Key: civa (SMALLINT)

Estructura:

CampoTipoConstraintsDescripción
civaSMALLINTPRIMARY KEY, NOT NULLCódigo/ID de condición IVA (1-16)
idesVARCHAR(30)NOT NULLNombre completo de la condición
rdesVARCHAR(5)NOT NULLAbreviación (RI, CF, MON, etc.)
icuitBOOLEANNULLSi requiere registrar CUIT/CUIL
ivadiscriCHAR(1)NULLSi discrimina IVA ('S'/'N')
habilitadoBOOLEANNOT NULL, DEFAULT TRUESi está habilitada para uso
defectoBOOLEANNOT NULL, DEFAULT FALSECondición por defecto (solo UNA puede ser TRUE)
iporDECIMAL(16,5)NULLPorcentaje IVA (sin uso actualmente)
ipor2DECIMAL(16,5)NULLSegundo porcentaje IVA (sin uso actualmente)
letraCHAR(2)NULLLetra de comprobante (sin uso, reemplazado por enum)

Índices: Ninguno adicional (solo PK)

Foreign Keys: Ninguna (tabla de referencia)

Constraints: Ninguno explícito en schema (lógica exclusividad defecto en código)


Schema SQL Completo

sql
CREATE TABLE ordiva (
    civa SMALLINT NOT NULL PRIMARY KEY,
    ides VARCHAR(30) NOT NULL,
    ipor DECIMAL(16,5),
    ipor2 DECIMAL(16,5),
    icuit BOOLEAN,
    ivadiscri CHAR(1),
    letra CHAR(2),
    rdes VARCHAR(5) NOT NULL,
    habilitado BOOLEAN NOT NULL DEFAULT TRUE,
    defecto BOOLEAN NOT NULL DEFAULT FALSE
);

Datos Predefinidos (Seed)

File: migrations/seeds/tenancy/Ordiva.php

Nivel: EMPRESA

Condiciones AFIP/ARCA (12 registros):

CIVANombreAbreviaciónRegistra CUITDiscrimina IVAHabilitadoDefecto
1RESP. INSCRIPTORI
2RESP. NO INSCRIPTORNI
4EXENTOEXE
5CONS. FINALCF
6RESP. MONOTRIBUTOMON
7SUJETO NO CATEGORIZADOSNC
8PROVEEDOR DEL EXTERIOREXT
9CLIENTE DEL EXTERIOREXTC
10IVA LIB. (LEY 19.640)LIB
13MONOTRIBUTISTA SOCIALMS
15IVA NO ALCANZADOINA
16MON. TRAB. IN. PROM.MTIP

Nota: Los códigos CIVA corresponden a los códigos oficiales de AFIP/ARCA para facturación electrónica.


Validaciones Implementadas

Validaciones de Estructura (Model Layer)

En getById() y update():

CampoValidaciónHTTP CodeMensaje
idRequerido400"Debe proporcionarse el código de condición de iva"
idDebe ser entero400"El código proporcionado es inválido"

En partialUpdate():

CampoValidaciónHTTP CodeMensaje
idDebe ser entero400"El código proporcionado es inválido"

Validaciones de Negocio

Exclusividad de defecto:

  • Implementada: Al activar defecto = TRUE en un registro, se desactiva en todos los demás.
  • ⚠️ Implementación: Lógica manual en partialUpdate(), NO constraint DB.
  • 🔒 Nivel de garantía: Aplicación (no garantizado si se modifica DB directamente).

Puntos de Integración

Módulos que Consumen CondicionIva

MóduloUsoTabla Relación
CtaCte (Clientes)Campo civa en clientesordcon.civaordiva.civa
Compras (Proveedores)Campo civa en proveedorescpdprov.civaordiva.civa
CRM (Contactos)Campo civa en contactoscontactos.civaordiva.civa
Membresía (Miembros)Enriquecimiento batch con getByIds()N/A (join virtual)
Ventas (Facturación)Determinar tipo comprobante (letra A/B/C)Vía CondicionIvaFallback enum

Dependency Injection (v3.13.2+)

Container Binding (index.php línea 129-133):

php
CondicionIvaModel::class => factory(function ($container) {
    $connManager = $container->get(ConnectionManager::class);
    $conn = $connManager->get('oficial');
    return new CondicionIvaModel($conn);
})

Consumido por: Módulo Membresía para inyectar en servicios de enriquecimiento.


Estrategia de Testing

Tests Existentes

Integration Tests:

  • Tests/Integration/Membresia/Domain/Facturacion/CondicionIvaIntegrationTest.php
    • Valida integración con módulo Membresía
    • Prueba enriquecimiento batch con condiciones IVA

Unit Tests:

  • Tests/Unit/Domain/Ventas/Facturacion/Strategy/CondicionIvaFallbackTest.php
    • Valida enum CondicionIvaFallback
    • Prueba getFallbackLetra() y requiereCuitEnFallback()

Tests Faltantes (Recomendados)

Unit Tests que deberían crearse:

php
Tests/Unit/Venta/CondicionIvaModelTest.php
├── testGetByIdWithValidId()
├── testGetByIdWithInvalidId()
├── testGetByIdThrowsExceptionWhenIdNotProvided()
├── testGetAll()
├── testUpdateWithValidData()
├── testUpdateThrowsExceptionWithInvalidId()
├── testPartialUpdateActivatesDefecto()
├── testPartialUpdateDeactivatesOtherDefectos()  // Validar exclusividad
├── testGetByIdsWithMultipleIds()
├── testGetByIdsReturnsIndexedArray()
└── testGetByIdsWithEmptyArray()

Integration Tests que deberían crearse:

php
Tests/Integration/Venta/CondicionIvaIntegrationTest.php
├── testGetAllReturnsAllRecords()
├── testUpdateChangesRecord()
├── testPartialUpdateEnforcesDefectoExclusivity()  // Validar exclusividad en DB real
└── testForeignKeyIntegrityWithCliente()  // Validar relación con ordcon

Consideraciones de Rendimiento

Optimizaciones Implementadas

Batch Loading: Método getByIds() implementado para prevenir N+1 queries en consultas masivas.

Ejemplo de uso:

php
// ❌ N+1 queries (sin batch loading)
foreach ($clientes as $cliente) {
    $condicion = $condicionIvaModel->getById($cliente['civa']);  // 1 query por cliente
}

// ✅ 1 query total (con batch loading)
$civas = array_column($clientes, 'civa');
$condiciones = $condicionIvaModel->getByIds($civas);  // 1 query para todos
foreach ($clientes as $cliente) {
    $condicion = $condiciones[$cliente['civa']];  // Lookup O(1)
}

Optimizaciones Recomendadas

⚠️ Índices faltantes: Considerar agregar índice en campos consultados frecuentemente:

  • habilitado (si se filtra por condiciones habilitadas)
  • defecto (si se consulta frecuentemente la condición por defecto)

⚠️ Caché: Condiciones IVA son datos de configuración que casi nunca cambian. Considerar:

  • Cache en memoria (APCu, Redis)
  • Invalidación solo en PUT/PATCH

Seguridad

Vulnerabilidades Identificadas

⚠️ SQL Injection: Parcialmente mitigado.

  • ✅ Métodos getById(), update(), partialUpdate(), getByIds() usan prepared statements
  • ❌ Seed file usa concatenación directa en línea 149:
    php
    $query = "SELECT * FROM ordiva WHERE civa = {$row['civa']}";
    Aunque es seed (no input usuario), debería usar prepared statements.

Auditoría

NO implementada: No hay registro de auditoría en operaciones CUD.

Recomendación: Implementar AuditableInterface + Auditable trait en Service layer (cuando se cree).

Permisos

NO validados: Backend legacy NO valida permisos antes de ejecutar operaciones.

Riesgo: Cualquier usuario autenticado puede modificar condiciones IVA.

Recomendación: Implementar validación de permisos en middleware o Service layer.


Multi-Tenancy

Nivel de Aislamiento

Nivel: EMPRESA (definido en migration línea 30)

Implicaciones:

  • ✅ Tabla creada SOLO en schema public
  • ✅ Compartida por TODAS las sucursales de la empresa
  • ✅ Compartida por TODAS las cajas de todas las sucursales
  • ✅ Datos consistentes en toda la organización

Schema search_path: Configurado automáticamente por ConnectionMiddleware vía header X-Schema.


Deuda Técnica Identificada

ÍtemSeveridadDescripciónRecomendación
Arquitectura Legacy🟡 MediaNo usa Slim routes, Service layer, Validator middlewareMigrar a arquitectura 5-layer DDD
Bug en SQL🔴 AltaFalta coma en update() línea 104Corregir inmediatamente
Constraint defecto🟡 MediaExclusividad de defecto solo en código, no DBAgregar constraint CHECK o trigger
Tests faltantes🟡 MediaNo hay tests unitarios del ModelCrear tests según sección "Estrategia de Testing"
Auditoría🟢 BajaNo hay log de auditoría en CUDImplementar cuando se cree Service layer
Permisos🔴 AltaNo valida permisos en operacionesImplementar validación de permisos
Campos sin uso🟢 Bajaipor, ipor2, letra no se usanConsiderar deprecar en futuras versiones
SQL Injection en Seed🟡 MediaSeed usa concatenación directaCambiar a prepared statements

Plan de Migración Sugerido

Fase 1: Correcciones Críticas (Inmediato)

  1. ✅ Corregir bug SQL en update() (falta coma línea 104)
  2. ✅ Implementar validación de permisos
  3. ✅ Crear tests unitarios del Model

Fase 2: Modernización Arquitectónica (Corto Plazo)

  1. ✅ Crear Slim routes en Routes/Venta/CondicionIvaRoute.php
  2. ✅ Crear Validator middleware Validators/Venta/CondicionIvaValidator.php
  3. ✅ Crear Service layer service/Venta/CondicionIvaService.php con:
    • Transaction management
    • Audit logging (Auditable trait)
    • Business validations
  4. ✅ Agregar constraint DB para exclusividad de defecto

Fase 3: Optimizaciones (Mediano Plazo)

  1. ✅ Implementar caché en memoria para condiciones IVA
  2. ✅ Agregar índices en campos consultados frecuentemente
  3. ✅ Crear tests de integración completos

Referencias


Historial de Cambios

VersiónFechaCambios
v3.13.22025-XX-XXAgregado DI container binding para CondicionIvaModel
v3.10.0+2024-08-23Campo defecto agregado en migration
Legacy2024-08-23Implementación inicial con backend legacy

⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Se recomienda:

  1. Corregir bug SQL en update() antes de usar en producción
  2. Implementar validación de permisos para evitar modificaciones no autorizadas
  3. Validar con equipo de desarrollo que la lógica de exclusividad de defecto cumple requisitos de negocio
  4. Crear tests según plan sugerido para garantizar estabilidad
  5. Considerar migración a arquitectura 5-layer DDD para consistencia con el resto del sistema