Skip to content

Instantly share code, notes, and snippets.

@jluisflo
Last active December 23, 2025 03:53
Show Gist options
  • Select an option

  • Save jluisflo/d41ea919b23e3233b4dcd23d0c81d2b9 to your computer and use it in GitHub Desktop.

Select an option

Save jluisflo/d41ea919b23e3233b4dcd23d0c81d2b9 to your computer and use it in GitHub Desktop.
Plan: IVA Proporcional en Pagos Parciales de Comisiones - Fineract

Plan: IVA Proporcional en Pagos Parciales de Comisiones

Problema

Cuando un pago solo cubre parcialmente el fee moratorio ($100), se necesita calcular y registrar el IVA proporcional (13% de los $50 pagados = $6.50). Actualmente Fineract trata cada charge como independiente.


OPCIÓN A: Charges Vinculados (parent_charge_id)

Decisiones Confirmadas

  • Enfoque: Nativo en Fineract (charges vinculados)
  • Vinculación: Campo explícito parent_charge_id al crear el charge
  • Migración: Solo aplica para préstamos nuevos (no migrar existentes)

Contexto Técnico

  • TaxGroups NO funciona para Loans - Solo está implementado para Savings
  • Charges son independientes - No hay vinculación padre-hijo entre fee moratorio y su IVA

Arquitectura de la Solución

LoanCharge (fee moratorio: $100)
    └── LoanCharge (IVA 13%: $13) [parent_charge_id = fee moratorio]

Escenario 1: Pago Parcial ($50 de $113 pendientes)

Pago de $50 (IVA INCLUIDO):
    1. Se recibe pago de $50
    2. Se detecta charge vinculado (IVA con tax_percentage=13%)
    3. Se calcula distribución:
       - IVA = $50 / 1.13 * 0.13 = $5.75
       - Fee neto = $50 - $5.75 = $44.25
    4. Se aplica $44.25 al fee moratorio (quedan $55.75 pendientes)
    5. Se aplica $5.75 al IVA (quedan $7.25 pendientes)
    6. Total pagado = $50

Estado después del pago:
    - Fee moratorio: $44.25 pagado / $55.75 pendiente
    - IVA: $5.75 pagado / $7.25 pendiente

Escenario 2: Pago Completo ($113 = fee + IVA)

Pago de $113 (IVA INCLUIDO):
    1. Se recibe pago de $113
    2. Se detecta charge vinculado (IVA con tax_percentage=13%)
    3. Se calcula distribución:
       - IVA = $113 / 1.13 * 0.13 = $13
       - Fee neto = $113 - $13 = $100
    4. Se aplica $100 al fee moratorio (queda $0 pendiente) ✓
    5. Se aplica $13 al IVA (queda $0 pendiente) ✓
    6. Ambos charges marcados como PAID
    7. Total pagado = $113

Estado después del pago:
    - Fee moratorio: PAGADO COMPLETO
    - IVA: PAGADO COMPLETO

Escenario 3: Pago que excede lo pendiente

Fee pendiente: $55.75, IVA pendiente: $7.25 (Total: $63)
Pago recibido: $100

Distribución calculada:
    - IVA = $100 / 1.13 * 0.13 = $11.50
    - Fee neto = $100 - $11.50 = $88.50

Pero pendiente es menor, entonces:
    - Fee aplicado = min($88.50, $55.75) = $55.75 ✓
    - IVA aplicado = min($11.50, $7.25) = $7.25 ✓
    - Sobrante = $100 - $55.75 - $7.25 = $37 → se aplica a otros charges/principal

Resultado:
    - Fee moratorio: PAGADO COMPLETO
    - IVA: PAGADO COMPLETO
    - Sobrante: $37 para otros conceptos

Fórmula General

IVA = monto / (1 + taxPercentage) * taxPercentage
Fee = monto - IVA

// Con límites
IVA_aplicado = min(IVA_calculado, IVA_pendiente)
Fee_aplicado = min(Fee_calculado, Fee_pendiente)
Sobrante = monto - IVA_aplicado - Fee_aplicado

Archivos a Modificar/Crear

1. Migración de Base de Datos

Crear: fineract-provider/src/main/resources/db/changelog/tenant/parts/XXXX_linked_charges.xml

ALTER TABLE m_loan_charge ADD COLUMN parent_charge_id BIGINT NULL;
ALTER TABLE m_loan_charge ADD COLUMN tax_percentage DECIMAL(5,2) NULL;
ALTER TABLE m_loan_charge ADD CONSTRAINT fk_loan_charge_parent
    FOREIGN KEY (parent_charge_id) REFERENCES m_loan_charge(id);
CREATE INDEX idx_loan_charge_parent ON m_loan_charge(parent_charge_id);

2. Entidad LoanCharge

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_charge_id")
private LoanCharge parentCharge;

@OneToMany(mappedBy = "parentCharge", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<LoanCharge> linkedCharges = new HashSet<>();

@Column(name = "tax_percentage", scale = 2, precision = 5)
private BigDecimal taxPercentage;

public boolean hasLinkedCharges() { ... }
public Set<LoanCharge> getActiveLinkedCharges() { ... }

// Calcula la distribución del pago entre fee e IVA (IVA incluido en el monto)
public PaymentSplit calculatePaymentSplit(Money totalPayment) {
    // taxAmount = totalPayment / (1 + taxPercentage) * taxPercentage
    // feeAmount = totalPayment - taxAmount
    BigDecimal divisor = BigDecimal.ONE.add(this.taxPercentage.divide(new BigDecimal("100")));
    Money taxAmount = totalPayment.dividedBy(divisor).multipliedBy(this.taxPercentage).dividedBy(100);
    Money feeAmount = totalPayment.minus(taxAmount);
    return new PaymentSplit(feeAmount, taxAmount);
}

3. Servicio de Pago

Modificar: fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java

En método payLoanCharge():

// Si el charge tiene un charge de IVA vinculado, distribuir el pago
if (loanCharge.hasLinkedCharges()) {
    LoanCharge taxCharge = loanCharge.getLinkedTaxCharge();

    // Calcular distribución proporcional
    PaymentSplit split = taxCharge.calculatePaymentSplit(paymentAmount);

    // Aplicar límites (no pagar más de lo pendiente)
    Money feeOutstanding = loanCharge.getAmountOutstanding(currency);
    Money taxOutstanding = taxCharge.getAmountOutstanding(currency);

    Money feeToApply = Money.min(split.getFeeAmount(), feeOutstanding);
    Money taxToApply = Money.min(split.getTaxAmount(), taxOutstanding);

    // Aplicar pagos
    applyChargePayment(loan, loanCharge, feeToApply, transactionDate);
    applyChargePayment(loan, taxCharge, taxToApply, transactionDate);

    // Calcular sobrante para otros conceptos
    Money totalApplied = feeToApply.plus(taxToApply);
    Money surplus = paymentAmount.minus(totalApplied);

    if (surplus.isGreaterThanZero()) {
        // El sobrante se aplica al siguiente charge/principal según PaymentAllocationStrategy
        applyRemainingToLoan(loan, surplus, transactionDate);
    }
} else {
    // Comportamiento normal (sin IVA vinculado)
    applyChargePayment(loan, loanCharge, paymentAmount, transactionDate);
}

3.1 Clase auxiliar PaymentSplit

Crear: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/PaymentSplit.java

public class PaymentSplit {
    private final Money feeAmount;
    private final Money taxAmount;

    public PaymentSplit(Money feeAmount, Money taxAmount) {
        this.feeAmount = feeAmount;
        this.taxAmount = taxAmount;
    }

    // getters
}

4. API y DTOs

Modificar:

  • LoanChargesApiResource.java - Aceptar parentChargeId y taxPercentage
  • LoanChargeData.java - Agregar campos nuevos
  • ChargesApiConstants.java - Constantes para nuevos parámetros

5. Repositorio

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java

@Query("SELECT lc FROM LoanCharge lc WHERE lc.parentCharge.id = :parentId AND lc.active = true")
List<LoanCharge> findActiveLinkedCharges(@Param("parentId") Long parentChargeId);

Resumen de Cambios

Archivo Acción Líneas Est.
XXXX_linked_charges.xml Crear ~30
LoanCharge.java Modificar ~100
PaymentSplit.java Crear ~30
LoanChargeRepository.java Modificar ~15
LoanChargeWritePlatformServiceImpl.java Modificar ~120
LoanChargeReadPlatformServiceImpl.java Modificar ~40
LoanChargeData.java Modificar ~25
LoanChargesApiResource.java Modificar ~30
ChargesApiConstants.java Modificar ~10
Tests unitarios Crear ~200
Tests integración Crear ~150

Total estimado: ~750 líneas


Fases de Implementación

Fase 1: Base de Datos y Entidad

  1. Crear migración XML para nuevas columnas
  2. Modificar LoanCharge.java con campos y relaciones
  3. Actualizar LoanChargeRepository.java

Fase 2: Lógica de Negocio

  1. Implementar calculateProportionalAmount() en LoanCharge
  2. Modificar payLoanCharge() para procesar charges vinculados
  3. Agregar validaciones (no permitir pagar charge vinculado directamente)

Fase 3: API y DTOs

  1. Actualizar LoanChargeData.java con nuevos campos
  2. Modificar LoanChargesApiResource.java para aceptar parentChargeId
  3. Actualizar serialización/deserialización

Fase 4: Testing

  1. Tests unitarios para cálculo proporcional
  2. Tests de integración para flujo completo de pago
  3. Tests de regresión para pagos normales

Rutas Completas de Archivos

fineract-provider/src/main/resources/db/changelog/tenant/parts/
├── XXXX_linked_charges.xml (CREAR)

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/
├── domain/
│   ├── LoanCharge.java (MODIFICAR)
│   ├── PaymentSplit.java (CREAR)
│   └── LoanChargeRepository.java (MODIFICAR)
├── data/
│   └── LoanChargeData.java (MODIFICAR)
└── service/
    └── LoanChargeReadPlatformServiceImpl.java (MODIFICAR)

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/
├── api/
│   └── LoanChargesApiResource.java (MODIFICAR)
└── service/
    └── LoanChargeWritePlatformServiceImpl.java (MODIFICAR)

fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/api/
└── ChargesApiConstants.java (MODIFICAR)

Validaciones Importantes

  1. Al crear charge vinculado: Validar que parentChargeId exista y pertenezca al mismo préstamo
  2. Al pagar fee principal: Si tiene IVA vinculado, distribuir automáticamente
  3. Al pagar charge de IVA directamente: Rechazar si tiene parentCharge (debe pagarse vía el padre)
  4. Monto máximo: Validar que el pago no exceda lo pendiente (fee + IVA)

Contabilidad (Ya soportado por Fineract)

El mapeo contable funciona automáticamente porque:

  1. Cada Charge tiene su propia cuenta GL (income_or_liability_account_id)
  2. Al pagar, se crea un LoanChargePaidBy por cada charge
  3. AccountingProcessorHelper.createJournalEntriesForLoanChargesInternal() genera asientos separados

Flujo Contable Esperado

Pago de $50 distribuido:
├── LoanChargePaidBy { charge: "Fee Moratorio", amount: $44.25 }
└── LoanChargePaidBy { charge: "IVA", amount: $5.75 }

Journal Entries generados:
├── Crédito $44.25 → Cuenta 4100 (Ingresos por comisiones)
├── Crédito $5.75  → Cuenta 2100 (IVA por pagar)
└── Débito  $50.00 → Cuenta Caja/Fondo

Configuración Requerida

Al crear los charges, asegurarse de configurar:

  • Fee Moratorio: income_or_liability_account_id = Cuenta de ingresos por comisiones
  • Charge IVA: income_or_liability_account_id = Cuenta de IVA por pagar


OPCIÓN B: Habilitar TaxGroups en Loans (Replicar patrón de Savings)

Descripción

Implementar el soporte de TaxGroups en el módulo de Loans, replicando el patrón que ya funciona en Savings. Esto aprovecha la infraestructura existente:

  • Charge ya tiene tax_group_id - Solo falta usarlo en LoanCharge
  • TaxUtils.splitTax() - Ya existe y calcula impuestos proporcionalmente
  • TaxComponent - Ya tiene cuentas GL configuradas para contabilidad

Ventajas sobre Opción A

Aspecto Opción A (Charges Vinculados) Opción B (TaxGroups)
Reutilización Código nuevo Replica patrón probado de Savings
Configuración 2 charges separados 1 charge + TaxGroup
Mantenimiento Lógica custom Usa TaxUtils estándar
Contabilidad Config por charge Config en TaxComponent
Múltiples impuestos N charges vinculados 1 TaxGroup con N componentes
Historial de tasas Manual TaxComponentHistory automático

Arquitectura de la Solución

Charge (fee moratorio)
    └── TaxGroup (IVA 13%)
            └── TaxComponent (IVA)
                    ├── percentage: 13.00
                    ├── creditAccount: Cuenta 2100 (IVA por pagar)
                    └── debitAccount: Cuenta de retención

LoanCharge hereda TaxGroup del Charge padre
    └── Al pagar, usa TaxUtils.splitTax() para calcular IVA

Flujo de Pago con TaxGroup (IVA ADICIONAL - Como Savings)

Situación Actual

  • Hoy creas 2 charges separados: Fee neto ($100) + Charge IVA ($13)
  • Problema: Al pagar parcialmente no se distribuye proporcionalmente

Con TaxGroup (Solución)

  • 1 solo charge: Fee neto ($100) con TaxGroup (IVA 13%)
  • El IVA se calcula automáticamente SOBRE el monto pagado
Charge configurado: Fee moratorio $100 (NETO) + TaxGroup IVA 13%
Total adeudado: $100 + $13 = $113

Escenario: Pago parcial de $50

1. Se recibe pago de $50
2. Se distribuye proporcionalmente:
   - Proporción pagada: $50 / $113 = 44.25%
   - Fee neto pagado: $100 × 44.25% = $44.25
   - IVA pagado: $13 × 44.25% = $5.75
3. Se usa TaxUtils.splitTax() existente:
   - IVA = $44.25 × 13% = $5.75 ✓
4. Se crea LoanTransactionTaxDetails:
   - taxComponent: IVA
   - amount: $5.75
5. Journal entries:
   - Crédito $44.25 → Cuenta ingresos comisiones
   - Crédito $5.75 → Cuenta 2100 (IVA por pagar)
   - Débito $50 → Cuenta caja

Estado después del pago:
- Fee pendiente: $100 - $44.25 = $55.75
- IVA pendiente: $13 - $5.75 = $7.25
- Total pendiente: $63

Fórmula IVA Adicional (TaxUtils.splitTax existente):

// IVA sobre el monto neto - YA EXISTE EN FINERACT
tax = netAmount × percentage / 100
// Ejemplo: $44.25 × 13% = $5.75

Ventaja: No necesitamos crear nuevo método, reutilizamos TaxUtils.splitTax() tal cual.


Archivos a Crear/Modificar

1. Nueva Entidad: LoanTransactionTaxDetails

Crear: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionTaxDetails.java

@Entity
@Table(name = "m_loan_transaction_tax_details")
public class LoanTransactionTaxDetails extends AbstractPersistableCustom<Long> {

    @ManyToOne
    @JoinColumn(name = "loan_transaction_id", nullable = false)
    private LoanTransaction loanTransaction;

    @ManyToOne
    @JoinColumn(name = "tax_component_id", nullable = false)
    private TaxComponent taxComponent;

    @Column(name = "amount", scale = 6, precision = 19, nullable = false)
    private BigDecimal amount;

    // Constructor, getters
}

2. Migración de Base de Datos

Crear: fineract-provider/src/main/resources/db/changelog/tenant/parts/XXXX_loan_tax_details.xml

CREATE TABLE m_loan_transaction_tax_details (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    loan_transaction_id BIGINT NOT NULL,
    tax_component_id BIGINT NOT NULL,
    amount DECIMAL(19,6) NOT NULL,
    CONSTRAINT fk_loan_tx_tax_loan_transaction
        FOREIGN KEY (loan_transaction_id) REFERENCES m_loan_transaction(id),
    CONSTRAINT fk_loan_tx_tax_component
        FOREIGN KEY (tax_component_id) REFERENCES m_tax_component(id)
);
CREATE INDEX idx_loan_tx_tax_transaction ON m_loan_transaction_tax_details(loan_transaction_id);

3. Modificar LoanTransaction

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java

// Agregar campo
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true,
           fetch = FetchType.EAGER, mappedBy = "loanTransaction")
private List<LoanTransactionTaxDetails> taxDetails = new ArrayList<>();

// Agregar método
public List<LoanTransactionTaxDetails> getTaxDetails() {
    return this.taxDetails;
}

// Agregar método estático para actualizar detalles
public static void updateTaxDetails(
        final Map<TaxComponent, BigDecimal> taxDetails,
        final LoanTransaction transaction) {
    if (taxDetails != null) {
        for (Map.Entry<TaxComponent, BigDecimal> entry : taxDetails.entrySet()) {
            transaction.getTaxDetails().add(
                new LoanTransactionTaxDetails(transaction, entry.getKey(), entry.getValue())
            );
        }
    }
}

4. Modificar LoanCharge para acceder al TaxGroup

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java

// Agregar método para obtener TaxGroup del Charge padre
public TaxGroup getTaxGroup() {
    return this.charge.getTaxGroup();
}

public boolean hasTaxGroup() {
    return this.charge.getTaxGroup() != null;
}

5. Modificar Servicio de Pago de Charges

Modificar: fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java

En método donde se procesa el pago:

// Si el charge tiene TaxGroup, calcular impuestos (IVA ADICIONAL)
if (loanCharge.hasTaxGroup()) {
    TaxGroup taxGroup = loanCharge.getTaxGroup();
    LocalDate transactionDate = DateUtils.getBusinessLocalDate();

    // Calcular impuestos usando TaxUtils.splitTax() EXISTENTE
    // El pago se distribuye proporcionalmente entre fee neto e IVA
    Map<TaxComponent, BigDecimal> taxSplit = TaxUtils.splitTax(
        feeNetAmount.getAmount(),  // Monto neto del fee pagado
        transactionDate,
        taxGroup.getTaxGroupMappings(),
        feeNetAmount.getAmount().scale()
    );

    BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit);

    if (totalTax.compareTo(BigDecimal.ZERO) > 0) {
        // Registrar detalles de impuestos en la transacción
        LoanTransaction.updateTaxDetails(taxSplit, loanTransaction);
    }
}

6. Modificar Contabilidad

Modificar: fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java

Agregar método similar a createCashBasedJournalEntriesAndReversalsForSavingsTax():

public void createJournalEntriesForLoanChargeTax(
        final Office office,
        final String currencyCode,
        final Long loanProductId,
        final Long loanId,
        final String transactionId,
        final LocalDate transactionDate,
        final List<TaxPaymentDTO> taxDetails) {

    for (TaxPaymentDTO taxPaymentDTO : taxDetails) {
        if (taxPaymentDTO.getAmount() != null &&
            taxPaymentDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {

            // Crédito a la cuenta del TaxComponent
            if (taxPaymentDTO.getCreditAccountId() != null) {
                createCreditJournalEntryForLoan(
                    office, currencyCode,
                    taxPaymentDTO.getCreditAccountId(),
                    loanId, transactionId, transactionDate,
                    taxPaymentDTO.getAmount()
                );
            }
        }
    }
}

7. Mapper para exponer datos de impuestos

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java

Agregar extracción de taxDetails similar a como se hace en Savings.


Resumen de Cambios - Opción B

Archivo Acción Líneas Est.
XXXX_loan_tax_details.xml Crear ~25
LoanTransactionTaxDetails.java Crear ~50
LoanTransaction.java Modificar ~40
LoanCharge.java Modificar ~15
LoanChargeWritePlatformServiceImpl.java Modificar ~60
AccountingProcessorHelper.java Modificar ~50
LoanAccountingBridgeMapper.java Modificar ~30
AccrualBasedAccountingProcessorForLoan.java Modificar ~40
Tests unitarios Crear ~150
Tests integración Crear ~100

Total estimado: ~560 líneas

Nota: No se requiere modificar TaxUtils.java - se reutiliza splitTax() existente.


Fases de Implementación - Opción B

Fase 1: Infraestructura de Datos

  1. Crear migración para m_loan_transaction_tax_details
  2. Crear entidad LoanTransactionTaxDetails
  3. Modificar LoanTransaction para incluir taxDetails

Fase 2: Lógica de Negocio

  1. Agregar métodos en LoanCharge para acceder al TaxGroup
  2. Modificar procesamiento de pagos para calcular impuestos con TaxUtils
  3. Registrar LoanTransactionTaxDetails al pagar

Fase 3: Contabilidad

  1. Modificar AccountingProcessorHelper para manejar taxDetails de Loans
  2. Asegurar que se generen asientos a las cuentas del TaxComponent

Fase 4: Testing

  1. Tests unitarios para cálculo de impuestos
  2. Tests de integración para flujo completo
  3. Validar asientos contables generados

Configuración Requerida - Opción B

1. Crear TaxComponent para IVA

POST /taxes/component
{
    "name": "IVA 13%",
    "percentage": 13.00,
    "creditAccountId": 2100,  // Cuenta IVA por pagar
    "startDate": "2024-01-01"
}

2. Crear TaxGroup

POST /taxes/group
{
    "name": "Impuestos Comisiones",
    "taxComponents": [
        { "taxComponentId": 1, "startDate": "2024-01-01" }
    ]
}

3. Asociar TaxGroup al Charge

POST /charges
{
    "name": "Fee Moratorio",
    "chargeAppliesTo": 1,  // LOAN
    "amount": 100,
    "taxGroupId": 1,  // TaxGroup creado
    ...
}

Comparación Final

Criterio Opción A Opción B
Líneas de código ~750 ~560
Complejidad Media Media
Reutilización Baja Alta (replica Savings)
Mantenimiento Lógica custom Usa infraestructura existente
Flexibilidad Solo IVA vinculado Múltiples impuestos, historial de tasas
Configuración Por charge Por TaxGroup (centralizada)
Fórmula IVA IVA incluido IVA adicional (estándar Fineract) ✓
Charges visibles 2 (fee + IVA) 1 (fee con TaxGroup)
TaxUtils Nuevo método Reutiliza splitTax() existente ✓

Recomendación

Opción B (TaxGroups) es preferible si:

  • Quieres reutilizar la infraestructura existente de Fineract
  • Necesitas flexibilidad para múltiples impuestos o cambios de tasas
  • Prefieres configuración centralizada
  • Solo quieres ver 1 charge (el fee), con el IVA calculado internamente

Opción A (Charges Vinculados) es preferible si:

  • Necesitas ver el IVA como un charge separado en la UI/API
  • Quieres control granular sobre cada charge de IVA
  • Prefieres no depender de la configuración de TaxGroups

Compatibilidad con Cronogramas y Estrategias de Pago (Opción B)

Tipos de Cronogramas

Cronograma Compatibilidad Notas
CUMULATIVE ✅ Alta Períodos fijos, sin recálculo dinámico
PROGRESSIVE ⚠️ Media-Alta Requiere validación con recálculo de interés

Estrategias de Pago (9+ implementaciones)

Estrategia Compatibilidad
HeavensFamily ✅ Compatible
Creocor ✅ Compatible
RBI ✅ Compatible
FineractStyle ✅ Compatible
EarlyPayment ✅ Compatible
InterestPrincipalPenaltyFees ✅ Compatible
PrincipalInterestPenaltyFees ✅ Compatible
DuePen/InAdvance ✅ Compatible
AdvancedPaymentSchedule ⚠️ Requiere testing adicional

Razón de Alta Compatibilidad

Todas las estrategias usan el mismo método base:

  • LoanCharge.updatePaidAmountBy() - Punto central donde se aplican pagos
  • AbstractLoanRepaymentScheduleTransactionProcessor.updateChargesPaidAmountBy() - Procesa pagos a charges

Al integrar TaxGroup en estos métodos, todas las estrategias heredan automáticamente el soporte de impuestos.

Consideración para AdvancedPaymentScheduleTransactionProcessor

El procesador avanzado tiene lógica adicional para:

  • Recálculo dinámico de interés
  • PaymentAllocationRules configurables
  • FutureInstallmentAllocationRule

Recomendación: Ejecutar tests de integración específicos para este procesador después de implementar.


EXTENSIÓN: IVA sobre Intereses Devengados

Contexto

Los intereses devengados también son un ingreso que requiere facturar IVA según la regulación financiera. Actualmente Fineract no soporta TaxGroups para intereses de préstamos.

Arquitectura Actual de Intereses

LoanTransaction.interestPortion → AccrualBasedAccountingProcessorForLoan
    → Crédito a INTEREST_ON_LOANS (cuenta de ingresos)
    → Débito a INTEREST_RECEIVABLE (activo) o FUND_SOURCE (en cobro)

Archivos clave:

  • LoanTransaction.java:94 - Campo interestPortion
  • AccrualBasedAccountingProcessorForLoan.java - Genera asientos contables
  • AccountingConstants.java:94 - INTEREST_ON_LOANS(3)

Solución Propuesta: TaxGroup a nivel de LoanProduct

Configuración

LoanProduct
    └── interestTaxGroup (NUEVO campo)
            └── TaxGroup (IVA 13%)
                    └── TaxComponent (IVA)
                            ├── percentage: 13.00
                            └── creditAccount: 2100 (IVA por pagar)

Flujo de Devengamiento (Accrual)

Interés devengado: $100 NETO

Cálculo con TaxGroup:
    IVA = $100 × 13% = $13
    Total = $113

Journal Entries (devengamiento):
├── Débito  $100 → INTEREST_RECEIVABLE (Activo)
├── Débito  $13  → IVA_RECEIVABLE (Activo) ← NUEVA CUENTA
├── Crédito $100 → INTEREST_ON_LOANS (Ingreso)
└── Crédito $13  → IVA_POR_PAGAR (Pasivo) ← del TaxComponent

Flujo de Cobro (Repayment)

Pago parcial de interés: $50 de $113 pendientes

Distribución proporcional:
    - Proporción: $50 / $113 = 44.25%
    - Interés neto cobrado: $100 × 44.25% = $44.25
    - IVA cobrado: $13 × 44.25% = $5.75

Journal Entries (cobro):
├── Débito  $50   → FUND_SOURCE (Caja)
├── Crédito $44.25 → INTEREST_RECEIVABLE
└── Crédito $5.75  → IVA_RECEIVABLE

Archivos a Modificar/Crear (Adicionales a Opción B)

1. Migración de Base de Datos

Modificar: XXXX_loan_tax_details.xml

-- Agregar TaxGroup a LoanProduct para intereses
ALTER TABLE m_product_loan ADD COLUMN interest_tax_group_id BIGINT NULL;
ALTER TABLE m_product_loan ADD CONSTRAINT fk_product_loan_interest_tax_group
    FOREIGN KEY (interest_tax_group_id) REFERENCES m_tax_group(id);

-- Nueva cuenta contable para IVA por cobrar sobre intereses
-- Se configura a nivel de AccrualAccountsForLoan

2. Modificar LoanProduct

Modificar: fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "interest_tax_group_id")
private TaxGroup interestTaxGroup;

public TaxGroup getInterestTaxGroup() {
    return this.interestTaxGroup;
}

public boolean hasInterestTaxGroup() {
    return this.interestTaxGroup != null;
}

3. Nueva Cuenta Contable: IVA_RECEIVABLE

Modificar: AccountingConstants.java

public enum AccrualAccountsForLoan {
    // ... existentes ...
    INTEREST_TAX_RECEIVABLE(26);  // NUEVA
}

public enum LoanProductAccountingParams {
    // ... existentes ...
    INTEREST_TAX_RECEIVABLE("interestTaxReceivableAccountId");  // NUEVA
}

4. Modificar Procesador de Devengamiento

Modificar: AccrualBasedAccountingProcessorForLoan.java

En el método que procesa accruals:

// Si el producto tiene TaxGroup para intereses, calcular impuestos
if (loanDTO.hasInterestTaxGroup() && MathUtil.isGreaterThanZero(interestAmount)) {
    TaxGroup taxGroup = loanDTO.getInterestTaxGroup();

    // Calcular impuestos usando TaxUtils.splitTax()
    Map<TaxComponent, BigDecimal> taxSplit = TaxUtils.splitTax(
        interestAmount, transactionDate,
        taxGroup.getTaxGroupMappings(), interestAmount.scale()
    );

    BigDecimal totalTax = TaxUtils.totalTaxAmount(taxSplit);

    // Registrar impuesto sobre interés
    for (Map.Entry<TaxComponent, BigDecimal> entry : taxSplit.entrySet()) {
        TaxComponent taxComponent = entry.getKey();
        BigDecimal taxAmount = entry.getValue();

        if (MathUtil.isGreaterThanZero(taxAmount)) {
            // Débito a IVA_RECEIVABLE (activo)
            populateDebitMap(loanProductId, taxAmount,
                AccrualAccountsForLoan.INTEREST_TAX_RECEIVABLE.getValue(),
                glAccountBalanceHolder);

            // Crédito a cuenta del TaxComponent (IVA por pagar)
            glAccountBalanceHolder.addToCredit(
                taxComponent.getCreditAcount(), taxAmount);
        }
    }

    // Registrar en LoanTransactionTaxDetails
    LoanTransaction.updateTaxDetails(taxSplit, loanTransaction);
}

5. Modificar LoanTransactionDTO

Modificar: fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanTransactionDTO.java

// Agregar campo para detalles de impuestos
private List<TaxPaymentDTO> interestTaxDetails;

6. Modificar LoanDTO

Modificar: fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java

private TaxGroup interestTaxGroup;

public boolean hasInterestTaxGroup() {
    return this.interestTaxGroup != null;
}

Resumen de Cambios Adicionales

Archivo Acción Líneas Est.
LoanProduct.java Modificar ~20
AccountingConstants.java Modificar ~15
AccrualBasedAccountingProcessorForLoan.java Modificar ~80
CashBasedAccountingProcessorForLoan.java Modificar ~60
LoanDTO.java Modificar ~15
LoanTransactionDTO.java Modificar ~10
LoanProductWritePlatformServiceImpl.java Modificar ~30
LoanProductReadPlatformServiceImpl.java Modificar ~20
Migración XML Modificar ~20
Tests Crear ~150

Total adicional estimado: ~420 líneas

Fases de Implementación (Adicionales)

Fase 5: Infraestructura para IVA en Intereses

  1. Agregar interest_tax_group_id a m_product_loan
  2. Agregar campo y relación en LoanProduct.java
  3. Agregar nueva cuenta INTEREST_TAX_RECEIVABLE en AccountingConstants

Fase 6: Lógica de Devengamiento con IVA

  1. Modificar AccrualBasedAccountingProcessorForLoan para calcular IVA
  2. Modificar CashBasedAccountingProcessorForLoan similarmente
  3. Generar asientos contables separados para IVA

Fase 7: Lógica de Cobro con IVA

  1. Distribuir pagos proporcionalmente entre interés neto e IVA
  2. Registrar LoanTransactionTaxDetails para intereses

Fase 8: API y Configuración

  1. Permitir configurar interestTaxGroupId al crear/editar producto
  2. Exponer información de impuestos en APIs de transacciones

Flujo Completo con Ejemplo

CONFIGURACIÓN:
- Producto: "Crédito Personal"
- interestTaxGroup: TaxGroup "IVA Intereses" (13%)
- Cuenta INTEREST_ON_LOANS: 4200 (Ingresos por intereses)
- Cuenta INTEREST_TAX_RECEIVABLE: 1150 (IVA por cobrar)
- TaxComponent.creditAccount: 2100 (IVA por pagar)

PRÉSTAMO:
- Principal: $10,000
- Interés mensual: $500 (NETO)
- IVA sobre interés: $65 (13%)
- Total interés + IVA: $565

DEVENGAMIENTO (día 1 del mes):
├── Débito  $500 → 1140 INTEREST_RECEIVABLE
├── Débito  $65  → 1150 INTEREST_TAX_RECEIVABLE
├── Crédito $500 → 4200 INTEREST_ON_LOANS
└── Crédito $65  → 2100 IVA_POR_PAGAR

PAGO PARCIAL ($300 de $565):
├── Débito  $300   → 1000 CAJA
├── Crédito $265.49 → 1140 INTEREST_RECEIVABLE (53.1%)
└── Crédito $34.51  → 1150 INTEREST_TAX_RECEIVABLE (53.1%)

Estado pendiente:
- Interés: $234.51
- IVA: $30.49
- Total: $265

Gist del Plan

URL: https://gist.github.com/jluisflo/d41ea919b23e3233b4dcd23d0c81d2b9

(Actualizar manualmente con el contenido final)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment