diff --git a/build.gradle b/build.gradle index 941042600a4..1a453c9a19a 100644 --- a/build.gradle +++ b/build.gradle @@ -477,7 +477,8 @@ configure(project.fineractJavaProjects) { errorprone "com.google.errorprone:error_prone_core:2.24.1" } - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' options.errorprone { enabled = project.gradle.startParameter.taskNames.contains('build') || project.gradle.startParameter.taskNames.contains('check') disableWarningsInGeneratedCode = true diff --git a/buildSrc/src/main/groovy/org/apache/fineract/gradle/service/EmailService.groovy b/buildSrc/src/main/groovy/org/apache/fineract/gradle/service/EmailService.groovy index ac93770bdcb..9f2138af647 100644 --- a/buildSrc/src/main/groovy/org/apache/fineract/gradle/service/EmailService.groovy +++ b/buildSrc/src/main/groovy/org/apache/fineract/gradle/service/EmailService.groovy @@ -40,15 +40,17 @@ class EmailService { this.properties = new Properties() this.properties.put("mail.smtp.host", config.host) - this.properties.put("mail.smtp.auth", "true") + this.properties.put("mail.smtp.auth", "false") this.properties.put("mail.smtp.starttls.enable", config.tls.toString()) if(config.ssl) { this.properties.put("mail.smtp.port", "465") this.properties.put("mail.smtp.socketFactory.port", "465"); this.properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); - } else { + } else if (config.tls) { this.properties.put("mail.smtp.port", "587") + } else { + this.properties.put("mail.smtp.port", "25") } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 9f2dbd50681..74f27618e07 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -1312,6 +1312,29 @@ public CommandWrapperBuilder assignDelinquency(final Long loanId) { return this; } + public CommandWrapperBuilder createClasificacionConcepto() { + this.actionName = "CREATE"; + this.entityName = "CLASIFICACION_CONCEPTO"; + this.href = "/clasificacionconceptos"; + return this; + } + + public CommandWrapperBuilder updateClasificacionConcepto(final Long id) { + this.actionName = "UPDATE"; + this.entityName = "CLASIFICACION_CONCEPTO"; + this.href = "/clasificacionconceptos"; + this.entityId = id; + return this; + } + + public CommandWrapperBuilder deleteClasificacionConcepto(final Long id) { + this.actionName = "DELETE"; + this.entityName = "CLASIFICACION_CONCEPTO"; + this.href = "/clasificacionconceptos"; + this.entityId = id; + return this; + } + public CommandWrapperBuilder createCodeValue(final Long codeId) { this.actionName = "CREATE"; this.entityName = "CODEVALUE"; @@ -4083,4 +4106,28 @@ public CommandWrapperBuilder excludeFromReclaim(final Long loanId) { this.href = "reclaim/exclude/" + loanId; return this; } + + public CommandWrapperBuilder createCollectionHouse() { + this.actionName = "CREATE"; + this.entityName = "PRODUCTCOLLECTIONHOUSE"; + this.entityId = null; + this.href = "/collectionhousemanagement"; + return this; + } + + public CommandWrapperBuilder updateCollectionHouse(final Long parameterId) { + this.actionName = "UPDATE"; + this.entityName = "PRODUCTCOLLECTIONHOUSE"; + this.entityId = parameterId; + this.href = "/collectionhousemanagement/" + parameterId; + return this; + } + + public CommandWrapperBuilder createCollectionHouseHistory() { + this.actionName = "CREATE"; + this.entityName = "COLLECTIONHOUSEHISTORY"; + this.entityId = null; + this.href = "/collectionhousehistory"; + return this; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/domain/CodeValueRepository.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/domain/CodeValueRepository.java index 60ba58e5447..e1d7a2b0f5b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/domain/CodeValueRepository.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/domain/CodeValueRepository.java @@ -18,12 +18,39 @@ */ package org.apache.fineract.infrastructure.codes.domain; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface CodeValueRepository extends JpaRepository, JpaSpecificationExecutor { CodeValue findByCodeNameAndId(String codeName, Long id); CodeValue findByCodeNameAndLabel(String codeName, String label); + + @Query(value = """ + SELECT + mv2.code_score AS ciudad_code_score, + mv2.code_value AS ciudad_code_value, + mcv.code_score AS departamento_code_score, + mcv.code_id AS departamento_code_id, + mcv.code_value AS departamento_code_value + FROM m_code_value mcv + INNER JOIN ( + SELECT *, + CASE + WHEN LEFT(LPAD(code_score::TEXT, 5, '0'), 2)::INTEGER < 10 THEN + LEFT(LPAD(code_score::TEXT, 5, '0'), 2)::INTEGER::VARCHAR + ELSE + LEFT(LPAD(code_score::TEXT, 5, '0'), 2) + END AS ciudadCoderScore + FROM m_code_value mcv2 + WHERE code_id = (select * from m_code mc where mc.code_name ='Ciudad') + ) mv2 + ON mv2.ciudadCoderScore = mcv.code_score + WHERE mcv.code_id = (select * from m_code mc where mc.code_name ='Departamento') AND mv2.code_score = :codeScore + """, nativeQuery = true) + Optional findCiudadAndDepartamentoData(@Param("codeScore") String codeScore); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index fdbaeee4c14..7f3a95f9814 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.configuration.domain; import java.time.LocalDate; +import java.util.List; import org.apache.fineract.infrastructure.cache.domain.CacheType; public interface ConfigurationDomainService { @@ -37,6 +38,8 @@ public interface ConfigurationDomainService { boolean allowTransactionsOnNonWorkingDayEnabled(); + boolean enableMonthlyInvoiceGenerationOnJobTrigger(); + boolean isConstraintApproachEnabledForDatatables(); boolean isEhcacheEnabled(); @@ -149,4 +152,14 @@ public interface ConfigurationDomainService { Long retriveMinimumDaysOfArrearsToWriteOff(); + Long retriveMinimumDaysInArrearsToSuspendLoanAccount(); + + Long retrieveInvoiceResolutionExpiryDays(); + + Long retrieveInvoiceThreshold(); + + List retrieveInvoiceJobNotificationEmails(); + + Integer retriveIvaConfiguration(); + } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index 2f648d08309..415b6ac71e1 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -20,6 +20,7 @@ import java.io.Serializable; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import lombok.Getter; import lombok.ToString; @@ -58,6 +59,7 @@ public class CommandProcessingResult implements Serializable { private Long usuarioId; private String rolId; private BigDecimal eaRate; + private final List> collectionHouseUpdates; private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId, @@ -82,6 +84,33 @@ private CommandProcessingResult(final Long commandId, final Long officeId, final this.subResourceId = subResourceId; this.resourceExternalId = resourceExternalId; this.subResourceExternalId = subResourceExternalId; + this.collectionHouseUpdates = null; + } + + public CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId, + final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId, + final Map changes, final Long productId, final Long gsimId, final Long glimId, + final Map creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId, + final ExternalId resourceExternalId, final ExternalId subResourceExternalId, List> collectionHouseUpdates) { + this.commandId = commandId; + this.officeId = officeId; + this.groupId = groupId; + this.clientId = clientId; + this.loanId = loanId; + this.savingsId = savingsId; + this.resourceIdentifier = resourceIdentifier; + this.resourceId = resourceId; + this.changes = changes; + this.transactionId = transactionId; + this.productId = productId; + this.gsimId = gsimId; + this.glimId = glimId; + this.creditBureauReportData = creditBureauReportData; + this.rollbackTransaction = rollbackTransaction; + this.subResourceId = subResourceId; + this.resourceExternalId = resourceExternalId; + this.subResourceExternalId = subResourceExternalId; + this.collectionHouseUpdates = collectionHouseUpdates; } protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map changes, @@ -120,6 +149,17 @@ public static CommandProcessingResult fromDetails(final Long commandId, final Lo resourceExternalId, subResourceExternalId); } + public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId, + final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId, + final Long glimId, final Map creditBureauReportData, final String transactionId, + final Map changes, final Long productId, final Boolean rollbackTransaction, final Long subResourceId, + final ExternalId resourceExternalId, final ExternalId subResourceExternalId, + final List> collectionHouseUpdates) { + return new CommandProcessingResult(commandId, officeId, groupId, clientId, loanId, savingsId, resourceIdentifier, entityId, + transactionId, changes, productId, gsimId, glimId, creditBureauReportData, rollbackTransaction, subResourceId, + resourceExternalId, subResourceExternalId, collectionHouseUpdates); + } + public static CommandProcessingResult commandOnlyResult(final Long commandId) { return new CommandProcessingResult(null, null, commandId, null); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java index 37a760468e2..320b68d8f69 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResultBuilder.java @@ -19,6 +19,7 @@ package org.apache.fineract.infrastructure.core.data; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -41,6 +42,7 @@ public class CommandProcessingResultBuilder { private String transactionId; private Map changes; private Map creditBureauReportData; + private List> collectionHouseUpdates; private Long productId; private boolean rollbackTransaction = false; private ExternalId entityExternalId = ExternalId.empty(); @@ -60,7 +62,7 @@ public CommandProcessingResult build() { CommandProcessingResult commandProcessingResult = CommandProcessingResult.fromDetails(this.commandId, this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.resourceIdentifier, this.entityId, this.gsimId, this.glimId, this.creditBureauReportData, this.transactionId, this.changes, this.productId, this.rollbackTransaction, this.subEntityId, - this.entityExternalId, this.subEntityExternalId); + this.entityExternalId, this.subEntityExternalId, this.collectionHouseUpdates); commandProcessingResult.setRegistroAnterior(this.registroAnterior); commandProcessingResult.setRegistroPosterior(this.registroPosterior); commandProcessingResult.setUsuarioNombre(this.usuarioNombre); @@ -147,6 +149,11 @@ public CommandProcessingResultBuilder withCreditReport(final Map return this; } + public CommandProcessingResultBuilder withCollectionHouse(final List> withCollectionHouse) { + this.collectionHouseUpdates = withCollectionHouse; + return this; + } + public CommandProcessingResultBuilder setRollbackTransaction(final boolean rollbackTransaction) { this.rollbackTransaction |= rollbackTransaction; return this; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index b2de4d1bda9..5415d9ce4c2 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -335,6 +335,10 @@ public static boolean isOnOrAfter(LocalDate first, LocalDate second) { return first != null && (second == null || first.isAfter(second) || first.isEqual(second)); } + public static boolean isOnOrBefore(LocalDate first, LocalDate second) { + return second != null && (first == null || first.isBefore(second) || first.isEqual(second)); + } + public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) { return DAYS.between(localDateBefore, localDateAfter); } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 550e1336eb4..4fe16f9c1c3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -60,13 +60,20 @@ public enum JobName { REACTIVATE_BLOCKED_APP_USERS("Reactivate Blocked App Users"), // BLOCK_INACTIVE_CLIENTS("Block Inactive Clients"), // LOAN_CUSTOM_CHARGE_HONORARIO_UPDATE("Loans - Custom Charge Honorario Update"), // - LIQUIDACION_DE_RECAUDOS("Liquidacion de Recaudos"), RECALCULATE_LOAN_INTEREST_AFTER_MAXIMUM_LEGAL_RATE_CHANGE( - "Recalculate Loan Interest After Maximum Legal Rate Change"), LIQUIDACION_DE_COMPRAS("Liquidacion de Compras"), COMPENSATION( - "Compensation"), COMPENSATION_ALERT_EMAIL("Compensation Alert Email"), DAILY_LOAN_ACCRUAL("Devengo de Interés diario"), // - INSURANCE_CHARGE_CANCELLATION_DUE_TO_DEFAULT("Cancel Default Insurance Charges"), INSTALLMENT_LOAN_CHARGE_ACCRUAL( - "Devengo de seguro"), ARCHIVE_LOAN_HISTORY("Archivo de cartera"), // - INSURANCE_CHARGE_SUSPENSION_DUE_TO_DEFAULT("Suspension temporal por mora") // ; - ; + LIQUIDACION_DE_RECAUDOS("Liquidacion de Recaudos"), // + RECALCULATE_LOAN_INTEREST_AFTER_MAXIMUM_LEGAL_RATE_CHANGE("Recalculate Loan Interest After Maximum Legal Rate Change"), // + LIQUIDACION_DE_COMPRAS("Liquidacion de Compras"), // + COMPENSATION("Compensation"), // + COMPENSATION_ALERT_EMAIL("Compensation Alert Email"), // + DAILY_LOAN_ACCRUAL("Devengo de Interés diario"), // + INSURANCE_CHARGE_CANCELLATION_DUE_TO_DEFAULT("Cancel Default Insurance Charges"), // + INSTALLMENT_LOAN_CHARGE_ACCRUAL("Devengo de seguro"), // + ARCHIVE_LOAN_HISTORY("Archivo de cartera"), // + INSURANCE_CHARGE_SUSPENSION_DUE_TO_DEFAULT("Suspension temporal por mora"), // + FACTURA_ELECTRONICA_MENSUAL("Factura Electronica Mensual"), // + INVOICE_NUMBERING_LIMIT("Control de Límite de Numeración de Facturación Electrónica"), // + INVOICE_EXPIRY_RESOLUTION("Control de Vencimiento de Resolución de Facturación Electrónica"), // + COLLECTION_HOUSE_HISTORY("Casas de Cobro para Clientes"); private final String name; diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index c7e290af69f..2f907b64610 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -396,6 +396,12 @@ public MonetaryCurrency getCurrency() { return monetaryCurrency(); } + public void setCurrency(final MonetaryCurrency currency) { + this.currencyCode = currency.getCode(); + this.currencyDigitsAfterDecimal = currency.getDigitsAfterDecimal(); + this.inMultiplesOf = currency.getCurrencyInMultiplesOf(); + } + private MonetaryCurrency monetaryCurrency() { return new MonetaryCurrency(this.currencyCode, this.currencyDigitsAfterDecimal, this.inMultiplesOf); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java index 6f13548633e..237f5faf364 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java @@ -433,6 +433,11 @@ public boolean isAvalCharge() { return ChargeCalculationType.fromInt(this.chargeCalculation).isPercentageOfAval(); } + public boolean isAvalChargeFlatForMigration() { + // Charge is distributed among the installments + return ChargeCalculationType.fromInt(this.chargeCalculation).isFlatAvalForMigration(); + } + public boolean isMandatoryInsurance() { // Charge is distributed among the installments return ChargeCalculationType.fromInt(this.chargeCalculation).isMandatoryInsuranceCharge(); @@ -1048,6 +1053,9 @@ public void validateChargeIsSetupCorrectly() { } else if (this.isFlatHono()) { verifyChargeConfiguration(code, ChargeCalculationTypeBaseItemsEnum.FLAT.getIndex(), ChargeCalculationTypeBaseItemsEnum.HOORARIOS.getIndex(), null, null, null); + } else if (this.isAvalChargeFlatForMigration()) { + verifyChargeConfiguration(code, ChargeCalculationTypeBaseItemsEnum.AVAL.getIndex(), + ChargeCalculationTypeBaseItemsEnum.FLAT.getIndex(), null, null, null); } else { throw new GeneralPlatformDomainRuleException("error.msg.charge.not.setup.correctly", "Charge not setup correctly", this.getName()); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeCalculationType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeCalculationType.java index ceb76f4f971..4cba601c465 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeCalculationType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeCalculationType.java @@ -1585,6 +1585,11 @@ public boolean isPercentageOfAval() { && isPercentageOfDisbursement(); } + public boolean isFlatAvalForMigration() { + return this.byteRepresentation.charAt(ChargeCalculationTypeBaseItemsEnum.AVAL.getIndex()) == '1' && this.equals(FLAT_AVAL) + && isFlat(); + } + public boolean isPercentageOfHonorarios() { return this.byteRepresentation.charAt(ChargeCalculationTypeBaseItemsEnum.HOORARIOS.getIndex()) == '1' && this.byteRepresentation.charAt(ChargeCalculationTypeBaseItemsEnum.FLAT.getIndex()) == '1'; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java index 8ad31172ce2..0fe73220769 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java @@ -28,6 +28,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.springframework.integration.annotation.Default; @Getter @Setter @@ -46,13 +47,31 @@ public class DelinquencyRange extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "max_age_days", nullable = true) private Integer maximumAgeDays; + @Column(name = "percentage_value", nullable = true) + private Integer percentageValue; + @Version private Long version; + @Default + protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays, + Integer percentageValue) { + this.classification = classification; + this.minimumAgeDays = minimumAgeDays; + this.maximumAgeDays = maximumAgeDays; + this.percentageValue = percentageValue; + } + protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays) { this.classification = classification; this.minimumAgeDays = minimumAgeDays; this.maximumAgeDays = maximumAgeDays; + this.percentageValue = null; + } + + public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge, + Integer percentageValue) { + return new DelinquencyRange(classification, minimumAge, maximumAge, percentageValue); } public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index 2e13ed78b5f..60298fb1f16 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -196,4 +196,9 @@ public interface LoanApiConstants { String GLOBAL_CONFIG_COMPENSATION_ALERT_EMAIL = "Correo Alerta por Compensacion"; String GLOBAL_CONFIG_MIN_DAYS_WRITEOFF = "Dias de mora mínimos castigar cartera"; String GLOBAL_CONFIG_MIN_DAYS_CLAIM = "Dias de mora mínimos para reclamar"; + String MIGRAR_NIT = "nit"; + String MIGRAR_CODE = "code"; + String MIGRAR_NUMERO_CREDITO = "numeroCredito"; + String MIGRAR_CEDULA = "cedula"; + String IS_MIGRAR_LOAN = "isMigratedLoan"; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java index 53e318fffa7..d8e8aaff0b8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java @@ -47,13 +47,15 @@ public final class DisbursementData implements Comparable { private String locale; private String note; private transient String linkAccountId; + private String channelName; public static DisbursementData importInstance(LocalDate actualDisbursementDate, String linkAccountId, Integer rowIndex, String locale, - String dateFormat) { - return new DisbursementData(actualDisbursementDate, linkAccountId, rowIndex, locale, dateFormat); + String dateFormat, String channelName) { + return new DisbursementData(actualDisbursementDate, linkAccountId, rowIndex, locale, dateFormat, channelName); } - private DisbursementData(LocalDate actualDisbursementDate, String linkAccountId, Integer rowIndex, String locale, String dateFormat) { + private DisbursementData(LocalDate actualDisbursementDate, String linkAccountId, Integer rowIndex, String locale, String dateFormat, + String channelName) { this.dateFormat = dateFormat; this.locale = locale; this.actualDisbursementDate = actualDisbursementDate; @@ -67,6 +69,7 @@ private DisbursementData(LocalDate actualDisbursementDate, String linkAccountId, this.chargeAmount = null; this.waivedChargeAmount = null; this.netDisbursalAmount = null; + this.channelName = channelName; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java index 32a7e6d669a..e0a2ce9b330 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java @@ -472,6 +472,39 @@ public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final BigDecim this.externalId = ExternalId.empty(); } + public LoanChargeData(final Long id, final BigDecimal amount) { + this.id = null; + this.chargeId = id; + this.name = null; + this.currency = null; + this.amount = amount; + this.amountPaid = null; + this.amountWaived = null; + this.amountWrittenOff = null; + this.amountOutstanding = null; + this.chargeTimeType = null; + this.submittedOnDate = null; + this.dueDate = null; + this.chargeCalculationType = null; + this.percentage = null; + this.amountPercentageAppliedTo = null; + this.penalty = false; + this.chargePaymentMode = null; + this.paid = false; + this.waived = false; + this.amountOrPercentage = null; + this.chargeOptions = null; + this.chargePayable = false; + this.loanId = null; + this.externalLoanId = ExternalId.empty(); + this.minCap = null; + this.maxCap = null; + this.installmentChargeData = null; + this.amountAccrued = null; + this.amountUnrecognized = null; + this.externalId = ExternalId.empty(); + } + public boolean isChargePayable() { boolean isAccountTransfer = false; if (this.chargePaymentMode != null) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java index 7b9858f07ca..56f2ee6e8eb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java @@ -20,12 +20,15 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.time.OffsetDateTime; import lombok.Getter; +import lombok.Setter; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; @Getter +@Setter public class LoanTermVariationsData implements Comparable { private final Long id; @@ -35,6 +38,8 @@ public class LoanTermVariationsData implements Comparable currentOutstandingLoanCharges; + private BigDecimal deliquencyPercentage; + private Integer ageOfOverdue; + private BigDecimal ivaPercentage; + private Boolean haveHono; + private Boolean isCalculate; public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId, Integer rowIndex, String locale, String dateFormat) { @@ -462,4 +467,5 @@ public LoanChargePaidByData loanChargePaidBySummary() { public void setLoanChargePaidBySummary(LoanChargePaidByData loanChargePaidBySummary) { this.loanChargePaidBySummary = loanChargePaidBySummary; } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptos.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptos.java new file mode 100644 index 00000000000..dbd72ddbe49 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptos.java @@ -0,0 +1,73 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.loanaccount.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Getter +@Setter +@Entity +@Table(name = "c_clasificacion_conceptos") +public class ClasificacionConceptos extends AbstractPersistableCustom { + + @Column(length = 50) + private String concepto; + + @Column + private boolean mandato; + + @Column + private boolean excluido; + + @Column + private boolean exento; + + @Column + private boolean gravado; + + @Column + private String norma; + + @Column(precision = 18, scale = 4) + private BigDecimal tarifa; + + public void update(JsonCommand command) { + this.concepto = command.stringValueOfParameterNamed("concepto"); + this.mandato = command.booleanPrimitiveValueOfParameterNamed("mandato"); + this.excluido = command.booleanPrimitiveValueOfParameterNamed("excluido"); + this.exento = command.booleanPrimitiveValueOfParameterNamed("exento"); + this.gravado = command.booleanPrimitiveValueOfParameterNamed("gravado"); + this.norma = command.stringValueOfParameterNamed("norma"); + this.tarifa = command.bigDecimalValueOfParameterNamed("tarifa"); + } + + public static ClasificacionConceptos create(JsonCommand command) { + ClasificacionConceptos clasificacionConceptos = new ClasificacionConceptos(); + clasificacionConceptos.update(command); + return clasificacionConceptos; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptosRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptosRepository.java new file mode 100644 index 00000000000..1b8480fbdc1 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ClasificacionConceptosRepository.java @@ -0,0 +1,7 @@ +package org.apache.fineract.portfolio.loanaccount.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ClasificacionConceptosRepository extends JpaRepository { + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 5877d88a3ca..948cd6c9b11 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -101,6 +101,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx; @@ -505,6 +506,30 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "interest_accrued_till") private LocalDate interestAccruedTill; + @Transient + private boolean isAnulado; + + @Transient + private boolean isAnuladoOnDisbursementDate; + + // Columns for migrated loans + @Column(name = "is_migrated_loan", nullable = false) + private boolean isMigratedLoan = false; + + @Column(name = "migrar_nit") + private String nit; + + @Column(name = "migrar_code") + private String code; + + @Column(name = "migrar_numerocredito") + private String numeroCredito; + + @Column(name = "migrar_cli_nroid") + private String cedula; + + /////////////// + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail, @@ -798,9 +823,11 @@ public void handleChargeAppliedTransactionPerInstallment(final List LocalDate installmentDate = installment.getFromDate(); // if the installment date is the same as the due date of the last installment we should move the date // to the next day - if (installmentNumber > 1) { - installmentDate = installmentDate.plusDays(1); - } + + // SU-444 => Generate accrual on installment start date + // if (installmentNumber > 1) { + // installmentDate = installmentDate.plusDays(1); + // } applyInstallmentCharge(loanInstallmentCharge, installment, loanCharge, installmentDate); } @@ -814,7 +841,9 @@ public void handleChargeAppliedTransactionPerInstallment(final List private void applyInstallmentCharge(LoanInstallmentCharge loanInstallmentCharge, LoanRepaymentScheduleInstallment installment, final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { - + if (loanInstallmentCharge == null) { + return; + } final Money chargeAmount = loanInstallmentCharge.getAmount(getCurrency()); if (chargeAmount.isZero()) { return; @@ -1529,11 +1558,30 @@ public void updateLoanSchedule(final Collection existingCharges = existingInstallment.getInstallmentCharges(); installment.getInstallmentCharges().addAll(existingCharges); existingCharges.forEach(c -> c.setInstallment(installment)); existingInstallment.getInstallmentCharges().clear(); + + if (installment.getId() == null && installment.getAdvancePrincipalAmount() != null + && installment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) { + // Sometimes advanced paid installment does not get deleted because of these mappings and two + // installments with same number appear + + List transactions = this.getLoanTransactions(); + for (LoanTransaction loanTransaction : transactions) { + if (loanTransaction.isPaymentTransaction() + && loanTransaction.hasPaidInstallmentInAdvance(installment.getInstallmentNumber())) { + Set existingMappings = loanTransaction + .getLoanTransactionToRepaymentScheduleMappings(); + installment.getLoanTransactionToRepaymentScheduleMappings().addAll(existingMappings); + existingMappings.forEach(c -> c.setInstallment(installment)); + } + } + } } addLoanRepaymentScheduleInstallment(installment); } @@ -2009,7 +2057,7 @@ private void recalculateLoanCharge(final LoanCharge loanCharge, final int penalt } - private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { + public void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { /* * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down * installment charges from the loanCharge and from each of the repayment installments and allow them to be @@ -2781,7 +2829,7 @@ && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { LocalDate currentDate = DateUtils.getLocalDateOfTenant(); for (LocalDate selectedDate = actualDisbursementDate; selectedDate .isBefore(currentDate); selectedDate = selectedDate.plusDays(1)) { - applyDailyAccruals(selectedDate); + applyDailyAccrualsAtDisbursement(selectedDate); } } @@ -2794,45 +2842,32 @@ && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { } // In the Loan class - public void applyDailyAccruals(LocalDate currentDate) { + public void applyDailyAccrualsAtDisbursement(LocalDate currentDate) { // validate to ensure that accrual has not been done for the date ExternalId externalIdentifier = ExternalId.empty(); if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { externalIdentifier = ExternalId.generate(); } - - List installments = getRepaymentScheduleInstallments(); - - BigDecimal dailyInterest = calculateDailyInterestForDate(currentDate, installments); - - // if (dailyInterest.compareTo(BigDecimal.ZERO) > 0) { - if (dailyInterest != null) { - Money dailyInterestMoney = Money.of(getCurrency(), dailyInterest); - LoanTransaction dailyAccrualTransaction = LoanTransaction.accrueDailyInterest(getOffice(), this, dailyInterestMoney, - currentDate, externalIdentifier); - addLoanTransaction(dailyAccrualTransaction); - setInterestAccruedTill(currentDate); - } - - } - - private BigDecimal calculateDailyInterestForDate(LocalDate date, List installments) { - for (LoanRepaymentScheduleInstallment installment : installments) { - if (!date.isBefore(installment.getFromDate()) && !date.isAfter(installment.getDueDate())) { - long daysInPeriod = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate())); - Money interestForInstallment = installment.getInterestCharged(getCurrency()); + Money dailyAccrualInterest = Money.zero(this.getCurrency()); + Integer accrualInstallmentNumber = null; + final List loanRepaymentScheduleInstallments = getRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment repaymentScheduleInstallment : loanRepaymentScheduleInstallments) { + if (!currentDate.isBefore(repaymentScheduleInstallment.getFromDate()) + && !currentDate.isAfter(repaymentScheduleInstallment.getDueDate().minusDays(1))) { + long daysInPeriod = Math.toIntExact( + ChronoUnit.DAYS.between(repaymentScheduleInstallment.getFromDate(), repaymentScheduleInstallment.getDueDate())); + Money interestForInstallment = repaymentScheduleInstallment.getInterestCharged(getCurrency()); BigDecimal dailyInterest; - // Adjust interest on the last day of the period to make up the difference - if (date.equals(installment.getDueDate())) { + if (currentDate.equals(repaymentScheduleInstallment.getDueDate().minusDays(1))) { // if the amount is whole number when divided across the days in the period, then do same for last // day if (interestForInstallment.getAmount().remainder(BigDecimal.valueOf(daysInPeriod)).compareTo(BigDecimal.ZERO) == 0) { dailyInterest = interestForInstallment.getAmount().divide(BigDecimal.valueOf(daysInPeriod), 2, RoundingMode.HALF_UP); } else { - BigDecimal totalAccruedInterest = installment.getAccruedInterest(getCurrency()).getAmount(); + BigDecimal totalAccruedInterest = repaymentScheduleInstallment.getAccruedInterest(getCurrency()).getAmount(); // This will ensure no rounding differences remain dailyInterest = interestForInstallment.getAmount().subtract(totalAccruedInterest); } @@ -2843,13 +2878,20 @@ private BigDecimal calculateDailyInterestForDate(LocalDate date, List 0) { + if (accrualInstallmentNumber != null && dailyAccrualInterest.isGreaterThanZero()) { + LoanTransaction dailyAccrualTransaction = LoanTransaction.accrueDailyInterest(getOffice(), this, dailyAccrualInterest, + currentDate, externalIdentifier, accrualInstallmentNumber); + addLoanTransaction(dailyAccrualTransaction); + setInterestAccruedTill(currentDate); + } - return null; } private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam, @@ -3245,9 +3287,8 @@ private void handleDisbursementTransaction(final LocalDate disbursedOn, final Pa } } - // we don't want to immediately generate ccharges if the disbursedOn is today - - if (!installmentalCharges.isEmpty() && disbursedOn.isBefore(DateUtils.getLocalDateOfTenant())) { + // SU-444 generate charges from disbursement day + if (!installmentalCharges.isEmpty()) { handleChargeAppliedTransactionPerInstallment(installmentalCharges, disbursedOn); } @@ -4219,9 +4260,17 @@ private Money calculateTotalOverpayment() { .plus(scheduledRepayment.getPenaltyChargesWrittenOff(currency)); cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments .plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency))) - .plus(scheduledRepayment.getFeeChargesPaid(currency)) - .plus(scheduledRepayment.getPenaltyChargesPaid(currency).plus(scheduledRepayment.getAdvancePrincipalAmount())) + .plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency)) .plus(scheduleWrittenOffValue); + if (scheduledRepayment.isLastInstallment(installments) && scheduledRepayment.isOverpaidInAdvance(currency) + && scheduledRepayment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) { + cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments + .plus(scheduledRepayment.getInterestWaived(currency)); + // Do not add advance payment amount if installment was overpaid + continue; + } else { + cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getAdvancePrincipalAmount()); + } cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency)); } @@ -4239,7 +4288,18 @@ private Money calculateTotalOverpayment() { // if total paid in transactions doesnt match repayment schedule then // theres an overpayment. - return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); + Money overpaid = totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); + + /* + * // This code was added initially under SU-320 to handle overpayments but later on it is no longer needed + * based on the way overpayment is now handled if (overpaid.isZero()) { Money totalPrincipalPaid = + * Money.zero(this.getCurrency()); for (final LoanRepaymentScheduleInstallment scheduledRepayment : + * installments) { totalPrincipalPaid = totalPrincipalPaid.add( + * scheduledRepayment.getPrincipalCompleted(this.getCurrency()).plus(scheduledRepayment. + * getAdvancePrincipalAmount())); } if (totalPrincipalPaid.isGreaterThan(this.getPrincipal())) { overpaid = + * totalPrincipalPaid.minus(this.getPrincipal()); } } + */ + return overpaid; } public Money calculateTotalRecoveredPayments() { @@ -5662,6 +5722,22 @@ public Set trancheCharges() { public List generateInstallmentLoanCharges(final LoanCharge loanCharge) { final List loanChargePerInstallments = new ArrayList<>(); if (loanCharge.isInstalmentFee()) { + // Loan canceled on disbursement date. Only charge hono + if (this.isAnulado && this.isAnuladoOnDisbursementDate) { + if (loanCharge.isCustomPercentageBasedOfAnotherCharge()) { + for (LoanCharge charge : this.getCharges()) { + if (charge.getCharge().getId() != null + && charge.getCharge().getId().equals(loanCharge.getCharge().getParentChargeId())) { + if (!charge.isFlatHono()) { + return loanChargePerInstallments; + } + } + } + } + if (!loanCharge.isFlatHono()) { + return loanChargePerInstallments; + } + } List installments = getRepaymentScheduleInstallmentsIgnoringTotalGrace().stream() .sorted(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList(); this.outstandingBalance = this.loanRepaymentScheduleDetail.getPrincipal(); @@ -5687,14 +5763,20 @@ public List generateInstallmentLoanCharges(final LoanChar } if (loanCharge.getChargeCalculation().isFlatHono() && !loanCharge.getCustomChargeHonorarioMaps().isEmpty()) { - for (CustomChargeHonorarioMap customCharge : loanCharge.getCustomChargeHonorarioMaps()) { - if (customCharge.getLoanInstallmentNr().equals(installment.getInstallmentNumber())) { - amount = customCharge.getFeeTotalAmount(); - break; + if (installment.isOverdueOn(LocalDate.now())) { + for (CustomChargeHonorarioMap customCharge : loanCharge.getCustomChargeHonorarioMaps()) { + if (customCharge.getLoanInstallmentNr().equals(installment.getInstallmentNumber())) { + amount = customCharge.getFeeTotalAmount(); + break; + } else { + amount = BigDecimal.ZERO; + } } - } - if (amount.compareTo(BigDecimal.ZERO) == 0) { - amount = loanCharge.amountOrPercentage(); + if (amount.compareTo(BigDecimal.ZERO) == 0) { + amount = loanCharge.amountOrPercentage(); + } + } else { + amount = BigDecimal.ZERO; } } } else { @@ -6388,8 +6470,7 @@ private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO gener } public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) { - LoanRepaymentScheduleInstallment installment = null; - + LoanRepaymentScheduleInstallment installment; if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { final MathContext mc = MoneyHelper.getMathContext(); @@ -7306,8 +7387,9 @@ public LoanProduct getLoanProduct() { return loanProduct; } - public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) { - Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate); + public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate, scheduleGeneratorDTO); Money totalPrincipal = Money.of(getCurrency(), this.getLoanSummary().getTotalPrincipalOutstanding()); totalPrincipal = totalPrincipal.minus(receivables[3]); final LocalDate currentDate = DateUtils.getBusinessLocalDate(); @@ -7431,47 +7513,53 @@ private void incrementOutstandingPenaltyChargeBalances(final Integer installment } } - public Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) { + public Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate, final ScheduleGeneratorDTO scheduleGeneratorDTO) { Money[] balances = new Money[4]; final MonetaryCurrency currency = getCurrency(); Money interest = Money.zero(currency); Money paidFromFutureInstallments = Money.zero(currency); Money fee = Money.zero(currency); Money penalty = Money.zero(currency); - int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper - .fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments); - + Money principalLoanBalanceOutstanding = this.getPrincipal(); for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) { interest = interest.plus(installment.getInterestOutstanding(currency)); penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency)); fee = fee.plus(installment.getFeeChargesOutstanding(currency)); } else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) { - Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, installment, isFirstNormalInstallment); - if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) { - interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]); + final Money[] balancesForCurrentPeriod = fetchInterestFeeAndPenaltyTillDate(paymentDate, installment, + principalLoanBalanceOutstanding, scheduleGeneratorDTO); + if (balancesForCurrentPeriod[0].isGreaterThan(balancesForCurrentPeriod[5])) { + interest = interest.plus(balancesForCurrentPeriod[0]).minus(balancesForCurrentPeriod[5]); } else { - paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[5]) - .minus(balancesForCurrentPeroid[0]); + paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeriod[5]) + .minus(balancesForCurrentPeriod[0]); } - if (balancesForCurrentPeroid[1].isGreaterThan(balancesForCurrentPeroid[3])) { - fee = fee.plus(balancesForCurrentPeroid[1].minus(balancesForCurrentPeroid[3])); + if (balancesForCurrentPeriod[1].isGreaterThan(balancesForCurrentPeriod[3])) { + fee = fee.plus(balancesForCurrentPeriod[1].minus(balancesForCurrentPeriod[3])); } else { paidFromFutureInstallments = paidFromFutureInstallments - .plus(balancesForCurrentPeroid[3].minus(balancesForCurrentPeroid[1])); + .plus(balancesForCurrentPeriod[3].minus(balancesForCurrentPeriod[1])); } - if (balancesForCurrentPeroid[2].isGreaterThan(balancesForCurrentPeroid[4])) { - penalty = penalty.plus(balancesForCurrentPeroid[2].minus(balancesForCurrentPeroid[4])); + if (balancesForCurrentPeriod[2].isGreaterThan(balancesForCurrentPeriod[4])) { + penalty = penalty.plus(balancesForCurrentPeriod[2].minus(balancesForCurrentPeriod[4])); } else { - paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[4]) - .minus(balancesForCurrentPeroid[2]); + paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeriod[4]) + .minus(balancesForCurrentPeriod[2]); } } else { + // When Foreclosure on disbursement date then pay fee charges + // When loan is canceled on same date then only pay hono charges + if (installment.getInstallmentNumber() == 1 && DateUtils.isEqual(paymentDate, installment.getFromDate())) { + fee = fee.plus(installment.getFeeChargesOutstanding(currency)); + if (this.isAnulado) { + fee = getPendingHonoAmountOfAnuladoLoanForInstallment(this, installment.getInstallmentNumber()); + } + } paidFromFutureInstallments = paidFromFutureInstallments.plus(installment.getInterestPaid(currency)) .plus(installment.getPenaltyChargesPaid(currency)).plus(installment.getFeeChargesPaid(currency)); } - + principalLoanBalanceOutstanding = principalLoanBalanceOutstanding.minus(installment.getPrincipal(currency)); } balances[0] = interest; balances[1] = fee; @@ -7480,52 +7568,112 @@ public Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) { return balances; } - private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final LoanRepaymentScheduleInstallment installment, - boolean isFirstNormalInstallment) { - Money penaltyForCurrentPeriod = Money.zero(getCurrency()); - Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency()); - int totalPeriodDays = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate())); - int tillDays = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), paymentDate)); - Money interestForCurrentPeriod = Money.of(getCurrency(), BigDecimal - .valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestCharged(getCurrency()).getAmount(), tillDays))); - Money interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()) - .plus(installment.getInterestPaid(getCurrency())).plus(installment.getInterestWrittenOff(getCurrency())); - Money feeForCurrentPeriod = installment.getFeeChargesCharged(getCurrency()); - Money feeAccountedForCurrentPeriod = installment.getFeeChargesWaived(getCurrency()) - .plus(installment.getFeeChargesPaid(getCurrency())).plus(installment.getFeeChargesWrittenOff(getCurrency())); - for (LoanCharge loanCharge : this.charges) { - if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = isFirstNormalInstallment - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate); - if (isDue) { - if (loanCharge.isPenaltyCharge()) { - penaltyForCurrentPeriod = penaltyForCurrentPeriod.plus(loanCharge.getAmount(getCurrency())); - penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod.plus(loanCharge.getAmountWaived(getCurrency()) - .plus(loanCharge.getAmountPaid(getCurrency())).plus(loanCharge.getAmountWrittenOff(getCurrency()))); + private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate transactionDate, + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment, final Money principalLoanBalanceOutstanding, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + final MonetaryCurrency currency = getCurrency(); + final Integer currencyInMultiplesOf = currency.getCurrencyInMultiplesOf(); + final String currencyCode = currency.getCode(); + final MonetaryCurrency interestCalculationCurrency = new MonetaryCurrency(currencyCode, 5, currencyInMultiplesOf); + final Money penaltyForCurrentPeriod = Money.zero(currency); + final Money penaltyAccountedForCurrentPeriod = Money.zero(currency); + principalLoanBalanceOutstanding.setCurrency(interestCalculationCurrency); + int totalPeriodDays = Math.toIntExact( + ChronoUnit.DAYS.between(loanRepaymentScheduleInstallment.getFromDate(), loanRepaymentScheduleInstallment.getDueDate())); + int tillDays = Math.toIntExact(ChronoUnit.DAYS.between(loanRepaymentScheduleInstallment.getFromDate(), transactionDate)); + BigDecimal interestCharged = loanRepaymentScheduleInstallment.getInterestCharged(getCurrency()).getAmount(); + if (loanRepaymentScheduleInstallment.originalInterestChargedAmount() != null + && loanRepaymentScheduleInstallment.originalInterestChargedAmount().compareTo(BigDecimal.ZERO) > 0) { + interestCharged = loanRepaymentScheduleInstallment.originalInterestChargedAmount(); + } + Money interestForCurrentPeriod = Money.of(getCurrency(), + BigDecimal.valueOf(calculateInterestForDays(totalPeriodDays, interestCharged, tillDays))); + final Money interestAccountedForCurrentPeriod = loanRepaymentScheduleInstallment.getInterestWaived(currency) + .plus(loanRepaymentScheduleInstallment.getInterestPaid(currency)) + .plus(loanRepaymentScheduleInstallment.getInterestWrittenOff(currency)); + final LoanApplicationTerms loanApplicationTerms = this.constructLoanApplicationTerms(scheduleGeneratorDTO); + final LoanTermVariationsDataWrapper loanTermVariationsDataWrapper = loanApplicationTerms.getLoanTermVariations(); + if (loanTermVariationsDataWrapper != null) { + List interestRatesFromInstallment = loanTermVariationsDataWrapper.getInterestRateFromInstallment(); + if (interestRatesFromInstallment != null && !interestRatesFromInstallment.isEmpty()) { + final LocalDate periodStartDate = loanRepaymentScheduleInstallment.getFromDate(); + interestRatesFromInstallment = interestRatesFromInstallment.stream().filter(interestRateVariation -> !DateUtils + .isAfter(interestRateVariation.getTermVariationApplicableFrom(), transactionDate)).toList(); + final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory() + .create(loanApplicationTerms.getLoanScheduleType(), loanApplicationTerms.getInterestMethod()); + if (!CollectionUtils.isEmpty(interestRatesFromInstallment)) { + final BigDecimal annualNominalInterestRate = loanApplicationTerms.getAnnualNominalInterestRate(); + Money totalForeclosureDailyInterestEarned = Money.zero(interestCalculationCurrency); + for (LocalDate localDate = periodStartDate; DateUtils.isBefore(localDate, + transactionDate); localDate = localDate.plusDays(1)) { + final Money foreclosureDailyInterestEarned = this.calculateForeclosureDailyInterestEarned( + interestRatesFromInstallment, localDate, principalLoanBalanceOutstanding, + loanRepaymentScheduleInstallment.getInstallmentNumber(), loanApplicationTerms, loanScheduleGenerator); + loanApplicationTerms.updateAnnualNominalInterestRate(annualNominalInterestRate); + totalForeclosureDailyInterestEarned = totalForeclosureDailyInterestEarned.plus(foreclosureDailyInterestEarned); } + interestForCurrentPeriod = totalForeclosureDailyInterestEarned; } } } - Money[] balances = new Money[6]; + final Money feeForCurrentPeriod = loanRepaymentScheduleInstallment.getFeeChargesCharged(currency); + final Money feeAccountedForCurrentPeriod = loanRepaymentScheduleInstallment.getFeeChargesWaived(currency) + .plus(loanRepaymentScheduleInstallment.getFeeChargesPaid(currency)) + .plus(loanRepaymentScheduleInstallment.getFeeChargesWrittenOff(currency)); + + // SU-446 Since penalty calculation for an installment is changed and penaltis till date are accumulated and + // charged. + // Below code is no longer valid as the charge amount has already been charged + /* + * for (LoanCharge loanCharge : this.charges) { if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) + * { boolean isDue = isFirstNormalInstallment ? + * loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate) : + * loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate); if (isDue) { if + * (loanCharge.isPenaltyCharge()) { penaltyForCurrentPeriod = + * penaltyForCurrentPeriod.plus(loanCharge.getAmount(getCurrency())); penaltyAccoutedForCurrentPeriod = + * penaltyAccoutedForCurrentPeriod.plus(loanCharge.getAmountWaived(getCurrency()) + * .plus(loanCharge.getAmountPaid(getCurrency())).plus(loanCharge.getAmountWrittenOff(getCurrency()))); } } } } + */ + + final Money[] balances = new Money[6]; balances[0] = interestForCurrentPeriod; balances[1] = feeForCurrentPeriod; balances[2] = penaltyForCurrentPeriod; balances[3] = feeAccountedForCurrentPeriod; - balances[4] = penaltyAccoutedForCurrentPeriod; + balances[4] = penaltyAccountedForCurrentPeriod; balances[5] = interestAccountedForCurrentPeriod; return balances; } - public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) { + public Money calculateForeclosureDailyInterestEarned(final List interestRatesFromInstallment, + final LocalDate currentDate, final Money principalLoanBalanceOutstanding, final int periodNumber, + final LoanApplicationTerms loanApplicationTerms, final LoanScheduleGenerator loanScheduleGenerator) { + final List interestRatesFromInstallmentV1 = interestRatesFromInstallment.stream() + .filter(interestRateVariation -> !DateUtils.isAfter(interestRateVariation.getTermVariationApplicableFrom(), currentDate)) + .toList(); + if (!CollectionUtils.isEmpty(interestRatesFromInstallmentV1)) { + final List interestRatesFromInstallmentV2 = interestRatesFromInstallment.stream().sorted( + Comparator.comparing(LoanTermVariationsData::getLastModifiedDate, Comparator.nullsLast(Comparator.reverseOrder()))) + .toList(); + final LoanTermVariationsData interestRateFromInstallment = interestRatesFromInstallmentV2.get(0); + final BigDecimal interestRate = interestRateFromInstallment.getDecimalValue(); + loanApplicationTerms.updateAnnualNominalInterestRate(interestRate); + } + final LocalDate currentDatePlusOne = currentDate.plusDays(1); + final boolean ignoreCurrencyDigitsAfterDecimal = true; + final PrincipalInterest principalInterest = loanScheduleGenerator.calculatePrincipalInterestComponents( + principalLoanBalanceOutstanding, loanApplicationTerms, periodNumber, currentDate, currentDatePlusOne, + ignoreCurrencyDigitsAfterDecimal); + return principalInterest.interest(); + } + + public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate, final ScheduleGeneratorDTO scheduleGeneratorDTO) { Money[] balances = new Money[3]; final MonetaryCurrency currency = getCurrency(); balances[0] = balances[1] = balances[2] = Money.zero(currency); - int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper - .fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments); + Money principalLoanBalanceOutstanding = this.getPrincipal(); for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); if (DateUtils.isEqual(paymentDate, installment.getDueDate())) { Money interest = installment.getInterestCharged(currency); Money fee = installment.getFeeChargesCharged(currency); @@ -7536,9 +7684,19 @@ public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) { break; } else if (DateUtils.isAfter(paymentDate, installment.getFromDate()) && DateUtils.isBefore(paymentDate, installment.getDueDate())) { - balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, installment, isFirstNormalInstallment); + balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, installment, principalLoanBalanceOutstanding, + scheduleGeneratorDTO); break; + } else if (installment.getInstallmentNumber() == 1 && DateUtils.isEqual(paymentDate, installment.getFromDate())) { + // Foreclosure being done on same date as disbursement date. Fee charge must be paid + // If loan is canceled on same date then only pay hono charge + Money fee = installment.getFeeChargesOutstanding(currency); + if (this.isAnulado) { + fee = getPendingHonoAmountOfAnuladoLoanForInstallment(this, installment.getInstallmentNumber()); + } + balances[1] = fee; } + principalLoanBalanceOutstanding = principalLoanBalanceOutstanding.minus(installment.getPrincipal(currency)); } return balances; @@ -7641,15 +7799,19 @@ public void validateForForeclosure(final LocalDate transactionDate) { } } - public void updateInstallmentsPostDate(LocalDate transactionDate) { + public void updateInstallmentsPostDate(final LocalDate transactionDate, final ScheduleGeneratorDTO scheduleGeneratorDTO) { List newInstallments = new ArrayList<>(this.repaymentScheduleInstallments); final MonetaryCurrency currency = getCurrency(); Money totalPrincipal = Money.zero(currency); - Money[] balances = retriveIncomeForOverlappingPeriod(transactionDate); + Money[] balances = retriveIncomeForOverlappingPeriod(transactionDate, scheduleGeneratorDTO); boolean isInterestComponent = false; for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) { totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); + if (installment.getAdvancePrincipalAmount() != null + && installment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) { + totalPrincipal = totalPrincipal.add(installment.getAdvancePrincipalAmount()); + } newInstallments.remove(installment); } } @@ -7684,8 +7846,27 @@ public void updateInstallmentsPostDate(LocalDate transactionDate) { if (DateUtils.isAfter(loanCharge.getDueLocalDate(), transactionDate)) { loanCharge.setActive(false); } else if (loanCharge.getDueLocalDate() == null) { - recalculateLoanCharge(loanCharge, penaltyWaitPeriod); - loanCharge.updateWaivedAmount(currency); + boolean ivaHono = false; + if (this.isAnulado && this.isAnuladoOnDisbursementDate) { + if (loanCharge.isCustomPercentageBasedOfAnotherCharge()) { + for (LoanCharge charge : charges) { + if (charge.isFlatHono() && charge.getCharge().getId() != null + && charge.getCharge().getId().equals(loanCharge.getCharge().getParentChargeId())) { + ivaHono = true; + } + } + } + } + if (this.isAnulado && this.isAnuladoOnDisbursementDate && (!loanCharge.isFlatHono() && !ivaHono)) { + // If loan is canceled on same day as disbursement then only charge hono charges + this.clearLoanInstallmentChargesBeforeRegeneration(loanCharge); + loanCharge.setAmount(loanCharge.getAmountPaid(currency).getAmount()); + loanCharge.setOutstandingAmount(BigDecimal.ZERO); + + } else { + recalculateLoanCharge(loanCharge, penaltyWaitPeriod); + loanCharge.updateWaivedAmount(currency); + } } } @@ -7711,6 +7892,34 @@ public void updateLoanScheduleOnForeclosure(final Collection honorariosCharges = loan.getLoanCharges().stream().filter(LoanCharge::isFlatHono).toList(); + Collection ivaCharges = loan.getLoanCharges().stream().filter(LoanCharge::isCustomPercentageBasedOfAnotherCharge) + .toList(); + + BigDecimal chargeAmount = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(installmentNumber, lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeAmount = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(installmentNumber, lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + honorariosAmount = honorariosAmount.add(honorariosTermChargeAmount).add(chargeAmount); + + return Money.of(loan.getCurrency(), honorariosAmount); + } + + public BigDecimal getAccruedInterestForInstallment(final Integer installmentNumber) { + return this.loanTransactions.stream() + .filter(loanTransaction -> loanTransaction.isAccrual() && loanTransaction.isDailyAccrual() + && installmentNumber.equals(loanTransaction.getInstallmentNumber())) + .map(loanTransaction -> loanTransaction.getInterestPortion(getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + public Integer getLoanSubStatus() { return this.loanSubStatus; } @@ -8102,4 +8311,65 @@ public Collection getInsuranceChargesForNoveltyIncidentReporting(boo } } + + public boolean isAnulado() { + return isAnulado; + } + + public void setAnulado(boolean anulado) { + this.isAnulado = anulado; + } + + public boolean isAnuladoOnDisbursementDate() { + return isAnuladoOnDisbursementDate; + } + + public void setAnuladoOnDisbursementDate(boolean anuladoOnDisbursementDate) { + isAnuladoOnDisbursementDate = anuladoOnDisbursementDate; + } + + public String cedula() { + return cedula; + } + + public void setCedula(String cedula) { + this.cedula = cedula; + } + + public String numeroCredito() { + return numeroCredito; + } + + public void setNumeroCredito(String numeroCredito) { + this.numeroCredito = numeroCredito; + } + + public String code() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String nit() { + return nit; + } + + public void setNit(String nit) { + this.nit = nit; + } + + public boolean isMigratedLoan() { + return isMigratedLoan; + } + + public void setMigratedLoan(boolean migratedLoan) { + isMigratedLoan = migratedLoan; + } + + public boolean hasPenaltiesInRepaymentSchedules() { + return this.getRepaymentScheduleInstallments().stream().anyMatch(LoanRepaymentScheduleInstallment::hasPenalties); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 884e2529849..deabecd7d0f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -321,7 +321,6 @@ private void populateDerivedFields(final BigDecimal amountPercentageAppliedTo, f final MathContext mc = MoneyHelper.getMathContext(); // Get one day of interest this.percentage = chargeAmount.divide(BigDecimal.valueOf(365), mc).setScale(5, roundingMode); - this.amountPercentageAppliedTo = amountPercentageAppliedTo; if (loanCharge.compareTo(BigDecimal.ZERO) == 0) { loanCharge = percentageOf(this.amountPercentageAppliedTo); @@ -498,6 +497,7 @@ public void update(final BigDecimal amount, final LocalDate dueDate, final BigDe case FLAT_AMOUNT: case FLAT_SEGOVOLUNTARIO: case FLAT_SEGO: + case FLAT_AVAL: if (isInstalmentFee()) { if (numberOfRepayments == null) { numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions(); @@ -659,6 +659,75 @@ public void resetAndUpdateInstallmentCharges() { updateInstallmentCharges(); } + public void updateInstallmentChargesHono(Integer numberOfRepayment) { + final Collection remove = new HashSet<>(); + final List newChargeInstallments = this.loan.generateInstallmentLoanCharges(this); + MonetaryCurrency currency = this.loan.getCurrency(); + int index = 0; + final List oldChargeInstallments = new ArrayList<>(); + if (this.loanInstallmentCharge != null && !this.loanInstallmentCharge.isEmpty()) { + oldChargeInstallments.addAll(this.loanInstallmentCharge); + } + Collections.sort(oldChargeInstallments); + final LoanInstallmentCharge[] loanChargePerInstallmentArray = newChargeInstallments + .toArray(new LoanInstallmentCharge[newChargeInstallments.size()]); + for (final LoanInstallmentCharge chargePerInstallment : oldChargeInstallments) { + if (index == loanChargePerInstallmentArray.length) { + remove.add(chargePerInstallment); + chargePerInstallment.getInstallment().getInstallmentCharges().remove(chargePerInstallment); + } else { + LoanInstallmentCharge newLoanInstallmentCharge = loanChargePerInstallmentArray[index++]; + newLoanInstallmentCharge.getInstallment().getInstallmentCharges().remove(newLoanInstallmentCharge); + if (newLoanInstallmentCharge.getLoanCharge().isFlatHono()) { + if (!chargePerInstallment.isPaid() + && newLoanInstallmentCharge.getInstallment().getInstallmentNumber() != numberOfRepayment) { + newLoanInstallmentCharge.setAmount(BigDecimal.ZERO); + newLoanInstallmentCharge.setIpaid(false); + } + } + chargePerInstallment.copyFrom(newLoanInstallmentCharge); + + } + } + this.loanInstallmentCharge.removeAll(remove); + while (index < loanChargePerInstallmentArray.length) { + this.loanInstallmentCharge.add(loanChargePerInstallmentArray[index++]); + } + + Money amount = Money.zero(this.loan.getCurrency()); + // adjust decimal difference in amount that comes due to division of Charge amount with number of repayments + if ((isCustomFlatDistributedCharge()) && this.charge.isInstallmentFee()) { + int i = 1; + for (LoanInstallmentCharge charge : this.loanInstallmentCharge) { + if (i == this.loanInstallmentCharge.size()) { + amount = amount.plus(charge.getAmount()); + if (amount.getAmount().compareTo(this.amount) != 0) { + if (amount.getAmount().compareTo(this.amount) < 0) { + BigDecimal difference = this.amount.subtract(amount.getAmount()); + charge.setAmount(charge.getAmount().add(difference)); + } + if (amount.getAmount().compareTo(this.amount) > 0) { + BigDecimal difference = amount.getAmount().subtract(this.amount); + charge.setAmount(charge.getAmount().subtract(difference)); + } + charge.setAmountOutstanding(charge.getAmount()); + amount = Money.of(amount.getCurrency(), this.amount); + } + } else { + amount = amount.plus(charge.getAmount()); + } + i++; + } + } else { + for (LoanInstallmentCharge charge : this.loanInstallmentCharge) { + amount = amount.plus(charge.getAmount()); + } + } + this.amount = amount.getAmount(); + this.amountOutstanding = calculateOutstanding(); + + } + private void updateInstallmentCharges() { final Collection remove = new HashSet<>(); final List newChargeInstallments = this.loan.generateInstallmentLoanCharges(this); @@ -1315,6 +1384,10 @@ public void undoWaived() { this.waived = false; } + public void setAmount(final BigDecimal amount) { + this.amount = amount; + } + public ExternalId getExternalId() { return externalId; } @@ -1436,6 +1509,17 @@ public BigDecimal calculateCustomFeeChargeToInstallment(Integer installmentNumbe BigDecimal computedAmount = LoanCharge.percentageOf(outstandingBalance.getAmount(), this.percentage); BigDecimal finalAmount = computedAmount.divide(installmentCount, 0, RoundingMode.HALF_UP); customAmout = customAmout.add(finalAmount); + } else if (this.isVoluntaryInsurance() || this.isAvalChargeFlatForMigration()) { + BigDecimal chargeAmount = BigDecimal.ZERO; + if (this.installmentCharges().isEmpty()) { + chargeAmount = this.amountOrPercentage; + } else { + final LoanInstallmentCharge installmentCharge = this.getInstallmentLoanCharge(installmentNumber); + if (installmentCharge != null) { + chargeAmount = installmentCharge.getAmount(); + } + } + customAmout = customAmout.add(chargeAmount); } return customAmout; } @@ -1465,7 +1549,12 @@ public boolean isCustomPercentageBasedDistributedCharge() { public boolean isAvalCharge() { // Charge is distributed among the installments - return getChargeCalculation().isPercentageOfAval(); + return getChargeCalculation().isPercentageOfAval() || getChargeCalculation().isFlatAvalForMigration(); + } + + public boolean isAvalChargeFlatForMigration() { + // Charge is distributed among the installments + return ChargeCalculationType.fromInt(this.chargeCalculation).isFlatAvalForMigration(); } public boolean isMandatoryInsurance() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 9e253e5a1d5..0be29f2d52a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -230,6 +230,10 @@ public Money updatePaidAmountBy(final Money incrementBy, final Money feeAmount, return amountPaidOnThisCharge; } + public void updateInstalmentAmount(BigDecimal amount) { + this.amount = amount; + } + public Money getAmountWrittenOff(final MonetaryCurrency currency) { return Money.of(currency, this.amountWrittenOff); } @@ -347,6 +351,10 @@ public void setAmount(BigDecimal amount) { this.amount = amount; } + public void setIpaid(boolean paid) { + this.paid = paid; + } + public void setAmountOutstanding(BigDecimal amountOutstanding) { this.amountOutstanding = amountOutstanding; } @@ -379,4 +387,16 @@ public void adjustChargeAmount(final Money adjustedAmount) { this.loancharge.adjustChargeAmount(adjustedAmount); } } + + public BigDecimal getAmountPaid() { + return amountPaid != null ? amountPaid : BigDecimal.ZERO; + } + + public BigDecimal getAmountWaived() { + return amountWaived != null ? amountWaived : BigDecimal.ZERO; + } + + public BigDecimal getAmountWrittenOff() { + return amountWrittenOff != null ? amountWrittenOff : BigDecimal.ZERO; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index ce617d24b67..678645c739e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -730,19 +730,33 @@ public Money payHonorariosChargesComponent(final LocalDate transactionDate, fina } - public Money payAvalChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining, + public Money payAvalChargesComponent(final LocalDate transactionDate, Money transactionAmountRemaining, final boolean isWriteOffTransaction, LoanTransaction loanTransaction) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); - Money loanChargePaidByPortion = Money.zero(currency); Money feePortionOfTransaction = Money.zero(currency); + Money loanChargePaidByPortion = Money.zero(currency); if (transactionAmountRemaining.isZero()) { return feePortionOfTransaction; } for (LoanInstallmentCharge installmentCharge : getInstallmentCharges()) { if (installmentCharge.getLoanCharge().isAvalCharge()) { + + for (LoanInstallmentCharge vatCharge : getInstallmentCharges()) { + if (Objects.equals(installmentCharge.getLoanCharge().getCharge().getId(), + vatCharge.getLoanCharge().getCharge().getParentChargeId())) { + feePortionOfTransaction = payLoanCharge(vatCharge, transactionDate, transactionAmountRemaining, currency, + feePortionOfTransaction, isWriteOffTransaction, loanChargePaidByPortion); + updateChargePaidByAmount(feePortionOfTransaction.minus(loanChargePaidByPortion), loanTransaction, + vatCharge.getLoanCharge()); + transactionAmountRemaining = transactionAmountRemaining.minus(feePortionOfTransaction); + loanChargePaidByPortion = feePortionOfTransaction.minus(loanChargePaidByPortion); + break; + } + } feePortionOfTransaction = payLoanCharge(installmentCharge, transactionDate, transactionAmountRemaining, currency, feePortionOfTransaction, isWriteOffTransaction, loanChargePaidByPortion); + updateChargePaidByAmount(feePortionOfTransaction.minus(loanChargePaidByPortion), loanTransaction, installmentCharge.getLoanCharge()); loanChargePaidByPortion = feePortionOfTransaction.minus(loanChargePaidByPortion); @@ -770,7 +784,7 @@ public Money payMandatoryInsuranceChargesComponent(final LocalDate transactionDa feePortionOfTransaction = payLoanCharge(vatCharge, transactionDate, transactionAmountRemaining, currency, feePortionOfTransaction, isWriteOffTransaction, loanChargePaidByPortion); updateChargePaidByAmount(feePortionOfTransaction.minus(loanChargePaidByPortion), loanTransaction, - installmentCharge.getLoanCharge()); + vatCharge.getLoanCharge()); transactionAmountRemaining = transactionAmountRemaining.minus(feePortionOfTransaction); loanChargePaidByPortion = feePortionOfTransaction.minus(loanChargePaidByPortion); break; @@ -836,7 +850,7 @@ public Money payVoluntaryInsuranceChargesComponent(final LocalDate transactionDa feePortionOfTransaction = payLoanCharge(vatCharge, transactionDate, transactionAmountRemaining, currency, feePortionOfTransaction, isWriteOffTransaction, loanChargePaidByPortion); updateChargePaidByAmount(feePortionOfTransaction.minus(loanChargePaidByPortion), loanTransaction, - installmentCharge.getLoanCharge()); + vatCharge.getLoanCharge()); loanChargePaidByPortion = feePortionOfTransaction.minus(loanChargePaidByPortion); } } @@ -990,6 +1004,8 @@ public Money payInterestComponent(final LocalDate transactionDate, final Money t this.originalInterestChargedAmount = this.interestCharged; this.interestCharged = getInterestPaid(currency).plus(getInterestWaived(currency)).plus(getInterestWrittenOff(currency)) .plus(interestDue).getAmount(); + } else { + this.originalInterestChargedAmount = BigDecimal.ZERO; } } else { @@ -1261,7 +1277,7 @@ public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate } } - private void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, + public void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, final Money amountPaidInRepaymentPeriod) { if (isInAdvance(transactionDate)) { this.totalPaidInAdvance = asMoney(this.totalPaidInAdvance, currency).plus(amountPaidInRepaymentPeriod).getAmount(); @@ -1330,6 +1346,7 @@ public void updateInterestCharged(final BigDecimal interestCharged) { } public void updateObligationMet(final Boolean obligationMet) { + this.obligationsMet = obligationMet; } @@ -1608,6 +1625,10 @@ public void addAccruedInterest(Money interestAccrued) { this.interestAccrued = defaultToZeroIfNull(this.interestAccrued).add(interestAccrued.getAmount()); } + public void setInterestAccrued(BigDecimal interestAccrued) { + this.interestAccrued = interestAccrued; + } + public Money getAccruedInterest(MonetaryCurrency currency) { return Money.of(currency, this.interestAccrued); } @@ -1668,4 +1689,25 @@ public void setInterestRecalculatedOnDate(LocalDate interestRecalculatedOnDate) this.interestRecalculatedOnDate = interestRecalculatedOnDate; } + public boolean isLastInstallment(List installments) { + return this.installmentNumber.equals(installments.get(installments.size() - 1).getInstallmentNumber()); + } + + public boolean isOverpaidInAdvance(MonetaryCurrency currency) { + return this.getPrincipal(currency).isGreaterThanZero() && this.getInterestCharged(currency).isZero() + && this.getFeeChargesCharged(currency).isZero() && this.getPenaltyChargesCharged(currency).isZero() + && this.isRecalculatedInterestComponent(); + } + + public boolean hasPenalties() { + return (this.penaltyCharges != null && this.penaltyCharges.compareTo(BigDecimal.ZERO) > 0) && !obligationsMet; + } + + public boolean isFullyGraced() { + return installmentNumber == 0; + } + + public BigDecimal originalInterestChargedAmount() { + return originalInterestChargedAmount; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 4357b03b0cb..bddd2dcb427 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -154,7 +154,7 @@ List findByClientIdAndGroupIdAndLoanStatus(@Param("clientId") Long clientI @Query("select loan from Loan loan where loan.loanStatus = 300 and " + "not exists (select transaction from LoanTransaction transaction " - + "where transaction.loan = loan and transaction.typeOf = 10 and transaction.dateOf = :localDate and transaction.dailyAccrual= true )") + + "where transaction.loan = loan and transaction.typeOf = 10 and transaction.dateOf = :localDate and transaction.dailyAccrual= true AND transaction.reversed = false)") List findActiveLoansWithNotYetPostedAccrual(@Param("localDate") LocalDate localDate); @Query("select loan from Loan loan where loan.loanStatus = 300") diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java index e357139d7dc..0bcc70ea391 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Set; @@ -37,7 +38,12 @@ public Money calculateTotalPrincipalRepaid(final List 0) { + continue; + } else { + total = total.plus(installment.getAdvancePrincipalAmount()); + } } return total; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java index 762cbf0904d..9f6d6e28223 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariations.java @@ -28,13 +28,13 @@ import java.math.BigDecimal; import java.time.LocalDate; import org.apache.fineract.infrastructure.core.data.EnumOptionData; -import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; @Entity @Table(name = "m_loan_term_variations") -public class LoanTermVariations extends AbstractPersistableCustom { +public class LoanTermVariations extends AbstractAuditableWithUTCDateTimeCustom { @ManyToOne(optional = false) @JoinColumn(name = "loan_id", nullable = false) @@ -114,9 +114,12 @@ public LoanTermVariationType getTermType() { } public LoanTermVariationsData toData() { - EnumOptionData type = LoanEnumerations.loanVariationType(this.termType); - return new LoanTermVariationsData(getId(), type, this.termApplicableFrom, this.decimalValue, this.dateValue, - this.isSpecificToInstallment); + final EnumOptionData type = LoanEnumerations.loanVariationType(this.termType); + final LoanTermVariationsData loanTermVariationsData = new LoanTermVariationsData(getId(), type, this.termApplicableFrom, + this.decimalValue, this.dateValue, this.isSpecificToInstallment); + loanTermVariationsData.setLastModifiedDate(this.getLastModifiedDate().orElse(null)); + loanTermVariationsData.setCreatedDate(this.getCreatedDate().orElse(null)); + return loanTermVariationsData; } public LocalDate getTermApplicableFrom() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index fc5cb161aea..b66826563ff 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -139,6 +139,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "claim_type") private String claimType; + @Column(name = "installment_number") + private Integer installmentNumber; + + @Column(name = "occurred_on_suspended_account", nullable = false) + private boolean occurredOnSuspendedAccount; + // This property is added to process vertical payments horizontally for Past Due and Due installments. // Advance Payments will be handled through VerticalPayment Scheme @Transient @@ -248,9 +254,10 @@ public static LoanTransaction accrueInterest(final Office office, final Loan loa } public static LoanTransaction accrueDailyInterest(final Office office, final Loan loan, final Money amount, - final LocalDate interestAppliedDate, final ExternalId externalId) { + final LocalDate interestAppliedDate, final ExternalId externalId, final Integer installmentNumber) { LoanTransaction loanTransaction = accrueInterest(office, loan, amount, interestAppliedDate, externalId); loanTransaction.setDailyAccrual(true); + loanTransaction.setInstallmentNumber(installmentNumber); return loanTransaction; } @@ -458,6 +465,14 @@ public void reverse(final ExternalId reversalExternalId) { this.reversalExternalId = reversalExternalId; } + public void markAsOccurredOnSuspendedAccount() { + this.occurredOnSuspendedAccount = true; + } + + public boolean hasOccurredOnSuspendedAccount() { + return this.occurredOnSuspendedAccount; + } + public void resetDerivedComponents() { this.principalPortion = null; this.interestPortion = null; @@ -1089,6 +1104,14 @@ public boolean isAvalClaim() { return claimType.equalsIgnoreCase("guarantor"); } + public Integer getInstallmentNumber() { + return this.installmentNumber; + } + + public void setInstallmentNumber(Integer installmentNumber) { + this.installmentNumber = installmentNumber; + } + public boolean doNotProcessAdvanceInstallments() { return doNotProcessAdvanceInstallments; } @@ -1099,4 +1122,5 @@ public void setDoNotProcessAdvanceInstallments(boolean doNotProcessAdvanceInstal // TODO missing hashCode(), equals(Object obj), but probably OK as long as // this is never stored in a Collection. + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java index 7763d2cb4bc..6a11d598681 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java @@ -167,4 +167,8 @@ public BigDecimal getPenaltyChargesPortion() { public void setLoanTransaction(LoanTransaction loanTransaction) { this.loanTransaction = loanTransaction; } + + public void setInstallment(LoanRepaymentScheduleInstallment installment) { + this.installment = installment; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 9f906ca3581..6b3ffc802a8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -714,6 +714,21 @@ protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, } final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining, installmentNumber, feeAmount, isWriteOffTransaction); + + // Ideally Java Set should not allow duplicates but here if the transaction is reprocessed then it adds a + // duplicate. + // This fix is made to stop that duplicate entry of loanChargepaidByObject + if (!loanTransaction.getLoanChargesPaid().isEmpty()) { + long count = loanTransaction.getLoanChargesPaid().stream() + .filter(p -> Objects.equals(p.getLoanCharge().getId(), unpaidCharge.getId()) + && Objects.equals(p.getInstallmentNumber(), installmentNumber)) + .count(); + if (count > 0) { + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + continue; + } + } + if (!amountPaidTowardsCharge.isZero()) { Set chargesPaidBies = loanTransaction.getLoanChargesPaid(); if (loanTransaction.isChargePayment()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index f30802f6bf6..508e33911c4 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -1172,10 +1172,14 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } // For having similar logic we are populating installment list even when the future installment // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. + // As per SU+ requirements, advance payment goes to outstanding balance so first immediate advance + // installment + // will always be seleted List inAdvanceInstallments = new ArrayList<>(); if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getFromDate())).toList(); + .filter(e -> loanTransaction.isBefore(e.getFromDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) .filter(e -> loanTransaction.isBefore(e.getFromDate())) @@ -1187,7 +1191,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); - + boolean stopProcessingAdvanceInstallment = false; for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { @@ -1220,7 +1224,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } } case IN_ADVANCE -> { - if (loanTransaction.doNotProcessAdvanceInstallments()) { + if (loanTransaction.doNotProcessAdvanceInstallments() || stopProcessingAdvanceInstallment) { // This condition will only be true if loan processing type is VERTICAL. // For vertical payments, Past Due and Due installments MUST be processed Horizontally exit = true; @@ -1252,18 +1256,49 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo Money zero = transactionAmountUnprocessed.zero(); for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { if (transactionAmountUnprocessed.isGreaterThanZero()) { - loanTransaction.updateComponents(transactionAmountUnprocessed, Money.zero(currency), - Money.zero(currency), Money.zero(currency)); - inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() - .add(transactionAmountUnprocessed.getAmount())); - inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI()); - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, inAdvanceInstallment, currency); - addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, - zero, zero, zero); + if (inAdvanceInstallment.isLastInstallment(installments) + && inAdvanceInstallment.isOverpaidInAdvance(currency) && transactionAmountUnprocessed + .isGreaterThanOrEqualTo(inAdvanceInstallment.getPrincipal(currency))) { + // This MUST be true only in case of advance overpayment after repayment + // schedule is regenerated + // Process principal and move the remaining amount to overpaid + + Money paidPrincipalComponent = inAdvanceInstallment.payPrincipalComponent( + loanTransaction.getTransactionDate(), transactionAmountUnprocessed, false, + loanTransaction); + + inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() + .add(transactionAmountUnprocessed.getAmount())); + + balances.setAggregatedPrincipalPortion( + balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed)); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, inAdvanceInstallment, currency); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, + zero, zero, zero); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPrincipalComponent); + stopProcessingAdvanceInstallment = true; + + } else { + balances.setAggregatedPrincipalPortion( + balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed)); + inAdvanceInstallment.checkIfRepaymentPeriodObligationsAreMet( + loanTransaction.getTransactionDate(), currency); + + inAdvanceInstallment.trackAdvanceAndLateTotalsForRepaymentPeriod( + loanTransaction.getTransactionDate(), currency, transactionAmountUnprocessed); + inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() + .add(transactionAmountUnprocessed.getAmount())); + inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI()); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, inAdvanceInstallment, currency); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, + zero, zero, zero); + + transactionAmountUnprocessed = Money.zero(currency); + } } } - transactionAmountUnprocessed = Money.zero(currency); exit = true; } else { exit = true; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index dc833c9020e..a96170b8836 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -22,12 +22,14 @@ import java.time.LocalDate; import java.time.temporal.ChronoUnit; import lombok.Getter; +import lombok.Setter; import org.apache.fineract.infrastructure.core.service.DateUtils; /** * Immutable data object that represents a period of a loan schedule. * */ +@Setter @Getter public final class LoanSchedulePeriodData { @@ -74,9 +76,25 @@ public final class LoanSchedulePeriodData { private final BigDecimal totalCredits; private final Boolean downPaymentPeriod; private BigDecimal mandatoryInsuranceDue = BigDecimal.ZERO; + private BigDecimal mandatoryInsurancePaid; + private BigDecimal mandatoryInsuranceWaived; + private BigDecimal mandatoryInsuranceWrittenOff; + private BigDecimal mandatoryInsuranceOutstanding; private BigDecimal voluntaryInsuranceDue = BigDecimal.ZERO; + private BigDecimal voluntaryInsurancePaid; + private BigDecimal voluntaryInsuranceWaived; + private BigDecimal voluntaryInsuranceWrittenOff; + private BigDecimal voluntaryInsuranceOutstanding; private BigDecimal avalDue = BigDecimal.ZERO; + private BigDecimal avalPaid; + private BigDecimal avalWaived; + private BigDecimal avalWrittenOff; + private BigDecimal avalOutstanding; private BigDecimal honorariosDue = BigDecimal.ZERO; + private BigDecimal honorariosPaid; + private BigDecimal honorariosWaived; + private BigDecimal honorariosWrittenOff; + private BigDecimal honorariosOutstanding; public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed, final BigDecimal feeChargesDueAtTimeOfDisbursement, final boolean isDisbursed) { @@ -487,40 +505,19 @@ public BigDecimal totalOutstandingForPeriod() { return defaultToZeroIfNull(this.totalOutstandingForPeriod); } - public BigDecimal getMandatoryInsuranceDue() { - return mandatoryInsuranceDue; + public BigDecimal getMandatoryInsuranceOutstanding() { + return defaultToZeroIfNull(this.mandatoryInsuranceOutstanding); } - public void setMandatoryInsuranceDue(BigDecimal mandatoryInsuranceDue) { - this.mandatoryInsuranceDue = mandatoryInsuranceDue; + public BigDecimal getVoluntaryInsuranceOutstanding() { + return defaultToZeroIfNull(this.voluntaryInsuranceOutstanding); } - public BigDecimal getVoluntaryInsuranceDue() { - return voluntaryInsuranceDue; + public BigDecimal getAvalOutstanding() { + return defaultToZeroIfNull(this.avalOutstanding); } - public void setVoluntaryInsuranceDue(BigDecimal voluntaryInsuranceDue) { - this.voluntaryInsuranceDue = voluntaryInsuranceDue; + public BigDecimal getHonorariosOutstanding() { + return defaultToZeroIfNull(this.honorariosOutstanding); } - - public BigDecimal getAvalDue() { - return avalDue; - } - - public void setAvalDue(BigDecimal avalDue) { - this.avalDue = avalDue; - } - - public BigDecimal getHonorariosDue() { - return honorariosDue; - } - - public void setHonorariosDue(BigDecimal honorariosDue) { - this.honorariosDue = honorariosDue; - } - - public Integer getPeriod() { - return this.period; - } - } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 64d5aaf4929..237049f8b0a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -651,7 +651,7 @@ private LocalDate getPeriodEndDate(final LocalDate startDate) { public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LocalDate periodStartDate, - final LocalDate periodEndDate) { + final LocalDate periodEndDate, boolean ignoreCurrencyDigitsAfterDecimal) { Money interestForInstallment = this.principal.zero(); Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); @@ -690,11 +690,11 @@ public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInO case DECLINING_BALANCE: final Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace( - calculator, mc, outstandingBalance, periodStartDate, periodEndDate); + calculator, mc, outstandingBalance, periodStartDate, periodEndDate, ignoreCurrencyDigitsAfterDecimal); final Money interestForThisInstallmentAfterGrace = calculateDecliningInterestDueForInstallmentAfterApplyingGrace(calculator, interestCalculationGraceOnRepaymentPeriodFraction, mc, outstandingBalance, periodNumber, periodStartDate, - periodEndDate); + periodEndDate, ignoreCurrencyDigitsAfterDecimal); interestForInstallment = interestForThisInstallmentAfterGrace; if (interestForThisInstallmentAfterGrace.isGreaterThanZero()) { @@ -1291,9 +1291,11 @@ private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Mon final Integer periodsRemaining = calculateNumberOfRemainingPrincipalPaymentPeriods(this.actualNumberOfRepayments, periodsElapsed); - - double installmentAmount = FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(), - principalDouble, futureValue, false); + double installmentAmount = balance.getAmount().doubleValue(); + if (periodsRemaining > 0) { + installmentAmount = FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(), + principalDouble, futureValue, false); + } if (this.installmentAmountInMultiplesOf != null) { installmentAmount = Money.roundToMultiplesOf(installmentAmount, this.installmentAmountInMultiplesOf); @@ -1304,24 +1306,26 @@ private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Mon } private Money calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, - final MathContext mc, final Money outstandingBalance, LocalDate periodStartDate, LocalDate periodEndDate) { - + final MathContext mc, final Money outstandingBalance, LocalDate periodStartDate, LocalDate periodEndDate, + final boolean ignoreCurrencyDigitsAfterDecimal) { Money interestDue = Money.zero(outstandingBalance.getCurrency()); boolean useDailyInterestCalculation = true; final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType, periodStartDate, periodEndDate, useDailyInterestCalculation); BigDecimal dueInterest = outstandingBalance.getAmount().multiply(periodicInterestRate); - dueInterest = dueInterest.setScale(0, RoundingMode.HALF_UP); + if (!ignoreCurrencyDigitsAfterDecimal) { + dueInterest = dueInterest.setScale(0, RoundingMode.HALF_UP); + } interestDue = interestDue.add(dueInterest); return interestDue; } private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final Money outstandingBalance, - final int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate) { + final int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate, final boolean ignoreCurrencyDigitsAfterDecimal) { Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, outstandingBalance, periodStartDate, - periodEndDate); + periodEndDate, ignoreCurrencyDigitsAfterDecimal); if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { interest = interest.zero(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 86c064d718f..6f1be751747 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -22,6 +22,7 @@ import java.time.LocalDate; import java.util.Set; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -46,4 +47,6 @@ LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency curr LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + PrincipalInterest calculatePrincipalInterestComponents(Money outstandingBalance, LoanApplicationTerms loanApplicationTerms, + int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate, boolean ignoreCurrencyDigitsAfterDecimal); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductType.java index dfd9b9881c0..87c8007b777 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductType.java @@ -26,7 +26,9 @@ public enum LoanProductType { SUMAS_PAY("SU+ PAY"), // SUMAS_EMPRESSAS("SU+ Empresas"), // - SUMAS_VEHICULOS("SU+ Vehiculos"); // + SUMAS_VEHICULOS("SU+ Vehiculos"), // + SUMAS_PRESTAMOS("SU+ Préstamos"), // + SUMAS_CASTIGADO("SU+ Castigado"); // private final String code; diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/collectionsettlement/CollectionSettlementTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/collectionsettlement/CollectionSettlementTasklet.java index 8454fa698c0..5ae34f418c8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/collectionsettlement/CollectionSettlementTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/collectionsettlement/CollectionSettlementTasklet.java @@ -95,6 +95,8 @@ public RepeatStatus execute(@NotNull StepContribution contribution, @NotNull Chu case "DIARIA": if (period.isBefore(now.minusDays(1))) { period = now; + } else if (period.isEqual(now)) { + period = now; } else { period = period.plusDays(1); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementConfig.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementConfig.java index e1b74cb453d..34f8260b256 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementConfig.java @@ -1,7 +1,9 @@ package org.apache.fineract.custom.portfolio.ally.jobs.compensationsettlement; import org.apache.fineract.custom.portfolio.ally.domain.AllyCompensationRepository; +import org.apache.fineract.custom.portfolio.ally.domain.ClientAllyRepository; import org.apache.fineract.custom.portfolio.ally.service.AllyCompensationReadWritePlatformService; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; @@ -29,6 +31,12 @@ public class CompensationOfSettlementConfig { @Autowired AllyCompensationRepository allyCompensationRepository; + @Autowired + ClientAllyRepository allyRepository; + + @Autowired + CodeValueRepository codeValueRepository; + @Bean public Step CompensationOfSettlementStep() { @@ -45,6 +53,7 @@ public Job CompensationOfSettlementJob() { @Bean public CompensationOfSettlementTasklet compensationOfSettlementTasklet() { - return new CompensationOfSettlementTasklet(allyCompensationReadWritePlatformService, allyCompensationRepository); + return new CompensationOfSettlementTasklet(allyCompensationReadWritePlatformService, allyCompensationRepository, allyRepository, + codeValueRepository); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementTasklet.java index fba78be51c0..73990d7fcdd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/compensationsettlement/CompensationOfSettlementTasklet.java @@ -1,5 +1,6 @@ package org.apache.fineract.custom.portfolio.ally.jobs.compensationsettlement; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -8,7 +9,11 @@ import org.apache.fineract.custom.portfolio.ally.data.ClientAllySettlementData; import org.apache.fineract.custom.portfolio.ally.domain.AllyCompensation; import org.apache.fineract.custom.portfolio.ally.domain.AllyCompensationRepository; +import org.apache.fineract.custom.portfolio.ally.domain.ClientAlly; +import org.apache.fineract.custom.portfolio.ally.domain.ClientAllyRepository; import org.apache.fineract.custom.portfolio.ally.service.AllyCompensationReadWritePlatformService; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; @@ -19,11 +24,16 @@ public class CompensationOfSettlementTasklet implements Tasklet { private AllyCompensationReadWritePlatformService allyCompensationReadWritePlatformService; private AllyCompensationRepository allyCompensationRepository; + private ClientAllyRepository allyRepository; + private CodeValueRepository codeValueRepository; public CompensationOfSettlementTasklet(AllyCompensationReadWritePlatformService allyCompensationReadWritePlatformService, - AllyCompensationRepository allyCompensationRepository) { + AllyCompensationRepository allyCompensationRepository, ClientAllyRepository allyRepository, + CodeValueRepository codeValueRepository) { this.allyCompensationReadWritePlatformService = allyCompensationReadWritePlatformService; this.allyCompensationRepository = allyCompensationRepository; + this.allyRepository = allyRepository; + this.codeValueRepository = codeValueRepository; } @Override @@ -48,11 +58,10 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon break; case "QUINCENAL": startDate = endDate.minusWeeks(2).plusDays(1); - System.out.println(" QUINCENAL " + startDate); + break; case "MENSUAL": startDate = endDate.minusMonths(1).plusDays(1); - System.out.println(" MENSUAL " + startDate); break; default: startDate = endDate; @@ -72,6 +81,17 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon .findBynitAndDate(clientAllySettlementData.getNit(), startDate, endDate); AllyCompensation allyCompensation = new AllyCompensation(); if (!compensationCheck.isPresent()) { + Optional clientAlly = allyRepository.findById(allySettlementCompansationData.get().getClientAllyId()); + BigDecimal percentageCommission = BigDecimal.ZERO; + BigDecimal vatCommissionAmount = BigDecimal.ZERO; + if (clientAlly.isPresent()) { + percentageCommission = clientAlly.get().getSettledComission().divide(BigDecimal.valueOf(100)); + } + + BigDecimal commissionAmount = allySettlementCompansationData.get().getPurchaseAmount(); + if (percentageCommission.compareTo(BigDecimal.ZERO) == 1) { + vatCommissionAmount = commissionAmount.multiply(percentageCommission); + } allyCompensation.setCompensationDate(purchaseDate); allyCompensation.setStartDate(startDate); @@ -84,8 +104,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon allyCompensation.setAccountNumber(allySettlementCompansationData.get().getAccountNumber()); allyCompensation.setPurchaseAmount(allySettlementCompansationData.get().getPurchaseAmount()); allyCompensation.setCollectionAmount(allySettlementCompansationData.get().getCollectionAmount()); - allyCompensation.setComissionAmount(allySettlementCompansationData.get().getCollectionAmount()); - allyCompensation.setVaComissionAmount(allySettlementCompansationData.get().getVaComissionAmount()); + allyCompensation.setComissionAmount(allySettlementCompansationData.get().getComissionAmount()); + allyCompensation.setVaComissionAmount(vatCommissionAmount); allyCompensation.setNetPurchaseAmount(allySettlementCompansationData.get().getNetPurchaseAmount()); allyCompensation.setNetOutstandingAmount(allySettlementCompansationData.get().getCompensationAmount()); allyCompensation.setNetOutstandingAmount(allySettlementCompansationData.get().getCompensationAmount()); @@ -94,17 +114,29 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon } else { AllyCompensation exisiting = compensationCheck.get(); if (exisiting.getSettlementStatus() != null) { + Optional clientAlly = allyRepository.findById(allySettlementCompansationData.get().getClientAllyId()); + BigDecimal percentageCommission = BigDecimal.ZERO; + BigDecimal vatCommissionAmount = BigDecimal.ZERO; + if (clientAlly.isPresent()) { + percentageCommission = clientAlly.get().getSettledComission().divide(BigDecimal.valueOf(100)); + } + BigDecimal commissionAmount = allySettlementCompansationData.get().getPurchaseAmount(); + if (percentageCommission.compareTo(BigDecimal.ZERO) == 1) { + vatCommissionAmount = commissionAmount.multiply(percentageCommission); + } if (!exisiting.getSettlementStatus()) { exisiting.setPurchaseAmount(allySettlementCompansationData.get().getPurchaseAmount()); exisiting.setCollectionAmount(allySettlementCompansationData.get().getCollectionAmount()); - exisiting.setComissionAmount(allySettlementCompansationData.get().getCollectionAmount()); - exisiting.setVaComissionAmount(allySettlementCompansationData.get().getVaComissionAmount()); + exisiting.setComissionAmount(allySettlementCompansationData.get().getComissionAmount()); + exisiting.setVaComissionAmount(vatCommissionAmount); exisiting.setNetPurchaseAmount(allySettlementCompansationData.get().getNetPurchaseAmount()); exisiting.setNetOutstandingAmount(allySettlementCompansationData.get().getCompensationAmount()); exisiting.setNetOutstandingAmount(allySettlementCompansationData.get().getCompensationAmount()); exisiting.setSettlementStatus(exisiting.getSettlementStatus()); allyCompensationRepository.save(exisiting); } + } else { + this.getAllyCompensationCheck(); } } } @@ -112,4 +144,58 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return RepeatStatus.FINISHED; } + + public void getAllyCompensationCheck() { + List compensations = allyCompensationRepository.findBySettlementStatus(); + for (AllyCompensation allyCompensation : compensations) { + Optional clientAlly = allyRepository.findById(allyCompensation.getClientAllyId()); + + if (clientAlly.isPresent()) { + Optional codeValue = codeValueRepository.findById(clientAlly.get().getLiquidationFrequencyCodeValueId()); + if (codeValue.isPresent()) { + CodeValue codeValue1 = codeValue.get(); + String frequency = codeValue1.getLabel(); + frequency = frequency.replaceAll("\\s", ""); + LocalDate startDate; + LocalDate endDate = allyCompensation.getCompensationDate(); + switch (frequency.toUpperCase()) { + case "SEMANAL": + startDate = endDate.minusWeeks(1).plusDays(1); + break; + case "QUINCENAL": + startDate = endDate.minusWeeks(2).plusDays(1); + + break; + case "MENSUAL": + startDate = endDate.minusMonths(1).plusDays(1); + break; + default: + startDate = endDate; + } + Optional allySettlementCompansationData = allyCompensationReadWritePlatformService + .getCompensationSettlementByNit(allyCompensation.getNit(), startDate, endDate); + if (allySettlementCompansationData.isPresent()) { + BigDecimal percentageCommission = BigDecimal.ZERO; + BigDecimal vatCommissionAmount = BigDecimal.ZERO; + if (clientAlly.isPresent()) { + percentageCommission = clientAlly.get().getSettledComission().divide(BigDecimal.valueOf(100)); + } + if (clientAlly.get().getId() == 9) { + System.out.println(" " + percentageCommission); + } + BigDecimal commissionAmount = allySettlementCompansationData.get().getPurchaseAmount(); + if (percentageCommission.compareTo(BigDecimal.ZERO) == 1) { + vatCommissionAmount = commissionAmount.multiply(percentageCommission); + } + allyCompensation.setStartDate(startDate); + allyCompensation.setEndDate(endDate); + allyCompensation.setComissionAmount(vatCommissionAmount); + allyCompensationRepository.save(allyCompensation); + } + } + + } + + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/purchasesettlement/PurchaseOfSettlementTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/purchasesettlement/PurchaseOfSettlementTasklet.java index 344e4d6f346..20150a3e4f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/purchasesettlement/PurchaseOfSettlementTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/jobs/purchasesettlement/PurchaseOfSettlementTasklet.java @@ -14,7 +14,6 @@ import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepository; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; -import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; @@ -85,6 +84,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon case "DIARIA": if (period.isBefore(now.minusDays(1))) { period = now; + } else if (period.isEqual(now)) { + period = now; } else { period = period.plusDays(1); } @@ -93,17 +94,11 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon } else { period = now; } - String worksday = workingDays.getRecurrence(); String[] arrayworksday = worksday.split(";"); String[] arrayweekdays = arrayworksday[2].split("BYDAY="); String[] arrayCount = arrayweekdays[1].split(","); Integer countWokringDay = arrayCount.length - 1; - if (!WorkingDaysUtil.isWorkingDay(workingDays, period)) { - do { - period = period.plusDays(1); - } while (period.getDayOfWeek().getValue() >= countWokringDay); - } isEqual = now.isEqual(period); if (isEqual) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/service/CompensationAlertEmailServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/service/CompensationAlertEmailServiceImpl.java index 989cef8c997..10d3dc40720 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/service/CompensationAlertEmailServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/ally/service/CompensationAlertEmailServiceImpl.java @@ -31,8 +31,8 @@ public void sendCompensationAlertEmail(String to, String subject, String body) { JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl(); javaMailSenderImpl.setHost(smtpCredentialsData.getHost()); javaMailSenderImpl.setPort(Integer.parseInt(smtpCredentialsData.getPort())); - javaMailSenderImpl.setUsername(smtpCredentialsData.getUsername()); - javaMailSenderImpl.setPassword(smtpCredentialsData.getPassword()); + // javaMailSenderImpl.setUsername(smtpCredentialsData.getUsername()); + // javaMailSenderImpl.setPassword(smtpCredentialsData.getPassword()); javaMailSenderImpl.setJavaMailProperties(this.getJavaMailProperties(smtpCredentialsData)); MimeMessage mimeMessage = javaMailSenderImpl.createMimeMessage(); @@ -52,15 +52,10 @@ public void sendCompensationAlertEmail(String to, String subject, String body) { private Properties getJavaMailProperties(SMTPCredentialsData smtpCredentialsData) { Properties properties = new Properties(); - properties.put("mail.smtp.starttls.enable", "true"); + properties.put("mail.smtp.starttls.enable", "false"); properties.put("mail.transport.protocol", "smtp"); - properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.auth", "false"); properties.put("mail.smtp.ssl.trust", smtpCredentialsData.getHost()); - if (smtpCredentialsData.isUseTLS()) { - if (smtpCredentialsData.getPort().equals("465")) { - properties.put("mail.smtp.starttls.enable", "false"); - } - } return properties; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/api/CustomChargeHonorarioMapMockApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/api/CustomChargeHonorarioMapMockApiResource.java index 08a4d32ae47..df3eeb3e765 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/api/CustomChargeHonorarioMapMockApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/api/CustomChargeHonorarioMapMockApiResource.java @@ -27,17 +27,25 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.*; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.custom.portfolio.externalcharge.honoratio.constants.CustomChargeHonorarioMapApiConstants; import org.apache.fineract.custom.portfolio.externalcharge.honoratio.data.CustomChargeHonorarioMapData; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -51,15 +59,22 @@ public class CustomChargeHonorarioMapMockApiResource { private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PlatformSecurityContext context; private final ApiRequestParameterHelper apiRequestParameterHelper; + private final LoanRepository loanRepository; + private final ConfigurationDomainService configurationDomainService; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; @Autowired public CustomChargeHonorarioMapMockApiResource(final DefaultToApiJsonSerializer toApiJsonSerializer, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, final PlatformSecurityContext context, - final ApiRequestParameterHelper apiRequestParameterHelper) { + final ApiRequestParameterHelper apiRequestParameterHelper, LoanRepository loanRepository, + ConfigurationDomainService configurationDomainService, DelinquencyReadPlatformService delinquencyReadPlatformService) { this.toApiJsonSerializer = toApiJsonSerializer; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; this.context = context; this.apiRequestParameterHelper = apiRequestParameterHelper; + this.loanRepository = loanRepository; + this.configurationDomainService = configurationDomainService; + this.delinquencyReadPlatformService = delinquencyReadPlatformService; } @GET @@ -83,33 +98,79 @@ private List> generateRandomMockedData(Long loanId) { List> jsonList = new ArrayList<>(); // Generate Random Data for each installment - for (int i = 1; i <= 4; i++) { - - double feeBaseAmount, feeVatAmount, feeTotalAmount; - if (i < 4) { - feeBaseAmount = 90; - feeVatAmount = 10; - feeTotalAmount = 100; + Optional loans = loanRepository.findById(loanId); + + if (loans.isPresent()) { + Loan loan = loans.get(); + Integer ageOverdue = loan.getAgeOfOverdueDays(DateUtils.getBusinessLocalDate()).intValue(); + BigDecimal delinquencyValue = BigDecimal.ZERO; + Integer vatConfig = configurationDomainService.retriveIvaConfiguration(); + BigDecimal vatPercentage = BigDecimal.valueOf(vatConfig).divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + MonetaryCurrency currency = loan.getCurrency(); + DelinquencyRangeData delinquencyRangeData = delinquencyReadPlatformService.retrieveCurrentDelinquencyTag(loan.getId()); + if (delinquencyRangeData != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRangeData.getPercentageValue()); } else { - do { - feeBaseAmount = (Math.random() * (120 - 100) + 100); - feeVatAmount = (Math.random() * (80 - 10) + 10); - feeTotalAmount = feeBaseAmount + feeVatAmount; - } while (feeTotalAmount <= 100 || feeTotalAmount >= 200); + DelinquencyRange delinquencyRange = delinquencyReadPlatformService.retrieveDelinquencyRangeCategeory(ageOverdue); + if (delinquencyRange != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRange.getPercentageValue()); + } } - // Create JSON payload - Map jsonMap = new HashMap<>(); - jsonMap.put("loanId", loanId); - jsonMap.put("nit", nit); - jsonMap.put("loanInstallmentNr", i); - jsonMap.put("feeTotalAmount", String.format("%.4f", feeTotalAmount)); - jsonMap.put("feeBaseAmount", String.format("%.4f", feeBaseAmount)); - jsonMap.put("feeVatAmount", String.format("%.4f", feeVatAmount)); - jsonMap.put("dateFormat", "dd/MM/yyyy"); - jsonMap.put("locale", "en"); - - jsonList.add(jsonMap); + BigDecimal deliquncyrange = delinquencyValue.divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + + if (ageOverdue > 0) { + List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loanRepaymentScheduleInstallments) { + BigDecimal feeBaseAmount = BigDecimal.ZERO; + BigDecimal feeVatAmount = BigDecimal.ZERO; + BigDecimal feeTotalAmount = BigDecimal.ZERO; + Map jsonMap = new HashMap<>(); + + if (LocalDate.now().isAfter(loanRepaymentScheduleInstallment.getDueDate()) + && !loanRepaymentScheduleInstallment.isObligationsMet()) { + BigDecimal paidAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency).getAmount(); + + BigDecimal delinquentPortion = paidAmount.divide( + BigDecimal.ONE.add(deliquncyrange.multiply(BigDecimal.ONE.add(vatPercentage))), 2, + MoneyHelper.getRoundingMode()); + + BigDecimal feewithTax = delinquentPortion.multiply(deliquncyrange.multiply(BigDecimal.ONE.add(vatPercentage))) + .setScale(2, MoneyHelper.getRoundingMode()); + BigDecimal feeBasis = feewithTax.divide(BigDecimal.ONE.add(vatPercentage), 2, MoneyHelper.getRoundingMode()); + + BigDecimal feeVat = feewithTax.subtract(feeBasis).setScale(2, MoneyHelper.getRoundingMode()); + BigDecimal feeHono = feeVat.add(feeBasis).setScale(0, MoneyHelper.getRoundingMode()); + + feeBaseAmount = feeBasis; + feeVatAmount = feeVat; + feeTotalAmount = feeHono; + + } + + jsonMap.put("loanId", loanId); + jsonMap.put("nit", nit); + jsonMap.put("loanInstallmentNr", loanRepaymentScheduleInstallment.getInstallmentNumber()); + if (feeTotalAmount.compareTo(BigDecimal.ZERO) == 0) { + jsonMap.put("feeTotalAmount", "0.00"); + } else { + jsonMap.put("feeTotalAmount", String.format("%.2f", feeTotalAmount)); + } + if (feeBaseAmount.compareTo(BigDecimal.ZERO) == 0) { + jsonMap.put("feeBaseAmount", "0.00"); + } else { + jsonMap.put("feeBaseAmount", String.format("%.2f", feeBaseAmount)); + } + if (feeVatAmount.compareTo(BigDecimal.ZERO) == 0) { + jsonMap.put("feeVatAmount", "0.00"); + } else { + jsonMap.put("feeVatAmount", String.format("%.2f", feeVatAmount)); + } + jsonMap.put("dateFormat", "dd/MM/yyyy"); + jsonMap.put("locale", "en"); + jsonList.add(jsonMap); + } + } } return jsonList; diff --git a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/service/CustomChargeHonorarioMapReadWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/service/CustomChargeHonorarioMapReadWritePlatformServiceImpl.java index afc7c35e852..7abadc271c8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/service/CustomChargeHonorarioMapReadWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/custom/portfolio/externalcharge/honoratio/service/CustomChargeHonorarioMapReadWritePlatformServiceImpl.java @@ -45,10 +45,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.client.domain.ClientRepository; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.domain.*; import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; @@ -167,11 +164,33 @@ public CommandProcessingResult create(final ExternalCustomChargeHonorarioMapData .sorted(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList(); for (LoanRepaymentScheduleInstallment inst : installments) { if (inst.getInstallmentNumber().equals(entity.getLoanInstallmentNr())) { + Optional loanCharges = curr.getActiveCharges().stream() + .filter(charge -> charge.isFlatHono() || charge.getChargeCalculation().isPercentageOfHonorarios()) + .findFirst(); if (inst.isObligationsMet()) { // Cannot throw exception here as it will rollback everything including any new fee rows // throw new LoanInstallmentAlreadyPaidException(loanId, inst.getInstallmentNumber()); insallmentAlreadyPaid = true; + if (loanCharges.isPresent()) { + LoanCharge loanCharge = loanCharges.get(); + LoanInstallmentCharge installmentCharge = loanCharge + .getInstallmentLoanCharge(inst.getInstallmentNumber()); + if (installmentCharge.isPaid()) { + insallmentAlreadyPaid = true; + break; + } + } break; + } else { + if (loanCharges.isPresent()) { + LoanCharge loanCharge = loanCharges.get(); + LoanInstallmentCharge installmentCharge = loanCharge + .getInstallmentLoanCharge(inst.getInstallmentNumber()); + if (installmentCharge.isPaid()) { + insallmentAlreadyPaid = true; + break; + } + } } } @@ -207,9 +226,13 @@ public CommandProcessingResult create(final ExternalCustomChargeHonorarioMapData for (LoanCharge loanCharge : loanToUpdate.getCharges()) { if (loanCharge.getChargeCalculation().isFlatHono()) { Set maps = loanCharge.getCustomChargeHonorarioMaps(); + for (CustomChargeHonorarioMap map : maps) { + LoanInstallmentCharge installmentCharge = loanCharge + .getInstallmentLoanCharge(map.getLoanInstallmentNr()); if (entityOpt.isPresent()) { - if (map.getLoanInstallmentNr().equals(current.getLoanInstallmentNr())) { + if (map.getLoanInstallmentNr().equals(current.getLoanInstallmentNr()) + || installmentCharge.isPaid()) { removeList.add(map); break; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/LoanConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/LoanConstants.java index 7d9fa3ce803..2e492997bf9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/LoanConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/LoanConstants.java @@ -76,6 +76,10 @@ private LoanConstants() { public static final int LOAN_COLLATERAL_QUANTITY = 48;// AW public static final int CHARGE_AMOUNT_TYPE_1 = 49;// AX public static final int CHARGE_AMOUNT_TYPE_2 = 50;// AY + public static final int NIT = 51;// AY + public static final int CODE = 52;// AY + public static final int LOAN_ID = 53;// AY + public static final int CEDULA = 54;// AY public static final String LOAN_TYPE_INDIVIDUAL = "Individual"; public static final String LOAN_TYPE_GROUP = "Group"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/clientcupodecrement/ClientCupoDecrementImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/clientcupodecrement/ClientCupoDecrementImportHandler.java index cffc0872c72..895089216ac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/clientcupodecrement/ClientCupoDecrementImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/clientcupodecrement/ClientCupoDecrementImportHandler.java @@ -29,6 +29,7 @@ import lombok.Data; import org.apache.commons.collections4.CollectionUtils; import org.apache.fineract.infrastructure.bulkimport.constants.ClientCupoDecrementConstants; +import org.apache.fineract.infrastructure.bulkimport.constants.ClientCupoIncrementConstants; import org.apache.fineract.infrastructure.bulkimport.constants.TemplatePopulateImportConstants; import org.apache.fineract.infrastructure.bulkimport.data.Count; import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandler; @@ -202,14 +203,25 @@ private Count importEntity(final Workbook workbook, final List 0) { + if (maximumCupoAmount.compareTo(totalOutstandingPrincipalAmount) < 0) { errorCount++; - errorMessage = "No puede modificarse ya que el nuevo cupo que sugiere es mayor al actual"; + errorMessage = "No se puede modificar ya que la nueva cuota sugerida es menor al capital pendiente actual: " + + totalOutstandingPrincipalAmount; + ImportHandlerUtils.writeErrorMessage(clientCupDecrementSheet, clientCupoDecrementData.getRowIndex(), errorMessage, + ClientCupoIncrementConstants.STATUS_COL); + clientCupDecrementSheet.setColumnWidth(ClientCupoIncrementConstants.STATUS_COL, + TemplatePopulateImportConstants.EXTRALARGE_COL_SIZE); + continue; + } + if (maximumCupoAmount.compareTo(previousMaximumCupoAmount) >= 0) { + errorCount++; + errorMessage = "No se puede modificar ya que la nueva cuota sugerida es mayor o igual a la actual."; ImportHandlerUtils.writeErrorMessage(clientCupDecrementSheet, clientCupoDecrementData.getRowIndex(), errorMessage, ClientCupoDecrementConstants.STATUS_COL); clientCupDecrementSheet.setColumnWidth(ClientCupoDecrementConstants.STATUS_COL, @@ -281,15 +293,17 @@ private Count importEntity(final Workbook workbook, final List errMesg; String errorMessage; final GsonBuilder gsonBuilder = GoogleGsonSerializerHelper.createGsonBuilder(); gsonBuilder.registerTypeAdapter(LocalDate.class, new DateSerializer(dateFormat, locale)); @@ -202,35 +201,27 @@ private Count importEntity(final Workbook workbook, final List 0) { - if (maximumCupoAmount.compareTo(totalOutstandingPrincipalAmount) < 0) { - errorCount++; - errorMessage = "No puede modificarse ya que el nuevo cupo que sugiere es menor al actual " - + totalOutstandingPrincipalAmount; - ImportHandlerUtils.writeErrorMessage(clientCupoIncrementSheet, clientCupoIncrementData.getRowIndex(), errorMessage, - ClientCupoIncrementConstants.STATUS_COL); - clientCupoIncrementSheet.setColumnWidth(ClientCupoIncrementConstants.STATUS_COL, - TemplatePopulateImportConstants.EXTRALARGE_COL_SIZE); - continue; - } + if (maximumCupoAmount.compareTo(totalOutstandingPrincipalAmount) < 0) { + errorCount++; + errorMessage = "No se puede modificar ya que la nueva cuota sugerida es menor al capital pendiente actual: " + + totalOutstandingPrincipalAmount; + ImportHandlerUtils.writeErrorMessage(clientCupoIncrementSheet, clientCupoIncrementData.getRowIndex(), errorMessage, + ClientCupoIncrementConstants.STATUS_COL); + clientCupoIncrementSheet.setColumnWidth(ClientCupoIncrementConstants.STATUS_COL, + TemplatePopulateImportConstants.EXTRALARGE_COL_SIZE); + continue; } - - if (maximumCupoAmount.compareTo(previousMaximumCupoAmount) < 0) { + if (maximumCupoAmount.compareTo(previousMaximumCupoAmount) <= 0) { errorCount++; - errorMessage = "No puede modificarse ya que el nuevo cupo que sugiere es menor al actual"; + errorMessage = "No se puede modificar ya que la nueva cuota sugerida es menor o igual a la actual."; ImportHandlerUtils.writeErrorMessage(clientCupoIncrementSheet, clientCupoIncrementData.getRowIndex(), errorMessage, ClientCupoIncrementConstants.STATUS_COL); clientCupoIncrementSheet.setColumnWidth(ClientCupoIncrementConstants.STATUS_COL, @@ -303,7 +294,7 @@ private Count importEntity(final Workbook workbook, final List 0) { final int affectedRows = jdbcTemplate.update(sql, maximumCupoAmount, clientId, documentNumber); if (affectedRows == 0) { errorCount++; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java index 089d9b92856..33852376023 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java @@ -128,7 +128,7 @@ private DisbursementData readDisbursalData(final Row row, final String locale, f } if (disbursedDate != null) { - return DisbursementData.importInstance(disbursedDate, linkAccountId, row.getRowNum(), locale, dateFormat); + return DisbursementData.importInstance(disbursedDate, linkAccountId, row.getRowNum(), locale, dateFormat, "Mifos"); } return null; } @@ -286,23 +286,34 @@ private LoanAccountData readLoan(final Workbook workbook, final Row row, final L String linkAccountId = ImportHandlerUtils.readAsString(LoanConstants.LINK_ACCOUNT_ID, row); + String nit = ImportHandlerUtils.readAsString(LoanConstants.NIT, row); + String code = ImportHandlerUtils.readAsString(LoanConstants.CODE, row); + String loanId = ImportHandlerUtils.readAsString(LoanConstants.LOAN_ID, row); + String cedula = ImportHandlerUtils.readAsString(LoanConstants.CEDULA, row); + + boolean isMigratedLoan = nit != null && code != null && loanId != null && cedula != null; + if (chargeOneId != null) { if (ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_1, row) != null) { - EnumOptionData chargeOneTimeTypeEnum = ImportHandlerUtils - .getChargeTimeTypeEmun(workbook.getSheet(TemplatePopulateImportConstants.CHARGE_SHEET_NAME), chargeOneName); - EnumOptionData chargeOneAmountTypeEnum = ImportHandlerUtils - .getChargeAmountTypeEnum(ImportHandlerUtils.readAsString(LoanConstants.CHARGE_AMOUNT_TYPE_1, row)); - - BigDecimal chargeAmount; - BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_1, row)); - if (chargeOneAmountTypeEnum.getValue().equalsIgnoreCase("1")) { - chargeAmount = amountOrPercentage; + if (!isMigratedLoan) { + EnumOptionData chargeOneTimeTypeEnum = ImportHandlerUtils + .getChargeTimeTypeEmun(workbook.getSheet(TemplatePopulateImportConstants.CHARGE_SHEET_NAME), chargeOneName); + EnumOptionData chargeOneAmountTypeEnum = ImportHandlerUtils + .getChargeAmountTypeEnum(ImportHandlerUtils.readAsString(LoanConstants.CHARGE_AMOUNT_TYPE_1, row)); + + BigDecimal chargeAmount; + BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_1, row)); + if (chargeOneAmountTypeEnum.getValue().equalsIgnoreCase("1")) { + chargeAmount = amountOrPercentage; + } else { + chargeAmount = LoanCharge.percentageOf(principal, amountOrPercentage); + } + charges.add(new LoanChargeData(chargeOneId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_1, row), + chargeAmount, chargeOneAmountTypeEnum, chargeOneTimeTypeEnum)); } else { - chargeAmount = LoanCharge.percentageOf(principal, amountOrPercentage); + BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_1, row)); + charges.add(new LoanChargeData(chargeOneId, amountOrPercentage)); } - - charges.add(new LoanChargeData(chargeOneId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_1, row), - chargeAmount, chargeOneAmountTypeEnum, chargeOneTimeTypeEnum)); } else { charges.add(new LoanChargeData(chargeOneId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_1, row), null)); } @@ -310,21 +321,26 @@ private LoanAccountData readLoan(final Workbook workbook, final Row row, final L if (chargeTwoId != null) { if (ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_2, row) != null) { - EnumOptionData chargeTwoTimeTypeEnum = ImportHandlerUtils - .getChargeTimeTypeEmun(workbook.getSheet(TemplatePopulateImportConstants.CHARGE_SHEET_NAME), chargeTwoName); - EnumOptionData chargeTwoAmountTypeEnum = ImportHandlerUtils - .getChargeAmountTypeEnum(ImportHandlerUtils.readAsString(LoanConstants.CHARGE_AMOUNT_TYPE_2, row)); - - BigDecimal chargeAmount; - BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_2, row)); - if (chargeTwoTimeTypeEnum.getValue().equalsIgnoreCase("1")) { - chargeAmount = amountOrPercentage; + if (!isMigratedLoan) { + EnumOptionData chargeTwoTimeTypeEnum = ImportHandlerUtils + .getChargeTimeTypeEmun(workbook.getSheet(TemplatePopulateImportConstants.CHARGE_SHEET_NAME), chargeTwoName); + EnumOptionData chargeTwoAmountTypeEnum = ImportHandlerUtils + .getChargeAmountTypeEnum(ImportHandlerUtils.readAsString(LoanConstants.CHARGE_AMOUNT_TYPE_2, row)); + + BigDecimal chargeAmount; + BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_2, row)); + if (chargeTwoTimeTypeEnum.getValue().equalsIgnoreCase("1")) { + chargeAmount = amountOrPercentage; + } else { + chargeAmount = LoanCharge.percentageOf(principal, amountOrPercentage); + } + + charges.add(new LoanChargeData(chargeTwoId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_2, row), + chargeAmount, chargeTwoAmountTypeEnum, chargeTwoTimeTypeEnum)); } else { - chargeAmount = LoanCharge.percentageOf(principal, amountOrPercentage); + BigDecimal amountOrPercentage = BigDecimal.valueOf(ImportHandlerUtils.readAsDouble(LoanConstants.CHARGE_AMOUNT_2, row)); + charges.add(new LoanChargeData(chargeTwoId, amountOrPercentage)); } - - charges.add(new LoanChargeData(chargeTwoId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_2, row), - chargeAmount, chargeTwoAmountTypeEnum, chargeTwoTimeTypeEnum)); } else { charges.add(new LoanChargeData(chargeTwoId, ImportHandlerUtils.readAsDate(LoanConstants.CHARGE_DUE_DATE_2, row), null)); } @@ -353,7 +369,7 @@ private LoanAccountData readLoan(final Workbook workbook, final Row row, final L nominalInterestRate, submittedOnDate, amortizationEnumOption, interestMethodEnum, interestCalculationPeriodEnum, arrearsTolerance, repaymentStrategyCode, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, firstRepaymentOnDate, row.getRowNum(), externalId, null, charges, linkAccountId, locale, - dateFormat, loanCollateralManagementData); + dateFormat, loanCollateralManagementData, nit, code, loanId, cedula, isMigratedLoan); } else if (loanType.equals("jlg")) { Long clientId = ImportHandlerUtils.getIdByName(workbook.getSheet(TemplatePopulateImportConstants.CLIENT_SHEET_NAME), clientOrGroupName); @@ -362,7 +378,7 @@ private LoanAccountData readLoan(final Workbook workbook, final Row row, final L nominalInterestRate, submittedOnDate, amortizationEnumOption, interestMethodEnum, interestCalculationPeriodEnum, arrearsTolerance, repaymentStrategyCode, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, firstRepaymentOnDate, row.getRowNum(), externalId, groupId, charges, linkAccountId, locale, - dateFormat, null); + dateFormat, null, nit, code, loanId, cedula, isMigratedLoan); } else { Long groupIdforGroupLoan = ImportHandlerUtils .getIdByName(workbook.getSheet(TemplatePopulateImportConstants.GROUP_SHEET_NAME), clientOrGroupName); @@ -406,11 +422,11 @@ private Count importEntity(final Workbook workbook, final List } if (progressLevel <= 1 && approvalDates.get(i) != null) { - progressLevel = importLoanApproval(approvalDates, result, i, dateFormat); + progressLevel = importLoanApproval(approvalDates, result, i, dateFormat, loanId); } if (progressLevel <= 2 && disbursalDates.get(i) != null) { - progressLevel = importDisbursalData(approvalDates, disbursalDates, result, i, dateFormat); + progressLevel = importDisbursalData(approvalDates, disbursalDates, result, i, dateFormat, loanId); } if (loanRepayments.get(i) != null) { @@ -479,7 +495,7 @@ private Integer importLoanRepayment(final List loanRepaymen } private Integer importDisbursalData(final List approvalDates, final List disbursalDates, - final CommandProcessingResult result, final int rowIndex, final String dateFormat) { + final CommandProcessingResult result, final int rowIndex, final String dateFormat, String loanId) { if (approvalDates.get(rowIndex) != null && disbursalDates.get(rowIndex) != null) { DisbursementData disbusalData = disbursalDates.get(rowIndex); @@ -496,7 +512,7 @@ private Integer importDisbursalData(final List approvalDates, } else { String payload = gsonBuilder.create().toJson(disbusalData); final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .disburseLoanApplication(result.getLoanId()) // + .disburseLoanApplication(result == null ? Long.parseLong(loanId) : result.getLoanId()) // .withJson(payload) // .build(); // @@ -507,13 +523,13 @@ private Integer importDisbursalData(final List approvalDates, } private Integer importLoanApproval(final List approvalDates, final CommandProcessingResult result, final int rowIndex, - final String dateFormat) { + final String dateFormat, String loanId) { if (approvalDates.get(rowIndex) != null) { GsonBuilder gsonBuilder = GoogleGsonSerializerHelper.createGsonBuilder(); gsonBuilder.registerTypeAdapter(LocalDate.class, new DateSerializer(dateFormat)); String payload = gsonBuilder.create().toJson(approvalDates.get(rowIndex)); final CommandWrapper commandRequest = new CommandWrapperBuilder() // - .approveLoanApplication(result.getLoanId()) // + .approveLoanApplication(result == null ? Long.parseLong(loanId) : result.getLoanId()) // .withJson(payload) // .build(); // @@ -527,10 +543,19 @@ private CommandProcessingResult importLoan(final List loans, fi gsonBuilder.registerTypeAdapter(LocalDate.class, new DateSerializer(dateFormat)); gsonBuilder.registerTypeAdapter(EnumOptionData.class, new EnumOptionDataValueSerializer()); JsonObject loanJsonOb = gsonBuilder.create().toJsonTree(loans.get(rowIndex)).getAsJsonObject(); + LoanAccountData loan = loans.get(rowIndex); + if (loan.getFundId() == null || loan.getFundId() == 0) { + loanJsonOb.remove("fundId"); + } + if (loan.getLoanOfficerId() == null || loan.getLoanOfficerId() == 0) { + loanJsonOb.remove("loanOfficerId"); + } + loanJsonOb.remove("isLoanProductLinkedToFloatingRate"); loanJsonOb.remove("isInterestRecalculationEnabled"); loanJsonOb.remove("isFloatingInterestRate"); loanJsonOb.remove("isRatesEnabled"); + JsonArray chargesJsonAr = loanJsonOb.getAsJsonArray("charges"); if (chargesJsonAr != null) { for (int i = 0; i < chargesJsonAr.size(); i++) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/ExtrasSheetPopulator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/ExtrasSheetPopulator.java index 34871012450..d86b435d31a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/ExtrasSheetPopulator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/ExtrasSheetPopulator.java @@ -92,28 +92,32 @@ public void populate(Workbook workbook, String dateFormat) { writeString(CURRENCY_CODE_COL, row, currencies.getCode()); } int channelRowIndex = 1; - for (ChannelData channelData : channelOptions) { - Row row; - if (channelRowIndex < currencyCodeRowIndex || currencyCodeRowIndex < paymentTypeRowIndex - || currencyCodeRowIndex < fundRowIndex) { - row = extrasSheet.getRow(channelRowIndex++); - } else { - row = extrasSheet.createRow(channelRowIndex++); + if (channelOptions != null) { + for (ChannelData channelData : channelOptions) { + Row row; + if (channelRowIndex < currencyCodeRowIndex || currencyCodeRowIndex < paymentTypeRowIndex + || currencyCodeRowIndex < fundRowIndex) { + row = extrasSheet.getRow(channelRowIndex++); + } else { + row = extrasSheet.createRow(channelRowIndex++); + } + writeLong(CHANNEL_ID_COL, row, channelData.getId()); + writeString(CHANNEL_NAME_COL, row, channelData.getName().trim().replaceAll("[ )(]", "_")); } - writeLong(CHANNEL_ID_COL, row, channelData.getId()); - writeString(CHANNEL_NAME_COL, row, channelData.getName().trim().replaceAll("[ )(]", "_")); } int bankRowIndex = 1; - for (CodeValueData bankCodeValueData : bankOptions) { - Row row; - if (bankRowIndex < channelRowIndex || channelRowIndex < currencyCodeRowIndex || currencyCodeRowIndex < paymentTypeRowIndex - || currencyCodeRowIndex < fundRowIndex) { - row = extrasSheet.getRow(bankRowIndex++); - } else { - row = extrasSheet.createRow(bankRowIndex++); + if (bankOptions != null) { + for (CodeValueData bankCodeValueData : bankOptions) { + Row row; + if (bankRowIndex < channelRowIndex || channelRowIndex < currencyCodeRowIndex || currencyCodeRowIndex < paymentTypeRowIndex + || currencyCodeRowIndex < fundRowIndex) { + row = extrasSheet.getRow(bankRowIndex++); + } else { + row = extrasSheet.createRow(bankRowIndex++); + } + writeLong(BANK_ID_COL, row, bankCodeValueData.getId()); + writeString(BANK_NAME_COL, row, bankCodeValueData.getName().trim().replaceAll("[ )(]", "_")); } - writeLong(BANK_ID_COL, row, bankCodeValueData.getId()); - writeString(BANK_NAME_COL, row, bankCodeValueData.getName().trim().replaceAll("[ )(]", "_")); } extrasSheet.protectSheet(""); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java index 091c2314ae4..68aa12bd8a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/data/EmailMessageWithAttachmentData.java @@ -33,10 +33,17 @@ public final class EmailMessageWithAttachmentData { private String text; private String subject; private List attachments; + private List recipients; public static EmailMessageWithAttachmentData createNew(final String to, final String text, final String subject, final List attachments) { return new EmailMessageWithAttachmentData().setTo(to).setText(text).setSubject(subject).setAttachments(attachments); } + public static EmailMessageWithAttachmentData createNew(final String text, final String subject, final List attachments, + final List recipients) { + return new EmailMessageWithAttachmentData().setText(text).setSubject(subject).setAttachments(attachments).setRecipients(recipients) + .setRecipients(recipients); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java index 43444bbac3c..f3a280d7cae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/email/service/EmailMessageJobEmailServiceImpl.java @@ -58,7 +58,12 @@ public void sendEmailWithAttachment(EmailMessageWithAttachmentData emailMessageW MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); mimeMessageHelper.setFrom(smtpCredentialsData.getFromEmail()); - mimeMessageHelper.setTo(emailMessageWithAttachmentData.getTo()); + // if recipient is a list of recipients use the multiple to + if (emailMessageWithAttachmentData.getRecipients() != null && !emailMessageWithAttachmentData.getRecipients().isEmpty()) { + mimeMessageHelper.setTo(emailMessageWithAttachmentData.getRecipients().toArray(new String[0])); + } else { + mimeMessageHelper.setTo(emailMessageWithAttachmentData.getTo()); + } mimeMessageHelper.setText(emailMessageWithAttachmentData.getText(), true); mimeMessageHelper.setSubject(emailMessageWithAttachmentData.getSubject()); final List attachments = emailMessageWithAttachmentData.getAttachments(); @@ -79,9 +84,9 @@ public void sendEmailWithAttachment(EmailMessageWithAttachmentData emailMessageW } private Properties getJavaMailProperties(SMTPCredentialsData smtpCredentialsData, Properties properties) { - properties.put("mail.smtp.starttls.enable", "true"); + properties.put("mail.smtp.starttls.enable", "false"); properties.put("mail.transport.protocol", "smtp"); - properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.auth", "false"); properties.put("mail.smtp.ssl.trust", smtpCredentialsData.getHost()); if (smtpCredentialsData.isUseTLS()) { // Needs to disable startTLS if the port is 465 in order to send the email successfully when using the diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java index d32c3d9f07f..bfa3472a06f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java @@ -298,71 +298,77 @@ private void notifyWriteOffLoanOwner(final LoanTransaction loanTransaction) { } } - private void sendSmsForLoanRepayment(LoanTransaction loanTransaction) { - List smsCampaigns = retrieveSmsCampaigns("Loan Repayment"); - if (!smsCampaigns.isEmpty()) { - for (SmsCampaign smsCampaign : smsCampaigns) { - try { - Loan loan = loanTransaction.getLoan(); - final Set groupClients = new HashSet<>(); - if (loan.hasInvalidLoanType()) { - throw new InvalidLoanTypeException("Loan Type cannot be Invalid for the Triggered Sms Campaign"); - } - if (loan.isGroupLoan()) { - Group group = this.groupRepository.findById(loan.getGroupId()) - .orElseThrow(() -> new GroupNotFoundException(loan.getGroupId())); - groupClients.addAll(group.getClientMembers()); - } else { - groupClients.add(loan.client()); - } - HashMap campaignParams = new ObjectMapper().readValue(smsCampaign.getParamValue(), - new TypeReference<>() { - - }); - - if (!groupClients.isEmpty()) { - for (Client client : groupClients) { - HashMap smsParams = processRepaymentDataForSms(loanTransaction, client); - for (Map.Entry entry : campaignParams.entrySet()) { - String value = entry.getValue(); - String spvalue = null; - boolean spkeycheck = smsParams.containsKey(entry.getKey()); - if (spkeycheck) { - spvalue = smsParams.get(entry.getKey()).toString(); - } - if (spkeycheck && !(value.equals("-1") || spvalue.equals(value))) { - if (entry.getKey().equals("officeId")) { - Long officeId = Long.valueOf(value); - Office campaignOffice = this.officeRepository.findById(Long.valueOf(value)) - .orElseThrow(() -> new OfficeNotFoundException(officeId)); - if (campaignOffice.doesNotHaveAnOfficeInHierarchyWithId(client.getOffice().getId())) { - throw new SmsRuntimeException("error.msg.no.office", "Office not found for the id"); + private void sendSmsForLoanRepayment(final LoanTransaction loanTransaction) { + final Loan loan = loanTransaction.getLoan(); + final LoanProduct loanProduct = loan.getLoanProduct(); + if (loanProduct != null) { + if (loanProduct.getCustomAllowCollectionsSms()) { + final List smsCampaigns = retrieveSmsCampaigns("Loan Repayment"); + if (!smsCampaigns.isEmpty()) { + for (SmsCampaign smsCampaign : smsCampaigns) { + try { + final Set groupClients = new HashSet<>(); + if (loan.hasInvalidLoanType()) { + throw new InvalidLoanTypeException("Loan Type cannot be Invalid for the Triggered Sms Campaign"); + } + if (loan.isGroupLoan()) { + Group group = this.groupRepository.findById(loan.getGroupId()) + .orElseThrow(() -> new GroupNotFoundException(loan.getGroupId())); + groupClients.addAll(group.getClientMembers()); + } else { + groupClients.add(loan.client()); + } + HashMap campaignParams = new ObjectMapper().readValue(smsCampaign.getParamValue(), + new TypeReference<>() { + + }); + + if (!groupClients.isEmpty()) { + for (Client client : groupClients) { + HashMap smsParams = processRepaymentDataForSms(loanTransaction, client); + for (Map.Entry entry : campaignParams.entrySet()) { + String value = entry.getValue(); + String spvalue = null; + boolean spkeycheck = smsParams.containsKey(entry.getKey()); + if (spkeycheck) { + spvalue = smsParams.get(entry.getKey()).toString(); + } + if (spkeycheck && !(value.equals("-1") || spvalue.equals(value))) { + if (entry.getKey().equals("officeId")) { + Long officeId = Long.valueOf(value); + Office campaignOffice = this.officeRepository.findById(Long.valueOf(value)) + .orElseThrow(() -> new OfficeNotFoundException(officeId)); + if (campaignOffice.doesNotHaveAnOfficeInHierarchyWithId(client.getOffice().getId())) { + throw new SmsRuntimeException("error.msg.no.office", "Office not found for the id"); + } + } else { + throw new SmsRuntimeException("error.msg.no.id.attribute", + "Office Id attribute is notfound"); + } } - } else { - throw new SmsRuntimeException("error.msg.no.id.attribute", "Office Id attribute is notfound"); + } + String message = this.smsCampaignWritePlatformCommandHandler + .compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), smsParams); + Object mobileNo = smsParams.get("mobileNo"); + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if (mobileNo != null) { + mobileNumber = mobileNo.toString(); + } + SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message, mobileNumber, + smsCampaign, smsCampaign.isNotification()); + Map> smsDataMap = new HashMap<>(); + smsDataMap.put(smsCampaign, Collections.singletonList(smsMessage)); + this.smsMessageScheduledJobService.sendTriggeredMessages(smsDataMap); } } } - String message = this.smsCampaignWritePlatformCommandHandler.compileSmsTemplate(smsCampaign.getMessage(), - smsCampaign.getCampaignName(), smsParams); - Object mobileNo = smsParams.get("mobileNo"); - if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { - String mobileNumber = null; - if (mobileNo != null) { - mobileNumber = mobileNo.toString(); - } - SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message, mobileNumber, smsCampaign, - smsCampaign.isNotification()); - Map> smsDataMap = new HashMap<>(); - smsDataMap.put(smsCampaign, Collections.singletonList(smsMessage)); - this.smsMessageScheduledJobService.sendTriggeredMessages(smsDataMap); - } + } catch (final IOException e) { + log.error("smsParams does not contain the key: ", e); + } catch (final RuntimeException e) { + log.debug("Client Office Id and SMS Campaign Office id doesn't match ", e); } } - } catch (final IOException e) { - log.error("smsParams does not contain the key: ", e); - } catch (final RuntimeException e) { - log.debug("Client Office Id and SMS Campaign Office id doesn't match ", e); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index aa87d2e6a3c..9820b14f780 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -20,6 +20,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -54,6 +55,9 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService public static final String CHARGE_ACCRUAL_DATE_CRITERIA = "charge-accrual-date"; public static final String NEXT_PAYMENT_DUE_DATE = "next-payment-due-date"; + public static final String INVOICE_RESOLUTION_EXPIRY = "Días previos para notificar vencimiento de la resolución de facturas"; + public static final String REMAINING_INVOICES_THRESHOLD = "Cantidad previa al límte de la numeración de la facturación para notificar"; + public static final String INVOICE_NOTIFICATION_EMAILS = "Correo/s para enviar alerta por factura electrónica por vencer o agotarse"; private final PermissionRepository permissionRepository; private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; @@ -120,6 +124,13 @@ public boolean allowTransactionsOnNonWorkingDayEnabled() { return property.isEnabled(); } + @Override + public boolean enableMonthlyInvoiceGenerationOnJobTrigger() { + final String propertyName = "enable-monthly-invoice-generation-on-job-trigger"; + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); + return property.isEnabled(); + } + @Override public boolean isConstraintApproachEnabledForDatatables() { final String propertyName = "constraint_approach_for_datatables"; @@ -145,14 +156,20 @@ public void updateCache(final CacheType cacheType) { public Long retrievePenaltyWaitPeriod() { final String propertyName = "penalty-wait-period"; final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); - return property.getValue(); + if (property.isEnabled()) { + return property.getValue(); + } + return 0L; } @Override public Long retrieveGraceOnPenaltyPostingPeriod() { final String propertyName = "grace-on-penalty-posting"; final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); - return property.getValue(); + if (property.isEnabled()) { + return property.getValue(); + } + return 0L; } @Override @@ -548,4 +565,46 @@ public Long retriveMinimumDaysOfArrearsToWriteOff() { return property.getValue(); } + @Override + public Long retriveMinimumDaysInArrearsToSuspendLoanAccount() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData( + "Dias a partir de los cuales empezar a considerar suspendido"); + return property.getValue(); + } + + @Override + public Long retrieveInvoiceResolutionExpiryDays() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(INVOICE_RESOLUTION_EXPIRY); + if (property.isEnabled()) { + return property.getValue(); + } + + return null; + } + + @Override + public Long retrieveInvoiceThreshold() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(REMAINING_INVOICES_THRESHOLD); + if (property.isEnabled()) { + return property.getValue(); + } + return null; + } + + @Override + public List retrieveInvoiceJobNotificationEmails() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(INVOICE_NOTIFICATION_EMAILS); + if (property.isEnabled()) { + return List.of(property.getStringValue().split(",")); + } + return List.of(); + } + + @Override + public Integer retriveIvaConfiguration() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData("IVA Por comision"); + int value = property.getValue().intValue(); + return value; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java index d1655e8d264..88fa93d2f9b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java @@ -45,6 +45,9 @@ public class ExternalServicesPropertiesCommandFromApiJsonDeserializer { .getAllValues(); private static final Set MASIVIAN_SERVICE_SUPPORTED_PARAMETERS = ExternalServicesConstants.MasivianJSONinputParams .getAllValues(); + + private static final Set COLLECTION_HOUSE_HISTORY_PROVIDER_SUPPORTED_PARAMETERS = ExternalServicesConstants.CollectionHouseHistoryJSONinputParams + .getAllValues(); private final FromJsonHelper fromApiJsonHelper; @Autowired @@ -69,6 +72,8 @@ public void validateForUpdate(final String json, final String externalServiceNam CUSTOM_CHARGE_HONORARIO_PROVIDER_SUPPORTED_PARAMETERS); case "MASIVIAN_SERVICE" -> this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, MASIVIAN_SERVICE_SUPPORTED_PARAMETERS); + case "COLLECTION_HOUSE_HISTORY_PROVIDER" -> this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + COLLECTION_HOUSE_HISTORY_PROVIDER_SUPPORTED_PARAMETERS); default -> throw new ExternalServiceConfigurationNotFoundException(externalServiceName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServiceWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServiceWritePlatformServiceJpaRepositoryImpl.java index db3a51607fb..df8dbe54ccb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServiceWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServiceWritePlatformServiceJpaRepositoryImpl.java @@ -56,9 +56,11 @@ public CommandProcessingResult updateExternalServicesProperties(String externalS // TODO Auto-generated method stub this.context.authenticatedUser(); this.fromApiJsonDeserializer.validateForUpdate(command.json(), externalServiceName); + System.out.println("name " + externalServiceName); Set keyName = this.fromApiJsonDeserializer.getNameKeys(command.json()); ExternalServicesData external = this.readPlatformService.getExternalServiceDetailsByServiceName(externalServiceName); Long externalServiceId = external.getId(); + System.out.println("id " + external.getId()); Iterator it = keyName.iterator(); Map changesList = new LinkedHashMap<>(); while (it.hasNext()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java index a5493b3b256..114190a9493 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java @@ -32,6 +32,8 @@ private ExternalServicesConstants() { public static final String CUSTOM_CHARGE_HONORARIO_URL = "URL"; public static final String CUSTOM_CHARGE_HONORARIO_API_KEY = "API_KEY"; + public static final String COLLECTION_HOUSE_HISTORY_PROVIDER_SERVICE_NAME = "COLLECTION_HOUSE_HISTORY_PROVIDER"; + public static final String S3_SERVICE_NAME = "S3"; public static final String S3_BUCKET_NAME = "s3_bucket_name"; public static final String S3_ACCESS_KEY = "s3_access_key"; @@ -253,6 +255,38 @@ public String getValue() { } } + public enum CollectionHouseHistoryJSONinputParams { + + COLLECTION_HOUSE_URL("COLLECTION_HOUSE_URL"), COLLECTION_HOUSE_API_KEY("COLLECTION_HOUSE_API_KEY"); + + private final String value; + + CollectionHouseHistoryJSONinputParams(final String value) { + this.value = value; + } + + private static final Set values = new HashSet<>(); + + static { + for (final CollectionHouseHistoryJSONinputParams type : CollectionHouseHistoryJSONinputParams.values()) { + values.add(type.value); + } + } + + public static Set getAllValues() { + return values; + } + + @Override + public String toString() { + return name().toString().replaceAll("_", " "); + } + + public String getValue() { + return this.value; + } + } + @Getter public enum MasivianJSONinputParams { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java index 056e69ccd37..729c2d83e9a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java @@ -225,6 +225,10 @@ public Collection retrieveOne(String serviceName serviceNameToUse = ExternalServicesConstants.MASIVIAN_SERVICE_NAME; break; + case "COLLECTION_HOUSE_HISTORY_PROVIDER": + serviceNameToUse = ExternalServicesConstants.COLLECTION_HOUSE_HISTORY_PROVIDER_SERVICE_NAME; + break; + default: throw new ExternalServiceConfigurationNotFoundException(serviceName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java index 7ccd3d4c685..89d161130af 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java @@ -64,6 +64,10 @@ public ExternalServicesData getExternalServiceDetailsByServiceName(String servic serviceNameToUse = ExternalServicesConstants.MASIVIAN_SERVICE_NAME; break; + case "COLLECTION_HOUSE_HISTORY_PROVIDER": + serviceNameToUse = ExternalServicesConstants.COLLECTION_HOUSE_HISTORY_PROVIDER_SERVICE_NAME; + break; + default: throw new ExternalServiceConfigurationNotFoundException(serviceName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/GmailBackedPlatformEmailService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/GmailBackedPlatformEmailService.java index fd1018c3f82..14ec6e48796 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/GmailBackedPlatformEmailService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/GmailBackedPlatformEmailService.java @@ -70,7 +70,7 @@ public void sendDefinedEmail(EmailDetail emailDetails) { Properties props = mailSender.getJavaMailProperties(); props.put("mail.transport.protocol", "smtp"); - props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.auth", "false"); props.put("mail.debug", "true"); // these are the added lines diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanInvoiceGenerationPostBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanInvoiceGenerationPostBusinessEvent.java new file mode 100644 index 00000000000..a1fce8f94f7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanInvoiceGenerationPostBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.loan.transaction; + +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanInvoiceGenerationPostBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanInvoiceGenerationPostBusinessEvent"; + + public LoanInvoiceGenerationPostBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/service/ReportMailingJobEmailServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/service/ReportMailingJobEmailServiceImpl.java index 374a614ccfa..063ad0ca984 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/service/ReportMailingJobEmailServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/service/ReportMailingJobEmailServiceImpl.java @@ -91,8 +91,8 @@ public void sendEmailWithAttachment(ReportMailingJobEmailData reportMailingJobEm private Properties getJavaMailProperties(Collection reportMailingJobConfigurationDataCollection) { Properties properties = new Properties(); - properties.setProperty("mail.smtp.auth", "true"); - properties.setProperty("mail.smtp.starttls.enable", "true"); + properties.setProperty("mail.smtp.auth", "false"); + properties.setProperty("mail.smtp.starttls.enable", "false"); properties.setProperty("mail.smtp.ssl.trust", this.getGmailSmtpServer(reportMailingJobConfigurationDataCollection)); return properties; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/AccountSummaryCollectionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/AccountSummaryCollectionData.java index 430de30faa7..67d4cf6d1f7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/AccountSummaryCollectionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/AccountSummaryCollectionData.java @@ -103,4 +103,8 @@ private Collection defaultGuarantorAccountsIfEmpty( } return returnCollection; } + + public Collection getLoanAccounts() { + return loanAccounts; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/LoanAccountSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/LoanAccountSummaryData.java index 9b0399d5eb9..0acced6c27c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/LoanAccountSummaryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/data/LoanAccountSummaryData.java @@ -49,14 +49,16 @@ public class LoanAccountSummaryData { private final BigDecimal loanBalance; private final BigDecimal amountPaid; private final BlockingReasonsData blockStatus; + private final String puntoDeVenta; public LoanAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus, final EnumOptionData loanType, final Integer loanCycle, final LoanApplicationTimelineData timeline, final Boolean inArrears, - final BigDecimal originalLoan, final BigDecimal loanBalance, final BigDecimal amountPaid, - final BlockingReasonsData blockStatus) { + final BigDecimal originalLoan, final BigDecimal loanBalance, final BigDecimal amountPaid, final BlockingReasonsData blockStatus, + String puntoDeVenta) { this.id = id; this.accountNo = accountNo; + this.puntoDeVenta = puntoDeVenta; this.parentAccountNumber = null; this.externalId = externalId; this.productId = productId; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java index b46b5fc4c05..f648d97b5fa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java @@ -457,29 +457,22 @@ public String loanAccountSummarySchema() { final StringBuilder accountsSummary = new StringBuilder("l.id as id, l.account_no as accountNo, l.external_id as externalId,"); accountsSummary.append(" l.product_id as productId, lp.name as productName, lp.short_name as shortProductName,") .append(" l.loan_status_id as statusId, l.loan_type_enum as loanType,") - - .append(" glim.account_number as parentAccountNumber,") - - .append("l.principal_disbursed_derived as originalLoan,").append("l.total_outstanding_derived as loanBalance,") - .append("l.total_repayment_derived as amountPaid,") - - .append(" l.loan_product_counter as loanCycle,") - - .append(" l.submittedon_date as submittedOnDate,") + .append(" glim.account_number as parentAccountNumber,").append(""" + pos.name AS point_of_sales_name, + pos.code AS point_of_sales_code, + pos.client_ally_id AS allyId, + """).append("l.principal_disbursed_derived as originalLoan,") + .append("l.total_outstanding_derived as loanBalance,").append("l.total_repayment_derived as amountPaid,") + .append(" l.loan_product_counter as loanCycle,").append(" l.submittedon_date as submittedOnDate,") .append(" sbu.username as submittedByUsername, sbu.firstname as submittedByFirstname, sbu.lastname as submittedByLastname,") - .append(" l.rejectedon_date as rejectedOnDate,") .append(" rbu.username as rejectedByUsername, rbu.firstname as rejectedByFirstname, rbu.lastname as rejectedByLastname,") - .append(" l.withdrawnon_date as withdrawnOnDate,") .append(" wbu.username as withdrawnByUsername, wbu.firstname as withdrawnByFirstname, wbu.lastname as withdrawnByLastname,") - .append(" l.approvedon_date as approvedOnDate,") .append(" abu.username as approvedByUsername, abu.firstname as approvedByFirstname, abu.lastname as approvedByLastname,") - .append(" l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate,") .append(" dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname,") - .append(" l.closedon_date as closedOnDate,") .append(" cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,") .append(" la.overdue_since_date_derived as overdueSinceDate, ") @@ -497,7 +490,19 @@ public String loanAccountSummarySchema() { .append(" left join m_appuser cobu on cobu.id = l.charged_off_by_userid") .append(" left join m_loan_arrears_aging la on la.loan_id = l.id") .append(" left join glim_accounts glim on glim.id=l.glim_id") - .append(" left join m_blocking_reason_setting brs on brs.id = l.block_status_id "); + .append(" left join m_blocking_reason_setting brs on brs.id = l.block_status_id ").append(""" + LEFT JOIN ( + SELECT + ps.name, + ps.code, + cbp.loan_id, + ps.client_ally_id + FROM + custom.c_client_ally_point_of_sales ps + JOIN custom.c_client_buy_process cbp ON cbp.point_if_sales_id = ps.id + JOIN custom.c_client_ally cca on cca.id = ps.client_ally_id + ) pos ON pos.loan_id = l.id + """); return accountsSummary.toString(); } @@ -574,6 +579,8 @@ public LoanAccountSummaryData mapRow(final ResultSet rs, @SuppressWarnings("unus blockStatusData = BlockingReasonsData.builder().id(rs.getLong("blockStatusId")).nameOfReason(blockStatusName) .level(rs.getString("blockStatusLevel")).priority(JdbcSupport.getInteger(rs, "blockStatusPriority")).build(); } + final String pointOfSalesName = rs.getString("point_of_sales_name"); + final String pointOfSalesCode = rs.getString("point_of_sales_code"); final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername, submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, rejectedByLastname, @@ -585,7 +592,7 @@ public LoanAccountSummaryData mapRow(final ResultSet rs, @SuppressWarnings("unus return new LoanAccountSummaryData(id, accountNo, parentAccountNumber, externalId, productId, loanProductName, shortLoanProductName, loanStatus, loanType, loanCycle, timeline, inArrears, originalLoan, loanBalance, amountPaid, - blockStatusData); + blockStatusData, pointOfSalesName); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientAdditionalFieldsData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientAdditionalFieldsData.java index a8bd128f53a..9f47f30a0f8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientAdditionalFieldsData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientAdditionalFieldsData.java @@ -43,6 +43,7 @@ public class ClientAdditionalFieldsData { private String clientName; private Integer legalForm; private BigDecimal totalOutstandingPrincipalAmount; + private BigDecimal otherTotalOutstandingPrincipalAmount; public boolean isPerson() { LegalForm legalForm = LegalForm.fromInt(this.legalForm); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientAdditionalFieldsMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientAdditionalFieldsMapper.java index bf73520e87e..21e69d0c9fe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientAdditionalFieldsMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientAdditionalFieldsMapper.java @@ -44,7 +44,8 @@ public String schema() { COALESCE(ccp."Cupo otros prestamos", cce."Cupo otros prestamos") AS otherLoansCupo, mc.legal_form_enum AS legalForm, mc.blocking_reason_id AS blockingReasonId, - (select COALESCE(SUM(ml.principal_outstanding_derived), 0) FROM m_loan ml WHERE ml.loan_status_id = 300 and client_id =mc.id) AS totalOutstandingPrincipalAmount + (SELECT COALESCE(SUM(ml.principal_outstanding_derived), 0) FROM m_loan ml INNER JOIN m_product_loan mpl ON mpl.id = ml.product_id WHERE ml.loan_status_id = 300 and ml.client_id = mc.id AND mpl.use_other_loans_cupo = FALSE) AS totalOutstandingPrincipalAmount, + (SELECT COALESCE(SUM(ml.principal_outstanding_derived), 0) FROM m_loan ml INNER JOIN m_product_loan mpl ON mpl.id = ml.product_id WHERE ml.loan_status_id = 300 and ml.client_id = mc.id AND mpl.use_other_loans_cupo = TRUE) AS otherTotalOutstandingPrincipalAmount FROM m_client mc LEFT JOIN campos_cliente_empresas cce ON cce.client_id = mc.id LEFT JOIN m_code_value tipo ON tipo.id = cce."Tipo ID_cd_Tipo ID" @@ -63,6 +64,7 @@ public ClientAdditionalFieldsData mapRow(@NotNull ResultSet rs, int rowNum) thro final int statusInt = rs.getInt("status"); final Long blockingReasonId = JdbcSupport.getLong(rs, "blockingReasonId"); final BigDecimal totalOutstandingPrincipalAmount = rs.getBigDecimal("totalOutstandingPrincipalAmount"); + final BigDecimal otherTotalOutstandingPrincipalAmount = rs.getBigDecimal("otherTotalOutstandingPrincipalAmount"); EnumOptionData statusData; if (blockingReasonId != null) { statusData = ClientEnumerations.status(ClientStatus.BLOCKED); @@ -72,6 +74,6 @@ public ClientAdditionalFieldsData mapRow(@NotNull ResultSet rs, int rowNum) thro final String clientName = rs.getString("clientName"); final Integer legalForm = JdbcSupport.getInteger(rs, "legalForm"); return new ClientAdditionalFieldsData(clientId, tipo, nit, cedula, cupo, otherLoansCupo, statusData, clientName, legalForm, - totalOutstandingPrincipalAmount); + totalOutstandingPrincipalAmount, otherTotalOutstandingPrincipalAmount); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 76211e83000..3e43f244008 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -1079,7 +1079,7 @@ public CommandProcessingResult blockClient(final Long clientId, final JsonComman || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { if (loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, - loanCharge, 0, incident, loan.getClosedOnDate(), BigDecimal.ZERO); + loanCharge, 0, incident, blockedOnDate, BigDecimal.ZERO); this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseApiResource.java new file mode 100644 index 00000000000..d4d6c40c2e2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseApiResource.java @@ -0,0 +1,79 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseConfigParameterizationData; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseReadWriteService; +import org.springframework.stereotype.Component; + +@Path("/v1/collectionhousemanagement") +@Component +@Tag(name = "Collection House Management", description = "Product configuration for Collection House Management") +@RequiredArgsConstructor +public class CollectionHouseApiResource { + + private static final String COLLECTION_HOUSE_PERMISSIONS = "COLLECTION_HOUSE"; + private final PlatformSecurityContext context; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final CollectionHouseReadWriteService collectionHouseReadWriteService; + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAll(@Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(COLLECTION_HOUSE_PERMISSIONS); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + final Collection collectionHouseConfigParameterizationData = collectionHouseReadWriteService + .retrieveAllCollectionHouseManagement(); + return this.toApiJsonSerializer.serialize(settings, collectionHouseConfigParameterizationData); + } + + @GET + @Path("{collectionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveOneCollectionHouse(@PathParam("collectionId") final Long collectionId, @Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(COLLECTION_HOUSE_PERMISSIONS); + final CollectionHouseConfigParameterizationData collectionHouseConfigParameterizationData = collectionHouseReadWriteService + .retrieveCollectionHouse(collectionId); + return this.toApiJsonSerializer.serialize(collectionHouseConfigParameterizationData); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createCollectionHouse(final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().createCollectionHouse().withJson(apiRequestBodyAsJson).build(); + final CommandProcessingResult commandProcessingResult = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(commandProcessingResult); + } + + @PUT + @Path("{collectionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String UpdateCollectionHouse(@PathParam("collectionId") final Long collectionId, final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCollectionHouse(collectionId).withJson(apiRequestBodyAsJson) + .build(); + final CommandProcessingResult commandProcessingResult = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(commandProcessingResult); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseHistoryApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseHistoryApiResource.java new file mode 100644 index 00000000000..b717e51a752 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/api/CollectionHouseHistoryApiResource.java @@ -0,0 +1,53 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.api; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.custom.portfolio.externalcharge.honoratio.data.CustomChargeHonorarioMapData; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/v1/collectionhousehistory") +@Component +@Scope("singleton") +public class CollectionHouseHistoryApiResource { + + private static final String COLLECTION_HOUSE_PERMISSIONS = "COLLECTION_HOUSE"; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final PlatformSecurityContext context; + private final ApiRequestParameterHelper apiRequestParameterHelper; + + @Autowired + public CollectionHouseHistoryApiResource(DefaultToApiJsonSerializer toApiJsonSerializer, + PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, PlatformSecurityContext context, + ApiRequestParameterHelper apiRequestParameterHelper) { + this.toApiJsonSerializer = toApiJsonSerializer; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + this.context = context; + this.apiRequestParameterHelper = apiRequestParameterHelper; + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createCollectionHouse(final String apiRequestBodyAsJson) { + this.context.authenticatedUser().validateHasReadPermission(COLLECTION_HOUSE_PERMISSIONS); + final CommandWrapper commandRequest = new CommandWrapperBuilder().createCollectionHouseHistory().withJson(apiRequestBodyAsJson) + .build(); + final CommandProcessingResult commandProcessingResult = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(commandProcessingResult); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigParameterizationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigParameterizationData.java new file mode 100644 index 00000000000..6b1467fe2a8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigParameterizationData.java @@ -0,0 +1,19 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(staticName = "instance") +public class CollectionHouseConfigParameterizationData { + + private Long id; + private String collectionName; + private String collectionNit; + private String collectionCode; + private Integer collectionVerificationCode; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigValidator.java new file mode 100644 index 00000000000..86c1fecc37d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseConfigValidator.java @@ -0,0 +1,63 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.data; + +import com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CollectionHouseConfigValidator { + + private final FromJsonHelper fromApiJsonHelper; + private final ConfigurationReadPlatformService configurationReadPlatformService; + private static final String COLLECTIONNAME = "collectionName"; + private static final String COLLECTIONNIT = "collectionNit"; + private static final String COLLECTIONCODE = "collectionCode"; + private static final String COLLECTIONVERIFICATIONCODE = "collectionVerificationCode"; + + public void validateForCreate(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final List dataValidationErrors = new ArrayList<>(); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("collectionhousemanagement"); + + String collectionName = this.fromApiJsonHelper.extractStringNamed(COLLECTIONNAME, element); + baseDataValidator.reset().parameter(COLLECTIONNAME).value(collectionName).notBlank(); + + String collectionNit = this.fromApiJsonHelper.extractStringNamed(COLLECTIONNIT, element); + baseDataValidator.reset().parameter(COLLECTIONNIT).value(collectionNit).notBlank(); + + String collectionCode = this.fromApiJsonHelper.extractStringNamed(COLLECTIONCODE, element); + baseDataValidator.reset().parameter(COLLECTIONCODE).value(collectionCode).notBlank(); + + String collectionVerificationCode = this.fromApiJsonHelper.extractStringNamed(COLLECTIONVERIFICATIONCODE, element); + baseDataValidator.reset().parameter(COLLECTIONVERIFICATIONCODE).value(collectionVerificationCode).notBlank(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + // + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public void validateForUpdate(final String json) { + validateForCreate(json); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseHistoryValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseHistoryValidator.java new file mode 100644 index 00000000000..3af31244deb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/data/CollectionHouseHistoryValidator.java @@ -0,0 +1,74 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.data; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.*; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CollectionHouseHistoryValidator { + + private final FromJsonHelper fromApiJsonHelper; + private final ConfigurationReadPlatformService configurationReadPlatformService; + + private static final String COLLECTIONNIT = "nit"; + private static final String COLLECTIONCODE = "collectionHouseCode"; + private static final String CLIENTACCOUNTNO = "clientAccountNo"; + + public void validateForCreateCollectionHouse(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final List dataValidationErrors = new ArrayList<>(); + final JsonElement element = this.fromApiJsonHelper.parse(json); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("collectionhousemanagement"); + final Set supportedParameters = new HashSet<>(Arrays.asList(COLLECTIONCODE, COLLECTIONNIT, CLIENTACCOUNTNO)); + if (this.fromApiJsonHelper.parameterExists("collectionHouseUpdates", element)) { + final JsonObject collectionHouseHistorylJsonElement = element.getAsJsonObject(); + final JsonArray array = collectionHouseHistorylJsonElement.get("collectionHouseUpdates").getAsJsonArray(); + for (int i = 1; i <= array.size(); i++) { + final Type arrayObjectParameterTypeOfMap = new TypeToken>() {}.getType(); + + final JsonObject collectionHouseHistoryElement = array.get(i - 1).getAsJsonObject(); + final String arrayObjectJson = this.fromApiJsonHelper.toJson(collectionHouseHistoryElement); + this.fromApiJsonHelper.checkForUnsupportedParameters(arrayObjectParameterTypeOfMap, arrayObjectJson, supportedParameters); + + String collectionNit = this.fromApiJsonHelper.extractStringNamed(COLLECTIONNIT, collectionHouseHistoryElement); + baseDataValidator.reset().parameter(COLLECTIONNIT).parameterAtIndexArray(COLLECTIONNIT, i).value(collectionNit).notBlank(); + + String collectionCode = this.fromApiJsonHelper.extractStringNamed(COLLECTIONCODE, collectionHouseHistoryElement); + baseDataValidator.reset().parameter(COLLECTIONCODE).parameterAtIndexArray(COLLECTIONCODE, i).value(collectionCode) + .notBlank(); + + String clientAccountNo = this.fromApiJsonHelper.extractStringNamed(CLIENTACCOUNTNO, collectionHouseHistoryElement); + baseDataValidator.reset().parameter(CLIENTACCOUNTNO).parameterAtIndexArray(CLIENTACCOUNTNO, i).value(clientAccountNo) + .notBlank(); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public void validateForUpdate(final String json) { + validateForCreateCollectionHouse(json); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfigRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfigRepository.java new file mode 100644 index 00000000000..7738d330f4d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfigRepository.java @@ -0,0 +1,9 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface CollectionHouseConfigRepository + extends JpaRepository, JpaSpecificationExecutor { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfiguration.java new file mode 100644 index 00000000000..019dbbe20b8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseConfiguration.java @@ -0,0 +1,45 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseConfigParameterizationData; + +@Entity +@Table(name = "m_collection_house_configuration") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(staticName = "instance") +public class CollectionHouseConfiguration extends AbstractAuditableWithUTCDateTimeCustom { + + @Column(name = "collection_name") + private String collectionName; + + @Column(name = "collection_nit") + private String collectionNit; + + @Column(name = "collection_code") + private String collectionCode; + + @Column(name = "collection_verification_code") + private Integer collectionVerificationCode; + + public CollectionHouseConfigParameterizationData toData() { + return CollectionHouseConfigParameterizationData.instance(getId(), this.collectionName, this.collectionNit, this.collectionCode, + this.collectionVerificationCode); + } + + public static CollectionHouseConfiguration createNewCollectionHouse(JsonCommand jsonCommand) { + String collectionName = jsonCommand.stringValueOfParameterNamed("collectionName"); + String collectionNit = jsonCommand.stringValueOfParameterNamed("collectionNit"); + String collectionCode = jsonCommand.stringValueOfParameterNamed("collectionCode"); + Integer collectionVerificationCode = jsonCommand.integerValueOfParameterNamed("collectionVerificationCode"); + return new CollectionHouseConfiguration(collectionName, collectionNit, collectionCode, collectionVerificationCode); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseHistoryRepository.java new file mode 100644 index 00000000000..d98719dc07a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/CollectionHouseHistoryRepository.java @@ -0,0 +1,14 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CollectionHouseHistoryRepository + extends JpaRepository, JpaSpecificationExecutor { + + @Query("select colletionHouseHistory from ColletionHouseHistory colletionHouseHistory where colletionHouseHistory.clientAccountNumber = :clientAccountNumber") + Optional getCollectionHouseHistoryByAccountNo(@Param("clientAccountNumber") String clientAccountNumber); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/ColletionHouseHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/ColletionHouseHistory.java new file mode 100644 index 00000000000..effc1e8e51d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/domain/ColletionHouseHistory.java @@ -0,0 +1,29 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Entity +@Table(name = "m_collection_house_history") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(staticName = "instance") +public class ColletionHouseHistory extends AbstractAuditableWithUTCDateTimeCustom { + + @Column(name = "client_account_number") + private String clientAccountNumber; + + @Column(name = "collection_nit") + private String collectionNit; + + @Column(name = "collection_house_code") + private String collectionCode; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/exception/CollectionHouseManagementNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/exception/CollectionHouseManagementNotFoundException.java new file mode 100644 index 00000000000..707e6d11c90 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/exception/CollectionHouseManagementNotFoundException.java @@ -0,0 +1,10 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class CollectionHouseManagementNotFoundException extends AbstractPlatformResourceNotFoundException { + + public CollectionHouseManagementNotFoundException(Long id) { + super("error.msg.collectionhouse.id.invalid", "Collection House with identifier " + id + " does not exist", id); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CollectionHouseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CollectionHouseCommandHandler.java new file mode 100644 index 00000000000..887dd80bd38 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CollectionHouseCommandHandler.java @@ -0,0 +1,29 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseHistoryReadWriteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "COLLECTIONHOUSEHISTORY", action = "CREATE") +public class CollectionHouseCommandHandler implements NewCommandSourceHandler { + + private final CollectionHouseHistoryReadWriteService collectionHouseHistoryReadWriteService; + + @Autowired + public CollectionHouseCommandHandler(CollectionHouseHistoryReadWriteService collectionHouseHistoryReadWriteService) { + this.collectionHouseHistoryReadWriteService = collectionHouseHistoryReadWriteService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.collectionHouseHistoryReadWriteService.createCollectionHouseHistory(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CreateCollectionHouseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CreateCollectionHouseCommandHandler.java new file mode 100644 index 00000000000..cca89f1e0da --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/CreateCollectionHouseCommandHandler.java @@ -0,0 +1,29 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseReadWriteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "PRODUCTCOLLECTIONHOUSE", action = "CREATE") +public class CreateCollectionHouseCommandHandler implements NewCommandSourceHandler { + + private final CollectionHouseReadWriteService collectionHouseConfigReadWriteServices; + + @Autowired + public CreateCollectionHouseCommandHandler(final CollectionHouseReadWriteService collectionHouseConfigReadWriteServices) { + this.collectionHouseConfigReadWriteServices = collectionHouseConfigReadWriteServices; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.collectionHouseConfigReadWriteServices.createCollectionHouseConfig(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/UpdateCollectionHouseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/UpdateCollectionHouseCommandHandler.java new file mode 100644 index 00000000000..80d1dfd5f8b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/handler/UpdateCollectionHouseCommandHandler.java @@ -0,0 +1,29 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseReadWriteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "PRODUCTCOLLECTIONHOUSE", action = "UPDATE") +public class UpdateCollectionHouseCommandHandler implements NewCommandSourceHandler { + + private final CollectionHouseReadWriteService collectionHouseConfigReadWriteServices; + + @Autowired + public UpdateCollectionHouseCommandHandler(final CollectionHouseReadWriteService collectionHouseConfigReadWriteServices) { + this.collectionHouseConfigReadWriteServices = collectionHouseConfigReadWriteServices; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.collectionHouseConfigReadWriteServices.updateCollectionHouseConfig(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseConfigTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseConfigTasklet.java new file mode 100644 index 00000000000..480e70346ff --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseConfigTasklet.java @@ -0,0 +1,61 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.jobs; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.ColletionHouseHistory; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseHistoryReadWriteService; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +@Slf4j +public class CollectionHouseConfigTasklet implements Tasklet { + + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + private final CollectionHouseHistoryReadWriteService collectionHouseHistoryReadWriteService; + + public CollectionHouseConfigTasklet(PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, + CollectionHouseHistoryReadWriteService collectionHouseHistoryReadWriteService) { + this.collectionHouseHistoryReadWriteService = collectionHouseHistoryReadWriteService; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + } + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + try { + // Fetch all collection house history records + List collectionHouseHistoryList = collectionHouseHistoryReadWriteService.findAllCollectionHouseHistory(); + + JsonArray updatesArray = new JsonArray(); + for (ColletionHouseHistory colletionHouseHistory : collectionHouseHistoryList) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("clientAccountNo", colletionHouseHistory.getClientAccountNumber()); + jsonObject.addProperty("nit", colletionHouseHistory.getCollectionNit()); + jsonObject.addProperty("collectionHouseCode", colletionHouseHistory.getCollectionCode()); + updatesArray.add(jsonObject); + } + + JsonObject jsonCommandData = new JsonObject(); + jsonCommandData.add("collectionHouseUpdates", updatesArray); + + CommandWrapper commandRequest = new CommandWrapperBuilder().createCollectionHouseHistory() + + .withJson(jsonCommandData.toString()).build(); + + commandsSourceWritePlatformService.logCommandSource(commandRequest); + + } catch (Exception e) { + log.error("Error executing collection house config tasklet", e); + throw e; + } + + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseHistoryConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseHistoryConfig.java new file mode 100644 index 00000000000..c35e8fbc5d8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/jobs/CollectionHouseHistoryConfig.java @@ -0,0 +1,48 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.jobs; + +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.collectionhousemanagement.service.CollectionHouseHistoryReadWriteService; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class CollectionHouseHistoryConfig { + + @Autowired + private JobRepository jobRepository; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Autowired + private PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @Autowired + CollectionHouseHistoryReadWriteService collectionHouseHistoryReadWriteService; + + @Bean + protected Step collectionHouseHistoryMapStep() { + return new StepBuilder(JobName.COLLECTION_HOUSE_HISTORY.name(), jobRepository) + .tasklet(collectionHouseHistoryMapTasklet(), transactionManager).build(); + } + + @Bean + public Job collectionHouseHistoryMapJob() { + return new JobBuilder(JobName.COLLECTION_HOUSE_HISTORY.name(), jobRepository).start(collectionHouseHistoryMapStep()) + .incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public CollectionHouseConfigTasklet collectionHouseHistoryMapTasklet() { + return new CollectionHouseConfigTasklet(commandsSourceWritePlatformService, collectionHouseHistoryReadWriteService); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteService.java new file mode 100644 index 00000000000..96bedd84238 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteService.java @@ -0,0 +1,15 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.service; + +import java.util.List; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.ColletionHouseHistory; + +public interface CollectionHouseHistoryReadWriteService { + + CommandProcessingResult createCollectionHouseHistory(JsonCommand command); + + ColletionHouseHistory findCollectionHouseHistoryByAcctountNo(String accountNo); + + List findAllCollectionHouseHistory(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteServiceImpl.java new file mode 100644 index 00000000000..f7dda0a822e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseHistoryReadWriteServiceImpl.java @@ -0,0 +1,116 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.*; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.portfolio.accountdetails.data.AccountSummaryCollectionData; +import org.apache.fineract.portfolio.accountdetails.data.LoanAccountSummaryData; +import org.apache.fineract.portfolio.accountdetails.service.AccountDetailsReadPlatformService; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseHistoryValidator; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.CollectionHouseHistoryRepository; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.ColletionHouseHistory; +import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CollectionHouseHistoryReadWriteServiceImpl implements CollectionHouseHistoryReadWriteService { + + private final CollectionHouseHistoryValidator collectionHouseHistoryValidator; + private final CollectionHouseHistoryRepository collectionHouseHistoryRepository; + private final AccountDetailsReadPlatformService accountDetailsReadPlatformService; + private final ClientRepositoryWrapper clientRepositoryWrapper; + + @Override + public CommandProcessingResult createCollectionHouseHistory(JsonCommand command) { + collectionHouseHistoryValidator.validateForCreateCollectionHouse(command.json()); + JsonArray updatesArray = command.arrayOfParameterNamed("collectionHouseUpdates"); + List> savedHistories = new ArrayList<>(); + + for (int i = 0; i < updatesArray.size(); i++) { + JsonObject updateObject = updatesArray.get(i).getAsJsonObject(); + String clientAccountNo = updateObject.get("clientAccountNo").getAsString(); + String nit = updateObject.get("nit").getAsString(); + String collectionHouseCode = updateObject.get("collectionHouseCode").getAsString(); + + try { + ColletionHouseHistory checkCollectionHouse = this.findCollectionHouseHistoryByAcctountNo(clientAccountNo); + Client client = clientRepositoryWrapper.getClientByAccountNumber(clientAccountNo); + Boolean hasLoanAccountsInArrears = false; + if (client != null) { + AccountSummaryCollectionData clientAccount = accountDetailsReadPlatformService + .retrieveClientAccountDetails(client.getId()); + Collection loanAccounts = clientAccount.getLoanAccounts(); + if (!loanAccounts.isEmpty()) { + for (LoanAccountSummaryData loanAccountSummaryData : loanAccounts) { + LoanStatusEnumData statusEnumData = loanAccountSummaryData.getStatus(); + Long status = statusEnumData.getId(); + if (loanAccountSummaryData.getInArrears() == true && LoanStatus.fromInt(status.intValue()).isActive()) { + hasLoanAccountsInArrears = true; + } + } + } else { + hasLoanAccountsInArrears = false; + } + } + + if (checkCollectionHouse == null && hasLoanAccountsInArrears) { + ColletionHouseHistory colletionHouseHistory = new ColletionHouseHistory(); + Map savedRecord = new HashMap<>(); + colletionHouseHistory.setClientAccountNumber(clientAccountNo); + colletionHouseHistory.setCollectionNit(nit); + colletionHouseHistory.setCollectionCode(collectionHouseCode); + collectionHouseHistoryRepository.saveAndFlush(colletionHouseHistory); + } else { + if (checkCollectionHouse != null && !hasLoanAccountsInArrears) { + collectionHouseHistoryRepository.delete(checkCollectionHouse); + } + } + + } catch (final JpaSystemException | DataIntegrityViolationException ex) { + throw new PlatformDataIntegrityException("error.msg.collectionHouseHistory.save.failed", + "Failed to save Collection House History for clientAccountNo: " + clientAccountNo, ex); + } + } + List colletionHouseHistories = this.findAllCollectionHouseHistory(); + savedHistories = colletionHouseHistories.stream().map(colletionHouseHistory -> { + Map map = new HashMap<>(); + map.put("clientAccountNumber", colletionHouseHistory.getClientAccountNumber()); + map.put("collectionHouseCode", colletionHouseHistory.getCollectionCode()); + map.put("collectionNit", colletionHouseHistory.getCollectionNit()); + return map; + }).collect(Collectors.toList()); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(command.entityId()) + .withCollectionHouse(savedHistories).build(); + } + + @Override + public ColletionHouseHistory findCollectionHouseHistoryByAcctountNo(String accountNo) { + Optional getColletionHouseHistory = collectionHouseHistoryRepository + .getCollectionHouseHistoryByAccountNo((accountNo)); + if (getColletionHouseHistory.isPresent()) { + ColletionHouseHistory colletionHouseHistory = getColletionHouseHistory.get(); + return colletionHouseHistory; + } + return null; + } + + @Override + public List findAllCollectionHouseHistory() { + List colletionHouseHistories = collectionHouseHistoryRepository.findAll(); + return colletionHouseHistories; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteService.java new file mode 100644 index 00000000000..2fb45b0db43 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteService.java @@ -0,0 +1,17 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.service; + +import java.util.Collection; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseConfigParameterizationData; + +public interface CollectionHouseReadWriteService { + + Collection retrieveAllCollectionHouseManagement(); + + CollectionHouseConfigParameterizationData retrieveCollectionHouse(Long collectionId); + + CommandProcessingResult createCollectionHouseConfig(JsonCommand command); + + CommandProcessingResult updateCollectionHouseConfig(Long Id, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteServiceImpl.java new file mode 100644 index 00000000000..be1f13fdc98 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionhousemanagement/service/CollectionHouseReadWriteServiceImpl.java @@ -0,0 +1,73 @@ +package org.apache.fineract.portfolio.collectionhousemanagement.service; + +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseConfigParameterizationData; +import org.apache.fineract.portfolio.collectionhousemanagement.data.CollectionHouseConfigValidator; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.CollectionHouseConfigRepository; +import org.apache.fineract.portfolio.collectionhousemanagement.domain.CollectionHouseConfiguration; +import org.apache.fineract.portfolio.collectionhousemanagement.exception.CollectionHouseManagementNotFoundException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CollectionHouseReadWriteServiceImpl implements CollectionHouseReadWriteService { + + private final CollectionHouseConfigRepository collectionHouseConfigRepository; + private final CollectionHouseConfigValidator collectionHouseConfigValidator; + + @Override + public Collection retrieveAllCollectionHouseManagement() { + return collectionHouseConfigRepository.findAll().stream().map(CollectionHouseConfiguration::toData).toList(); + } + + @Override + public CollectionHouseConfigParameterizationData retrieveCollectionHouse(Long collectionId) { + try { + return collectionHouseConfigRepository.getReferenceById(collectionId).toData(); + } catch (Exception e) { + throw new CollectionHouseManagementNotFoundException(collectionId); + } + } + + @Override + public CommandProcessingResult createCollectionHouseConfig(JsonCommand command) { + collectionHouseConfigValidator.validateForCreate(command.json()); + CollectionHouseConfiguration collectionHouseConfiguration = CollectionHouseConfiguration.createNewCollectionHouse(command); + CollectionHouseConfiguration saveCollectionHouse = collectionHouseConfigRepository.saveAndFlush(collectionHouseConfiguration); + try { + return CommandProcessingResult.commandOnlyResult(saveCollectionHouse.getId()); + } catch (final JpaSystemException | DataIntegrityViolationException dv) { + return CommandProcessingResult.empty(); + } + } + + @Override + public CommandProcessingResult updateCollectionHouseConfig(Long collectionId, JsonCommand command) { + collectionHouseConfigValidator.validateForUpdate(command.json()); + + try { + CollectionHouseConfiguration findcollectionHouseConfiguration = collectionHouseConfigRepository.getReferenceById(collectionId); + String collectionName = command.stringValueOfParameterNamed("collectionName"); + String colllectionNit = command.stringValueOfParameterNamed("collectionNit"); + String collectionCode = command.stringValueOfParameterNamed("collectionCode"); + Integer collectionVerificationCode = command.integerValueOfParameterNamed("collectionVerificationCode"); + findcollectionHouseConfiguration.setCollectionName(collectionName); + findcollectionHouseConfiguration.setCollectionNit(colllectionNit); + findcollectionHouseConfiguration.setCollectionCode(collectionCode); + findcollectionHouseConfiguration.setCollectionVerificationCode(collectionVerificationCode); + CollectionHouseConfiguration saveCollectionHouse = collectionHouseConfigRepository.save(findcollectionHouseConfiguration); + return CommandProcessingResult.commandOnlyResult(saveCollectionHouse.getId()); + } catch (Exception e) { + throw new CollectionHouseManagementNotFoundException(collectionId); + } + + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java index d4bd7d4108a..0908493915b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java @@ -30,5 +30,6 @@ private DelinquencyApiConstants() { public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays"; public static final String MAXIMUMAGEDAYS_PARAM_NAME = "maximumAgeDays"; public static final String LOCALE_PARAM_NAME = "locale"; + public static final String PERCENTAGEVALUE = "percentageValue"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java index 52a11cdddb0..349b4066a25 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java @@ -19,13 +19,10 @@ package org.apache.fineract.portfolio.delinquency.data; import java.io.Serializable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import lombok.*; @ToString -@AllArgsConstructor +@NoArgsConstructor @Getter @Setter public class DelinquencyRangeData implements Serializable { @@ -34,11 +31,33 @@ public class DelinquencyRangeData implements Serializable { private String classification; private Integer minimumAgeDays; private Integer maximumAgeDays; + private Integer percentageValue; + + public DelinquencyRangeData(Long id, String classification, Integer minimumAgeDays, Integer maximumAgeDays) { + this.id = id; + this.classification = classification; + this.minimumAgeDays = minimumAgeDays; + this.maximumAgeDays = maximumAgeDays; + this.percentageValue = null; + } + + public DelinquencyRangeData(Long id, String classification, Integer minimumAgeDays, Integer maximumAgeDays, Integer percentageValue) { + this.id = id; + this.classification = classification; + this.minimumAgeDays = minimumAgeDays; + this.maximumAgeDays = maximumAgeDays; + this.percentageValue = percentageValue; + } public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays) { return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays); } + public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays, + Integer percentageValue) { + return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays, percentageValue); + } + public static DelinquencyRangeData reference(Long id) { return new DelinquencyRangeData(id, "", 0, 0); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java index da8085600dd..7aadf5127c5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java @@ -21,9 +21,14 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface DelinquencyRangeRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByClassification(String classification); + @Query("select delinquencyRange from DelinquencyRange delinquencyRange where delinquencyRange.minimumAgeDays <= :range and delinquencyRange.maximumAgeDays >= :range") + Optional findByRange(@Param("range") Integer range); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java index f54a45d4f7b..a360f273a51 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java @@ -24,6 +24,7 @@ import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData; import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; @@ -47,4 +48,6 @@ public interface DelinquencyReadPlatformService { List retrieveLoanDelinquencyActions(Long loanId); + DelinquencyRange retrieveDelinquencyRangeCategeory(Integer range); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index e12ab385b80..0d7f4eb4732 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -225,4 +225,14 @@ public List retrieveLoanDelinquencyActions(Long loanId) { return List.of(); } + @Override + public DelinquencyRange retrieveDelinquencyRangeCategeory(Integer range) { + Optional delinquencyRange = repositoryRange.findByRange(range); + if (delinquencyRange.isPresent()) { + DelinquencyRange delinquencyRange1 = delinquencyRange.get(); + return delinquencyRange1; + } + return null; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 29451c169dd..86b77ffcd37 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -294,10 +294,11 @@ private DelinquencyRange createDelinquencyRange(DelinquencyRangeData data, Map data.getMaximumAgeDays()) { final String errorMessage = "The age days values are invalid, the maximum age days can't be lower than minimum age days"; - throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays()); + throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays(), + data.getPercentageValue()); } DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(), - data.getMaximumAgeDays()); + data.getMaximumAgeDays(), data.getPercentageValue()); return repositoryRange.saveAndFlush(newDelinquencyRange); } else { throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated", @@ -319,6 +320,10 @@ private DelinquencyRange updateDelinquencyRange(DelinquencyRange delinquencyRang delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays()); changes.put(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, data.getMaximumAgeDays()); } + if (!data.getPercentageValue().equals(delinquencyRange.getPercentageValue())) { + delinquencyRange.setPercentageValue(data.getPercentageValue()); + changes.put(DelinquencyApiConstants.PERCENTAGEVALUE, data.getPercentageValue()); + } if (!changes.isEmpty()) { delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java index e1030e46786..08bf6342027 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java @@ -56,7 +56,8 @@ private DelinquencyRangeData validateAndParseUpdate(final DataValidatorBuilder d jsonHelper.checkForUnsupportedParameters(element, List.of(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, - DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.LOCALE_PARAM_NAME)); + DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.LOCALE_PARAM_NAME, + DelinquencyApiConstants.PERCENTAGEVALUE)); final String localeValue = jsonHelper.extractStringNamed(DelinquencyApiConstants.LOCALE_PARAM_NAME, element); dataValidator.reset().parameter(DelinquencyApiConstants.LOCALE_PARAM_NAME).value(localeValue).notBlank(); @@ -65,13 +66,16 @@ private DelinquencyRangeData validateAndParseUpdate(final DataValidatorBuilder d final String classification = jsonHelper.extractStringNamed(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, element); final Integer minimumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, element, locale); final Integer maximumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, element, locale); + final Integer percentageValue = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.PERCENTAGEVALUE, element, locale); dataValidator.reset().parameter(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME).value(classification).notBlank(); dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME).value(minimumAge).notBlank() .integerGreaterThanNumber(0); dataValidator.reset().parameter(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME).value(maximumAge).ignoreIfNull() .integerGreaterThanNumber(0); + dataValidator.reset().parameter(DelinquencyApiConstants.PERCENTAGEVALUE).value(percentageValue).ignoreIfNull() + .integerGreaterThanNumber(0); - return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge); + return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge, percentageValue); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/ClasificacionConceptosApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/ClasificacionConceptosApiResource.java new file mode 100644 index 00000000000..3b7b14fd142 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/ClasificacionConceptosApiResource.java @@ -0,0 +1,121 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.api; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.loanaccount.invoice.data.ClasificacionConceptosData; +import org.apache.fineract.portfolio.loanaccount.service.ClasificacionConceptosService; +import org.springframework.stereotype.Component; + +@Path("/v1/clasificacionconceptos") +@Component +@Tag(name = "Clasificacion Conceptos", description = "") +@RequiredArgsConstructor +public class ClasificacionConceptosApiResource { + + private final PlatformSecurityContext context; + private final ClasificacionConceptosService clasificacionConceptosService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String getClasificacionConceptos(@Context final UriInfo uriInfo) { + + final List clasificacionConceptosDataList = this.clasificacionConceptosService.retrieveAll(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, clasificacionConceptosDataList); + } + + @GET + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String getClasificacionConcepto(@PathParam("id") final Long id, @Context final UriInfo uriInfo) { + + final ClasificacionConceptosData clasificacionConceptosData = this.clasificacionConceptosService.retrieveOne(id); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, clasificacionConceptosData); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String ClasificacionConcepto(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().createClasificacionConcepto().withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @PUT + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateClasificacionConcepto(@PathParam("id") final Long id, final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateClasificacionConcepto(id).withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String deleteClasificacionConcepto(@PathParam("id") final Long id) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteClasificacionConcepto(id).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index a214d82d961..21d3b9c2ffb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -569,7 +569,15 @@ private String retrieveTransactionTemplate(Long loanId, String loanExternalIdStr } else { transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); } - transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(resolvedLoanId, transactionDate); + transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(resolvedLoanId, transactionDate, false); + } else if (CommandParameterUtil.is(commandParam, "anulado")) { + LocalDate transactionDate; + if (transactionDateParam == null) { + transactionDate = DateUtils.getBusinessLocalDate(); + } else { + transactionDate = transactionDateParam.getDate("transactionDate", dateFormat, locale); + } + transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(resolvedLoanId, transactionDate, true); } else if (CommandParameterUtil.is(commandParam, "special-write-off")) { transactionData = this.loanReadPlatformService.retrieveLoanSpecialWriteOffTemplate(resolvedLoanId); } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 29a252a1fb5..37beb90a6ae 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -140,6 +140,7 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanBlockReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanDebtProjectionService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; @@ -288,6 +289,7 @@ public class LoansApiResource { private final DefaultToApiJsonSerializer jsonSerializerTagHistory; private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final LoanBlockReadPlatformService loanBlockingReasonReadPlatformService; + private final LoanDebtProjectionService loanDebtProjectionService; @Autowired private SpringTemplateEngine templateEngine; @@ -895,7 +897,6 @@ public void getScheduleReportPDF(@Context HttpServletResponse response, LoanAccountData loanBasicDetails = this.loanReadPlatformService.retrieveOne(loanId); LoanScheduleData repaymentSchedule = null; Collection disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(loanId); - ; final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = loanBasicDetails.getTimeline().repaymentScheduleRelatedData( loanBasicDetails.getCurrency(), loanBasicDetails.getPrincipal(), loanBasicDetails.getApprovedPrincipal(), loanBasicDetails.getInArrearsTolerance(), loanBasicDetails.getFeeChargesAtDisbursementCharged()); @@ -1493,4 +1494,21 @@ public String excludeLoanFromReclaim(@PathParam("loanId") @Parameter(description return toApiJsonSerializer.serialize(result); } + + @GET() + @Path("/{loanId}/projections") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveDebtProjection(@PathParam("loanId") final Long loanId, @QueryParam("projectionDate") final String projectionDate, + @QueryParam("dateFormat") final String dateFormat, @Context final UriInfo uriInfo) { + + // Verify user has permissions + this.context.authenticatedUser(); + + // Calculate projection + LoanDebtProjectionData projection = this.loanDebtProjectionService.calculateDebtProjection(loanId, projectionDate, dateFormat); + + return this.toApiJsonSerializer.serialize(projection); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DefaultOrCancelInsuranceInstallmentData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DefaultOrCancelInsuranceInstallmentData.java index 7b854d0ecca..fcd8ea769da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DefaultOrCancelInsuranceInstallmentData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DefaultOrCancelInsuranceInstallmentData.java @@ -1,5 +1,6 @@ package org.apache.fineract.portfolio.loanaccount.data; +import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,6 +13,7 @@ public class DefaultOrCancelInsuranceInstallmentData { private Long loanId; private Long loanChargeId; private Integer installment; + private LocalDate suspensionDate; public Long loanId() { return loanId; @@ -36,4 +38,12 @@ public Integer installment() { public void setInstallment(Integer installment) { this.installment = installment; } + + public LocalDate suspensionDate() { + return suspensionDate; + } + + public void setSuspensionDate(LocalDate suspensionDate) { + this.suspensionDate = suspensionDate; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 8cebbe5c1d0..a99fc280c69 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -289,6 +289,12 @@ public class LoanAccountData { private BigDecimal valorGiro; private Boolean rediferir; + private String nit; + private String code; + private String numeroCredito; + private String cedula; + private boolean isMigratedLoan; + public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, Integer repaymentEvery, EnumOptionData repaidEveryFrequencyEnums, Integer loanTermFrequency, @@ -297,7 +303,8 @@ public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEn BigDecimal inArrearsTolerance, String transactionProcessingStrategyCode, Integer graceOnPrincipalPayment, Integer graceOnInterestPayment, Integer graceOnInterestCharged, LocalDate interestChargedFromDate, LocalDate repaymentsStartingFromDate, Integer rowIndex, ExternalId externalId, Long groupId, Collection charges, - String linkAccountId, String locale, String dateFormat, List loanCollateralManagementData) { + String linkAccountId, String locale, String dateFormat, List loanCollateralManagementData, + String nit, String code, String loanId, String cedula, boolean isMigratedLoan) { return new LoanAccountData().setLoanType(loanTypeEnumOption).setClientId(clientId).setProductId(productId) .setLoanOfficerId(loanOfficerId).setSubmittedOnDate(submittedOnDate).setFundId(fundId).setPrincipal(principal) @@ -311,7 +318,8 @@ public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEn .setGraceOnInterestCharged(graceOnInterestCharged).setInterestChargedFromDate(interestChargedFromDate) .setRepaymentsStartingFromDate(repaymentsStartingFromDate).setRowIndex(rowIndex).setExternalId(externalId) .setGroupId(groupId).setCharges(charges).setLinkAccountId(linkAccountId).setLocale(locale).setDateFormat(dateFormat) - .setCollateral(loanCollateralManagementData); + .setCollateral(loanCollateralManagementData).setNit(nit).setCode(code).setNumeroCredito(loanId).setCedula(cedula) + .setMigratedLoan(isMigratedLoan); } public static LoanAccountData importInstanceGroup(EnumOptionData loanTypeEnumOption, Long groupIdforGroupLoan, Long productId, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanArchiveHistoryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanArchiveHistoryData.java index 097e5f24f16..8c58a81b189 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanArchiveHistoryData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanArchiveHistoryData.java @@ -63,6 +63,7 @@ public class LoanArchiveHistoryData { private Integer numeroDeReprogramaciones; private String departamento; private String estadoCuota; + private String celularReferencia; private String referencia; private String nitEmpresaAliada; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDebtProjectionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDebtProjectionData.java new file mode 100644 index 00000000000..32c4d9e0ae3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDebtProjectionData.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +public class LoanDebtProjectionData { + + private Long diasDeMoraProyectados; // projectedOverdueDays; + + private OverdueBalanceDetails saldoVencidoDiscriminado; // overdueBalanceDetails; + + private TotalBalanceDetails saldoTotalDiscriminado;// totalBalanceDetails; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class OverdueBalanceDetails { + + private BigDecimal saldoVencidoCuotas;// overdueInstallmentBalance; + + private BigDecimal saldoVencidoIntMora; // overdueInterestBalance; + + private BigDecimal saldoVencidoHonorario; // overdueFeesBalance; + + public BigDecimal getTotal() { + return saldoVencidoCuotas.add(saldoVencidoIntMora).add(saldoVencidoHonorario); + } + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TotalBalanceDetails { + + private BigDecimal saldoTotalVencido; // totalOverdueBalance; + + private BigDecimal saldoTotalFuturo; // totalFutureBalance; + + private BigDecimal saldoTotal; // totalBalance; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 1f426591327..85eca2f0846 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -20,16 +20,14 @@ import java.math.BigDecimal; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.custom.portfolio.externalcharge.honoratio.domain.CustomChargeHonorarioMap; +import org.apache.fineract.custom.portfolio.externalcharge.honoratio.domain.CustomChargeHonorarioMapRepository; import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockLevel; import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockingReasonSetting; import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockingReasonSettingEnum; @@ -54,6 +52,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCreditBalanceRefundPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanForeClosurePostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanForeClosurePreBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanInvoiceGenerationPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanRefundPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanRefundPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; @@ -70,14 +69,12 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.*; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; @@ -89,6 +86,8 @@ import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -96,6 +95,7 @@ import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; @@ -112,6 +112,7 @@ import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @@ -149,6 +150,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final BlockingReasonSettingsRepositoryWrapper blockingReasonSettingsRepositoryWrapper; private final LoanBlockingReasonRepository loanBlockingReasonRepository; + private final PlatformSecurityContext platformSecurityContext; + @Autowired + private CustomChargeHonorarioMapRepository customChargeHonorarioMapRepository; + @Transactional @Override public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, final Loan loan, @@ -216,11 +221,37 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact } final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDto); + List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); + Integer numberOfRepayment = 0; + if (loan.getAgeOfOverdueDays(DateUtils.getBusinessLocalDate()) > 0) { + for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loanRepaymentScheduleInstallments) { + if (loanRepaymentScheduleInstallment.isOverdueOn(transactionDate) && !loanRepaymentScheduleInstallment.isObligationsMet() + && !loanRepaymentScheduleInstallment.isPartlyPaid()) { + + numberOfRepayment = loanRepaymentScheduleInstallment.getInstallmentNumber(); + updateCalculationHonoLoanChargeOverDueVat(repaymentAmount.getAmount(), loanRepaymentScheduleInstallment); + break; + } + if (loanRepaymentScheduleInstallment.isPartlyPaid()) { + numberOfRepayment = loanRepaymentScheduleInstallment.getInstallmentNumber(); + break; + } + } + } + final CollectionData collectionData = this.delinquencyReadPlatformService.calculateLoanCollectionData(loan.getId()); + final Long daysInArrears = collectionData.getPastDueDays(); final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, scheduleGeneratorDTO, isHolidayValidationDone); + Optional charges = loan.getLoanCharges().stream() + .filter(charge -> charge.isFlatHono() || charge.getChargeCalculation().isPercentageOfHonorarios()).findFirst(); + if (charges.isPresent()) { + LoanCharge charge = charges.get(); + charge.updateInstallmentChargesHono(numberOfRepayment); + } + saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); /*** @@ -238,23 +269,33 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact } loan.getLoanCustomizationDetail().recordActivity(); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - + for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + updateRepaymentInstalmentCharge(installment, numberOfRepayment); + } if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newRepaymentTransaction, noteText); this.noteRepository.save(note); } - + for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + updateRepaymentInstalmentCharge(installment, numberOfRepayment); + } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); recalculateAccruals(loan); setLoanDelinquencyTag(loan, transactionDate); - + Long minimumDaysInArrearsToSuspendLoanAccount = this.configurationDomainService.retriveMinimumDaysInArrearsToSuspendLoanAccount(); + if (minimumDaysInArrearsToSuspendLoanAccount == null) { + minimumDaysInArrearsToSuspendLoanAccount = 90L; + } if (!repaymentTransactionType.isChargeRefund()) { - LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType, - isRecoveryRepayment, newRepaymentTransaction); + final LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent( + repaymentTransactionType, isRecoveryRepayment, newRepaymentTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent); + if (daysInArrears >= minimumDaysInArrearsToSuspendLoanAccount) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanInvoiceGenerationPostBusinessEvent(newRepaymentTransaction)); + } } // disable all active standing orders linked to this loan if status @@ -291,10 +332,116 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact } setStatusToCanceledOnClosedLoan(loan, transactionDate); - return newRepaymentTransaction; } + private void updateRepaymentInstalmentCharge(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment, + Integer numberOfRepayment) { + if (loanRepaymentScheduleInstallment.getInstallmentNumber() != numberOfRepayment) { + + MonetaryCurrency currency = loanRepaymentScheduleInstallment.getLoan().getCurrency(); + BigDecimal feeChargePortion = BigDecimal.ZERO; + Collection loanCharges = loanRepaymentScheduleInstallment.getLoan().getLoanCharges(); + for (LoanCharge loanCharge : loanCharges) { + LoanInstallmentCharge installmentCharge = loanCharge + .getInstallmentLoanCharge(loanRepaymentScheduleInstallment.getInstallmentNumber()); + if (installmentCharge != null) { + feeChargePortion = feeChargePortion.add(installmentCharge.getAmount()); + } + + } + + loanRepaymentScheduleInstallment.updateChargePortion(Money.of(currency, feeChargePortion), + loanRepaymentScheduleInstallment.getFeeChargesWaived(currency), + loanRepaymentScheduleInstallment.getFeeChargesWrittenOff(currency), + loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency), + loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency), + loanRepaymentScheduleInstallment.getPenaltyChargesWrittenOff(currency)); + + } + } + + private void updateCalculationHonoLoanChargeOverDueVat(BigDecimal repaymentAmount, + LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) { + Integer ageOverdue = loanRepaymentScheduleInstallment.getLoan().getAgeOfOverdueDays(DateUtils.getBusinessLocalDate()).intValue(); + BigDecimal delinquencyValue = BigDecimal.ZERO; + Integer vatConfig = configurationDomainService.retriveIvaConfiguration(); + BigDecimal vatPercentage = BigDecimal.valueOf(vatConfig).divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + MonetaryCurrency currency = loanRepaymentScheduleInstallment.getLoan().getCurrency(); + DelinquencyRangeData delinquencyRangeData = delinquencyReadPlatformService + .retrieveCurrentDelinquencyTag(loanRepaymentScheduleInstallment.getLoan().getId()); + if (delinquencyRangeData != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRangeData.getPercentageValue()); + } else { + DelinquencyRange delinquencyRange = delinquencyReadPlatformService.retrieveDelinquencyRangeCategeory(ageOverdue); + if (delinquencyRange != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRange.getPercentageValue()); + } + } + Loan loan = loanRepaymentScheduleInstallment.getLoan(); + Optional charges = loan.getActiveCharges().stream() + .filter(charge -> charge.getChargeCalculation().isFlatHono() || charge.getChargeCalculation().isPercentageOfHonorarios()) + .findFirst(); + BigDecimal chargeFeeHono = BigDecimal.ZERO; + if (charges.isPresent()) { + Optional customChargeHonorarioMaps = charges.get().getCustomChargeHonorarioMaps().stream() + .filter(customChargeHonorarioMap -> customChargeHonorarioMap.getLoanInstallmentNr() == loanRepaymentScheduleInstallment + .getInstallmentNumber() && !loanRepaymentScheduleInstallment.isObligationsMet()) + .findFirst(); + if (customChargeHonorarioMaps.isPresent()) { + chargeFeeHono = customChargeHonorarioMaps.get().getFeeTotalAmount(); + repaymentAmount = repaymentAmount.subtract(chargeFeeHono); + } + } + + // Step 1: Value of delinquent portion / (1 + (delinquency percentage * (1 + vat percentage))) + BigDecimal deliquncyrange = delinquencyValue.divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + BigDecimal delinquentPortion = repaymentAmount + .divide(BigDecimal.ONE.add(deliquncyrange.multiply(BigDecimal.ONE.add(vatPercentage))), 2, MoneyHelper.getRoundingMode()); + // Step 2: Value of the fee with VAT = Step 1 value * (delinquency percentage * 1+ vat Percentage) + BigDecimal feewithTax = delinquentPortion.multiply(deliquncyrange.multiply(BigDecimal.ONE.add(vatPercentage))).setScale(2, + MoneyHelper.getRoundingMode()); + // Step 3: Fee basis = Fee with VAT / (1 + VAT percentage) + BigDecimal feeBasis = feewithTax.divide(BigDecimal.ONE.add(vatPercentage), 2, MoneyHelper.getRoundingMode()); + + // Step 4: Fee VAT = Value of fee with VAT - Fee basis + BigDecimal feeVat = feewithTax.subtract(feeBasis).setScale(2, MoneyHelper.getRoundingMode()); + BigDecimal feeHono = feeVat.add(feeBasis).setScale(0, MoneyHelper.getRoundingMode()); + // End Calculate + + if (charges.isPresent()) { + LoanCharge chargeHono = charges.get(); + Optional customChargeHonorarioMaps = chargeHono.getCustomChargeHonorarioMaps().stream() + .filter(customChargeHonorarioMap -> customChargeHonorarioMap.getLoanInstallmentNr() == loanRepaymentScheduleInstallment + .getInstallmentNumber() && !loanRepaymentScheduleInstallment.isObligationsMet()) + .findFirst(); + if (!customChargeHonorarioMaps.isPresent()) { + CustomChargeHonorarioMap newCustomChargeHonorarioMap = new CustomChargeHonorarioMap(); + newCustomChargeHonorarioMap.setNit("120843958"); + newCustomChargeHonorarioMap.setLoanId(loan.getId()); + newCustomChargeHonorarioMap.setLoanInstallmentNr(loanRepaymentScheduleInstallment.getInstallmentNumber()); + newCustomChargeHonorarioMap.setFeeBaseAmount(feeBasis); + newCustomChargeHonorarioMap.setFeeTotalAmount(feeHono); + newCustomChargeHonorarioMap.setFeeVatAmount(feeVat); + newCustomChargeHonorarioMap.setCreatedBy(this.platformSecurityContext.authenticatedUser().getId()); + newCustomChargeHonorarioMap.setCreatedAt(DateUtils.getLocalDateTimeOfTenant()); + newCustomChargeHonorarioMap.setLoanChargeId(chargeHono.getId()); + customChargeHonorarioMapRepository.saveAndFlush(newCustomChargeHonorarioMap); + chargeHono.update(feeHono, null, loanRepaymentScheduleInstallment.getInstallmentNumber()); + loan.updateLoanScheduleAfterCustomChargeApplied(); + saveLoanWithDataIntegrityViolationChecks(loan); + } else { + CustomChargeHonorarioMap customChargeHonorarioMap = customChargeHonorarioMaps.get(); + customChargeHonorarioMap.setFeeTotalAmount(feeHono); + customChargeHonorarioMap.setFeeVatAmount(feeVat); + customChargeHonorarioMap.setFeeBaseAmount(feeBasis); + customChargeHonorarioMapRepository.save(customChargeHonorarioMap); + } + + } + + } + private void setStatusToCanceledOnClosedLoan(final Loan loan, final LocalDate transactionDate) { if ((loan != null) && (loan.getStatus() != null) && loan.getStatus().isClosedObligationsMet()) { final BlockingReasonSetting blockingReasonSetting = blockingReasonSettingsRepositoryWrapper @@ -722,6 +869,7 @@ private void generateLoanScheduleAccrualData(final LocalDate accruedTill, } } } + LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(), accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(), installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(), @@ -848,15 +996,24 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final List existingReversedTransactionIds = new ArrayList<>(); existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - final ScheduleGeneratorDTO scheduleGeneratorDTO = null; - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate, scheduleGeneratorDTO); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { loan.reverseAccrualsAfter(foreClosureDate); Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate); Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]); Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]); + // If foreclosure or cancel on disbursement date + LoanRepaymentScheduleInstallment inst = loan.getRepaymentScheduleInstallments().get(0); + if (DateUtils.isEqual(foreClosureDate, inst.getFromDate())) { + feePortion = inst.getFeeChargesOutstanding(currency); + if (loan.isAnulado()) { + feePortion = loan.getPendingHonoAmountOfAnuladoLoanForInstallment(loan, inst.getInstallmentNumber()); + } + } Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]); + Money total = interestPortion.plus(feePortion).plus(penaltyPortion); if (total.isGreaterThanZero()) { ExternalId accrualExternalId = externalIdFactory.create(); @@ -887,7 +1044,7 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency); Money payPrincipal = foreCloseDetail.getPrincipal(currency); - loan.updateInstallmentsPostDate(foreClosureDate); + loan.updateInstallmentsPostDate(foreClosureDate, scheduleGeneratorDTO); LoanTransaction payment = null; @@ -950,12 +1107,10 @@ public LoanTransaction claimLoan(Loan loan, final LocalDate claimDate, final Ext MonetaryCurrency currency = loan.getCurrency(); List newTransactions = new ArrayList<>(); - final List existingTransactionIds = new ArrayList<>(); - final List existingReversedTransactionIds = new ArrayList<>(); - existingTransactionIds.addAll(loan.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - final ScheduleGeneratorDTO scheduleGeneratorDTO = null; - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(claimDate); + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(claimDate, scheduleGeneratorDTO); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(claimDate, loan.getAccruedTill()))) { loan.reverseAccrualsAfter(claimDate); @@ -1011,7 +1166,7 @@ public LoanTransaction claimLoan(Loan loan, final LocalDate claimDate, final Ext } } ///////////// - loan.updateInstallmentsPostDate(claimDate); + loan.updateInstallmentsPostDate(claimDate, scheduleGeneratorDTO); if (loan.claimType() != null) { LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); @@ -1203,12 +1358,10 @@ public LoanTransaction writeoffPunishLoan(Loan loan, final LocalDate writeOffDat MonetaryCurrency currency = loan.getCurrency(); List newTransactions = new ArrayList<>(); - final List existingTransactionIds = new ArrayList<>(); - final List existingReversedTransactionIds = new ArrayList<>(); - existingTransactionIds.addAll(loan.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - final ScheduleGeneratorDTO scheduleGeneratorDTO = null; - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(writeOffDate); + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(writeOffDate, scheduleGeneratorDTO); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(writeOffDate, loan.getAccruedTill()))) { loan.reverseAccrualsAfter(writeOffDate); @@ -1246,7 +1399,7 @@ public LoanTransaction writeoffPunishLoan(Loan loan, final LocalDate writeOffDat Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency); Money payPrincipal = foreCloseDetail.getPrincipal(currency); - loan.updateInstallmentsPostDate(writeOffDate); + loan.updateInstallmentsPostDate(writeOffDate, scheduleGeneratorDTO); LoanTransaction payment = null; @@ -1289,4 +1442,26 @@ public LoanTransaction writeoffPunishLoan(Loan loan, final LocalDate writeOffDat return payment; } + private Money getPendingHonoAmountForAnuladoLoan(Loan loan) { + BigDecimal honorariosAmount = BigDecimal.ZERO; + Collection honorariosCharges = loan.getLoanCharges().stream().filter(LoanCharge::isFlatHono).toList(); + Collection ivaCharges = loan.getLoanCharges().stream().filter(LoanCharge::isCustomPercentageBasedOfAnotherCharge) + .toList(); + for (LoanRepaymentScheduleInstallment repaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) { + + BigDecimal chargeAmount = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeAmount = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + honorariosAmount = honorariosAmount.add(honorariosTermChargeAmount).add(chargeAmount); + } + return Money.of(loan.getCurrency(), honorariosAmount); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanArchiveHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanArchiveHistory.java index b7fb36aa675..a5069345ca7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanArchiveHistory.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanArchiveHistory.java @@ -134,4 +134,6 @@ public class LoanArchiveHistory { private BigDecimal montoInicial; @Column(name = "nit_empresa_aliada") private String nitEmpresaAliada; + @Column(name = "celular_familiar_1") + private String celularReferencia; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/ClasificacionConceptosNotFound.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/ClasificacionConceptosNotFound.java new file mode 100644 index 00000000000..9034d0dd22a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/ClasificacionConceptosNotFound.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.loanaccount.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ClasificacionConceptosNotFound extends AbstractPlatformDomainRuleException { + + public ClasificacionConceptosNotFound(final Long id) { + super("error.msg.clasificacion.conceptos.id.invalid", "Clasificacion Conceptos with identifier " + id + " does not exist"); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreateClasificacionConceptosCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreateClasificacionConceptosCommandHandler.java new file mode 100644 index 00000000000..91817046ef8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CreateClasificacionConceptosCommandHandler.java @@ -0,0 +1,23 @@ +package org.apache.fineract.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.ClasificacionConceptosService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "CLASIFICACION_CONCEPTO", action = "CREATE") +public class CreateClasificacionConceptosCommandHandler implements NewCommandSourceHandler { + + private final ClasificacionConceptosService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.create(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteClasificacionConceptosCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteClasificacionConceptosCommandHandler.java new file mode 100644 index 00000000000..4f0aed3ed2d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/DeleteClasificacionConceptosCommandHandler.java @@ -0,0 +1,22 @@ +package org.apache.fineract.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.ClasificacionConceptosService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "CLASIFICACION_CONCEPTO", action = "DELETE") +public class DeleteClasificacionConceptosCommandHandler implements NewCommandSourceHandler { + + private final ClasificacionConceptosService writePlatformService; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return this.writePlatformService.delete(command.entityId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateClasificacionConceptosCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateClasificacionConceptosCommandHandler.java new file mode 100644 index 00000000000..a228c4813d8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/UpdateClasificacionConceptosCommandHandler.java @@ -0,0 +1,23 @@ +package org.apache.fineract.portfolio.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.service.ClasificacionConceptosService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "CLASIFICACION_CONCEPTO", action = "UPDATE") +public class UpdateClasificacionConceptosCommandHandler implements NewCommandSourceHandler { + + private final ClasificacionConceptosService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.update(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/ClasificacionConceptosData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/ClasificacionConceptosData.java new file mode 100644 index 00000000000..e9800c3e45d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/ClasificacionConceptosData.java @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.invoice.data; + +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.loanaccount.domain.ClasificacionConceptos; + +@Data +@Builder +@RequiredArgsConstructor +@AllArgsConstructor +public class ClasificacionConceptosData { + + private Long id; + private String concepto; + private boolean mandato; + private boolean excluido; + private boolean exento; + private boolean gravado; + private String norma; + private BigDecimal tarifa; + + public ClasificacionConceptosData(ClasificacionConceptos clasificacionConceptos) { + this.id = clasificacionConceptos.getId(); + this.concepto = clasificacionConceptos.getConcepto(); + this.mandato = clasificacionConceptos.isMandato(); + this.excluido = clasificacionConceptos.isExcluido(); + this.exento = clasificacionConceptos.isExento(); + this.gravado = clasificacionConceptos.isGravado(); + this.norma = clasificacionConceptos.getNorma(); + this.tarifa = clasificacionConceptos.getTarifa(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/LoanDocumentData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/LoanDocumentData.java new file mode 100644 index 00000000000..67d335e3871 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/data/LoanDocumentData.java @@ -0,0 +1,222 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.invoice.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.client.domain.LegalForm; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicaMensual; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LoanDocumentData { + + /** Client fields */ + private Long loanId; + private Long clientId; + private Integer clientLegalForm; + private LocalDate overdueSinceDate; + private Integer daysInArrears; + private Long productTypeId; + private String productTypeName; + private String clientDisplayName; + private String clientLastName; + private String clientEmailAddress; + private String clientIdNumber; + private String loanProductName; + private String companyNIT; + private String companyDocType; + private String companyDeptCode; + private String companyDeptName; + private String companyCityCode; + private String companyCityName; + private String companyAddress; + private String companyTelephone; + private String clientCedula; + private String clientAddress; + private String clientCityCode; + private String clientCityName; + private String clientTelephone; + + /** Resolution fields */ + private Long productTypeParamId; + private String billingPrefix; + private String billingResolutionNumber; + private Long rangeStartNumber; + private Long rangeEndNumber; + private Long lastInvoiceNumber; + private Long lastCreditNoteNumber; + private Long lastDebitNoteNumber; + private String technicalKey; + private String nota; + private LoanDocumentType documentType; + + /** Document fields */ + private LocalDate lastDayOfMonth; + private LocalDate firstDayOfMonth; + private LocalDate secondLastDayOfMonth; + + /** Loan Paid amount fields */ + private BigDecimal interestPaid; + private BigDecimal mandatoryInsurancePaid; + private BigDecimal voluntaryInsurancePaid; + private BigDecimal honorariosPaid; + private BigDecimal penaltyChargesPaid; + private BigDecimal totalPaid; + private String voluntaryInsuranceCode; + private String mandatoryInsuranceCode; + private String voluntaryInsuranceName; + private String mandatoryInsuranceName; + private Integer loansCount; + private Integer itemsCount; + private Long loanTransactionId; + + public FacturaElectronicaMensual toEntity() { + final FacturaElectronicaMensual facturaElectronicaMensual = new FacturaElectronicaMensual(); + final LocalDate businessLocalDate = DateUtils.getBusinessLocalDate(); + int conceptCount = 0; + if (this.interestPaid.compareTo(BigDecimal.ZERO) > 0) { + conceptCount++; + } + if (this.mandatoryInsurancePaid.compareTo(BigDecimal.ZERO) > 0) { + conceptCount++; + } + if (this.voluntaryInsurancePaid.compareTo(BigDecimal.ZERO) > 0) { + conceptCount++; + } + if (this.honorariosPaid.compareTo(BigDecimal.ZERO) > 0) { + conceptCount++; + } + if (this.penaltyChargesPaid.compareTo(BigDecimal.ZERO) > 0) { + conceptCount++; + } + this.itemsCount = conceptCount; + + // INFORMATION AT RESOLUTION LEVEL + facturaElectronicaMensual.setCreatedDate(DateUtils.getAuditOffsetDateTime()); + facturaElectronicaMensual.setNum_resolucion(this.billingResolutionNumber); + facturaElectronicaMensual.setPrefijo(this.billingPrefix); + facturaElectronicaMensual.setFec_desde(this.firstDayOfMonth); + facturaElectronicaMensual.setFec_hasta(this.secondLastDayOfMonth); + facturaElectronicaMensual.setConsecutivo_inicial(this.rangeStartNumber); + facturaElectronicaMensual.setConsecutivo_final(this.rangeEndNumber); + facturaElectronicaMensual.setClave_tecnica(this.technicalKey); + facturaElectronicaMensual.setNota(this.nota); + + // INFORMATION AT INVOICE LEVEL + final String logo = "3"; + facturaElectronicaMensual.setFecha_factura(businessLocalDate); + facturaElectronicaMensual.setMoneda("COP"); + facturaElectronicaMensual.setForma_pago("1"); + facturaElectronicaMensual.setMedio_pago("31"); + facturaElectronicaMensual.setFecha_vence(businessLocalDate); + facturaElectronicaMensual.setFecha_inicial(this.firstDayOfMonth); + facturaElectronicaMensual.setFecha_final(this.lastDayOfMonth); + facturaElectronicaMensual.setEst_fact("C"); + facturaElectronicaMensual.setTotal_unidades(String.valueOf(this.itemsCount)); + facturaElectronicaMensual.setLogo(logo); + if (LoanDocumentType.INVOICE.equals(this.documentType)) { + facturaElectronicaMensual.setTipo_factura("1"); + facturaElectronicaMensual.setTip_doc(LoanDocumentType.INVOICE.getCode()); + } else { + facturaElectronicaMensual.setTipo_factura("9"); + facturaElectronicaMensual.setTip_doc(LoanDocumentType.CREDIT_NOTE.getCode()); + } + + // INFORMATION AT COMPANY LEVEL + final String taxInformation = "RESPONSABLE DEL IVA. No Somos Grandes Contribuyentes. Autorretenedores Renta según Resol. No. 04314 may 16 de 20028. Auterretenedores especiales según Decreto No. 2201 dic 30 de 2016. Autorretenedores de ICA según Resol. No. 202150186360 del 22 de dic de 2021 Medellín"; + facturaElectronicaMensual.setInf_tributaria(taxInformation); + facturaElectronicaMensual.setCantidad(BigDecimal.ONE); + if (LegalForm.fromInt(this.clientLegalForm).isEntity()) { + final String companyCountryCode = "CO"; + final String companyCountryName = "COLOMBIA"; + facturaElectronicaMensual.setNit_emisor(this.companyNIT); + if ("NIT".equalsIgnoreCase(this.companyDocType)) { + facturaElectronicaMensual.setTipo_docid("31"); + } else { + facturaElectronicaMensual.setTipo_docid("13"); + } + facturaElectronicaMensual.setNom_emisor(this.clientDisplayName); + facturaElectronicaMensual.setCod_pais_tienda(companyCountryCode); + facturaElectronicaMensual.setNom_pais_tienda(companyCountryName); + facturaElectronicaMensual.setDep_tienda(this.companyDeptCode); + facturaElectronicaMensual.setNom_dep_tienda(this.companyDeptName); + facturaElectronicaMensual.setCod_mun_tienda("05001"); + facturaElectronicaMensual.setCiudad_tienda(this.companyCityName); + facturaElectronicaMensual.setDireccion_tienda(this.companyAddress); + facturaElectronicaMensual.setNombre_tienda(this.clientDisplayName); + facturaElectronicaMensual.setTel_tienda(this.companyTelephone); + facturaElectronicaMensual.setEmail_tienda(this.clientEmailAddress); + facturaElectronicaMensual.setTipo_pers(1L); + facturaElectronicaMensual.setCodigopostal(this.companyCityCode); + facturaElectronicaMensual.setTelefono(this.companyTelephone); + facturaElectronicaMensual.setEmail(this.clientEmailAddress); + facturaElectronicaMensual.setId_cliente(this.clientIdNumber); + facturaElectronicaMensual.setNombre_cliente(this.clientDisplayName); + } + + // INFORMATION AT INDIVIDUAL CLIENT LEVEL + if (LegalForm.fromInt(this.clientLegalForm).isPerson()) { + facturaElectronicaMensual.setId_cliente(this.clientIdNumber); + facturaElectronicaMensual.setTipo_docid("13"); + facturaElectronicaMensual.setTipo_pers(2L); + facturaElectronicaMensual.setNombre_cliente(this.clientDisplayName); + facturaElectronicaMensual.setApellido_cliente(this.clientLastName); + facturaElectronicaMensual.setDireccion(this.clientAddress); + facturaElectronicaMensual.setCiudad(this.clientCityName); + facturaElectronicaMensual.setCodigopostal(this.clientCityCode); + facturaElectronicaMensual.setTelefono(this.clientTelephone); + facturaElectronicaMensual.setEmail(this.clientEmailAddress); + } + + // INFORMATION AT TAX LEVEL + facturaElectronicaMensual.setIva_codigo("01"); + facturaElectronicaMensual.setIva_name("IVA"); + facturaElectronicaMensual.setBase(BigDecimal.ZERO); + facturaElectronicaMensual.setPorcentaje_impuesto(BigDecimal.ZERO); + facturaElectronicaMensual.setImpuesto(BigDecimal.ZERO); + facturaElectronicaMensual.setNota2("Estos valores corresponden a los cobros asociados a tu crédito: " + this.productTypeName); + facturaElectronicaMensual.setTipo_prod(this.productTypeName); + + facturaElectronicaMensual.setPor_dto(this.totalPaid); + facturaElectronicaMensual.setVal_dto(this.totalPaid); + facturaElectronicaMensual.setTotal(this.totalPaid); + facturaElectronicaMensual.setLoan_transaction_id(this.loanTransactionId); + return facturaElectronicaMensual; + } + + @AllArgsConstructor + @Getter + public enum LoanDocumentType { + + INVOICE("INVOIC"), // + CREDIT_NOTE("NC"), // + DEBIT_NOTE("ND"); // + + private final String code; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicMensualRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicMensualRepository.java new file mode 100644 index 00000000000..45ab59098ac --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicMensualRepository.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.invoice.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface FacturaElectronicMensualRepository + extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT f FROM FacturaElectronicaMensual f WHERE f.id_cliente = :id_cliente AND f.tipo_prod = :tipo_prod AND f.tip_doc = 'INVOIC' ORDER BY f.createdDate, f.id") + List findById_clienteAndTipo_prod(@Param("id_cliente") String id_cliente, + @Param("tipo_prod") String tipo_prod); + + @Query("SELECT f FROM FacturaElectronicaMensual f WHERE f.loan_transaction_id = :loanTransactionId") + List findByLoanTransactionId(final Long loanTransactionId); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicaMensual.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicaMensual.java new file mode 100644 index 00000000000..601385823e0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/FacturaElectronicaMensual.java @@ -0,0 +1,260 @@ +package org.apache.fineract.portfolio.loanaccount.invoice.domain; + +import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_DATE_DB_FIELD; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "c_facturacion_electronica") +@Getter +@Setter +public class FacturaElectronicaMensual extends AbstractPersistableCustom implements Cloneable { + + @Column(name = CREATED_DATE_DB_FIELD) + private OffsetDateTime createdDate; + + @Column(name = "num_resolucion") + private String num_resolucion; + + @Column(name = "prefijo") + private String prefijo; + + @Column(name = "fec_desde") + private LocalDate fec_desde; + + @Column(name = "fec_hasta") + private LocalDate fec_hasta; + + @Column(name = "consecutivo_inicial") + private Long consecutivo_inicial; + + @Column(name = "consecutivo_final") + private Long consecutivo_final; + + @Column(name = "clave_tecnica") + private String clave_tecnica; + + @Column(name = "nota") + private String nota; + + @Column(name = "numero_doc") + private String numero_doc; + + @Column(name = "tip_doc") + private String tip_doc; + + @Column(name = "fecha_factura") + private LocalDate fecha_factura; + + @Column(name = "tipo_factura") + private String tipo_factura; + + @Column(name = "moneda") + private String moneda; + + @Column(name = "forma_pago") + private String forma_pago; + + @Column(name = "medio_pago") + private String medio_pago; + + @Column(name = "fecha_vence") + private LocalDate fecha_vence; + + @Column(name = "fecha_inicial") + private LocalDate fecha_inicial; + + @Column(name = "fecha_final") + private LocalDate fecha_final; + + @Column(name = "est_fact") + private String est_fact; + + @Column(name = "num_facafect") + private String num_facafect; + + @Column(name = "fec_facafect") + private LocalDate fec_facafect; + + @Column(name = "total_unidades") + private String total_unidades; + + @Column(name = "logo") + private String logo; + + @Column(name = "nit_emisor") + private String nit_emisor; + + @Column(name = "nom_emisor") + private String nom_emisor; + + @Column(name = "inf_tributaria") + private String inf_tributaria; + + @Column(name = "cod_pais_tienda") + private String cod_pais_tienda; + + @Column(name = "nom_pais_tienda") + private String nom_pais_tienda; + + @Column(name = "dep_tienda") + private String dep_tienda; + + @Column(name = "nom_dep_tienda") + private String nom_dep_tienda; + + @Column(name = "cod_mun_tienda") + private String cod_mun_tienda; + + @Column(name = "ciudad_tienda") + private String ciudad_tienda; + + @Column(name = "nombre_tienda") + private String nombre_tienda; + + @Column(name = "direccion_tienda") + private String direccion_tienda; + + @Column(name = "tel_tienda") + private String tel_tienda; + + @Column(name = "email_tienda") + private String email_tienda; + + @Column(name = "id_cliente") + private String id_cliente; + + @Column(name = "tipo_docid") + private String tipo_docid; + + @Column(name = "tipo_pers") + private Long tipo_pers; + + @Column(name = "nombre_cliente") + private String nombre_cliente; + + @Column(name = "apellido_cliente") + private String apellido_cliente; + + @Column(name = "direccion") + private String direccion; + + @Column(name = "ciudad") + private String ciudad; + + @Column(name = "codigopostal") + private String codigopostal; + + @Column(name = "telefono") + private String telefono; + + @Column(name = "email") + private String email; + + @Column(name = "posicion") + private Long posicion; + + @Column(name = "cantidad") + private BigDecimal cantidad; + + @Column(name = "costo_total") + private BigDecimal costo_total; + + @Column(name = "precio_unitario") + private BigDecimal precio_unitario; + + @Column(name = "sku") + private String sku; + + @Column(name = "nom_articulo") + private String nom_articulo; + + @Column(name = "referencia") + private String referencia; + + @Column(name = "id_mandante") + private String id_mandante; + + @Column(name = "descripcion_mandante") + private String descripcion_mandante; + + @Column(name = "codigo_descuento") + private String codigo_descuento; + + @Column(name = "porcentajedescuento") + private BigDecimal porcentajedescuento; + + @Column(name = "descuento") + private BigDecimal descuento; + + @Column(name = "porcentaje_impuesto_item") + private BigDecimal porcentaje_impuesto_item; + + @Column(name = "impuesto_item") + private BigDecimal impuesto_item; + + @Column(name = "iva_codigo") + private String iva_codigo; + + @Column(name = "iva_name") + private String iva_name; + + @Column(name = "base") + private BigDecimal base; + + @Column(name = "porcentaje_impuesto") + private BigDecimal porcentaje_impuesto; + + @Column(name = "impuesto") + private BigDecimal impuesto; + + @Column(name = "por_dto") + private BigDecimal por_dto; + + @Column(name = "val_dto") + private BigDecimal val_dto; + + @Column(name = "total") + private BigDecimal total; + + @Column(name = "nota2") + private String nota2; + + @Column(name = "tas_cambmon") + private String tas_cambmon; + + @Column(name = "cod_moncamb") + private String cod_moncamb; + + @Column(name = "tot_basimpo") + private BigDecimal tot_basimpo; + + @Column(name = "tot_facmon") + private BigDecimal tot_facmon; + + @Column(name = "tip_factexport") + private BigDecimal tip_factexport; + + @Column(name = "tipo_prod") + private String tipo_prod; + + @Column(name = "loan_transaction_id") + private Long loan_transaction_id; + + @Override + public FacturaElectronicaMensual clone() { + try { + return (FacturaElectronicaMensual) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/LoanDocumentConcept.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/LoanDocumentConcept.java new file mode 100644 index 00000000000..39094eed5ca --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/invoice/domain/LoanDocumentConcept.java @@ -0,0 +1,18 @@ +package org.apache.fineract.portfolio.loanaccount.invoice.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LoanDocumentConcept { + + INT_CORRIENTE("INT", "INTERÉS DE FINANCIACION"), // + INT_DE_MORA("IPM", "INTERÉS POR MORA"), // + SEGURO_OBLIGATORIO("SEGU", "SEGURO"), // + HONORARIOS("HON", "HONORARIOS"), // + SEGUROS_VOLUNTARIOS("SEGV", "SEGURO VOLUNTARIO"); + + private final String sku; + private final String name; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java index 056a2b76042..a948000ddeb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentConfig.java @@ -20,6 +20,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.Job; @@ -46,6 +47,8 @@ public class ApplyChargeToOverdueLoanInstallmentConfig { private LoanReadPlatformService loanReadPlatformService; @Autowired private LoanChargeWritePlatformService loanChargeWritePlatformService; + @Autowired + private ChargeRepositoryWrapper chargeRepository; @Bean protected Step applyChargeToOverdueLoanInstallmentStep() { @@ -62,6 +65,6 @@ public Job applyChargeToOverdueLoanInstallmentsJob() { @Bean public ApplyChargeToOverdueLoanInstallmentTasklet applyChargeToOverdueLoanInstallmentTasklet() { return new ApplyChargeToOverdueLoanInstallmentTasklet(configurationDomainService, loanReadPlatformService, - loanChargeWritePlatformService); + loanChargeWritePlatformService, chargeRepository); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java index bd09da8fcb3..f118b1b6636 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applychargetooverdueloaninstallment/ApplyChargeToOverdueLoanInstallmentTasklet.java @@ -30,6 +30,8 @@ import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; @@ -45,6 +47,7 @@ public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet { private final ConfigurationDomainService configurationDomainService; private final LoanReadPlatformService loanReadPlatformService; private final LoanChargeWritePlatformService loanChargeWritePlatformService; + private final ChargeRepositoryWrapper chargeRepository; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -56,12 +59,28 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon if (!overdueLoanScheduledInstallments.isEmpty()) { final Map> overdueScheduleData = new HashMap<>(); for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduledInstallments) { - if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) { - overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment); - } else { - Collection loanData = new ArrayList<>(); - loanData.add(overdueInstallment); - overdueScheduleData.put(overdueInstallment.getLoanId(), loanData); + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(overdueInstallment.getChargeId()); + if (chargeDefinition.getParentChargeId() == null) { + if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) { + overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment); + } else { + Collection loanData = new ArrayList<>(); + loanData.add(overdueInstallment); + overdueScheduleData.put(overdueInstallment.getLoanId(), loanData); + } + } + } + + for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduledInstallments) { + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(overdueInstallment.getChargeId()); + if (chargeDefinition.getParentChargeId() != null) { + if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) { + overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment); + } else { + Collection loanData = new ArrayList<>(); + loanData.add(overdueInstallment); + overdueScheduleData.put(overdueInstallment.getLoanId(), loanData); + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryConfig.java index b610c156d6a..00bf459d668 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryConfig.java @@ -7,6 +7,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanArchiveHistoryRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanArchiveHistoryReadWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.job.builder.JobBuilder; @@ -37,6 +38,8 @@ public class ArchiveLoansHistoryConfig { private ClientAllyPointOfSalesRepository clientAllyPointOfSalesRepository; @Autowired private CodeValueRepository codeValueRepository; + @Autowired + private LoanUtilService loanUtilService; @Bean protected Step archiveLoanHistoryStep() { @@ -53,6 +56,6 @@ public Job archiveLoansHistoryJob() { @Bean public ArchiveLoansHistoryTasklet archiveLoansHistoryTasklet() { return new ArchiveLoansHistoryTasklet(loanArchiveHistoryService, loanArchiveHistoryRepository, loanRepository, - delinquencyReadPlatformService, clientAllyPointOfSalesRepository, codeValueRepository); + delinquencyReadPlatformService, clientAllyPointOfSalesRepository, codeValueRepository, loanUtilService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryTasklet.java index 29067f0f703..4e07452dced 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/archiveloanhistory/ArchiveLoansHistoryTasklet.java @@ -10,12 +10,16 @@ import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.client.domain.ClientStatus; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanArchiveHistoryData; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.*; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanArchiveHistoryReadWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; +import org.apache.fineract.portfolio.loanproduct.domain.LoanCustomizationDetail; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; @@ -24,17 +28,19 @@ @Slf4j public class ArchiveLoansHistoryTasklet implements Tasklet { - private LoanArchiveHistoryReadWritePlatformService loanArchiveHistoryService; - private LoanArchiveHistoryRepository loanArchiveHistoryRepository; - private LoanRepositoryWrapper loanRepository; - private DelinquencyReadPlatformService delinquencyReadPlatformService; - private ClientAllyPointOfSalesRepository clientAllyPointOfSalesRepository; - private CodeValueRepository codeValueRepository; + private final LoanArchiveHistoryReadWritePlatformService loanArchiveHistoryService; + private final LoanArchiveHistoryRepository loanArchiveHistoryRepository; + private final LoanRepositoryWrapper loanRepository; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; + private final ClientAllyPointOfSalesRepository clientAllyPointOfSalesRepository; + private final CodeValueRepository codeValueRepository; + private final LoanUtilService loanUtilService; public ArchiveLoansHistoryTasklet(LoanArchiveHistoryReadWritePlatformService loanArchiveHistoryService, LoanArchiveHistoryRepository loanArchiveHistoryRepository, LoanRepositoryWrapper loanRepository, DelinquencyReadPlatformService delinquencyReadPlatformService, - ClientAllyPointOfSalesRepository clientAllyPointOfSalesRepository, CodeValueRepository codeValueRepository) { + ClientAllyPointOfSalesRepository clientAllyPointOfSalesRepository, CodeValueRepository codeValueRepository, + LoanUtilService loanUtilService) { this.loanArchiveHistoryService = loanArchiveHistoryService; this.loanArchiveHistoryRepository = loanArchiveHistoryRepository; this.loanRepository = loanRepository; @@ -42,6 +48,7 @@ public ArchiveLoansHistoryTasklet(LoanArchiveHistoryReadWritePlatformService loa this.clientAllyPointOfSalesRepository = clientAllyPointOfSalesRepository; this.codeValueRepository = codeValueRepository; + this.loanUtilService = loanUtilService; } @Override @@ -207,20 +214,57 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon } } + if (actividadLaboral.equals("PENSIONADO")) { + if (dataLoan.getCiudadPuntoCredito() != null) { + Optional getDepartamento = codeValueRepository + .findById(Long.valueOf(dataLoan.getCiudadPuntoCredito())); + if (getDepartamento.isPresent()) { + CodeValue departamento = getDepartamento.get(); + String departamentoScore = departamento.getScore(); + Optional personalDepartemento = codeValueRepository + .findCiudadAndDepartamentoData(departamentoScore); + if (personalDepartemento.isPresent()) { + departamentoCity = personalDepartemento.get().getLabel(); + } + } + } + } + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan + .fetchLoanForeclosureDetail(LocalDate.now(), scheduleGeneratorDTO); + BigDecimal creSaldo = loanRepaymentScheduleInstallment.getTotalOutstanding(loan.getCurrency()).getAmount(); + + String creEstad = ClientStatus.fromInt(loan.getClient().getStatus()).name(); + if (loan.getLoanCustomizationDetail() != null) { + LoanCustomizationDetail loanCustomizationDetail = loan.getLoanCustomizationDetail(); + if (loanCustomizationDetail.getBlockStatus() != null) { + creEstad = "BLOCKING"; + } + } + + String estadoCuota = loan.getStatus().name(); + String cuoEstado = estadoCuota; + + String estadoCliente = dataLoan.getEstadoCliente(); + if (estadoCliente == null || estadoCliente.equals("CASTIGO")) { + estadoCliente = ClientStatus.fromInt(loan.getClient().getStatus()).name(); + } + if (existingLoanArchive.isPresent()) { + LoanArchiveHistory existingEntry = existingLoanArchive.get(); existingEntry.setIdentificacion(dataLoan.getNitEmpresa()); existingEntry.setPrimerNombre(dataLoan.getPrimerNombre()); existingEntry.setSegundoNombre(dataLoan.getSegundoNombre()); existingEntry.setPrimerApellido(dataLoan.getPrimerApellido()); existingEntry.setSegundoApellido(dataLoan.getSegundoApellido()); - existingEntry.setEstadoCliente(dataLoan.getEstadoCliente()); + existingEntry.setEstadoCliente(estadoCliente); existingEntry .setNumeroObligacion(dataLoan.getNumeroObligacion() + "+" + currentInstallment.getInstallmentNumber()); existingEntry.setNitEmpresa("800139398"); existingEntry.setTelefonoSac(dataLoan.getTelefonoSac()); existingEntry.setCelularSac(dataLoan.getCelularSac()); - existingEntry.setCelularSac2(dataLoan.getCelularSac2()); + existingEntry.setCelularReferencia(dataLoan.getCelularReferencia()); existingEntry.setEmailSac(dataLoan.getEmailSac()); existingEntry.setDireccionSac(dataLoan.getDireccionSac()); existingEntry.setBarrioSac("OTRO"); @@ -251,17 +295,18 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon existingEntry.setAbono(BigDecimal.ZERO); existingEntry.setActividadLaboral(actividadLaboral); existingEntry.setNumeroDeReprogramaciones(numberReschedule); - existingEntry.setCreSaldo(loan.getLoanSummary().getTotalOutstanding()); + existingEntry.setCreSaldo(creSaldo); existingEntry.setCuoSaldo(currentInstallment.getTotalOutstanding(loan.getCurrency()).getAmount()); existingEntry.setMontoInicial(loan.getApprovedPrincipal()); - existingEntry.setCuoEstado(dataLoan.getCuoEstado()); + existingEntry.setCuoEstado(cuoEstado); if (dataLoan.getFechaNacimiento() != null) { existingEntry.setFechaNacimiento(LocalDate.parse(dataLoan.getFechaNacimiento())); } + existingEntry.setEmpresa(ally); existingEntry.setMarca(brand); existingEntry.setCiudadPuntoCredito(cityPoinfsales); - existingEntry.setEstadoCuota(dataLoan.getEstadoCuota()); + existingEntry.setEstadoCuota(estadoCuota); existingEntry.setIvaInteresDeMora(BigDecimal.ZERO); existingEntry.setFechaFinanciacion(loan.getDisbursementDate()); existingEntry.setPuntoDeVenta(pointOfSale); @@ -269,6 +314,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon existingEntry.setParentescoFamiliar(parentescoFamiliar); existingEntry.setEstadoCivil(estadoCivil); existingEntry.setNitEmpresaAliada(dataLoan.getNitEmpresaAliada()); + existingEntry.setCreEstado(creEstad); + loanArchiveHistoryRepository.save(existingEntry); } else { LoanArchiveHistory loanArchiveHistory = new LoanArchiveHistory(); loanArchiveHistory.setTitle("Archive Loan " + loan.getId()); @@ -277,13 +324,13 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon loanArchiveHistory.setSegundoNombre(dataLoan.getSegundoNombre()); loanArchiveHistory.setPrimerApellido(dataLoan.getPrimerApellido()); loanArchiveHistory.setSegundoApellido(dataLoan.getSegundoApellido()); - loanArchiveHistory.setEstadoCliente(dataLoan.getEstadoCliente()); + loanArchiveHistory.setEstadoCliente(estadoCliente); loanArchiveHistory .setNumeroObligacion(dataLoan.getNumeroObligacion() + "+" + currentInstallment.getInstallmentNumber()); loanArchiveHistory.setNitEmpresa("800139398"); loanArchiveHistory.setTelefonoSac(dataLoan.getTelefonoSac()); loanArchiveHistory.setCelularSac(dataLoan.getCelularSac()); - loanArchiveHistory.setCelularSac2(dataLoan.getCelularSac2()); + loanArchiveHistory.setCelularReferencia(dataLoan.getCelularReferencia()); loanArchiveHistory.setEmailSac(dataLoan.getEmailSac()); loanArchiveHistory.setDireccionSac(dataLoan.getDireccionSac()); loanArchiveHistory.setBarrioSac("OTRO"); @@ -314,22 +361,23 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon loanArchiveHistory.setAbono(BigDecimal.ZERO); loanArchiveHistory.setActividadLaboral(actividadLaboral); loanArchiveHistory.setNumeroDeReprogramaciones(numberReschedule); - loanArchiveHistory.setCreSaldo(loan.getLoanSummary().getTotalOutstanding()); + loanArchiveHistory.setCreSaldo(creSaldo); loanArchiveHistory.setCuoSaldo(currentInstallment.getTotalOutstanding(loan.getCurrency()).getAmount()); - loanArchiveHistory.setCuoEstado(dataLoan.getCuoEstado()); + loanArchiveHistory.setCuoEstado(cuoEstado); if (dataLoan.getFechaNacimiento() != null) { loanArchiveHistory.setFechaNacimiento(LocalDate.parse(dataLoan.getFechaNacimiento())); } loanArchiveHistory.setEmpresa(ally); loanArchiveHistory.setMarca(brand); loanArchiveHistory.setCiudadPuntoCredito(cityPoinfsales); - loanArchiveHistory.setEstadoCuota(dataLoan.getEstadoCuota()); + loanArchiveHistory.setEstadoCuota(estadoCuota); loanArchiveHistory.setIvaInteresDeMora(BigDecimal.ZERO); loanArchiveHistory.setFechaFinanciacion(loan.getDisbursementDate()); loanArchiveHistory.setPuntoDeVenta(pointOfSale); loanArchiveHistory.setTipoDocumento(dataLoan.getTipoDocumento()); loanArchiveHistory.setEstadoCivil(estadoCivil); loanArchiveHistory.setNitEmpresaAliada(dataLoan.getNitEmpresaAliada()); + loanArchiveHistory.setCreEstado(creEstad); loanArchiveHistoryRepository.save(loanArchiveHistory); } if (!currentInstallment.isObligationsMet()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualConfig.java new file mode 100644 index 00000000000..11ce49190b8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualConfig.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.jobs.facturaelectronicamensual; + +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicMensualRepository; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterizationRepository; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public class FacturaElectronicaMensualConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final FacturaElectronicMensualRepository facturaElectronicMensualRepository; + private final JdbcTemplate jdbcTemplate; + private final LoanProductParameterizationRepository productParameterizationRepository; + private final ConfigurationDomainService configurationDomainService; + private final LoanWritePlatformService loanWritePlatformService; + + @Autowired + public FacturaElectronicaMensualConfig(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, + final FacturaElectronicMensualRepository facturaElectronicMensualRepository, final JdbcTemplate jdbcTemplate, + final LoanProductParameterizationRepository productParameterizationRepository, + ConfigurationDomainService configurationDomainService, LoanWritePlatformService loanWritePlatformService) { + this.jobRepository = jobRepository; + this.transactionManager = transactionManager; + this.facturaElectronicMensualRepository = facturaElectronicMensualRepository; + this.jdbcTemplate = jdbcTemplate; + this.productParameterizationRepository = productParameterizationRepository; + this.configurationDomainService = configurationDomainService; + this.loanWritePlatformService = loanWritePlatformService; + } + + @Bean + protected Step facturaElectronicaMensualTaskletStep() { + return new StepBuilder(JobName.FACTURA_ELECTRONICA_MENSUAL.name(), jobRepository) + .tasklet(facturaElectronicaMensualTaskletStepTasklet(), transactionManager).build(); + } + + @Bean + public Job facturaElectronicaMensualJob() { + return new JobBuilder(JobName.FACTURA_ELECTRONICA_MENSUAL.name(), jobRepository).start(facturaElectronicaMensualTaskletStep()) + .incrementer(new RunIdIncrementer()).build(); + } + + @Bean + public FacturaElectronicaMensualTasklet facturaElectronicaMensualTaskletStepTasklet() { + return new FacturaElectronicaMensualTasklet(facturaElectronicMensualRepository, jdbcTemplate, productParameterizationRepository, + configurationDomainService, loanWritePlatformService); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualTasklet.java new file mode 100644 index 00000000000..d612856375c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/facturaelectronicamensual/FacturaElectronicaMensualTasklet.java @@ -0,0 +1,828 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.jobs.facturaelectronicamensual; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.invoice.data.LoanDocumentData; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicMensualRepository; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicaMensual; +import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterization; +import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterizationRepository; +import org.jetbrains.annotations.NotNull; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +@Slf4j +public class FacturaElectronicaMensualTasklet implements Tasklet { + + private final FacturaElectronicMensualRepository facturaElectronicMensualRepository; + private final JdbcTemplate jdbcTemplate; + private final LoanProductParameterizationRepository productParameterizationRepository; + private final ConfigurationDomainService configurationDomainService; + private final LoanWritePlatformService loanWritePlatformService; + + @Autowired + public FacturaElectronicaMensualTasklet(final FacturaElectronicMensualRepository facturaElectronicMensualRepository, + final JdbcTemplate jdbcTemplate, final LoanProductParameterizationRepository productParameterizationRepository, + final ConfigurationDomainService configurationDomainService, LoanWritePlatformService loanWritePlatformService) { + this.facturaElectronicMensualRepository = facturaElectronicMensualRepository; + this.jdbcTemplate = jdbcTemplate; + this.productParameterizationRepository = productParameterizationRepository; + this.configurationDomainService = configurationDomainService; + this.loanWritePlatformService = loanWritePlatformService; + } + + @Override + public RepeatStatus execute(@NotNull StepContribution contribution, @NotNull ChunkContext chunkContext) throws Exception { + log.info("FacturaElectronicaMensualTasklet execute method called"); + final LocalDate businessLocalDate = DateUtils.getBusinessLocalDate(); + final YearMonth yearMonth = YearMonth.from(businessLocalDate); + final LocalDate lastDayOfMonth = yearMonth.atEndOfMonth(); + final LocalDate firstDayOfMonth = businessLocalDate.withDayOfMonth(1); + final LocalDate secondLastDayOfMonth = lastDayOfMonth.minusDays(1); + final boolean enableMonthlyInvoiceGenerationOnJobTrigger = this.configurationDomainService + .enableMonthlyInvoiceGenerationOnJobTrigger(); + Long minimumDaysInArrearsToSuspendLoanAccount = this.configurationDomainService.retriveMinimumDaysInArrearsToSuspendLoanAccount(); + if (minimumDaysInArrearsToSuspendLoanAccount == null) { + minimumDaysInArrearsToSuspendLoanAccount = 90L; + } + if (businessLocalDate.equals(secondLastDayOfMonth) || enableMonthlyInvoiceGenerationOnJobTrigger) { + final List loanProductParameterizations = this.productParameterizationRepository.findAll(); + final LoanInvoiceMapper loanInvoiceMapper = new LoanInvoiceMapper(); + final String invoiceQuery = "SELECT " + loanInvoiceMapper.invoiceSchema(); + final String creditNoteQuery = "SELECT " + loanInvoiceMapper.creditNoteSchema(); + final List loanInvoiceDataList = this.jdbcTemplate.query(invoiceQuery, loanInvoiceMapper, firstDayOfMonth, + secondLastDayOfMonth, minimumDaysInArrearsToSuspendLoanAccount); + final List groupedLoanInvoices = groupByClientIdAndProductType(loanInvoiceDataList); + final List facturaElectronicaMensuals = new ArrayList<>(); + for (final LoanDocumentData groupedLoanInvoice : groupedLoanInvoices) { + groupedLoanInvoice.setDocumentType(LoanDocumentData.LoanDocumentType.INVOICE); + this.loanWritePlatformService.processAndSaveLoanDocument(groupedLoanInvoice); + } + this.facturaElectronicMensualRepository.saveAllAndFlush(facturaElectronicaMensuals); + this.productParameterizationRepository.saveAllAndFlush(loanProductParameterizations); + final List loanCreditNoteDataList = this.jdbcTemplate.query(creditNoteQuery, loanInvoiceMapper, + firstDayOfMonth, secondLastDayOfMonth, minimumDaysInArrearsToSuspendLoanAccount); + final List groupedLoanCreditNotes = groupByClientIdAndProductType(loanCreditNoteDataList); + for (final LoanDocumentData groupedLoanCreditNote : groupedLoanCreditNotes) { + groupedLoanCreditNote.setDocumentType(LoanDocumentData.LoanDocumentType.CREDIT_NOTE); + this.loanWritePlatformService.processAndSaveLoanDocument(groupedLoanCreditNote); + } + } + return RepeatStatus.FINISHED; + } + + private List groupByClientIdAndProductType(final List loanDocumentDataList) { + final LocalDate businessLocalDate = DateUtils.getBusinessLocalDate(); + final YearMonth yearMonth = YearMonth.from(businessLocalDate); + final LocalDate lastDayOfMonth = yearMonth.atEndOfMonth(); + final LocalDate firstDayOfMonth = businessLocalDate.withDayOfMonth(1); + final LocalDate secondLastDayOfMonth = lastDayOfMonth.minusDays(1); + return loanDocumentDataList.stream() + .collect(Collectors.groupingBy(li1 -> Arrays.asList(li1.getClientId(), li1.getProductTypeName()), + Collectors.collectingAndThen(Collectors.toList(), list -> { + final BigDecimal interestPaid = list.stream().map(LoanDocumentData::getInterestPaid).reduce(BigDecimal.ZERO, + BigDecimal::add); + final BigDecimal penaltyChargesPaid = list.stream().map(LoanDocumentData::getPenaltyChargesPaid) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal mandatoryInsurancePaid = list.stream().map(LoanDocumentData::getMandatoryInsurancePaid) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal voluntaryInsurancePaid = list.stream().map(LoanDocumentData::getVoluntaryInsurancePaid) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal honorariosPaid = list.stream().map(LoanDocumentData::getHonorariosPaid).reduce(BigDecimal.ZERO, + BigDecimal::add); + final BigDecimal totalPaid = list.stream().map(LoanDocumentData::getTotalPaid).reduce(BigDecimal.ZERO, + BigDecimal::add); + final Integer loansCount = list.size(); + final LoanDocumentData loanDocumentData = list.get(0); + return LoanDocumentData.builder().clientIdNumber(loanDocumentData.getClientIdNumber()) + .productTypeParamId(loanDocumentData.getProductTypeParamId()) + .billingPrefix(loanDocumentData.getBillingPrefix()) + .billingResolutionNumber(loanDocumentData.getBillingResolutionNumber()) + .rangeStartNumber(loanDocumentData.getRangeStartNumber()) + .rangeEndNumber(loanDocumentData.getRangeEndNumber()) + .lastInvoiceNumber(loanDocumentData.getLastInvoiceNumber()) + .lastCreditNoteNumber(loanDocumentData.getLastCreditNoteNumber()) + .lastDebitNoteNumber(loanDocumentData.getLastDebitNoteNumber()) + .technicalKey(loanDocumentData.getTechnicalKey()).nota(loanDocumentData.getNota()) + .nota(loanDocumentData.getNota()).clientDisplayName(loanDocumentData.getClientDisplayName()) + .clientLastName(loanDocumentData.getClientLastName()) + .clientLegalForm(loanDocumentData.getClientLegalForm()).clientId(loanDocumentData.getClientId()) + .clientEmailAddress(loanDocumentData.getClientEmailAddress()).loanId(loanDocumentData.getLoanId()) + .productTypeId(loanDocumentData.getProductTypeId()) + .productTypeName(loanDocumentData.getProductTypeName()) + .overdueSinceDate(loanDocumentData.getOverdueSinceDate()) + .daysInArrears(loanDocumentData.getDaysInArrears()).interestPaid(interestPaid) + .penaltyChargesPaid(penaltyChargesPaid).mandatoryInsurancePaid(mandatoryInsurancePaid) + .voluntaryInsurancePaid(voluntaryInsurancePaid).honorariosPaid(honorariosPaid).totalPaid(totalPaid) + .loansCount(loansCount).firstDayOfMonth(firstDayOfMonth).secondLastDayOfMonth(secondLastDayOfMonth) + .lastDayOfMonth(lastDayOfMonth).loanProductName(loanDocumentData.getLoanProductName()) + .companyNIT(loanDocumentData.getCompanyNIT()).companyDocType(loanDocumentData.getCompanyDocType()) + .companyDeptCode(loanDocumentData.getCompanyDeptCode()) + .companyDeptName(loanDocumentData.getCompanyDeptName()) + .companyCityCode(loanDocumentData.getCompanyCityCode()) + .companyCityName(loanDocumentData.getCompanyCityName()) + .companyAddress(loanDocumentData.getCompanyAddress()) + .companyTelephone(loanDocumentData.getCompanyTelephone()) + .clientCedula(loanDocumentData.getClientCedula()).clientAddress(loanDocumentData.getClientAddress()) + .clientCityCode(loanDocumentData.getClientCityCode()) + .clientCityName(loanDocumentData.getClientCityName()) + .clientTelephone(loanDocumentData.getClientTelephone()).build(); + }))) + .values().stream().toList(); + + } + + public static class LoanInvoiceMapper implements RowMapper { + + public String invoiceSchema() { + return """ + mc.id AS "clientId", + mc.legal_form_enum AS "clientLegalForm", + ml.id AS "loanId", + mc.display_name AS "clientDisplayName", + mc.lastname AS "clientLastName", + mlaa.overdue_since_date_derived AS "overdueSinceDate", + COALESCE(CURRENT_DATE - mlaa.overdue_since_date_derived::DATE,0) AS "daysInArrears", + prodtype.id AS "productTypeId", + prodtype.code_value AS "productTypeName", + COALESCE(cce."NIT", ccp."Cedula") AS "clientIdNumber", + pp.id AS "productTypeParamId", + pp.billing_prefix AS "billingPrefix", + pp.billing_resolution_number AS "billingResolutionNumber", + pp.range_start_number AS "rangeStartNumber", + pp.range_end_number AS "rangeEndNumber", + pp.last_invoice_number AS "lastInvoiceNumber", + pp.last_credit_note_number AS "lastCreditNoteNumber", + pp.last_debit_note_number AS "lastDebitNoteNumber", + pp.clave_tecnica AS "technicalKey", + pp.nota AS nota, + mpl.name AS "loanProductName", + cce."NIT" AS "companyNIT", + dept.code_score AS "companyDeptCode", + dept.code_value AS "companyDeptName", + companycity.code_score AS "companyCityCode", + companycity.code_value AS "companyCityName", + cce."Direccion" AS "companyAddress", + mc.email_address AS "clientEmailAddress", + cce."Telefono" AS "companyTelephone", + ccp."Cedula" AS "clientCedula", + ccp."Direccion" AS "clientAddress", + clientcity.code_score AS "clientCityCode", + clientcity.code_value AS "clientCityName", + ccp."Telefono" AS "clientTelephone", + companydoctype.code_value AS "companyDocType", + mlt."interest" AS "interestPaid", + mlt."mandatoryInsurance" AS "mandatoryInsurancePaid", + mlt."voluntaryInsurance" AS "voluntaryInsurancePaid", + mlt."honorarios" AS "honorariosPaid", + mlt."penaltyCharges" AS "penaltyChargesPaid", + mlt."totalPaid" AS "totalPaid", + mandatory_insurance_code."codeValue" AS "mandatoryInsuranceCode", + voluntary_insurance_code."codeValue" AS "voluntaryInsuranceCode", + mandatory_insurance_code."codeName" AS "mandatoryInsuranceName", + voluntary_insurance_code."codeName" AS "voluntaryInsuranceName" + FROM m_loan ml + INNER JOIN m_client mc ON mc.id = ml.client_id + INNER JOIN m_product_loan mpl ON mpl.id = ml.product_id + INNER JOIN m_code_value prodtype ON prodtype.id = mpl.product_type + INNER JOIN ( + SELECT + mlt.loan_id AS "loanId", + SUM(COALESCE(mlt.interest_portion_derived, 0)) AS "interest", + SUM(COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0)) AS "mandatoryInsurance", + SUM(COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0)) AS "voluntaryInsurance", + SUM(COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0)) AS "honorarios", + SUM(COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "penaltyCharges", + SUM(COALESCE(mlt.interest_portion_derived, 0) + + COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0) + + COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0) + + COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0) + + COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "totalPaid" + FROM m_loan_transaction mlt + LEFT JOIN ( + SELECT mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) mandatory_insurance ON mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) vat_mandatory_insurance ON vat_mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) voluntary_insurance ON voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) vat_voluntary_insurance ON vat_voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) aval ON aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) vat_aval ON vat_aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) hono ON hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) vat_hono ON vat_hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) penalty ON penalty.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 + AND mc.is_penalty = TRUE + AND parent.charge_calculation_enum = 1009 + AND parent.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) vat_penalty ON vat_penalty.loan_transaction_id = mlt.id + WHERE mlt.is_reversed = FALSE AND mlt.occurred_on_suspended_account = FALSE + AND mlt.transaction_type_enum = 2 + AND (mlt.transaction_date BETWEEN ? AND ?) + GROUP BY mlt.loan_id + ) mlt ON mlt."loanId" = ml.id + INNER JOIN ( + SELECT DISTINCT ON (mptp.product_type) * + FROM m_product_type_parameters mptp + WHERE mptp.expiration_date >= CURRENT_DATE + ) pp ON pp.product_type = prodtype.code_value + LEFT JOIN m_loan_arrears_aging mlaa ON mlaa.loan_id = ml.id + LEFT JOIN campos_cliente_empresas cce ON cce.client_id = mc.id + LEFT JOIN m_code_value companydoctype ON companydoctype.id = cce."Tipo ID_cd_Tipo ID" + LEFT JOIN m_code_value dept ON dept.id = cce."Departamento_cd_Departamento" + LEFT JOIN m_code_value companycity ON companycity.id = cce."Ciudad_cd_Ciudad" + LEFT JOIN campos_cliente_persona ccp ON ccp.client_id = mc.id + LEFT JOIN m_code_value clientcity ON clientcity.id = ccp."Ciudad_cd_Ciudad" + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlc.loan_id + ) mandatory_insurance_code ON mandatory_insurance_code.loan_id = ml.id + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlc.loan_id + ) voluntary_insurance_code ON voluntary_insurance_code.loan_id = ml.id + WHERE ml.loan_status_id = 300 + AND COALESCE(CURRENT_DATE - mlaa.overdue_since_date_derived::DATE, 0) < ? + AND mlt."totalPaid" > 0 + ORDER BY mc.id, ml.id + """; + } + + public String creditNoteSchema() { + return """ + mc.id AS "clientId", + mc.legal_form_enum AS "clientLegalForm", + ml.id AS "loanId", + mc.display_name AS "clientDisplayName", + mc.lastname AS "clientLastName", + mlaa.overdue_since_date_derived AS "overdueSinceDate", + COALESCE(CURRENT_DATE - mlaa.overdue_since_date_derived::DATE,0) AS "daysInArrears", + prodtype.id AS "productTypeId", + prodtype.code_value AS "productTypeName", + COALESCE(cce."NIT", ccp."Cedula") AS "clientIdNumber", + pp.id AS "productTypeParamId", + pp.billing_prefix AS "billingPrefix", + pp.billing_resolution_number AS "billingResolutionNumber", + pp.range_start_number AS "rangeStartNumber", + pp.range_end_number AS "rangeEndNumber", + pp.last_invoice_number AS "lastInvoiceNumber", + pp.last_credit_note_number AS "lastCreditNoteNumber", + pp.last_debit_note_number AS "lastDebitNoteNumber", + pp.clave_tecnica AS "technicalKey", + pp.nota AS nota, + mpl.name AS "loanProductName", + cce."NIT" AS "companyNIT", + dept.code_score AS "companyDeptCode", + dept.code_value AS "companyDeptName", + companycity.code_score AS "companyCityCode", + companycity.code_value AS "companyCityName", + cce."Direccion" AS "companyAddress", + mc.email_address AS "clientEmailAddress", + cce."Telefono" AS "companyTelephone", + ccp."Cedula" AS "clientCedula", + ccp."Direccion" AS "clientAddress", + clientcity.code_score AS "clientCityCode", + clientcity.code_value AS "clientCityName", + ccp."Telefono" AS "clientTelephone", + companydoctype.code_value AS "companyDocType", + mlt."interest" AS "interestPaid", + mlt."mandatoryInsurance" AS "mandatoryInsurancePaid", + mlt."voluntaryInsurance" AS "voluntaryInsurancePaid", + mlt."honorarios" AS "honorariosPaid", + mlt."penaltyCharges" AS "penaltyChargesPaid", + mlt."totalPaid" AS "totalPaid", + mandatory_insurance_code."codeValue" AS "mandatoryInsuranceCode", + voluntary_insurance_code."codeValue" AS "voluntaryInsuranceCode", + mandatory_insurance_code."codeName" AS "mandatoryInsuranceName", + voluntary_insurance_code."codeName" AS "voluntaryInsuranceName" + FROM m_loan ml + INNER JOIN m_client mc ON mc.id = ml.client_id + INNER JOIN m_product_loan mpl ON mpl.id = ml.product_id + INNER JOIN m_code_value prodtype ON prodtype.id = mpl.product_type + INNER JOIN ( + SELECT + mlt.loan_id AS "loanId", + SUM(COALESCE(mlt.interest_portion_derived, 0)) AS "interest", + SUM(COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0)) AS "mandatoryInsurance", + SUM(COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0)) AS "voluntaryInsurance", + SUM(COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0)) AS "honorarios", + SUM(COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "penaltyCharges", + SUM(COALESCE(mlt.interest_portion_derived, 0) + + COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0) + + COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0) + + COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0) + + COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "totalPaid" + FROM m_loan_transaction mlt + LEFT JOIN ( + SELECT mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) mandatory_insurance ON mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) vat_mandatory_insurance ON vat_mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) voluntary_insurance ON voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) vat_voluntary_insurance ON vat_voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) aval ON aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) vat_aval ON vat_aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) hono ON hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) vat_hono ON vat_hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) penalty ON penalty.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 + AND mc.is_penalty = TRUE + AND parent.charge_calculation_enum = 1009 + AND parent.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) vat_penalty ON vat_penalty.loan_transaction_id = mlt.id + INNER JOIN m_loan_credit_note mlcn ON mlcn.transaction_id = mlt.id + WHERE mlt.is_reversed = FALSE AND mlt.occurred_on_suspended_account = FALSE + AND (mlt.transaction_type_enum = 6 AND mlt.is_special_writeoff = TRUE) + AND (mlt.transaction_date BETWEEN ? AND ?) + GROUP BY mlt.loan_id + ) mlt ON mlt."loanId" = ml.id + INNER JOIN ( + SELECT DISTINCT ON (mptp.product_type) * + FROM m_product_type_parameters mptp + WHERE mptp.expiration_date >= CURRENT_DATE + ) pp ON pp.product_type = prodtype.code_value + LEFT JOIN m_loan_arrears_aging mlaa ON mlaa.loan_id = ml.id + LEFT JOIN campos_cliente_empresas cce ON cce.client_id = mc.id + LEFT JOIN m_code_value companydoctype ON companydoctype.id = cce."Tipo ID_cd_Tipo ID" + LEFT JOIN m_code_value dept ON dept.id = cce."Departamento_cd_Departamento" + LEFT JOIN m_code_value companycity ON companycity.id = cce."Ciudad_cd_Ciudad" + LEFT JOIN campos_cliente_persona ccp ON ccp.client_id = mc.id + LEFT JOIN m_code_value clientcity ON clientcity.id = ccp."Ciudad_cd_Ciudad" + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlc.loan_id + ) mandatory_insurance_code ON mandatory_insurance_code.loan_id = ml.id + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlc.loan_id + ) voluntary_insurance_code ON voluntary_insurance_code.loan_id = ml.id + WHERE ml.loan_status_id = 300 + AND COALESCE(CURRENT_DATE - mlaa.overdue_since_date_derived::DATE, 0) < ? + AND mlt."totalPaid" > 0 + ORDER BY mc.id, ml.id + """; + } + + public String transactionSchema() { + return """ + mc.id AS "clientId", + mc.legal_form_enum AS "clientLegalForm", + ml.id AS "loanId", + mc.display_name AS "clientDisplayName", + mc.lastname AS "clientLastName", + mlaa.overdue_since_date_derived AS "overdueSinceDate", + COALESCE(CURRENT_DATE - mlaa.overdue_since_date_derived::DATE,0) AS "daysInArrears", + prodtype.id AS "productTypeId", + prodtype.code_value AS "productTypeName", + COALESCE(cce."NIT", ccp."Cedula") AS "clientIdNumber", + pp.id AS "productTypeParamId", + pp.billing_prefix AS "billingPrefix", + pp.billing_resolution_number AS "billingResolutionNumber", + pp.range_start_number AS "rangeStartNumber", + pp.range_end_number AS "rangeEndNumber", + pp.last_invoice_number AS "lastInvoiceNumber", + pp.last_credit_note_number AS "lastCreditNoteNumber", + pp.last_debit_note_number AS "lastDebitNoteNumber", + pp.clave_tecnica AS "technicalKey", + pp.nota AS nota, + mpl.name AS "loanProductName", + cce."NIT" AS "companyNIT", + dept.code_score AS "companyDeptCode", + dept.code_value AS "companyDeptName", + companycity.code_score AS "companyCityCode", + companycity.code_value AS "companyCityName", + cce."Direccion" AS "companyAddress", + mc.email_address AS "clientEmailAddress", + cce."Telefono" AS "companyTelephone", + ccp."Cedula" AS "clientCedula", + ccp."Direccion" AS "clientAddress", + clientcity.code_score AS "clientCityCode", + clientcity.code_value AS "clientCityName", + ccp."Telefono" AS "clientTelephone", + companydoctype.code_value AS "companyDocType", + mlt."interest" AS "interestPaid", + mlt."mandatoryInsurance" AS "mandatoryInsurancePaid", + mlt."voluntaryInsurance" AS "voluntaryInsurancePaid", + mlt."honorarios" AS "honorariosPaid", + mlt."penaltyCharges" AS "penaltyChargesPaid", + mlt."totalPaid" AS "totalPaid", + mandatory_insurance_code."codeValue" AS "mandatoryInsuranceCode", + voluntary_insurance_code."codeValue" AS "voluntaryInsuranceCode", + mandatory_insurance_code."codeName" AS "mandatoryInsuranceName", + voluntary_insurance_code."codeName" AS "voluntaryInsuranceName" + FROM m_loan ml + INNER JOIN m_client mc ON mc.id = ml.client_id + INNER JOIN m_product_loan mpl ON mpl.id = ml.product_id + INNER JOIN m_code_value prodtype ON prodtype.id = mpl.product_type + INNER JOIN ( + SELECT + mlt.loan_id AS "loanId", + mlt.id AS "transactionId", + SUM(COALESCE(mlt.interest_portion_derived, 0)) AS "interest", + SUM(COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0)) AS "mandatoryInsurance", + SUM(COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0)) AS "voluntaryInsurance", + SUM(COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0)) AS "honorarios", + SUM(COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "penaltyCharges", + SUM(COALESCE(mlt.interest_portion_derived, 0) + + COALESCE(mandatory_insurance.amount, 0) + COALESCE(vat_mandatory_insurance.amount, 0) + + COALESCE(voluntary_insurance.amount, 0) + COALESCE(vat_voluntary_insurance.amount, 0) + + COALESCE(hono.amount, 0) + COALESCE(vat_hono.amount, 0) + + COALESCE(penalty.amount, 0) + COALESCE(vat_penalty.amount, 0)) AS "totalPaid" + FROM m_loan_transaction mlt + LEFT JOIN ( + SELECT mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) mandatory_insurance ON mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlcpd.loan_transaction_id + ) vat_mandatory_insurance ON vat_mandatory_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) voluntary_insurance ON voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1034 + GROUP BY mlcpd.loan_transaction_id + ) vat_voluntary_insurance ON vat_voluntary_insurance.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) aval ON aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 41 + GROUP BY mlcpd.loan_transaction_id + ) vat_aval ON vat_aval.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) hono ON hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 AND parent.charge_calculation_enum = 1009 + GROUP BY mlcpd.loan_transaction_id + ) vat_hono ON vat_hono.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id , + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + WHERE mlc.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) penalty ON penalty.loan_transaction_id = mlt.id + LEFT JOIN ( + SELECT + mlcpd.loan_transaction_id, + SUM(mlcpd.amount) amount + FROM m_loan_charge_paid_by mlcpd + JOIN m_loan_charge mlc ON mlc.id = mlcpd.loan_charge_id + JOIN m_charge mc ON mc.id = mlc.charge_id + JOIN m_charge parent ON parent.id = mc.parent_charge_id + WHERE mc.charge_calculation_enum = 342 + AND mc.is_penalty = TRUE + AND parent.charge_calculation_enum = 1009 + AND parent.is_penalty = TRUE + GROUP BY mlcpd.loan_transaction_id + ) vat_penalty ON vat_penalty.loan_transaction_id = mlt.id + WHERE mlt.is_reversed = FALSE + GROUP BY mlt.loan_id, mlt.id + ) mlt ON mlt."loanId" = ml.id + INNER JOIN ( + SELECT DISTINCT ON (mptp.product_type) * + FROM m_product_type_parameters mptp + WHERE mptp.expiration_date >= CURRENT_DATE + ) pp ON pp.product_type = prodtype.code_value + LEFT JOIN m_loan_arrears_aging mlaa ON mlaa.loan_id = ml.id + LEFT JOIN campos_cliente_empresas cce ON cce.client_id = mc.id + LEFT JOIN m_code_value companydoctype ON companydoctype.id = cce."Tipo ID_cd_Tipo ID" + LEFT JOIN m_code_value dept ON dept.id = cce."Departamento_cd_Departamento" + LEFT JOIN m_code_value companycity ON companycity.id = cce."Ciudad_cd_Ciudad" + LEFT JOIN campos_cliente_persona ccp ON ccp.client_id = mc.id + LEFT JOIN m_code_value clientcity ON clientcity.id = ccp."Ciudad_cd_Ciudad" + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum IN (468, 575, 231) + GROUP BY mlc.loan_id + ) mandatory_insurance_code ON mandatory_insurance_code.loan_id = ml.id + LEFT JOIN ( + SELECT mlc.loan_id, + MAX(mc.insurance_code) AS "codeValue", + MAX(mc.insurance_name) AS "codeName" + FROM m_loan_charge mlc + INNER JOIN m_charge mc ON mc.id = mlc.charge_id + WHERE mlc.charge_calculation_enum = 1034 + GROUP BY mlc.loan_id + ) voluntary_insurance_code ON voluntary_insurance_code.loan_id = ml.id + """; + } + + @Override + public LoanDocumentData mapRow(@NotNull ResultSet rs, int rowNum) throws SQLException { + return LoanDocumentData.builder().loanId(rs.getLong("loanId")).clientId(rs.getLong("clientId")) + .clientLegalForm(rs.getInt("clientLegalForm")).clientDisplayName(rs.getString("clientDisplayName")) + .clientLastName(rs.getString("clientLastName")).clientEmailAddress(rs.getString("clientEmailAddress")) + .overdueSinceDate(JdbcSupport.getLocalDate(rs, "overdueSinceDate")).daysInArrears(rs.getInt("daysInArrears")) + .productTypeId(rs.getLong("productTypeId")).productTypeName(rs.getString("productTypeName")) + .clientIdNumber(rs.getString("clientIdNumber")).productTypeParamId(rs.getLong("productTypeParamId")) + .billingPrefix(rs.getString("billingPrefix")).billingResolutionNumber(rs.getString("billingResolutionNumber")) + .rangeStartNumber(JdbcSupport.getLong(rs, "rangeStartNumber")).rangeEndNumber(JdbcSupport.getLong(rs, "rangeEndNumber")) + .lastInvoiceNumber(JdbcSupport.getLong(rs, "lastInvoiceNumber")) + .lastCreditNoteNumber(JdbcSupport.getLong(rs, "lastCreditNoteNumber")) + .lastDebitNoteNumber(JdbcSupport.getLong(rs, "lastDebitNoteNumber")).technicalKey(rs.getString("technicalKey")) + .nota(rs.getString("nota")).loanProductName(rs.getString("loanProductName")).companyNIT(rs.getString("companyNIT")) + .companyDocType(rs.getString("companyDocType")).companyDeptCode(rs.getString("companyDeptCode")) + .companyDeptName(rs.getString("companyDeptName")).companyCityCode(rs.getString("companyCityCode")) + .companyCityName(rs.getString("companyCityName")).companyAddress(rs.getString("companyAddress")) + .companyTelephone(rs.getString("companyTelephone")).clientCedula(rs.getString("clientCedula")) + .clientAddress(rs.getString("clientAddress")).clientCityCode(rs.getString("clientCityCode")) + .clientCityName(rs.getString("clientCityName")).clientTelephone(rs.getString("clientTelephone")) + .interestPaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestPaid")) + .penaltyChargesPaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesPaid")) + .mandatoryInsurancePaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "mandatoryInsurancePaid")) + .voluntaryInsurancePaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "voluntaryInsurancePaid")) + .honorariosPaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "honorariosPaid")) + .totalPaid(JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPaid")) + .mandatoryInsuranceCode(rs.getString("mandatoryInsuranceCode")) + .voluntaryInsuranceCode(rs.getString("voluntaryInsuranceCode")) + .voluntaryInsuranceName(rs.getString("voluntaryInsuranceName")) + .mandatoryInsuranceName(rs.getString("mandatoryInsuranceName")).build(); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/installmentalchargeaccrual/InstallmentChargeAccrualTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/installmentalchargeaccrual/InstallmentChargeAccrualTasklet.java index 76bc6f92ee8..f756f301ff2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/installmentalchargeaccrual/InstallmentChargeAccrualTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/installmentalchargeaccrual/InstallmentChargeAccrualTasklet.java @@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -38,7 +37,6 @@ @Component public class InstallmentChargeAccrualTasklet implements Tasklet { - private final LoanReadPlatformService loanReadPlatformService; private final LoanWritePlatformService loanWritePlatformService; @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesConfig.java index 63aba9b7879..e0c5a0b0de5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesConfig.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.jobs.suspensionduetodefaultCharges; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; @@ -46,6 +47,9 @@ public class SuspensionDueToDefaultChargesConfig { @Autowired private LoanReadPlatformService loanReadPlatformService; + @Autowired + private ConfigurationDomainService configurationDomainService; + @Bean protected Step suspensionDueToDefaultChargesStep() { return new StepBuilder(JobName.INSURANCE_CHARGE_SUSPENSION_DUE_TO_DEFAULT.name(), jobRepository) @@ -60,6 +64,6 @@ public Job temporarySuspensionDueToDefaultChargesJob() { @Bean public SuspensionDueToDefaultChargesTasklet suspensionDueToDefaultChargesTasklet() { - return new SuspensionDueToDefaultChargesTasklet(loanWritePlatformService, loanReadPlatformService); + return new SuspensionDueToDefaultChargesTasklet(loanWritePlatformService, loanReadPlatformService, configurationDomainService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesTasklet.java index 84680571280..0b3cd9df5b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/suspensionduetodefaultCharges/SuspensionDueToDefaultChargesTasklet.java @@ -21,6 +21,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.portfolio.loanaccount.data.DefaultOrCancelInsuranceInstallmentData; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; @@ -35,11 +36,16 @@ public class SuspensionDueToDefaultChargesTasklet implements Tasklet { private final LoanWritePlatformService loanWritePlatformService; private final LoanReadPlatformService loanReadPlatformService; + private final ConfigurationDomainService configurationDomainService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + Long minimumDaysInArrearsToSuspendLoanAccount = this.configurationDomainService.retriveMinimumDaysInArrearsToSuspendLoanAccount(); + if (minimumDaysInArrearsToSuspendLoanAccount == null) { + minimumDaysInArrearsToSuspendLoanAccount = 90L; + } List defaultLoanIds = this.loanReadPlatformService - .getLoanDataWithDefaultMandatoryInsurance(90L); + .getLoanDataWithDefaultMandatoryInsurance(minimumDaysInArrearsToSuspendLoanAccount); loanWritePlatformService.temporarySuspendDefaultInsuranceCharges(defaultLoanIds); return RepeatStatus.FINISHED; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 947195ac20a..3549917c060 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -38,6 +38,7 @@ import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO; import org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; @@ -203,6 +204,10 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe boolean isLastInstallmentPeriod = false; Integer numberOfInstallmentsToIgnore = loanApplicationTerms.getNumberOfInstallmentsToIgnore(); while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) { + // In some cases outstanding balance becomes less than zero and above condition still holds valid + if (scheduleParams.getOutstandingBalance().isLessThanZero()) { + break; + } LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate(); scheduleParams.setActualRepaymentDate(getScheduledDateGenerator() .generateNextRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment)); @@ -376,78 +381,49 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe } //////////// - if (!isMidTermRescheduling || isSameInterestRates) { + if (!isMidTermRescheduling) { + principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFractionParam, totalCumulativePrincipal, totalCumulativeInterest, totalInterestDueForLoan, cumulatingInterestPaymentDueToGrace, outstandingBalance, loanApplicationTerms, periodNumber, mc, principalVariation, compoundingMap, periodStartDateApplicableForInterest, periodEndDate, interestRates, accruedInterestByAdvancePmt); - } else { Money totalMidPeriodInterestRates = Money.zero(currency); + Money totalMidPrincipal = Money.zero(currency); LocalDate nextDates = rescheduleFromDate; if (!loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment().isEmpty()) { LocalDate startDate = null; + LocalDate endDate = null; BigDecimal currentInterst = null; List list = loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment(); - ListIterator iterator = list.listIterator(); - int prevIndex = 0; - while (iterator.hasNext()) { - int index = iterator.nextIndex(); - int nextIndex = prevIndex + 1; - LoanTermVariationsData midInterst = iterator.next(); - currentInterst = midInterst.getDecimalValue(); - LocalDate currentDates = midInterst.getTermVariationApplicableFrom(); - - if (index == list.size() - 1) { - nextIndex = index; - } - nextDates = list.get(nextIndex).getTermVariationApplicableFrom(); - if (!currentInterst.equals(interestRatePerPeriod) && !currentInterst.equals(annualNominalInterestRate)) { + List> loanTermVariationsData = processLoanTermVariations(list, + periodStartDateApplicableForInterest); + for (Map entry : loanTermVariationsData) { - if (iterator.hasNext()) { - nextDates = list.get(index + 1).getTermVariationApplicableFrom(); - } - if (startDate == null) { - startDate = currentDates; - rescheduleFromDate = currentDates; - } else { - if (!iterator.hasPrevious()) { - startDate = currentDates; - } else { - startDate = list.get(index - 1).getTermVariationApplicableFrom(); - } - } - - loanApplicationTerms.setAnnualNominalInterestRate(currentInterst); - final PrincipalInterest midPrincipalInterestForThisPeriods = calculatePrincipalInterestComponentsForPeriod( - calculator, interestCalculationGraceOnRepaymentPeriodFractionParam, totalCumulativePrincipal, - totalCumulativeInterest, totalInterestDueForLoan, cumulatingInterestPaymentDueToGrace, - outstandingBalance, loanApplicationTerms, periodNumber, mc, principalVariation, compoundingMap, - startDate, nextDates, interestRates); - Money midPeriodInterestRates = midPrincipalInterestForThisPeriods.interest(); - totalMidPeriodInterestRates = midPeriodInterestRates; + startDate = (LocalDate) entry.get("startDate"); + endDate = (LocalDate) entry.get("nextDate"); + currentInterst = (BigDecimal) entry.get("currentInterst"); + nextDates = endDate; + loanApplicationTerms.setAnnualNominalInterestRate(currentInterst); + if (startDate.equals(periodStartDate)) { + loanApplicationTerms.setAnnualNominalInterestRate(interestRatePerPeriod); } - prevIndex = nextIndex; - - } - } + final PrincipalInterest midPrincipalInterestForThisPeriods = calculatePrincipalInterestComponentsForPeriod( + calculator, interestCalculationGraceOnRepaymentPeriodFractionParam, totalCumulativePrincipal, + totalCumulativeInterest, totalInterestDueForLoan, cumulatingInterestPaymentDueToGrace, outstandingBalance, + loanApplicationTerms, periodNumber, mc, principalVariation, compoundingMap, startDate, endDate, + interestRates); - /* - * Start mind interest calculation - */ - loanApplicationTerms.setAnnualNominalInterestRate(interestRatePerPeriod); - final PrincipalInterest midPrincipalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(calculator, - interestCalculationGraceOnRepaymentPeriodFractionParam, totalCumulativePrincipal, totalCumulativeInterest, - totalInterestDueForLoan, cumulatingInterestPaymentDueToGrace, outstandingBalance, loanApplicationTerms, - periodNumber, mc, principalVariation, compoundingMap, periodStartDateApplicableForInterest, rescheduleFromDate, - interestRates); + Money midPeriodInterestRates = midPrincipalInterestForThisPeriods.interest(); - Money midPeriodInterestRate = midPrincipalInterestForThisPeriod.interest().add(totalMidPeriodInterestRates); + totalMidPeriodInterestRates = totalMidPeriodInterestRates.add(midPeriodInterestRates); + totalMidPrincipal = midPrincipalInterestForThisPeriods.principal().add(midPeriodInterestRates); - Money interestPrincipal = midPeriodInterestRate.add(midPrincipalInterestForThisPeriod.principal()); + } + } /* * End mind interest calculation Start End interest Calculation @@ -461,10 +437,13 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe accruedInterestByAdvancePmt); final Money periodEndInterestRate = endPrincipalInterestForThisPeriod.interest(); + final Money totalInterestRate = totalMidPeriodInterestRates.add(periodEndInterestRate); + final Money periodEndPrincipal = endPrincipalInterestForThisPeriod.principal(); - final Money totalInterestRate = midPeriodInterestRate.add(periodEndInterestRate); - - final Money totalPrincipal = interestPrincipal.minus(totalInterestRate); + Money totalPrincipal = totalMidPrincipal.minus(totalInterestRate); + if (totalPrincipal.equals(periodEndPrincipal)) { + totalPrincipal = periodEndPrincipal; + } /* * End Calc for end Insterst @@ -611,6 +590,60 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } + private List> processLoanTermVariations(List list, LocalDate periodeStartDate) { + List> resultList = new ArrayList<>(); + Set uniquePairs = new HashSet<>(); // To keep track of unique startDate and nextDate pairs + ListIterator iterator = list.listIterator(); + + LocalDate startDate = null; + LocalDate nextDates; + BigDecimal currentInterst; + BigDecimal interest = BigDecimal.ZERO; + + while (iterator.hasNext()) { + int index = iterator.nextIndex(); + LoanTermVariationsData midInterst = iterator.next(); + currentInterst = midInterst.getDecimalValue(); + LocalDate currentDates = midInterst.getTermVariationApplicableFrom(); + + int nextIndex = (index == list.size() - 1) ? index : index + 1; + nextDates = list.get(nextIndex).getTermVariationApplicableFrom(); + + if (startDate == null) { + startDate = periodeStartDate; + interest = currentInterst; + nextDates = currentDates; + } else { + if (!iterator.hasPrevious()) { + startDate = currentDates; + interest = currentInterst; + } else { + interest = list.get(index - 1).getDecimalValue(); + startDate = list.get(index - 1).getTermVariationApplicableFrom(); + } + } + + // Create a unique key based on startDate and nextDate + String uniqueKey = startDate.toString() + "-" + nextDates.toString(); + + // Save only if the pair is unique + if (!uniquePairs.contains(uniqueKey)) { + uniquePairs.add(uniqueKey); + + // Store currentInterst, startDate, and nextDates in a map + Map result = new HashMap<>(); + result.put("currentInterst", interest); + result.put("startDate", startDate); + result.put("nextDate", nextDates); + + // Add the map to the result list + resultList.add(result); + } + } + + return resultList; + } + private void updateIndividualChargeAmountForInstallment(Set loanCharges, LoanScheduleModelPeriod installment) { Collection mandatoryInsuranceCharges = loanCharges.stream().filter(LoanCharge::isMandatoryInsurance).toList(); @@ -3030,6 +3063,11 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math processInstallmentsInstallments = fetchRetainedInstallmentsForProgressiveLoans(loan.getRepaymentScheduleInstallments(), rescheduleFrom, currency); } + // if processed installements is empty , check for graced installments + if (processInstallmentsInstallments.isEmpty()) { + processInstallmentsInstallments = loan.getRepaymentScheduleInstallments().stream() + .filter(LoanRepaymentScheduleInstallment::isFullyGraced).toList(); + } final List newRepaymentScheduleInstallments = new ArrayList<>(); // Block process the installment and creates the period if it falls @@ -3037,6 +3075,11 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math // This will create the recalculation details by applying the // transactions for (LoanRepaymentScheduleInstallment installment : processInstallmentsInstallments) { + if (installment.isFullyGraced()) { + periods.add(createLoanScheduleModelDownPaymentPeriod(installment, outstandingBalance)); + newRepaymentScheduleInstallments.add(installment); + continue; + } if (installment.isDownPayment()) { instalmentNumber++; periods.add(createLoanScheduleModelDownPaymentPeriod(installment, outstandingBalance)); @@ -3070,6 +3113,29 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math advancePayments.add(new RecalculationDetail(loanTransaction.getTransactionDate(), loanTransaction)); } } + // Extra amount paid in advance + if (outstandingBalance.isZero() || outstandingBalance.isLessThanZero()) { + // Check outstandingBalance.isZero() in case customer pays exact amount equal to outstanding + // balance + // If overpaid in advance then add the last installment to keep track of advance payment amount + outstandingBalanceAsPerRest = outstandingBalance.zero(); + outstandingBalance = outstandingBalance.zero(); + BigDecimal remainingLoanPrincipal = principalToBeScheduled.getAmount(); + for (LoanRepaymentScheduleInstallment inst : loan.getRepaymentScheduleInstallments()) { + if (inst.getInstallmentNumber().intValue() < installment.getInstallmentNumber()) { + remainingLoanPrincipal = remainingLoanPrincipal + .subtract(inst.getPrincipal(loanApplicationTerms.getCurrency()).getAmount() + .subtract(inst.getAdvancePrincipalAmount())); + } + } + installment.setPrincipal(remainingLoanPrincipal); + installment.setInterestCharged(BigDecimal.ZERO); + installment.setFeeChargesCharged(BigDecimal.ZERO); + installment.setPenaltyCharges(BigDecimal.ZERO); + installment.setRecalculatedInterestComponent(true); + installment.getInstallmentCharges().clear(); + newRepaymentScheduleInstallments.add(installment); + } //// break; } @@ -3262,7 +3328,7 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math .plus(totalPenaltyChargesCharged); // for partial schedule generation - if (!newRepaymentScheduleInstallments.isEmpty() && totalCumulativeInterest.isGreaterThanZero()) { + if (!newRepaymentScheduleInstallments.isEmpty()) { Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency); loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForPartialUpdate(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, @@ -3291,7 +3357,8 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math loanScheduleParams); for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { - if (loanScheduleModelPeriod.isRepaymentPeriod() || loanScheduleModelPeriod.isDownPaymentPeriod()) { + if ((loanScheduleModelPeriod.isRepaymentPeriod() || loanScheduleModelPeriod.isDownPaymentPeriod()) + && !loanScheduleModelPeriod.isTotalGracePeriod()) { // adding newly created repayment periods to installments addLoanRepaymentScheduleInstallment(retainedInstallments, loanScheduleModelPeriod); } @@ -3341,7 +3408,7 @@ private List fetchRetainedInstallmentsForProgr MonetaryCurrency currency) { List newRepaymentScheduleInstallments = new ArrayList<>(); for (LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { - if (DateUtils.isBefore(installment.getFromDate(), rescheduleFrom)) { + if (DateUtils.isOnOrBefore(installment.getFromDate(), rescheduleFrom)) { newRepaymentScheduleInstallments.add(installment); } else { // Check if there is any installment having advance payment then add the installment to calculate @@ -3627,4 +3694,17 @@ public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod( int periodNumber, MathContext mc, TreeMap principalVariation, Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, Collection termVariations, Money accruedInterestForAdvancePmt); + + @Override + public PrincipalInterest calculatePrincipalInterestComponents(final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final LocalDate periodStartDate, + final LocalDate periodEndDate, boolean ignoreCurrencyDigitsAfterDecimal) { + final MathContext mc = MoneyHelper.getMathContext(); + final PaymentPeriodsInOneYearCalculator calculator = getPaymentPeriodsInOneYearCalculator(); + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction = BigDecimal.ZERO; + final Money cumulatingInterestPaymentDueToGrace = outstandingBalance.zero(); + return loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, + periodNumber, mc, cumulatingInterestPaymentDueToGrace, outstandingBalance, periodStartDate, periodEndDate, + ignoreCurrencyDigitsAfterDecimal); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java index 065eace81d9..fddd2cdaefa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java @@ -33,6 +33,7 @@ import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; @@ -610,4 +611,17 @@ private LoanTermVariationParams applyLoanTermVariations(final LoanApplicationTer return new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, variationsData); } + + @Override + public PrincipalInterest calculatePrincipalInterestComponents(final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final LocalDate periodStartDate, + final LocalDate periodEndDate, final boolean ignoreCurrencyDigitsAfterDecimal) { + final MathContext mc = MoneyHelper.getMathContext(); + final PaymentPeriodsInOneYearCalculator calculator = getPaymentPeriodsInOneYearCalculator(); + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction = BigDecimal.ZERO; + final Money cumulatingInterestPaymentDueToGrace = outstandingBalance.zero(); + return loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, + periodNumber, mc, cumulatingInterestPaymentDueToGrace, outstandingBalance, periodStartDate, periodEndDate, + ignoreCurrencyDigitsAfterDecimal); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterestCalculator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterestCalculator.java index 9b73449ce49..427977404aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterestCalculator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterestCalculator.java @@ -22,9 +22,10 @@ public PrincipalInterest principalInterestComponentsForFlatInterestLoans(final P @SuppressWarnings("unused") Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, @SuppressWarnings("unused") Collection termVariations) { + final boolean ignoreCurrencyDigitsAfterDecimal = false; final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, - outstandingBalance, periodStartDate, periodEndDate); + outstandingBalance, periodStartDate, periodEndDate, ignoreCurrencyDigitsAfterDecimal); Money interestForThisInstallment = result.interest(); Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, @@ -90,9 +91,10 @@ public PrincipalInterest principalInterestComponentsForDecliningBalanceLoan(fina if (!DateUtils.isAfter(principal.getKey(), periodEndDate)) { int interestForDays = Math.toIntExact(ChronoUnit.DAYS.between(interestStartDate, principal.getKey())); if (interestForDays > 0) { + final boolean ignoreCurrencyDigitsAfterDecimal = false; final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace, - balanceForInterestCalculation, interestStartDate, principal.getKey()); + balanceForInterestCalculation, interestStartDate, principal.getKey(), ignoreCurrencyDigitsAfterDecimal); interestForThisInstallment = interestForThisInstallment.plus(result.interest()); cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); interestStartDate = principal.getKey(); @@ -123,9 +125,10 @@ public PrincipalInterest principalInterestComponentsForDecliningBalanceLoan(fina } } + final boolean ignoreCurrencyDigitsAfterDecimal = false; final PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace, - balanceForInterestCalculation, interestStartDate, periodEndDate); + balanceForInterestCalculation, interestStartDate, periodEndDate, ignoreCurrencyDigitsAfterDecimal); interestForThisInstallment = interestForThisInstallment.plus(result.interest()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 2e5474a1133..6c24ee5f12a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -232,7 +232,11 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement final boolean requireInterestRatePoint = loanProduct.isRequirePoints(); Long interestRatePoints = null; BigDecimal interestRatePerPeriod = BigDecimal.ZERO; - if (loanProduct.getInterestRate() != null) { + Boolean isMigratedLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.IS_MIGRAR_LOAN, element); + if (isMigratedLoan == null) { + isMigratedLoan = Boolean.FALSE; + } + if (loanProduct.getInterestRate() != null && !isMigratedLoan) { final InterestRate interestRate = loanProduct.getInterestRate(); if (!interestRate.isActive()) { throw new GeneralPlatformDomainRuleException("error.msg.loan.interest.rate.not.active", @@ -263,6 +267,8 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement } interestRatePerPeriod = interestRatePerPeriod.add(BigDecimal.valueOf(interestRatePoints)); } + } else if (isMigratedLoan) { + interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); } final PeriodFrequencyType interestRatePeriodFrequencyType = PeriodFrequencyType.YEARS; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index c81a86e715e..f2e5242b731 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -285,39 +285,124 @@ public void getFeeChargesDetail(LoanScheduleData loanScheduleData, final Long lo for (LoanRepaymentScheduleInstallment repaymentScheduleInstallment : loan.getRepaymentScheduleInstallmentsIgnoringTotalGrace()) { // Calculate individual charge amounts - - Collection mandatoryInsuranceCharges = loan.getLoanCharges().stream().filter(LoanCharge::isMandatoryInsurance) - .toList(); - Collection voluntaryInsuranceCharges = loan.getLoanCharges().stream().filter(LoanCharge::isVoluntaryInsurance) + final Collection mandatoryInsuranceCharges = loan.getLoanCharges().stream().filter(LoanCharge::isMandatoryInsurance) .toList(); - Collection avalCharges = loan.getLoanCharges().stream().filter(LoanCharge::isAvalCharge).toList(); - Collection honorariosCharges = loan.getLoanCharges().stream().filter(LoanCharge::isFlatHono).toList(); - Collection ivaCharges = loan.getLoanCharges().stream().filter(LoanCharge::isCustomPercentageBasedOfAnotherCharge) + final Collection voluntaryInsuranceCharges = loan.getLoanCharges().stream().filter(LoanCharge::isVoluntaryInsurance) .toList(); + final Collection avalCharges = loan.getLoanCharges().stream().filter(LoanCharge::isAvalCharge).toList(); + final Collection honorariosCharges = loan.getLoanCharges().stream().filter(LoanCharge::isFlatHono).toList(); + final Collection ivaCharges = loan.getLoanCharges().stream() + .filter(LoanCharge::isCustomPercentageBasedOfAnotherCharge).toList(); BigDecimal mandatoryInsuranceAmount = mandatoryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsurancePaid = mandatoryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceWaived = mandatoryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceWrittenOff = mandatoryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceOutstanding = mandatoryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceAmount = voluntaryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsurancePaid = voluntaryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceWaived = voluntaryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceWrittenOff = voluntaryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceOutstanding = voluntaryInsuranceCharges.stream().flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalAmount = avalCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalPaid = avalCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalWaived = avalCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalWrittenOff = avalCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalOutstanding = avalCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosAmount = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosPaid = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosWaived = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosWrittenOff = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosOutstanding = honorariosCharges.stream().flatMap(lic -> lic.installmentCharges().stream()).filter( + lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); // Calculate term Charge BigDecimal mandatoryInsuranceTermChargeAmount = ivaCharges.stream() .filter(lc -> mandatoryInsuranceCharges.stream() - .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .anyMatch(mic -> Objects.equals(mic.getCharge().getId(), lc.getCharge().getParentChargeId()))) .flatMap(lic -> lic.installmentCharges().stream()) .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceTermChargePaid = ivaCharges.stream() + .filter(lc -> mandatoryInsuranceCharges.stream() + .anyMatch(mic -> Objects.equals(mic.getCharge().getId(), lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceTermChargeWaived = ivaCharges.stream() + .filter(lc -> mandatoryInsuranceCharges.stream() + .anyMatch(mic -> Objects.equals(mic.getCharge().getId(), lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceTermChargeWrittenOff = ivaCharges.stream() + .filter(lc -> mandatoryInsuranceCharges.stream() + .anyMatch(mic -> Objects.equals(mic.getCharge().getId(), lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal mandatoryInsuranceTermChargeOutstanding = ivaCharges.stream() + .filter(lc -> mandatoryInsuranceCharges.stream() + .anyMatch(mic -> Objects.equals(mic.getCharge().getId(), lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceTermChargeAmount = ivaCharges.stream() .filter(lc -> voluntaryInsuranceCharges.stream() .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) @@ -325,12 +410,66 @@ public void getFeeChargesDetail(LoanScheduleData loanScheduleData, final Long lo .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceTermChargePaid = ivaCharges.stream() + .filter(lc -> voluntaryInsuranceCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceTermChargeWaived = ivaCharges.stream() + .filter(lc -> voluntaryInsuranceCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceTermChargeWrittenOff = ivaCharges.stream() + .filter(lc -> voluntaryInsuranceCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal voluntaryInsuranceTermChargeOutstanding = ivaCharges.stream() + .filter(lc -> voluntaryInsuranceCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalTermChargeAmount = ivaCharges.stream() .filter(lc -> avalCharges.stream().anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) .flatMap(lic -> lic.installmentCharges().stream()) .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalTermChargePaid = ivaCharges.stream() + .filter(lc -> avalCharges.stream().anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalTermChargeWaived = ivaCharges.stream() + .filter(lc -> avalCharges.stream().anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalTermChargeWrittenOff = ivaCharges.stream() + .filter(lc -> avalCharges.stream().anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal avalTermChargeOutstanding = ivaCharges.stream() + .filter(lc -> avalCharges.stream().anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeAmount = ivaCharges.stream() .filter(lc -> honorariosCharges.stream() .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) @@ -338,11 +477,58 @@ public void getFeeChargesDetail(LoanScheduleData loanScheduleData, final Long lo .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), lc.getInstallment().getInstallmentNumber())) .map(LoanInstallmentCharge::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargePaid = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountPaid).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeWaived = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWaived).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeWrittenOff = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountWrittenOff).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal honorariosTermChargeOutstanding = ivaCharges.stream() + .filter(lc -> honorariosCharges.stream() + .anyMatch(mic -> mic.getCharge().getId().equals(lc.getCharge().getParentChargeId()))) + .flatMap(lic -> lic.installmentCharges().stream()) + .filter(lc -> Objects.equals(repaymentScheduleInstallment.getInstallmentNumber(), + lc.getInstallment().getInstallmentNumber())) + .map(LoanInstallmentCharge::getAmountOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); mandatoryInsuranceAmount = mandatoryInsuranceAmount.add(mandatoryInsuranceTermChargeAmount); + mandatoryInsurancePaid = mandatoryInsurancePaid.add(mandatoryInsuranceTermChargePaid); + mandatoryInsuranceWaived = mandatoryInsuranceWaived.add(mandatoryInsuranceTermChargeWaived); + mandatoryInsuranceWrittenOff = mandatoryInsuranceWrittenOff.add(mandatoryInsuranceTermChargeWrittenOff); + mandatoryInsuranceOutstanding = mandatoryInsuranceOutstanding.add(mandatoryInsuranceTermChargeOutstanding); + voluntaryInsuranceAmount = voluntaryInsuranceAmount.add(voluntaryInsuranceTermChargeAmount); + voluntaryInsurancePaid = voluntaryInsurancePaid.add(voluntaryInsuranceTermChargePaid); + voluntaryInsuranceWaived = voluntaryInsuranceWaived.add(voluntaryInsuranceTermChargeWaived); + voluntaryInsuranceWrittenOff = voluntaryInsuranceWrittenOff.add(voluntaryInsuranceTermChargeWrittenOff); + voluntaryInsuranceOutstanding = voluntaryInsuranceOutstanding.add(voluntaryInsuranceTermChargeOutstanding); + avalAmount = avalAmount.add(avalTermChargeAmount); + avalPaid = avalPaid.add(avalTermChargePaid); + avalWaived = avalWaived.add(avalTermChargeWaived); + avalWrittenOff = avalWrittenOff.add(avalTermChargeWrittenOff); + avalOutstanding = avalOutstanding.add(avalTermChargeOutstanding); + honorariosAmount = honorariosAmount.add(honorariosTermChargeAmount); + honorariosPaid = honorariosPaid.add(honorariosTermChargePaid); + honorariosWaived = honorariosWaived.add(honorariosTermChargeWaived); + honorariosWrittenOff = honorariosWrittenOff.add(honorariosTermChargeWrittenOff); + honorariosOutstanding = honorariosOutstanding.add(honorariosTermChargeOutstanding); totalMandatoryInsuranceCharged = totalMandatoryInsuranceCharged.add(mandatoryInsuranceAmount); totalVoluntaryInsuranceCharged = totalVoluntaryInsuranceCharged.add(voluntaryInsuranceAmount); @@ -365,9 +551,28 @@ public void getFeeChargesDetail(LoanScheduleData loanScheduleData, final Long lo if (periodData.getPeriod() != null && periodData.getPeriod().equals(repaymentScheduleInstallment.getInstallmentNumber()) && isChargeApplicable) { periodData.setAvalDue(avalAmount); + periodData.setAvalPaid(avalPaid); + periodData.setAvalWaived(avalWaived); + periodData.setAvalWrittenOff(avalWrittenOff); + periodData.setAvalOutstanding(avalOutstanding); + periodData.setHonorariosDue(honorariosAmount); + periodData.setHonorariosPaid(honorariosPaid); + periodData.setHonorariosWaived(honorariosWaived); + periodData.setHonorariosWrittenOff(honorariosWrittenOff); + periodData.setHonorariosOutstanding(honorariosOutstanding); + periodData.setMandatoryInsuranceDue(mandatoryInsuranceAmount); + periodData.setMandatoryInsurancePaid(mandatoryInsurancePaid); + periodData.setMandatoryInsuranceWaived(mandatoryInsuranceWaived); + periodData.setMandatoryInsuranceWrittenOff(mandatoryInsuranceWrittenOff); + periodData.setMandatoryInsuranceOutstanding(mandatoryInsuranceOutstanding); + periodData.setVoluntaryInsuranceDue(voluntaryInsuranceAmount); + periodData.setVoluntaryInsurancePaid(voluntaryInsurancePaid); + periodData.setVoluntaryInsuranceWaived(voluntaryInsuranceWaived); + periodData.setVoluntaryInsuranceWrittenOff(voluntaryInsuranceWrittenOff); + periodData.setVoluntaryInsuranceOutstanding(voluntaryInsuranceOutstanding); break; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanArchiveHistoryServiceReadWritePlatformImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanArchiveHistoryServiceReadWritePlatformImpl.java index fc894053d5a..d3364139edf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanArchiveHistoryServiceReadWritePlatformImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanArchiveHistoryServiceReadWritePlatformImpl.java @@ -49,11 +49,11 @@ public String schema() { + " CASE WHEN mc.legal_form_enum = 2 THEN 'NIT' ELSE 'Cédula' END AS tipo_documento,\n" + " COALESCE(ccp.\"Direccion\",cce.\"Direccion\") AS direction, \n" + " COALESCE(ccp.\"Ciudad_cd_Ciudad\",cce.\"Ciudad_cd_Ciudad\") as ciudad, \n" - + " cce.\"Departamento_cd_Departamento\" as departamento,\n" + " mcbr.name_of_reason AS creaditBlock, \n" - + " ccp.\"Referencia\"AS referencia,\n" + " ccp.\"Media de ingresos\" AS media_de_ingreso, \n" - + " ccp.\"Nombre empresa\" AS nombre_empresa, \n" + " caps.client_ally_id,\n" - + " ca.nit as nit_empresa_aliada,\n" + " caps.name as point_of_sale_name,\n" - + " ccp.\"Parentesco_cd_Parentesco\" AS parentesco,\n" + + " COALESCE(cce.\"Departamento_cd_Departamento\",(select id from m_code_value where code_id =40 and code_score = mcv.departement_id)) as departamento,\n" + + " mcbr.name_of_reason AS creaditBlock, \n" + " ccp.\"Referencia\"AS referencia,\n" + + " ccp.\"Media de ingresos\" AS media_de_ingreso, \n" + " ccp.\"Nombre empresa\" AS nombre_empresa, \n" + + " caps.client_ally_id,\n" + " ca.nit as nit_empresa_aliada,\n" + + " caps.name as point_of_sale_name,\n" + " ccp.\"Parentesco_cd_Parentesco\" AS parentesco,\n" + " mc.created_on_utc, ccp.\"Estado Civil_cd_Estado civil\" as estado_civil, \n" + " ml.disbursedon_date, \n" + " mc.date_of_birth,\n" + " (SELECT code_value \n" + " FROM m_code_value \n" @@ -66,8 +66,13 @@ public String schema() { + "LEFT JOIN campos_cliente_persona ccp ON ccp.client_id = mc.id\n" + "LEFT JOIN campos_cliente_empresas cce ON cce.client_id = mc.id \n" + "LEFT JOIN (\n" + " SELECT loan_id, name_of_reason, priority\n" + " FROM RankedCreaditReasons\n" + " WHERE row_num = 1\n" - + ") AS mcbr ON mcbr.loan_id = ml.id\n" + "WHERE total_outstanding_derived > 0 \n" - + " and loan_status_id NOT IN (500, 601, 602, 600)\n" + "ORDER BY mc.id, ml.id DESC;\n"; + + ") AS mcbr ON mcbr.loan_id = ml.id\n" + " LEFT JOIN (\n" + "SELECT id,CASE \n" + + " WHEN LEFT(LPAD(code_score::TEXT, 5, '0'), 2)::INTEGER < 10 THEN \n" + + " LEFT(LPAD(code_score::TEXT, 5, '0'), 2)::INTEGER::VARCHAR\n" + " ELSE \n" + + " LEFT(LPAD(code_score::TEXT, 5, '0'), 2)\n" + " END as departement_id\n" + + "\tfrom m_code_value ciudad where ciudad.code_id =41\n" + " ) mcv on mcv.id= ccp.\"Ciudad_cd_Ciudad\" " + + "WHERE total_outstanding_derived > 0 \n" + " and loan_status_id NOT IN (500, 601, 602, 600)\n" + + "ORDER BY mc.id, ml.id DESC;\n"; } @Override @@ -84,7 +89,7 @@ public LoanArchiveHistoryData mapRow(ResultSet rs, int rowNum) throws SQLExcepti .ingresos(rs.getBigDecimal("media_de_ingreso")).antiguedadCliente(rs.getString("disbursedon_date")) .actividadLaboral(rs.getString("activeLab")).creSaldo(rs.getBigDecimal("cupo")).cuoSaldo(rs.getBigDecimal("cupo")) .cuoEstado(rs.getString("name_of_reason")).estadoCivil(rs.getString("estado_civil")) - .tipoDocumento(rs.getString("tipo_documento")).celularSac2(rs.getString("celular_referencia")) + .tipoDocumento(rs.getString("tipo_documento")).celularReferencia(rs.getString("celular_referencia")) .referencia(rs.getString("referencia")).empresaLabora(rs.getString("nombre_empresa")) .nitEmpresaAliada(rs.getString("nit_empresa_aliada")).fechaNacimiento(rs.getString("date_of_birth")) .estadoCuota(rs.getString("creaditBlock")).build(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java index 9d54b7fe272..37ef3ed31b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java @@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; @@ -42,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestEnumerations; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestStatusEnumData; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestTimelineData; +import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; @@ -56,13 +58,15 @@ public class LoanRescheduleRequestReadPlatformServiceImpl implements LoanResched private final LoanRepositoryWrapper loanRepositoryWrapper; private static final LoanRescheduleRequestRowMapper LOAN_RESCHEDULE_REQUEST_ROW_MAPPER = new LoanRescheduleRequestRowMapper(); private final CodeValueReadPlatformService codeValueReadPlatformService; + private final LoanUtilService loanUtilService; @Autowired public LoanRescheduleRequestReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, LoanRepositoryWrapper loanRepositoryWrapper, - final CodeValueReadPlatformService codeValueReadPlatformService) { + final CodeValueReadPlatformService codeValueReadPlatformService, LoanUtilService loanUtilService) { this.jdbcTemplate = jdbcTemplate; this.loanRepositoryWrapper = loanRepositoryWrapper; this.codeValueReadPlatformService = codeValueReadPlatformService; + this.loanUtilService = loanUtilService; } private static final class LoanRescheduleRequestRowMapper implements RowMapper { @@ -277,7 +281,9 @@ public LoanRescheduleRequestData retrieveAllRescheduleReasons(final String loanR final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); final MonetaryCurrency currency = loan.getCurrency(); final LocalDate foreClosureDate = DateUtils.getBusinessLocalDate(); - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(foreClosureDate); + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(foreClosureDate, + scheduleGeneratorDTO); rediferirAmount = loanRepaymentScheduleInstallment.getRediferirAmount(currency).getAmount(); } loanRescheduleRequestData.setRediferirAmount(rediferirAmount); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 1c3f1c6f271..d152c4ce831 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -379,8 +379,9 @@ private void createLoanTermVariationsForRegularLoans(final Loan loan, final Inte throw new GeneralPlatformDomainRuleException("error.msg.loan.reschedule.rediferir.periods.less.than.unpaid.installments", "Rediferir periods less than unpaid installments", rediferirTerms); } - - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate); + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate, + scheduleGeneratorDTO); final MonetaryCurrency currency = loan.getCurrency(); final BigDecimal rediferirAmount = loanRepaymentScheduleInstallment.getRediferirAmount(currency).getAmount(); if (!Money.of(currency, rediferirAmount).isGreaterThanZero()) { @@ -496,8 +497,8 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { } final LoanTermVariations rediferirTermVariationValue = rediferirVariations.get(0); final int rediferirPeriods = rediferirTermVariationValue.getTermValue().intValue(); - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan - .fetchLoanForeclosureDetail(businessLocalDate); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(businessLocalDate, + scheduleGeneratorDTO); final MonetaryCurrency currency = loan.getCurrency(); final BigDecimal rediferirAmount = loanRepaymentScheduleInstallment.getRediferirAmount(currency).getAmount(); if (!Money.of(currency, rediferirAmount).isGreaterThanZero()) { @@ -578,8 +579,8 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { final ExternalId txnExternalId = externalIdFactory.create(); final PaymentDetail paymentDetail = null; final boolean isAccountTransfer = false; - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan - .fetchLoanForeclosureDetail(businessLocalDate); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(businessLocalDate, + scheduleGeneratorDTO); final MonetaryCurrency currency = loan.getCurrency(); final BigDecimal rediferirAmount = loanRepaymentScheduleInstallment.getRediferirAmount(currency).getAmount(); final LoanTransaction newRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/ClasificacionConceptosCommandFromApiValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/ClasificacionConceptosCommandFromApiValidator.java new file mode 100644 index 00000000000..2c84018d10f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/ClasificacionConceptosCommandFromApiValidator.java @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.loanaccount.serialization; + +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class ClasificacionConceptosCommandFromApiValidator { + + public static final String CONCEPTO = "concepto"; + public static final String MANDATO = "mandato"; + public static final String EXCLUIDO = "excluido"; + public static final String EXENTO = "exento"; + public static final String GRAVADO = "gravado"; + public static final String NORMA = "norma"; + public static final String TARIFA = "tarifa"; + public static final String ID = "id"; + public static final String LOCALE = "locale"; + public static final String DATE_FORMAT = "dateFormat"; + private static final String[] SUPPORTED_PARAMETERS = new String[] { DATE_FORMAT, ID, LOCALE, CONCEPTO, MANDATO, EXCLUIDO, EXENTO, + GRAVADO, NORMA, TARIFA }; + private final FromJsonHelper fromApiJsonHelper; + + public void validate(String json) { + + final Type typeOfMap = new TypeToken>() {}.getType(); + + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, Arrays.stream(SUPPORTED_PARAMETERS).toList()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 6054605b921..d0cc8949737 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -107,7 +107,9 @@ public final class LoanApplicationCommandFromApiJsonHelper { LoanApiConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanApiConstants.FRAUD_ATTRIBUTE_NAME, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanApiConstants.INTEREST_RATE_POINTS, LoanApiConstants.POINT_OF_SALE_CODE, LoanApiConstants.CLIENT_ID_NUMBER, LoanApiConstants.DISCOUNT_VALUE, LoanApiConstants.DISCOUNT_TRANSFER_VALUE, - "isWriteoffPunish", LoanApiConstants.AllYID, LoanApiConstants.POINT_OF_SALE_CODE)); + "isWriteoffPunish", LoanApiConstants.AllYID, LoanApiConstants.POINT_OF_SALE_CODE, LoanApiConstants.MIGRAR_NIT, + LoanApiConstants.MIGRAR_CODE, LoanApiConstants.MIGRAR_NUMERO_CREDITO, LoanApiConstants.MIGRAR_CEDULA, + LoanApiConstants.IS_MIGRAR_LOAN)); public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; private final FromJsonHelper fromApiJsonHelper; @@ -528,13 +530,8 @@ public void validateForCreate(final String json, final boolean isMeetingMandator if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.chargeExpireDate, loanChargeElement)) { String expddate = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.chargeExpireDate, loanChargeElement); - if (amount.compareTo(BigDecimal.ZERO) == 0 && !charge.isPercentageOfAnotherCharge()) { - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).notNull(); - } else { - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).ignoreIfNull(); - } + baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) + .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).notNull(); } baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) @@ -1111,13 +1108,8 @@ public void validateForModify(final String json, final LoanProduct loanProduct, if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.chargeExpireDate, loanChargeElement)) { String expddate = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.chargeExpireDate, loanChargeElement); - if (amount.compareTo(BigDecimal.ZERO) == 0 && !charge.isPercentageOfAnotherCharge()) { - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).notNull(); - } else { - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).ignoreIfNull(); - } + baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) + .parameterAtIndexArray(LoanApiConstants.chargeExpireDate, i).value(expddate).notNull(); } baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) .parameterAtIndexArray(LoanApiConstants.amountParameterName, i).value(amount).notNull(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index 7583ba916ae..dece67ee257 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -435,7 +435,7 @@ public void validateChargeOffTransaction(final String json) { } final Set chargeOffParameters = new HashSet<>( - Arrays.asList("transactionDate", "note", "locale", "dateFormat", "chargeOffReasonId", "externalId")); + Arrays.asList("transactionDate", "note", "locale", "dateFormat", "chargeOffReasonId", "externalId", "incidentTypeId")); final Type typeOfMap = new TypeToken>() {}.getType(); fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, chargeOffParameters); @@ -456,6 +456,9 @@ public void validateChargeOffTransaction(final String json) { final Long chargeOffReasonId = fromApiJsonHelper.extractLongNamed("chargeOffReasonId", element); baseDataValidator.reset().parameter("chargeOffReasonId").value(chargeOffReasonId).ignoreIfNull().integerGreaterThanZero(); + final Long incidentTypeId = fromApiJsonHelper.extractLongNamed("incidentTypeId", element); + baseDataValidator.reset().parameter("incidentTypeId").value(incidentTypeId).ignoreIfNull().integerGreaterThanZero(); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosService.java new file mode 100644 index 00000000000..b06f32937ef --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosService.java @@ -0,0 +1,38 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.loanaccount.service; + +import java.util.List; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.invoice.data.ClasificacionConceptosData; + +public interface ClasificacionConceptosService { + + List retrieveAll(); + + ClasificacionConceptosData retrieveOne(Long id); + + CommandProcessingResult delete(Long id); + + CommandProcessingResult update(Long id, JsonCommand command); + + CommandProcessingResult create(JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosServiceImpl.java new file mode 100644 index 00000000000..338b98e4bbe --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ClasificacionConceptosServiceImpl.java @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.loanaccount.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.domain.ClasificacionConceptos; +import org.apache.fineract.portfolio.loanaccount.domain.ClasificacionConceptosRepository; +import org.apache.fineract.portfolio.loanaccount.exception.ClasificacionConceptosNotFound; +import org.apache.fineract.portfolio.loanaccount.invoice.data.ClasificacionConceptosData; +import org.apache.fineract.portfolio.loanaccount.serialization.ClasificacionConceptosCommandFromApiValidator; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class ClasificacionConceptosServiceImpl implements ClasificacionConceptosService { + + private final ClasificacionConceptosRepository clasificacionConceptosRepository; + private final ClasificacionConceptosCommandFromApiValidator fromApiValidator; + + @Override + public List retrieveAll() { + List clasificacionConceptos = this.clasificacionConceptosRepository.findAll(); + return clasificacionConceptos.stream().map(ClasificacionConceptosData::new).toList(); + } + + @Override + public ClasificacionConceptosData retrieveOne(Long id) { + ClasificacionConceptos clasificacionConceptos = this.clasificacionConceptosRepository.findById(id) + .orElseThrow(() -> new ClasificacionConceptosNotFound(id)); + return new ClasificacionConceptosData(clasificacionConceptos); + } + + @Override + public CommandProcessingResult delete(Long id) { + ClasificacionConceptos clasificacionConceptos = this.clasificacionConceptosRepository.findById(id) + .orElseThrow(() -> new ClasificacionConceptosNotFound(id)); + this.clasificacionConceptosRepository.delete(clasificacionConceptos); + return CommandProcessingResult.resourceResult(id); + } + + @Override + public CommandProcessingResult update(Long id, JsonCommand command) { + this.fromApiValidator.validate(command.json()); + ClasificacionConceptos clasificacionConceptos = this.clasificacionConceptosRepository.findById(id) + .orElseThrow(() -> new ClasificacionConceptosNotFound(id)); + clasificacionConceptos.update(command); + this.clasificacionConceptosRepository.save(clasificacionConceptos); + return CommandProcessingResult.resourceResult(clasificacionConceptos.getId()); + } + + @Override + public CommandProcessingResult create(JsonCommand command) { + this.fromApiValidator.validate(command.json()); + ClasificacionConceptos clasificacionConceptos = ClasificacionConceptos.create(command); + this.clasificacionConceptosRepository.save(clasificacionConceptos); + return CommandProcessingResult.resourceResult(clasificacionConceptos.getId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 33be3f6e1c4..249d0aeac4a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -244,7 +244,13 @@ public CommandProcessingResult submitApplication(final JsonCommand command) { isTopUp = this.fromJsonHelper.extractBooleanNamed("isTopup", command.parsedJson()); } if (!isTopUp && !isAjuste) { - this.fromApiJsonDeserializer.validateClientBlockingList(entityId); + Boolean isMigratedLoan = this.fromJsonHelper.extractBooleanNamed(LoanApiConstants.IS_MIGRAR_LOAN, command.parsedJson()); + if (isMigratedLoan == null) { + isMigratedLoan = Boolean.FALSE; + } + if (!isMigratedLoan) { + this.fromApiJsonDeserializer.validateClientBlockingList(entityId); + } } boolean isMeetingMandatoryForJLGLoans = configurationDomainService.isMeetingMandatoryForJLGLoans(); @@ -1584,7 +1590,7 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); } BigDecimal loanOutstanding = this.loanReadPlatformService - .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount(); + .retrieveLoanForeclosureTemplate(loanIdToClose, expectedDisbursementDate, false).getAmount(); final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount(); if (loanToClose.claimType() == null || !loanToClose.claimType().equals("castigado")) { if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java index 85c02cd9b89..b9f1f17a59d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java @@ -32,6 +32,7 @@ import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockLevel; import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockingReasonSetting; import org.apache.fineract.infrastructure.clientblockingreasons.domain.BlockingReasonSettingsRepositoryWrapper; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -88,6 +89,7 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService { private final PlatformSecurityContext context; private final InsuranceIncidentRepository insuranceIncidentRepository; private final InsuranceIncidentNoveltyNewsRepository insuranceIncidentNoveltyNewsRepository; + private final ConfigurationDomainService configurationDomainService; @PostConstruct public void registerForNotification() { @@ -641,6 +643,10 @@ private void handleUnBlockingCredit(Loan loan) { } private void createSuspensionRemovedNews(Loan loan) { + Long minimumDaysInArrearsToSuspendLoanAccount = this.configurationDomainService.retriveMinimumDaysInArrearsToSuspendLoanAccount(); + if (minimumDaysInArrearsToSuspendLoanAccount == null) { + minimumDaysInArrearsToSuspendLoanAccount = 90L; + } /// CREATE Salida de suspensión news if loan is in TEMPORARY_SUSPENSION_DUE_TO_DEFAULT novelty news status and /// arrears less than 90 days Integer daysInArrears = null; @@ -652,7 +658,19 @@ private void createSuspensionRemovedNews(Loan loan) { // not in arrears daysInArrears = 0; } - if (daysInArrears >= 90) { + if (daysInArrears >= minimumDaysInArrearsToSuspendLoanAccount) { + // In case of transaction rollback, a loan can move again to suspension + LocalDate date = null; + try { + date = this.jdbcTemplate.queryForObject( + "select overdue_since_date_derived aging_days from m_loan_arrears_aging mlaa where mlaa.loan_id =?", + LocalDate.class, loan.getId()); + } catch (final EmptyResultDataAccessException e) { + // not in arrears + } + if (date != null) { + temporarySuspendDefaultInsuranceCharges(loan, date.plusDays(minimumDaysInArrearsToSuspendLoanAccount)); + } return; } final LocalDate currentDate = DateUtils.getBusinessLocalDate(); @@ -670,9 +688,17 @@ private void createSuspensionRemovedNews(Loan loan) { // Do not add suspension news if loan is already in suspension return; } + } else { + // Loan was never in arrears + return; } Collection loanCharges = loan.getInsuranceChargesForNoveltyIncidentReporting(incident.isMandatory(), incident.isVoluntary()); + LocalDate transactionDate = currentDate; + List transactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); + if (!transactions.isEmpty()) { + transactionDate = transactions.get(transactions.size() - 1).getTransactionDate(); + } if (loanCharges != null && !loanCharges.isEmpty()) { for (LoanCharge loanCharge : loanCharges) { if (loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { @@ -680,7 +706,7 @@ private void createSuspensionRemovedNews(Loan loan) { || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { BigDecimal cumulative = BigDecimal.ZERO; InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, - null, incident, currentDate, cumulative); + null, incident, transactionDate, cumulative); this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); } @@ -689,4 +715,37 @@ private void createSuspensionRemovedNews(Loan loan) { } } + private void temporarySuspendDefaultInsuranceCharges(Loan loan, LocalDate suspensionDate) { + final LocalDate currentDate = DateUtils.getBusinessLocalDate(); + InsuranceIncident incident = this.insuranceIncidentRepository + .findByIncidentType(InsuranceIncidentType.TEMPORARY_SUSPENSION_DUE_TO_DEFAULT); + InsuranceIncident suspensionRemovedIncident = this.insuranceIncidentRepository + .findByIncidentType(InsuranceIncidentType.SUSPENSION_REMOVED); + if (incident == null || (!incident.isMandatory() && !incident.isVoluntary())) { + throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.TEMPORARY_SUSPENSION_DUE_TO_DEFAULT.name()); + } + + Optional lastSuspensionNewsOptional = this.insuranceIncidentNoveltyNewsRepository + .findLastSuspensionIfPresent(loan.getId(), incident.getId(), suspensionRemovedIncident.getId()); + if (lastSuspensionNewsOptional.isPresent()) { + InsuranceIncidentNoveltyNews news = lastSuspensionNewsOptional.get(); + if (news.getInsuranceIncident().getIncidentType().equals(InsuranceIncidentType.TEMPORARY_SUSPENSION_DUE_TO_DEFAULT)) { + // Do not add suspension news if loan is already in suspension + return; + } + } + for (LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { + if ((incident.isMandatory() && loanCharge.isMandatoryInsurance()) + || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { + BigDecimal cumulative = BigDecimal.ZERO; + InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, + null, incident, suspensionDate, cumulative); + + this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + } + } + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index c6a691c0efa..091c2cb68df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -364,6 +364,24 @@ private Loan assembleApplication(final JsonElement element, final Long clientId, BigDecimal valorDescuento = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("valorDescuento", element); loanApplication.updateValorDescuento(valorDescuento); + + /// Migrated loan details + Boolean isMigratedLoan = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.IS_MIGRAR_LOAN, element); + if (isMigratedLoan == null) { + isMigratedLoan = Boolean.FALSE; + } + if (isMigratedLoan) { + String nit = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.MIGRAR_NIT, element); + String code = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.MIGRAR_CODE, element); + String creditNumber = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.MIGRAR_NUMERO_CREDITO, element); + String cedula = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.MIGRAR_CEDULA, element); + loanApplication.setNit(nit); + loanApplication.setCode(code); + loanApplication.setNumeroCredito(creditNumber); + loanApplication.setCedula(cedula); + loanApplication.setMigratedLoan(true); + } + //////////// return loanApplication; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java index 190d3c09e45..c5133c83202 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java @@ -105,7 +105,7 @@ public Set fromParsedJson(final JsonElement element, List loanCharges = new LinkedHashSet<>(); - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); + BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); final Integer numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); final Long productId = this.fromApiJsonHelper.extractLongNamed("productId", element); final LoanProduct loanProduct = this.loanProductRepository.findById(productId) @@ -174,6 +174,18 @@ public Set fromParsedJson(final JsonElement element, List fromParsedJson(final JsonElement element, List fromParsedJson(final JsonElement element, List overdueLoanScheduleDataList) { + public void applyOverdueChargesForLoan(final Long loanId, Collection overdueUnsortedLoanScheduleDataList) { + // order the overdueLoanScheduleDataList by period number + Collection overdueLoanScheduleDataList = overdueUnsortedLoanScheduleDataList.stream() + .sorted(Comparator.comparing(OverdueLoanScheduleData::getPeriodNumber)).toList(); Loan loan = this.loanAssembler.assembleFrom(loanId); if (loan.isChargedOff()) { @@ -750,6 +754,11 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection scheduleDates = new HashMap<>(); - final Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod(); - final Long penaltyPostingWaitPeriodValue = this.configurationDomainService.retrieveGraceOnPenaltyPostingPeriod(); + Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod(); + Long penaltyPostingWaitPeriodValue = this.configurationDomainService.retrieveGraceOnPenaltyPostingPeriod(); + + // we only want grace to be applied on the first installment to be overdue which coule be the first or any other + // so , we can just check if the loan has any penalty charged yet or not + boolean doesPenaltyExist = loan.hasPenaltiesInRepaymentSchedules(); + if (doesPenaltyExist) { + penaltyPostingWaitPeriodValue = 0L; + penaltyWaitPeriodValue = 0L; + } + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); long diff = penaltyWaitPeriodValue + 1 - penaltyPostingWaitPeriodValue; if (diff < 1) { diff = 1L; } + LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue + 1L); int frequencyNumber = 1; if (feeFrequency == null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCreditNoteWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCreditNoteWriteServiceImpl.java index eb0aad7cf7a..cc098b0e6e1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCreditNoteWriteServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCreditNoteWriteServiceImpl.java @@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.custom.infrastructure.core.service.CustomDateUtils; import org.apache.fineract.infrastructure.bulkimport.importhandler.helper.DateSerializer; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; @@ -23,6 +24,8 @@ import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCreditNoteBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.SpecialWriteOffPayload; @@ -51,11 +54,15 @@ public class LoanCreditNoteWriteServiceImpl implements LoanCreditNoteWriteServic private final FromJsonHelper fromApiJsonHelper; private final BusinessEventNotifierService businessEventNotifierService; private final LoanTransactionRepository loanTransactionRepository; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; + private final ConfigurationDomainService configurationDomainService; @Override public CommandProcessingResult addLoanCreditNote(Long loanId, JsonCommand command) { // first assemble the associated loan final Loan loan = this.loanAssembler.assembleFrom(loanId); + final CollectionData collectionData = this.delinquencyReadPlatformService.calculateLoanCollectionData(loan.getId()); + final Long daysInArrears = collectionData.getPastDueDays(); final LoanProduct loanProduct = loan.loanProduct(); if (!loanProduct.getCustomAllowCreditNote()) { throw new GeneralPlatformDomainRuleException("error.msg.loan.credit.note.not.allowed", @@ -65,7 +72,13 @@ public CommandProcessingResult addLoanCreditNote(Long loanId, JsonCommand comman final LoanCreditNote creditNote = this.assembleLoanCreditNote(loan, command); final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(creditNote.getTransactionId()) .orElseThrow(() -> new LoanTransactionNotFoundException(creditNote.getTransactionId())); - this.businessEventNotifierService.notifyPostBusinessEvent(new LoanCreditNoteBusinessEvent(loanTransaction)); + Long minimumDaysInArrearsToSuspendLoanAccount = this.configurationDomainService.retriveMinimumDaysInArrearsToSuspendLoanAccount(); + if (minimumDaysInArrearsToSuspendLoanAccount == null) { + minimumDaysInArrearsToSuspendLoanAccount = 90L; + } + if (daysInArrears >= minimumDaysInArrearsToSuspendLoanAccount) { + this.businessEventNotifierService.notifyPostBusinessEvent(new LoanCreditNoteBusinessEvent(loanTransaction)); + } return CommandProcessingResult.commandOnlyResult(creditNote.getId()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDebtProjectionService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDebtProjectionService.java new file mode 100644 index 00000000000..c673c15b8e7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDebtProjectionService.java @@ -0,0 +1,162 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.loanaccount.data.LoanDebtProjectionData; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoanDebtProjectionService { + + private final LoanRepositoryWrapper loanRepositoryWrapper; + + public LoanDebtProjectionData calculateDebtProjection(Long loanId, String projectionDate, String dateFormat) { + // Find the loan and validate + Loan loan = validateLoanForProjection(loanId); + LocalDate projectedFutureDate = DateUtils.parseLocalDate(projectionDate, dateFormat); + + // Get overdue and future installments + List overdueInstallments = getOverdueInstallments(loan, projectedFutureDate); + List futureInstallments = getFutureInstallments(loan, projectedFutureDate); + + // Calculate projected overdue days + Long projectedOverdueDays = calculateProjectedOverdueDays(overdueInstallments, projectedFutureDate); + + // Calculate discriminated past due balance + LoanDebtProjectionData.OverdueBalanceDetails overdueBalanceDetails = calculateDiscriminatedPastDueBalance(overdueInstallments, + loan.getCurrency()); + + // Calculate total balance details + LoanDebtProjectionData.TotalBalanceDetails totalBalanceDetails = calculateTotalBalanceDetails(loan, projectedFutureDate, + overdueBalanceDetails, futureInstallments); + + return new LoanDebtProjectionData(projectedOverdueDays, overdueBalanceDetails, totalBalanceDetails); + } + + private Loan validateLoanForProjection(Long loanId) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + + // Validate loan is active + if (loan.isClosed()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.is.closed", "Loan is closed and cannot be projected", + loan.getId()); + } + + return loan; + } + + private List getOverdueInstallments(Loan loan, LocalDate projectedFutureDate) { + return loan.getRepaymentScheduleInstallments().stream() + .filter(installment -> installment.getDueDate().isBefore(projectedFutureDate) + || installment.getDueDate().equals(projectedFutureDate)) + .filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).toList(); + } + + private List getFutureInstallments(Loan loan, LocalDate projectedFutureDate) { + LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + return loan.getRepaymentScheduleInstallments().stream().filter( + installment -> installment.getDueDate().isAfter(currentDate) && installment.getDueDate().isBefore(projectedFutureDate)) + .toList(); + } + + private LoanDebtProjectionData.OverdueBalanceDetails calculateDiscriminatedPastDueBalance( + List overdueInstallments, MonetaryCurrency currency) { + if (overdueInstallments.isEmpty()) { + return new LoanDebtProjectionData.OverdueBalanceDetails(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); + } + + // Calculate Past Due Installment Balance + BigDecimal pastDueInstallmentBalance = overdueInstallments.stream() + .map(installment -> installment.getPrincipal(currency).add(installment.getInterestCharged(currency)) + .add(installment.getFeeChargesCharged(currency)).add(installment.getPenaltyChargesCharged(currency)).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2, RoundingMode.HALF_UP); + + // Calculate Delinquency Interest + BigDecimal delinquencyInterest = overdueInstallments.stream() + .map(installment -> installment.getPenaltyChargesCharged(currency).getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add) + .setScale(2, RoundingMode.HALF_UP); + + // Calculate Fee + BigDecimal fee = overdueInstallments.stream().map(installment -> installment.getFeeChargesCharged(currency).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2, RoundingMode.HALF_UP); + + return new LoanDebtProjectionData.OverdueBalanceDetails(pastDueInstallmentBalance, delinquencyInterest, fee); + } + + private LoanDebtProjectionData.TotalBalanceDetails calculateTotalBalanceDetails(Loan loan, LocalDate projectedFutureDate, + LoanDebtProjectionData.OverdueBalanceDetails overdueDetails, List futureInstallments) { + // Calculate Future Balance + BigDecimal futureBalance = calculateFutureBalance(loan, projectedFutureDate, futureInstallments); + + // Calculate Total Overdue Balance + BigDecimal totalOverdueBalance = calculateTotalOverdueBalance(overdueDetails); + + // Calculate Total Balance + BigDecimal totalBalance = totalOverdueBalance.add(futureBalance).setScale(2, RoundingMode.HALF_UP); + + return new LoanDebtProjectionData.TotalBalanceDetails(totalOverdueBalance, futureBalance, totalBalance); + } + + private BigDecimal calculateFutureBalance(Loan loan, LocalDate projectedFutureDate, + List futureInstallments) { + // Calculate principal balance from last due date + BigDecimal principalBalance = calculatePrincipalBalance(loan); + + // Calculate current interest for additional days + BigDecimal currentInterest = futureInstallments.stream() + .map(installment -> calculateCurrentInterest(loan, installment.getDueDate(), projectedFutureDate)) + .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2, RoundingMode.HALF_UP); + + return principalBalance.add(currentInterest).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal calculatePrincipalBalance(Loan loan) { + // Implement logic to calculate remaining principal balance + return loan.getLoanSummary().getTotalOutstanding(); + } + + private BigDecimal calculateCurrentInterest(Loan loan, LocalDate lastRepaymentDate, LocalDate projectedFutureDate) { + // Implement logic to calculate interest for additional days + return BigDecimal.ZERO; + } + + private BigDecimal calculateTotalOverdueBalance(LoanDebtProjectionData.OverdueBalanceDetails overdueDetails) { + return overdueDetails.getTotal(); + } + + private Long calculateProjectedOverdueDays(List overdueInstallments, LocalDate projectedFutureDate) { + if (overdueInstallments.isEmpty()) { + return 0L; + } + return overdueInstallments.stream().map(LoanRepaymentScheduleInstallment::getDueDate) + .map(dueDate -> DateUtils.getDifferenceInDays(dueDate, projectedFutureDate)).max(Long::compareTo).orElse(0L); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 27b81d3a1f6..806e05c1542 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -137,7 +137,7 @@ LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelated Collection retrieveLoanIdsWithPendingIncomePostingTransactions(); - LoanTransactionData retrieveLoanForeclosureTemplate(Long loanId, LocalDate transactionDate); + LoanTransactionData retrieveLoanForeclosureTemplate(Long loanId, LocalDate transactionDate, boolean isAnulado); LoanTransactionData retrieveLoanSpecialWriteOffTemplate(Long loanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index ece1e27004d..ec432a376bf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -52,6 +52,7 @@ import org.apache.fineract.custom.infrastructure.channel.data.ChannelData; import org.apache.fineract.custom.infrastructure.channel.domain.ChannelType; import org.apache.fineract.custom.infrastructure.channel.service.ChannelReadWritePlatformService; +import org.apache.fineract.custom.portfolio.externalcharge.honoratio.domain.CustomChargeHonorarioMap; import org.apache.fineract.infrastructure.clientblockingreasons.data.BlockingReasonsData; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; @@ -70,10 +71,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.*; import org.apache.fineract.organisation.staff.data.StaffData; import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.portfolio.account.data.AccountTransferData; @@ -99,6 +97,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData; import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService; @@ -109,18 +108,7 @@ import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.*; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; -import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.*; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; @@ -469,9 +457,39 @@ public LoanTransactionData retrieveLoanTransactionTemplate(final Long loanId) { final Collection paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); final SearchParameters channelSearchParameters = SearchParameters.builder().channelType(ChannelType.REPAYMENT.getValue()) .active(true).build(); + + Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + Boolean isCalculate = false; + Boolean isHasCustomHonoraioMap = loan.getActiveCharges().stream() + .anyMatch(charge -> charge.getChargeCalculation().isFlatHono() || charge.getChargeCalculation().isPercentageOfHonorarios()); + Optional loanCharge = loan.getActiveCharges().stream() + .filter(chg -> chg.isFlatHono() || chg.getChargeCalculation().isPercentageOfHonorarios()).findFirst(); + if (loanCharge.isPresent()) { + isCalculate = true; + Set customChargeHonorarioMaps = loanCharge.get().getCustomChargeHonorarioMaps(); + if (!customChargeHonorarioMaps.isEmpty()) { + isCalculate = false; + } + } + Integer ageOverdue = loan.getAgeOfOverdueDays(DateUtils.getBusinessLocalDate()).intValue(); + DelinquencyRangeData delinquencyRangeData = delinquencyReadPlatformService.retrieveCurrentDelinquencyTag(loan.getId()); + BigDecimal delinquencyValue = BigDecimal.ZERO; + if (delinquencyRangeData != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRangeData.getPercentageValue()); + } else { + DelinquencyRange delinquencyRange = delinquencyReadPlatformService.retrieveDelinquencyRangeCategeory(ageOverdue); + if (delinquencyRange != null) { + delinquencyValue = BigDecimal.valueOf(delinquencyRange.getPercentageValue()); + } + } + BigDecimal deliquncyrange = delinquencyValue.divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + Integer vatConfig = configurationDomainService.retriveIvaConfiguration(); + BigDecimal vatPercentage = BigDecimal.valueOf(vatConfig).divide(new BigDecimal(100), 2, MoneyHelper.getRoundingMode()); + final List channelOptions = channelReadWritePlatformService.findBySearchParam(channelSearchParameters); final LoanTransactionData loanTransactionTemplate = LoanTransactionData.templateOnTop(loanTransactionData, paymentOptions); final Collection bankOptions = codeValueReadPlatformService.retrieveCodeValuesByCode("Bancos"); + loanTransactionTemplate.setChannelOptions(channelOptions); loanTransactionTemplate.setBankOptions(bankOptions); @@ -480,6 +498,12 @@ public LoanTransactionData retrieveLoanTransactionTemplate(final Long loanId) { loanTransactionTemplate.setLoanScheduleType(loanTransactionData.getLoanScheduleType()); loanTransactionTemplate.setLoanProductType(loanTransactionData.getLoanProductType()); + loanTransactionTemplate.setDeliquencyPercentage(deliquncyrange); + loanTransactionTemplate.setAgeOfOverdue(ageOverdue); + loanTransactionTemplate.setIvaPercentage(vatPercentage); + loanTransactionTemplate.setHaveHono(isHasCustomHonoraioMap); + loanTransactionTemplate.setIsCalculate(isCalculate); + return loanTransactionTemplate; } @@ -1525,8 +1549,10 @@ public LoanScheduleData extractData(@NotNull final ResultSet rs) throws SQLExcep this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalDue); } BigDecimal advancePrincipalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "advancePrincipalAmount"); - this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount); - outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount); + if (this.outstandingLoanPrincipalBalance.compareTo(BigDecimal.ZERO) > 0) { + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount); + outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount); + } final boolean isDownPayment = rs.getBoolean("isDownPayment"); LoanSchedulePeriodData periodData; @@ -1670,7 +1696,7 @@ left join ( select mlcpd.loan_transaction_id , sum(mlcpd.amount) amount from m_loan_charge_paid_by mlcpd join m_loan_charge mlc on mlc.id = mlcpd.loan_charge_id - where mlc.charge_calculation_enum = 41 + where mlc.charge_calculation_enum in (41, 286) group by mlcpd.loan_transaction_id ) aval on aval.loan_transaction_id = tr.id left join ( @@ -1680,7 +1706,7 @@ select mlcpd.loan_transaction_id, sum(mlcpd.amount) amount from join m_charge mc on mc.id = mlc.charge_id join m_charge parent on parent.id = mc.parent_charge_id where mc.charge_calculation_enum = 342 - and parent.charge_calculation_enum = 41 + and parent.charge_calculation_enum in (41, 286) group by mlcpd.loan_transaction_id ) vat_aval on vat_aval.loan_transaction_id = tr.id left join ( @@ -2014,6 +2040,8 @@ public Collection retrieveAllLoansWithOverdueInstallmen // Only apply for duedate = yesterday (so that we don't apply // penalties on the duedate itself) sqlBuilder.append(" and ls.duedate >= " + sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "(? + 1)", "day")); + // order by installment duedate + sqlBuilder.append(" order by ls.duedate"); return this.jdbcTemplate.query(sqlBuilder.toString(), rm, penaltyWaitPeriod, penaltyWaitPeriod); } @@ -2834,9 +2862,13 @@ public Collection retrieveLoanIdsWithPendingIncomePostingTransactions() { } @Override - public LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, final LocalDate transactionDate) { + public LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, final LocalDate transactionDate, boolean isAnulado) { this.context.authenticatedUser(); final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + if (isAnulado && transactionDate.equals(loan.getDisbursementDate())) { + loan.setAnulado(true); + loan.setAnuladoOnDisbursementDate(true); + } loan.validateForForeclosure(transactionDate); final MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); @@ -2845,7 +2877,9 @@ public LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, fi final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate(); - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate); + final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate, + scheduleGeneratorDTO); BigDecimal unrecognizedIncomePortion = null; final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT); final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); @@ -3309,7 +3343,8 @@ ml.id loanId, min(mlrs.installment) installment, mlc.id loanChargeId public String suspensionSchema() { return """ - distinct ml.id loanId, min(mlrs.installment) installment, mlc.id loanChargeId + distinct ml.id loanId, min(mlrs.installment) installment, mlc.id loanChargeId, mlaa.overdue_since_date_derived overdue_date, + ? suspension_period from m_loan ml join m_loan_arrears_aging mlaa on mlaa.loan_id = ml.id @@ -3328,8 +3363,10 @@ public DefaultOrCancelInsuranceInstallmentData mapRow(@NotNull ResultSet rs, int final Long loanId = JdbcSupport.getLong(rs, "loanId"); final Integer installment = JdbcSupport.getInteger(rs, "installment"); final Long loanChargeId = JdbcSupport.getLong(rs, "loanChargeId"); + final LocalDate overDueDate = JdbcSupport.getLocalDate(rs, "overdue_date"); + final Long suspensionPeriod = JdbcSupport.getLong(rs, "suspension_period"); - return new DefaultOrCancelInsuranceInstallmentData(loanId, loanChargeId, installment); + return new DefaultOrCancelInsuranceInstallmentData(loanId, loanChargeId, installment, overDueDate.plusDays(suspensionPeriod)); } } @@ -3340,7 +3377,7 @@ public List getLoanDataWithDefaultOrCan final DefaultInsuranceMapper rowMapper = new DefaultInsuranceMapper(); String sql = "SELECT " + rowMapper.schema(); Object[] params = null; - sql = sql + " and mc.charge_calculation_enum = " + ChargeCalculationType.FLAT_SEGOVOLUNTARIO.getValue(); + // sql = sql + " and mc.charge_calculation_enum = " + ChargeCalculationType.FLAT_SEGOVOLUNTARIO.getValue(); if (loanId == null && insuranceCode == null) { sql = sql + " and mlrs.duedate < CURRENT_DATE " + " and CURRENT_DATE - mlrs.duedate = 60 "; params = new Object[] {}; @@ -3367,8 +3404,9 @@ public List getLoanDataWithDefaultManda final DefaultInsuranceMapper rowMapper = new DefaultInsuranceMapper(); String sql = "SELECT " + rowMapper.suspensionSchema(); - Object[] params = new Object[] { numberOfDays }; - sql = sql + " and (CURRENT_DATE - mlaa.overdue_since_date_derived) >= ? " + " group by ml.id, mlc.id order by ml.id"; + Object[] params = new Object[] { numberOfDays, numberOfDays }; + sql = sql + " and (CURRENT_DATE - mlaa.overdue_since_date_derived) >= ? " + + " group by ml.id, mlc.id, mlaa.overdue_since_date_derived order by ml.id"; return this.jdbcTemplate.query(sql, rowMapper, params); // NOSONAR } @@ -3481,7 +3519,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum = 41 + where mlc.charge_calculation_enum in (41, 286) group by mlc.loan_id ) aval_chg ON aval_chg.loan_id = ml.id LEFT JOIN ( @@ -3493,7 +3531,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum = 41 + WHERE parent_charge.charge_calculation_enum in (41, 286) group by mlc2.loan_id ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id LEFT join ( @@ -3503,7 +3541,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) group by mlc.loan_id ) other_chg ON other_chg.loan_id = ml.id LEFT JOIN ( @@ -3515,7 +3553,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41) + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) group by mlc2.loan_id ) other_vat_chg ON other_vat_chg.loan_id = ml.id LEFT join ( @@ -3609,7 +3647,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum = 41 + where mlc.charge_calculation_enum in (41, 286) group by mlc.loan_id ) aval_chg ON aval_chg.loan_id = ml.id LEFT JOIN ( @@ -3621,7 +3659,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum = 41 + WHERE parent_charge.charge_calculation_enum in (41, 286) group by mlc2.loan_id ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id LEFT join ( @@ -3631,7 +3669,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) group by mlc.loan_id ) other_chg ON other_chg.loan_id = ml.id LEFT JOIN ( @@ -3643,7 +3681,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41) + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) group by mlc2.loan_id ) other_vat_chg ON other_vat_chg.loan_id = ml.id LEFT join ( @@ -3737,7 +3775,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum = 41 + where mlc.charge_calculation_enum in (41, 286) group by mlc.loan_id ) aval_chg ON aval_chg.loan_id = ml.id LEFT JOIN ( @@ -3749,7 +3787,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum = 41 + WHERE parent_charge.charge_calculation_enum in (41, 286) group by mlc2.loan_id ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id LEFT join ( @@ -3759,7 +3797,7 @@ select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from and mlrs.completed_derived != true and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) - where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) group by mlc.loan_id ) other_chg ON other_chg.loan_id = ml.id LEFT JOIN ( @@ -3771,7 +3809,7 @@ SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FRO and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id - WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41) + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) group by mlc2.loan_id ) other_vat_chg ON other_vat_chg.loan_id = ml.id LEFT join ( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index f5b3362639f..09f0aa5ecc0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -35,6 +35,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.invoice.data.ClasificacionConceptosData; +import org.apache.fineract.portfolio.loanaccount.invoice.data.LoanDocumentData; import org.springframework.transaction.annotation.Transactional; public interface LoanWritePlatformService { @@ -142,4 +144,8 @@ void applyMeetingDateChanges(Calendar calendar, Collection loa @Transactional CommandProcessingResult claimLoan(Long loanId, JsonCommand command); + + void processAndSaveLoanDocument(LoanDocumentData loanDocumentData); + + ClasificacionConceptosData getClasificacionConceptosData(String concepto); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 4dc4a1b3ac7..e5a2fc505f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -6,9 +6,9 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * + *

* http://www.apache.org/licenses/LICENSE-2.0 - * + *

* Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -27,9 +27,11 @@ import jakarta.annotation.PostConstruct; import java.math.BigDecimal; import java.math.MathContext; +import java.math.RoundingMode; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; +import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -99,6 +101,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanChargebackTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseAsRescheduleBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCreditNoteBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDebitNoteBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDisbursalBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanInitiateTransferBusinessEvent; @@ -117,6 +120,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanInvoiceGenerationPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent; @@ -189,13 +193,14 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; import org.apache.fineract.portfolio.loanaccount.data.LoanRescheduleData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; -import org.apache.fineract.portfolio.loanaccount.domain.LoanBlockingReasonRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; @@ -229,6 +234,12 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; +import org.apache.fineract.portfolio.loanaccount.invoice.data.ClasificacionConceptosData; +import org.apache.fineract.portfolio.loanaccount.invoice.data.LoanDocumentData; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicMensualRepository; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.FacturaElectronicaMensual; +import org.apache.fineract.portfolio.loanaccount.invoice.domain.LoanDocumentConcept; +import org.apache.fineract.portfolio.loanaccount.jobs.facturaelectronicamensual.FacturaElectronicaMensualTasklet; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; @@ -236,6 +247,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestData; @@ -250,6 +262,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductType; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; +import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterization; +import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterizationRepository; +import org.apache.fineract.portfolio.loanproductparameterization.exception.LoanProductParameterizationNotFoundException; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; @@ -332,11 +347,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final InsuranceIncidentNoveltyNewsRepository insuranceIncidentNoveltyNewsRepository; private final LoanScheduleGeneratorFactory loanScheduleFactory; private final BlockingReasonSettingsRepositoryWrapper blockingReasonSettingsRepositoryWrapper; - private final LoanBlockingReasonRepository blockingReasonRepository; + private final FacturaElectronicMensualRepository facturaElectronicMensualRepository; + private final LoanProductParameterizationRepository productParameterizationRepository; @PostConstruct public void registerForNotification() { businessEventNotifierService.addPostBusinessEventListener(LoanDisbursalBusinessEvent.class, new DisbursementEventListener()); + businessEventNotifierService.addPostBusinessEventListener((LoanInvoiceGenerationPostBusinessEvent.class), + new LoanInvoiceGenerationPostBusinessEventListener()); + businessEventNotifierService.addPostBusinessEventListener((LoanCreditNoteBusinessEvent.class), + new LoanCreditNoteGenerationPostBusinessEventListener()); } @Transactional @@ -561,8 +581,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand "Disbursal date of this loan application " + loan.getDisbursementDate() + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); } - - final LoanRepaymentScheduleInstallment foreCloseDetail = loanToClose.fetchLoanForeclosureDetail(actualDisbursementDate); + final LoanRepaymentScheduleInstallment foreCloseDetail = loanToClose.fetchLoanForeclosureDetail(actualDisbursementDate, + scheduleGeneratorDTO); BigDecimal loanOutstanding = foreCloseDetail.getTotalOutstanding(loanToClose.getCurrency()).getAmount(); /* * BigDecimal loanOutstanding = this.loanReadPlatformService @@ -1164,6 +1184,10 @@ public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final } Loan loan = this.loanAssembler.assembleFrom(loanId); final LoanProduct loanProduct = loan.loanProduct(); + if (!loanProduct.getCustomAllowCollections()) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.collection.not.allowed.on.this.product", + "Collection is not allowed for this loan product", loanProduct.getName()); + } final Long repaymentChannelId = command.longValueOfParameterNamed("repaymentChannelId"); final boolean isImportedTransaction = command.booleanPrimitiveValueOfParameterNamed("isImportedTransaction"); ChannelData channelData; @@ -1219,19 +1243,26 @@ public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final boolean recalculateEMI = command.booleanPrimitiveValueOfParameterNamed("reduceInstallmentAmount"); loan.setRecalculateEMI(recalculateEMI); + BigDecimal totalExpectedRepayment = loan.getLoanSummary().getTotalExpectedRepayment(); + final LoanStatus loanStatus = loan.getStatus(); + final boolean isBankChannel = channelData.getName().equalsIgnoreCase("Bancos") + || channelData.getHash().equalsIgnoreCase("1ae8d4db830eed577c6023998337d0hags546f1a3ba08e5df1ef0d1673431a3"); + + if ((transactionAmount.compareTo(totalExpectedRepayment) > 0 && !isBankChannel)) { + final String totalOverpaid = transactionAmount.subtract(totalExpectedRepayment).toString(); + handleOverPaidException(totalOverpaid); + } + LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); loan = loanTransaction.getLoan(); - final LoanStatus loanStatus = loan.getStatus(); - final boolean isBankChannel = channelData.getName().equalsIgnoreCase("Bancos") - || channelData.getHash().equalsIgnoreCase("1ae8d4db830eed577c6023998337d0hags546f1a3ba08e5df1ef0d1673431a3"); - if (loanStatus.isOverpaid() && !isBankChannel) { + + // we also want to validate that the repayment amount is not greater than the outstanding amount + + if ((loanStatus.isOverpaid() && !isBankChannel)) { final String totalOverpaid = Money.of(loan.getCurrency(), loan.getTotalOverpaid()).toString(); - throw new GeneralPlatformDomainRuleException("error.msg.loan.channel.repayment.is.greater.than.outstanding.amount", - String.format("Repayment rejected for this channel! Repayment amount is greater than the outstanding amount by %s", - totalOverpaid), - totalOverpaid); + handleOverPaidException(totalOverpaid); } // Update loan transaction on repayment. @@ -1252,18 +1283,7 @@ public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final } if (loan.getStatus().isClosed()) { - InsuranceIncident incident = this.insuranceIncidentRepository - .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); - if (incident != null) { - BigDecimal cumulative = BigDecimal.ZERO; - List loanCharges = loan.getLoanCharges().stream().filter(lc -> lc.getChargeCalculation().isVoluntaryInsurance()) - .toList(); - for (LoanCharge loanCharge : loanCharges) { - InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, 0, - incident, loan.getClosedOnDate(), cumulative); - this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); - } - } + createCancellationNoveltyNews(loan, loan.getClosedOnDate()); } return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // @@ -1277,6 +1297,34 @@ public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final .build(); } + private static void handleOverPaidException(String totalOverpaid) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.channel.repayment.is.greater.than.outstanding.amount", + String.format("Repayment rejected for this channel! Repayment amount is greater than the outstanding amount by %s", + totalOverpaid), + totalOverpaid); + } + + private void createCancellationNoveltyNews(Loan loan, LocalDate writeOffDate) { + InsuranceIncident incident = this.insuranceIncidentRepository + .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); + if (incident == null || (!incident.isMandatory() && !incident.isVoluntary())) { + throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION.name()); + } + + for (LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { + if ((incident.isMandatory() && loanCharge.isMandatoryInsurance()) + || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { + BigDecimal cumulative = BigDecimal.ZERO; + InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, + null, incident, writeOffDate, cumulative); + + this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + } + } + } + } + private ChannelData validateRepaymentChannel(final String channelName, final LoanProduct loanProduct) { if (StringUtils.isBlank(channelName)) { throw new GeneralPlatformDomainRuleException("validation.msg.channel.is.blank", "Channel is blank"); @@ -1388,18 +1436,7 @@ public Map makeLoanBulkRepayment(final CollectionSheetBulkRepaym transactionIds.add(loanTransaction.getId()); if (loan.getStatus().isClosed()) { - InsuranceIncident incident = this.insuranceIncidentRepository - .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); - if (incident != null) { - BigDecimal cumulative = BigDecimal.ZERO; - List loanCharges = loan.getLoanCharges().stream() - .filter(lc -> lc.getChargeCalculation().isVoluntaryInsurance()).toList(); - for (LoanCharge loanCharge : loanCharges) { - InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, - loanCharge, 0, incident, loan.getClosedOnDate(), cumulative); - this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); - } - } + createCancellationNoveltyNews(loan, loan.getClosedOnDate()); } } } @@ -1441,13 +1478,6 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo "Loan transaction:" + transactionId + " update not allowed as loan transaction is linked to other transactions", transactionId); } - - if (transactionToAdjust.isSpecialWriteOff()) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.update.not.allowed", - "Loan transaction:" + transactionId + " update not allowed as loan transaction is a special write off transaction", - transactionId); - } - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); @@ -1459,7 +1489,7 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo throw new InvalidLoanTransactionTypeException("transaction", "error.msg.loan.transaction.update.not.allowed", errorMessage); } - // We dont need auto generation for reversal external id... if it is not provided, it remains null (empty) + // We don't need auto generation for reversal external id... if it is not provided, it remains null (empty) final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId); @@ -1525,11 +1555,9 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo } ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId); - boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); if (thereIsNewTransaction) { if (paymentDetail != null) { @@ -1542,11 +1570,7 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo || channelData.getHash().equalsIgnoreCase("1ae8d4db830eed577c6023998337d0hags546f1a3ba08e5df1ef0d1673431a3"); if (loanStatus.isOverpaid() && !isBankChannel) { final String totalOverpaid = Money.of(loan.getCurrency(), loan.getTotalOverpaid()).toString(); - throw new GeneralPlatformDomainRuleException("error.msg.loan.channel.repayment.is.greater.than.outstanding.amount", - String.format( - "Repayment rejected for this channel! Repayment amount is greater than the outstanding amount by %s", - totalOverpaid), - totalOverpaid); + handleOverPaidException(totalOverpaid); } } } @@ -1565,7 +1589,14 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.isProgressiveLoan()) { + scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + } + scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null); + loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); @@ -1618,6 +1649,15 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo if (!isAdjustCommand) { this.businessEventNotifierService.notifyPostBusinessEvent(new LoanTxReversalBusinessEvent(transactionToAdjust)); } + final LocalDate previousTransactionDate = transactionToAdjust.getTransactionDate(); + if (DateUtils.isEqual(previousTransactionDate, transactionDate) && transactionToAdjust.hasOccurredOnSuspendedAccount()) { + final Long loanTransactionId = transactionToAdjust.getId(); + final List facturaElectronicMensuals = this.facturaElectronicMensualRepository + .findByLoanTransactionId(loanTransactionId); + if (CollectionUtils.isNotEmpty(facturaElectronicMensuals)) { + this.facturaElectronicMensualRepository.deleteAll(facturaElectronicMensuals); + } + } return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(entityId) // @@ -1912,8 +1952,10 @@ public CommandProcessingResult writeOff(final Long loanId, final JsonCommand com loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccountDomainService.recalculateAccruals(loan); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + createWriteOffNoveltyNews(loan, transactionDate); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(writeOff.getId()) // @@ -1925,6 +1967,27 @@ public CommandProcessingResult writeOff(final Long loanId, final JsonCommand com .with(changes).build(); } + private void createWriteOffNoveltyNews(Loan loan, LocalDate writeOffDate) { + InsuranceIncident incident = this.insuranceIncidentRepository + .findByIncidentType(InsuranceIncidentType.PORTFOLIO_WRITE_OFF_CANCELLATION); + if (incident == null || (!incident.isMandatory() && !incident.isVoluntary())) { + throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.PORTFOLIO_WRITE_OFF_CANCELLATION.name()); + } + + for (LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) { + if ((incident.isMandatory() && loanCharge.isMandatoryInsurance()) + || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { + BigDecimal cumulative = BigDecimal.ZERO; + InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, + null, incident, writeOffDate, cumulative); + + this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + } + } + } + } + @Transactional @Override public CommandProcessingResult specialWriteOff(final Long loanId, final JsonCommand command) { @@ -3487,27 +3550,23 @@ public CommandProcessingResult forecloseLoan(final Long loanId, final JsonComman this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, loanRescheduleRequest); - List cancelInsuranceInstallmentIds = this.loanReadPlatformService - .getLoanDataWithDefaultOrCancelInsurance(loanId, null, transactionDate); - InsuranceIncident incident = this.insuranceIncidentRepository - .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); - if (incident == null) { - throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION.name()); - } - for (DefaultOrCancelInsuranceInstallmentData data : cancelInsuranceInstallmentIds) { - LoanCharge loanCharge = null; - Optional loanChargeOptional = loan.getLoanCharges().stream() - .filter(lc -> Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); - if (loanChargeOptional.isPresent()) { - loanCharge = loanChargeOptional.get(); - } - BigDecimal cumulative = BigDecimal.ZERO; - cumulative = processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, true); - InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, - data.installment(), incident, transactionDate, cumulative); - - this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); - } + /* + * List cancelInsuranceInstallmentIds = this.loanReadPlatformService + * .getLoanDataWithDefaultOrCancelInsurance(loanId, null, transactionDate); InsuranceIncident incident = + * this.insuranceIncidentRepository .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); if + * (incident == null) { throw new + * InsuranceIncidentNotFoundException(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION.name()); } for + * (DefaultOrCancelInsuranceInstallmentData data : cancelInsuranceInstallmentIds) { LoanCharge loanCharge = + * null; Optional loanChargeOptional = loan.getLoanCharges().stream() .filter(lc -> + * Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); if (loanChargeOptional.isPresent()) { + * loanCharge = loanChargeOptional.get(); } BigDecimal cumulative = BigDecimal.ZERO; cumulative = + * processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, true); InsuranceIncidentNoveltyNews + * insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, data.installment(), + * incident, transactionDate, cumulative); + * + * this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); } + */ + createCancellationNoveltyNews(loan, transactionDate); LoanTransaction foreclosureTransaction = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText, externalId, changes); @@ -3544,25 +3603,25 @@ public CommandProcessingResult cancelLoan(final Long loanId, final JsonCommand c } } this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, null); - List cancelInsuranceInstallmentIds = this.loanReadPlatformService - .getLoanDataWithDefaultOrCancelInsurance(loanId, null, transactionDate); - InsuranceIncident incident = this.insuranceIncidentRepository - .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); - if (incident == null) { - throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION.name()); - } - for (final DefaultOrCancelInsuranceInstallmentData data : cancelInsuranceInstallmentIds) { - LoanCharge loanCharge = null; - Optional loanChargeOptional = loan.getLoanCharges().stream() - .filter(lc -> Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); - if (loanChargeOptional.isPresent()) { - loanCharge = loanChargeOptional.get(); - } - BigDecimal cumulative = BigDecimal.ZERO; - cumulative = processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, true); - InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, - data.installment(), incident, transactionDate, cumulative); - this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + /* + * List cancelInsuranceInstallmentIds = this.loanReadPlatformService + * .getLoanDataWithDefaultOrCancelInsurance(loanId, null, transactionDate); InsuranceIncident incident = + * this.insuranceIncidentRepository .findByIncidentType(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION); if + * (incident == null) { throw new + * InsuranceIncidentNotFoundException(InsuranceIncidentType.DEFINITIVE_FINAL_CANCELLATION.name()); } for (final + * DefaultOrCancelInsuranceInstallmentData data : cancelInsuranceInstallmentIds) { LoanCharge loanCharge = null; + * Optional loanChargeOptional = loan.getLoanCharges().stream() .filter(lc -> + * Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); if (loanChargeOptional.isPresent()) { + * loanCharge = loanChargeOptional.get(); } BigDecimal cumulative = BigDecimal.ZERO; cumulative = + * processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, true); InsuranceIncidentNoveltyNews + * insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, data.installment(), + * incident, transactionDate, cumulative); + * this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); } + */ + createCancellationNoveltyNews(loan, transactionDate); + if (transactionDate.equals(loan.getDisbursementDate())) { + loan.setAnulado(true); + loan.setAnuladoOnDisbursementDate(true); } final LoanTransaction foreclosureTransaction = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText, externalId, changes); @@ -3612,18 +3671,40 @@ public CommandProcessingResult chargeOff(JsonCommand command) { checkIfProductAllowsCancelationOrReversal(loan); - businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeOffPreBusinessEvent(loan)); + loan.markAsChargedOff(transactionDate, currentUser, null); - if (command.hasParameter(LoanApiConstants.chargeOffReasonIdParamName)) { - Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName); - CodeValue chargeOffReason = this.codeValueRepository - .findOneByCodeNameAndIdWithNotFoundDetection(LoanApiConstants.CHARGE_OFF_REASONS, chargeOffReasonId); - changes.put(LoanApiConstants.chargeOffReasonIdParamName, chargeOffReasonId); - loan.markAsChargedOff(transactionDate, currentUser, chargeOffReason); - } else { - loan.markAsChargedOff(transactionDate, currentUser, null); + InsuranceIncidentType incidentType = InsuranceIncidentType.DEATH_CANCELLATION; + if (command.hasParameter("incidentTypeId")) { + Integer incidentTypeId = command.integerValueOfParameterNamed("incidentTypeId"); + incidentType = InsuranceIncidentType.fromInt(incidentTypeId); + } + + this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, null); + List cancelInsuranceInstallmentIds = this.loanReadPlatformService + .getLoanDataWithDefaultOrCancelInsurance(loanId, null, transactionDate); + InsuranceIncident incident = this.insuranceIncidentRepository.findByIncidentType(incidentType); + if (incident == null) { + throw new InsuranceIncidentNotFoundException(InsuranceIncidentType.DEATH_CANCELLATION.name()); + } + for (final DefaultOrCancelInsuranceInstallmentData data : cancelInsuranceInstallmentIds) { + LoanCharge loanCharge = null; + Optional loanChargeOptional = loan.getLoanCharges().stream() + .filter(lc -> Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); + if (loanChargeOptional.isPresent()) { + loanCharge = loanChargeOptional.get(); + if ((incident.isMandatory() && loanCharge.isMandatoryInsurance()) + || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { + BigDecimal cumulative = BigDecimal.ZERO; + cumulative = processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, true); + InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, + data.installment(), incident, transactionDate, cumulative); + this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + } + } } + businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeOffPreBusinessEvent(loan)); + final List existingTransactionIds = loan.findExistingTransactionIds(); final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); loan.getLoanCustomizationDetail().recordActivity(); @@ -3640,6 +3721,11 @@ public CommandProcessingResult chargeOff(JsonCommand command) { this.noteRepository.save(note); } + this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText, txnExternalId, changes); + final BlockingReasonSetting blockingReasonSetting = loanBlockingReasonRepository.getSingleBlockingReasonSettingByReason( + BlockingReasonSettingEnum.CREDIT_CANCELADO.getDatabaseString(), BlockLevel.CREDIT.toString()); + loanBlockWritePlatformService.blockLoan(loan.getId(), blockingReasonSetting, "CANCELADO", DateUtils.getLocalDateOfTenant()); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction)); return new CommandProcessingResultBuilder() // @@ -3741,17 +3827,18 @@ private void validateTransactionsForTransfer(final Loan loan, final LocalDate tr @Override public void recalculateInterestForMaximumLegalRate() throws JobExecutionException { - List exceptions = new ArrayList<>(); + final List exceptions = new ArrayList<>(); final MaximumCreditRateConfigurationData maximumCreditRateConfigurationData = this.loanProductReadPlatformService .retrieveMaximumCreditRateConfigurationData(); final LocalDate appliedOnDate = maximumCreditRateConfigurationData.getAppliedOnDate(); final BigDecimal maximumLegalAnnualNominalRateValue = maximumCreditRateConfigurationData.getAnnualNominalRate(); final LoanRescheduleMapper rm = new LoanRescheduleMapper(); final String sql = "SELECT " + rm.schema(); - final Object[] params = new Object[] { appliedOnDate, appliedOnDate, appliedOnDate, maximumLegalAnnualNominalRateValue }; - List loanLoanRescheduleDataList = this.jdbcTemplate.query(sql, rm, params); + final Object[] params = new Object[] { appliedOnDate, appliedOnDate, appliedOnDate, maximumLegalAnnualNominalRateValue, + maximumLegalAnnualNominalRateValue }; + final List loanLoanRescheduleDataList = this.jdbcTemplate.query(sql, rm, params); if (CollectionUtils.isNotEmpty(loanLoanRescheduleDataList)) { - final String locale = "en"; + final String locale = "es"; final String dateFormat = "dd MMMM yyyy"; final String submittedOnDate = DateUtils.format(DateUtils.getBusinessLocalDate(), dateFormat, Locale.forLanguageTag(locale)); LoanRescheduleRequestData loanRescheduleReasons = this.loanRescheduleRequestReadPlatformService @@ -3768,7 +3855,6 @@ public void recalculateInterestForMaximumLegalRate() throws JobExecutionExceptio rescheduleJsonObject.addProperty("locale", locale); rescheduleJsonObject.addProperty("rescheduleReasonId", rescheduleReasonId); rescheduleJsonObject.addProperty("submittedOnDate", submittedOnDate); - rescheduleJsonObject.addProperty("rescheduleReasonComment", "Recalcular la tasa de interés al máximo legal"); rescheduleJsonObject.addProperty("adjustedDueDate", ""); rescheduleJsonObject.addProperty("graceOnPrincipal", ""); rescheduleJsonObject.addProperty("extraTerms", ""); @@ -3798,6 +3884,10 @@ public void recalculateInterestForMaximumLegalRate() throws JobExecutionExceptio final String rescheduleFromDateString = DateUtils.format(appliedOnDate, dateFormat, Locale.forLanguageTag(locale)); rescheduleJsonObject.addProperty("rescheduleFromDate", rescheduleFromDateString); rescheduleJsonObject.addProperty("loanId", loanId); + final String rescheduleReasonComment = String.format( + "Recalcular la tasa de interés al máximo legal: [Nueva tasa de interés: %s, Tasa máxima legal: %s, Fecha de reprogramación: %s]", + newInterestRate, maximumLegalAnnualNominalRateValue, rescheduleFromDateString); + rescheduleJsonObject.addProperty("rescheduleReasonComment", rescheduleReasonComment); final String rescheduleRequestBodyAsJson = rescheduleJsonObject.toString(); CommandWrapper commandWrapper = new CommandWrapperBuilder() .createLoanRescheduleRequest(RescheduleLoansApiConstants.ENTITY_NAME).withJson(rescheduleRequestBodyAsJson).build(); @@ -3869,8 +3959,11 @@ SELECT DISTINCT ON (ltv.loan_id) ltv.loan_id, ltv.applicable_date, ltv.decimal_v WHERE ltv.term_type = 10 AND ltv.is_active = TRUE AND ltv.applied_on_loan_status = 300 AND ltv.applicable_date >= ? ORDER BY ltv.loan_id, ltv.id DESC ) term_variation ON term_variation.loan_id = ml.id - WHERE ml.loan_status_id = 300 AND mlrs.duedate >= ? - AND (CASE WHEN term_variation.decimal_value IS NOT NULL THEN term_variation.decimal_value ELSE ml.annual_nominal_interest_rate END) != ? + WHERE ml.loan_status_id = 300 AND mlrs.duedate >= ? AND ml.is_charged_off = FALSE + AND (CASE + WHEN term_variation.decimal_value IS NOT NULL THEN term_variation.decimal_value != ? + WHEN term_variation.decimal_value IS NULL THEN ml.annual_nominal_interest_rate > ? + END) GROUP BY term_variation.loan_id, ml.annual_nominal_interest_rate, term_variation.decimal_value, ml.id ORDER BY ml.id """; @@ -3951,6 +4044,353 @@ public void onBusinessEvent(LoanDisbursalBusinessEvent event) { } } + private final class LoanInvoiceGenerationPostBusinessEventListener + implements BusinessEventListener { + + @Override + public void onBusinessEvent(final LoanInvoiceGenerationPostBusinessEvent event) { + final LoanTransaction loanTransaction = event.get(); + if (loanTransaction != null && loanTransaction.getTypeOf().isRepaymentType()) { + generateLoanTransactionDocument(loanTransaction); + } + } + } + + private final class LoanCreditNoteGenerationPostBusinessEventListener implements BusinessEventListener { + + @Override + public void onBusinessEvent(final LoanCreditNoteBusinessEvent event) { + final LoanTransaction loanTransaction = event.get(); + if (loanTransaction != null) { + generateLoanTransactionDocument(loanTransaction); + } + } + } + + private void generateLoanTransactionDocument(final LoanTransaction loanTransaction) { + final Long loanTransactionId = loanTransaction.getId(); + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final YearMonth yearMonth = YearMonth.from(transactionDate); + final LocalDate lastDayOfMonth = yearMonth.atEndOfMonth(); + final LocalDate firstDayOfMonth = transactionDate.withDayOfMonth(1); + final LocalDate secondLastDayOfMonth = lastDayOfMonth.minusDays(1); + final LoanTransactionType loanTransactionType = loanTransaction.getTypeOf(); + final FacturaElectronicaMensualTasklet.LoanInvoiceMapper transactionMapper = new FacturaElectronicaMensualTasklet.LoanInvoiceMapper(); + final String transactionSQL = "SELECT " + transactionMapper.transactionSchema() + " WHERE mlt.\"transactionId\" = ? "; + final List loanDocumentDataList = this.jdbcTemplate.query(transactionSQL, transactionMapper, loanTransactionId); + if (!loanDocumentDataList.isEmpty()) { + final LoanDocumentData loanDocumentData = loanDocumentDataList.get(0); + loanDocumentData.setFirstDayOfMonth(firstDayOfMonth); + loanDocumentData.setSecondLastDayOfMonth(secondLastDayOfMonth); + loanDocumentData.setLastDayOfMonth(lastDayOfMonth); + loanDocumentData.setLoanTransactionId(loanTransactionId); + if (loanTransactionType.isRepaymentType()) { + loanDocumentData.setDocumentType(LoanDocumentData.LoanDocumentType.INVOICE); + } else { + loanDocumentData.setDocumentType(LoanDocumentData.LoanDocumentType.CREDIT_NOTE); + } + processAndSaveLoanDocument(loanDocumentData); + loanTransaction.markAsOccurredOnSuspendedAccount(); + this.loanTransactionRepository.saveAndFlush(loanTransaction); + } + } + + @Override + public void processAndSaveLoanDocument(final LoanDocumentData loanDocumentData) { + final List facturaElectronicaMensuals = new ArrayList<>(); + final FacturaElectronicaMensual facturaElectronicaMensual = loanDocumentData.toEntity(); + final Integer itemsCount = loanDocumentData.getItemsCount(); + facturaElectronicaMensual.setTotal_unidades(String.valueOf(itemsCount)); + final BigDecimal interestPaid = loanDocumentData.getInterestPaid(); + final BigDecimal penaltyChargesPaid = loanDocumentData.getPenaltyChargesPaid(); + final BigDecimal mandatoryInsurancePaid = loanDocumentData.getMandatoryInsurancePaid(); + final BigDecimal voluntaryInsurancePaid = loanDocumentData.getVoluntaryInsurancePaid(); + final BigDecimal honorariosPaid = loanDocumentData.getHonorariosPaid(); + final Long productTypeParamId = loanDocumentData.getProductTypeParamId(); + final LoanProductParameterization loanProductParameterization = this.productParameterizationRepository.findById(productTypeParamId) + .orElseThrow(() -> new LoanProductParameterizationNotFoundException(productTypeParamId)); + final Long rangeStartNumber = loanProductParameterization.getRangeStartNumber(); + final Long invoiceCounter = loanProductParameterization.getInvoiceCounter(); + final Long creditNoteCounter = loanProductParameterization.getCreditNoteCounter(); + final Long rangeEndNumber = loanProductParameterization.getRangeEndNumber(); + long documentNumber; + String documentNumberString; + Long currentCounter; + List invoicesToKnockOff = new ArrayList<>(); + final LoanDocumentData.LoanDocumentType documentType = loanDocumentData.getDocumentType(); + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + currentCounter = ObjectUtils.defaultIfNull(creditNoteCounter, 0L) + 1L; + documentNumber = rangeStartNumber + currentCounter; + documentNumberString = "NC" + documentNumber; + loanProductParameterization.setCreditNoteCounter(currentCounter); + invoicesToKnockOff = this.facturaElectronicMensualRepository.findById_clienteAndTipo_prod(loanDocumentData.getClientIdNumber(), + loanDocumentData.getProductTypeName()); + } else { + currentCounter = ObjectUtils.defaultIfNull(invoiceCounter, 0L) + 1L; + documentNumber = rangeStartNumber + currentCounter; + documentNumberString = loanDocumentData.getBillingPrefix() + documentNumber; + loanProductParameterization.setInvoiceCounter(currentCounter); + } + if (currentCounter > rangeEndNumber) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.invoice.counter.exceeds.range.end.number", + String.format("Invoice counter exceeds the range end number: %s and product type: %s", rangeEndNumber, + loanProductParameterization.getProductType())); + } + facturaElectronicaMensual.setNumero_doc(documentNumberString); + facturaElectronicaMensual.setReferencia(String.valueOf(documentNumber)); + facturaElectronicaMensual.setCodigo_descuento("0"); + facturaElectronicaMensual.setPorcentajedescuento(BigDecimal.ZERO); + facturaElectronicaMensual.setDescuento(BigDecimal.ZERO); + facturaElectronicaMensual.setPorcentaje_impuesto_item(BigDecimal.ZERO); + facturaElectronicaMensual.setImpuesto_item(BigDecimal.ZERO); + long itemPosition = 0L; + if (interestPaid.compareTo(BigDecimal.ZERO) > 0) { + final LoanDocumentConcept loanDocumentConcept = LoanDocumentConcept.INT_CORRIENTE; + final FacturaElectronicaMensual facturaElectronicaMensualDuplicate = facturaElectronicaMensual.clone(); + itemPosition = itemPosition + 1; + facturaElectronicaMensualDuplicate.setPosicion(itemPosition); + facturaElectronicaMensualDuplicate.setCosto_total(interestPaid); + facturaElectronicaMensualDuplicate.setPrecio_unitario(interestPaid); + facturaElectronicaMensualDuplicate.setSku(loanDocumentConcept.getSku()); + facturaElectronicaMensualDuplicate.setNom_articulo(loanDocumentConcept.getName()); + facturaElectronicaMensualDuplicate.setId_mandante(null); + facturaElectronicaMensualDuplicate.setDescripcion_mandante(null); + facturaElectronicaMensuals.add(facturaElectronicaMensualDuplicate); + final ClasificacionConceptosData clasificacionConceptosData = this.getClasificacionConceptosData(loanDocumentConcept.name()); + if (clasificacionConceptosData != null) { + if (!clasificacionConceptosData.isExcluido() && clasificacionConceptosData.isGravado()) { + final BigDecimal tarifa = clasificacionConceptosData.getTarifa(); + final BigDecimal impuestoItem = interestPaid.multiply(tarifa).divide(BigDecimal.valueOf(100), + MoneyHelper.getRoundingMode()); + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(tarifa); + facturaElectronicaMensualDuplicate.setImpuesto_item(impuestoItem); + } else { + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(null); + facturaElectronicaMensualDuplicate.setImpuesto_item(BigDecimal.ZERO); + } + if (clasificacionConceptosData.isExento()) { + facturaElectronicaMensualDuplicate.setImpuesto(BigDecimal.ZERO); + } else if (clasificacionConceptosData.isExcluido()) { + facturaElectronicaMensualDuplicate.setImpuesto(null); + } else if (clasificacionConceptosData.isGravado()) { + facturaElectronicaMensualDuplicate.setImpuesto(clasificacionConceptosData.getTarifa()); + } + } + + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + if (!invoicesToKnockOff.isEmpty()) { + knockOffRecursively(invoicesToKnockOff, facturaElectronicaMensualDuplicate, facturaElectronicaMensuals); + } + } + } + if (penaltyChargesPaid.compareTo(BigDecimal.ZERO) > 0) { + final LoanDocumentConcept loanDocumentConcept = LoanDocumentConcept.INT_DE_MORA; + final FacturaElectronicaMensual facturaElectronicaMensualDuplicate = facturaElectronicaMensual.clone(); + itemPosition = itemPosition + 1; + facturaElectronicaMensualDuplicate.setPosicion(itemPosition); + facturaElectronicaMensualDuplicate.setCosto_total(penaltyChargesPaid); + facturaElectronicaMensualDuplicate.setPrecio_unitario(penaltyChargesPaid); + facturaElectronicaMensualDuplicate.setSku(loanDocumentConcept.getSku()); + facturaElectronicaMensualDuplicate.setNom_articulo(loanDocumentConcept.getName()); + facturaElectronicaMensualDuplicate.setId_mandante(null); + facturaElectronicaMensualDuplicate.setDescripcion_mandante(null); + facturaElectronicaMensuals.add(facturaElectronicaMensualDuplicate); + final ClasificacionConceptosData clasificacionConceptosData = this.getClasificacionConceptosData(loanDocumentConcept.name()); + if (clasificacionConceptosData != null) { + if (!clasificacionConceptosData.isExcluido() && clasificacionConceptosData.isGravado()) { + final BigDecimal tarifa = clasificacionConceptosData.getTarifa(); + final BigDecimal impuestoItem = penaltyChargesPaid.multiply(tarifa).divide(BigDecimal.valueOf(100), + MoneyHelper.getRoundingMode()); + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(tarifa); + facturaElectronicaMensualDuplicate.setImpuesto_item(impuestoItem); + } else { + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(null); + facturaElectronicaMensualDuplicate.setImpuesto_item(BigDecimal.ZERO); + } + if (clasificacionConceptosData.isExento()) { + facturaElectronicaMensualDuplicate.setImpuesto(BigDecimal.ZERO); + } else if (clasificacionConceptosData.isExcluido()) { + facturaElectronicaMensualDuplicate.setImpuesto(null); + } else if (clasificacionConceptosData.isGravado()) { + facturaElectronicaMensualDuplicate.setImpuesto(clasificacionConceptosData.getTarifa()); + } + } + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + if (!invoicesToKnockOff.isEmpty()) { + knockOffRecursively(invoicesToKnockOff, facturaElectronicaMensualDuplicate, facturaElectronicaMensuals); + } + } + } + if (mandatoryInsurancePaid.compareTo(BigDecimal.ZERO) > 0) { + final LoanDocumentConcept loanDocumentConcept = LoanDocumentConcept.SEGURO_OBLIGATORIO; + final FacturaElectronicaMensual facturaElectronicaMensualDuplicate = facturaElectronicaMensual.clone(); + itemPosition = itemPosition + 1; + facturaElectronicaMensualDuplicate.setPosicion(itemPosition); + facturaElectronicaMensualDuplicate.setCosto_total(mandatoryInsurancePaid); + facturaElectronicaMensualDuplicate.setPrecio_unitario(mandatoryInsurancePaid); + facturaElectronicaMensualDuplicate.setSku(loanDocumentConcept.getSku()); + facturaElectronicaMensualDuplicate.setNom_articulo(loanDocumentConcept.getName()); + facturaElectronicaMensuals.add(facturaElectronicaMensualDuplicate); + final ClasificacionConceptosData clasificacionConceptosData = this.getClasificacionConceptosData(loanDocumentConcept.name()); + if (clasificacionConceptosData != null) { + final String idMandante = clasificacionConceptosData.isMandato() ? loanDocumentData.getMandatoryInsuranceCode() : null; + final String descripcionMandante = clasificacionConceptosData.isMandato() ? loanDocumentData.getMandatoryInsuranceName() + : null; + facturaElectronicaMensualDuplicate.setId_mandante(idMandante); + facturaElectronicaMensualDuplicate.setDescripcion_mandante(descripcionMandante); + if (!clasificacionConceptosData.isExcluido() && clasificacionConceptosData.isGravado()) { + final BigDecimal tarifa = clasificacionConceptosData.getTarifa(); + final BigDecimal impuestoItem = mandatoryInsurancePaid.multiply(tarifa).divide(BigDecimal.valueOf(100), + MoneyHelper.getRoundingMode()); + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(tarifa); + facturaElectronicaMensualDuplicate.setImpuesto_item(impuestoItem); + } else { + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(null); + facturaElectronicaMensualDuplicate.setImpuesto_item(BigDecimal.ZERO); + } + if (clasificacionConceptosData.isExento()) { + facturaElectronicaMensualDuplicate.setImpuesto(BigDecimal.ZERO); + } else if (clasificacionConceptosData.isExcluido()) { + facturaElectronicaMensualDuplicate.setImpuesto(null); + } else if (clasificacionConceptosData.isGravado()) { + facturaElectronicaMensualDuplicate.setImpuesto(clasificacionConceptosData.getTarifa()); + } + } + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + if (!invoicesToKnockOff.isEmpty()) { + knockOffRecursively(invoicesToKnockOff, facturaElectronicaMensualDuplicate, facturaElectronicaMensuals); + } + } + } + if (voluntaryInsurancePaid.compareTo(BigDecimal.ZERO) > 0) { + final LoanDocumentConcept loanDocumentConcept = LoanDocumentConcept.SEGUROS_VOLUNTARIOS; + final FacturaElectronicaMensual facturaElectronicaMensualDuplicate = facturaElectronicaMensual.clone(); + itemPosition = itemPosition + 1; + facturaElectronicaMensualDuplicate.setPosicion(itemPosition); + facturaElectronicaMensualDuplicate.setCosto_total(voluntaryInsurancePaid); + facturaElectronicaMensualDuplicate.setPrecio_unitario(voluntaryInsurancePaid); + facturaElectronicaMensualDuplicate.setSku(loanDocumentConcept.getSku()); + facturaElectronicaMensualDuplicate.setNom_articulo(loanDocumentConcept.getName()); + facturaElectronicaMensuals.add(facturaElectronicaMensualDuplicate); + final ClasificacionConceptosData clasificacionConceptosData = this.getClasificacionConceptosData(loanDocumentConcept.name()); + if (clasificacionConceptosData != null) { + final String idMandante = clasificacionConceptosData.isMandato() ? loanDocumentData.getVoluntaryInsuranceCode() : null; + final String descripcionMandante = clasificacionConceptosData.isMandato() ? loanDocumentData.getVoluntaryInsuranceName() + : null; + facturaElectronicaMensualDuplicate.setId_mandante(idMandante); + facturaElectronicaMensualDuplicate.setDescripcion_mandante(descripcionMandante); + if (clasificacionConceptosData.isExento()) { + facturaElectronicaMensualDuplicate.setImpuesto(BigDecimal.ZERO); + } else if (clasificacionConceptosData.isExcluido()) { + facturaElectronicaMensualDuplicate.setImpuesto(null); + } else if (clasificacionConceptosData.isGravado()) { + facturaElectronicaMensualDuplicate.setImpuesto(clasificacionConceptosData.getTarifa()); + } + } + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + if (!invoicesToKnockOff.isEmpty()) { + knockOffRecursively(invoicesToKnockOff, facturaElectronicaMensualDuplicate, facturaElectronicaMensuals); + } + } + } + if (honorariosPaid.compareTo(BigDecimal.ZERO) > 0) { + final LoanDocumentConcept loanDocumentConcept = LoanDocumentConcept.HONORARIOS; + final FacturaElectronicaMensual facturaElectronicaMensualDuplicate = facturaElectronicaMensual.clone(); + itemPosition = itemPosition + 1; + facturaElectronicaMensualDuplicate.setPosicion(itemPosition); + facturaElectronicaMensualDuplicate.setCosto_total(honorariosPaid); + facturaElectronicaMensualDuplicate.setPrecio_unitario(honorariosPaid); + facturaElectronicaMensualDuplicate.setSku(loanDocumentConcept.getSku()); + facturaElectronicaMensualDuplicate.setNom_articulo(loanDocumentConcept.getName()); + facturaElectronicaMensuals.add(facturaElectronicaMensualDuplicate); + final ClasificacionConceptosData clasificacionConceptosData = this.getClasificacionConceptosData(loanDocumentConcept.name()); + if (clasificacionConceptosData != null) { + if (!clasificacionConceptosData.isExcluido() && clasificacionConceptosData.isGravado()) { + final BigDecimal tarifa = clasificacionConceptosData.getTarifa(); + final BigDecimal impuestoItem = honorariosPaid.multiply(tarifa).divide(BigDecimal.valueOf(100), + MoneyHelper.getRoundingMode()); + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(tarifa); + facturaElectronicaMensualDuplicate.setImpuesto_item(impuestoItem); + } else { + facturaElectronicaMensualDuplicate.setPorcentaje_impuesto_item(null); + facturaElectronicaMensualDuplicate.setImpuesto_item(BigDecimal.ZERO); + } + if (clasificacionConceptosData.isExento()) { + facturaElectronicaMensualDuplicate.setImpuesto(BigDecimal.ZERO); + } else if (clasificacionConceptosData.isExcluido()) { + facturaElectronicaMensualDuplicate.setImpuesto(null); + } else if (clasificacionConceptosData.isGravado()) { + facturaElectronicaMensualDuplicate.setImpuesto(clasificacionConceptosData.getTarifa()); + } + } + if (LoanDocumentData.LoanDocumentType.CREDIT_NOTE.equals(documentType)) { + if (!invoicesToKnockOff.isEmpty()) { + knockOffRecursively(invoicesToKnockOff, facturaElectronicaMensualDuplicate, facturaElectronicaMensuals); + } + } + + } + this.facturaElectronicMensualRepository.saveAllAndFlush(facturaElectronicaMensuals); + this.productParameterizationRepository.saveAndFlush(loanProductParameterization); + } + + private void knockOffRecursively(final List invoicesToKnockOff, + final FacturaElectronicaMensual currentElectronicaMensual, final List facturaElectronicaMensuals) { + BigDecimal remainingAmount = currentElectronicaMensual.getCosto_total(); + if (!invoicesToKnockOff.isEmpty()) { + for (final FacturaElectronicaMensual invoiceToKnockOff : invoicesToKnockOff) { + remainingAmount = remainingAmount.subtract(invoiceToKnockOff.getCosto_total()); + if (remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { + final String lastInvoiceNumber = invoiceToKnockOff.getNumero_doc(); + final LocalDate lastInvoiceDate = invoiceToKnockOff.getFecha_factura(); + currentElectronicaMensual.setNum_facafect(lastInvoiceNumber); + currentElectronicaMensual.setFec_facafect(lastInvoiceDate); + break; + } else { + final FacturaElectronicaMensual knockOffCreditNote = currentElectronicaMensual.clone(); + final BigDecimal amountToKnockOff = invoiceToKnockOff.getCosto_total(); + currentElectronicaMensual.setCosto_total(remainingAmount); + knockOffCreditNote.setCosto_total(amountToKnockOff); + knockOffCreditNote.setPrecio_unitario(amountToKnockOff); + knockOffCreditNote.setNum_facafect(invoiceToKnockOff.getNumero_doc()); + knockOffCreditNote.setFec_facafect(invoiceToKnockOff.getFecha_factura()); + remainingAmount = remainingAmount.subtract(invoiceToKnockOff.getCosto_total()); + facturaElectronicaMensuals.add(knockOffCreditNote); + } + } + } + } + + @Override + public ClasificacionConceptosData getClasificacionConceptosData(final String concepto) { + String sql = """ + SELECT + ccc.id AS id, + ccc.concepto AS concepto, + ccc.mandato AS mandato, + ccc.excluido AS excluido, + ccc.exento AS exento, + ccc.gravado AS gravado, + ccc.norma AS norma, + ccc.tarifa AS tarifa + FROM c_clasificacion_conceptos ccc + WHERE ccc.concepto = ? + """; + final List results = this.jdbcTemplate.query(sql, (rs, rowNum) -> { + final Long id = rs.getLong("id"); + final String concept = rs.getString("concepto"); + final boolean mandato = rs.getBoolean("mandato"); + final boolean excluido = rs.getBoolean("excluido"); + final boolean exento = rs.getBoolean("exento"); + final boolean gravado = rs.getBoolean("gravado"); + final String norma = rs.getString("norma"); + final BigDecimal tarifa = rs.getBigDecimal("tarifa"); + return ClasificacionConceptosData.builder().id(id).concepto(concept).mandato(mandato).excluido(excluido).exento(exento) + .gravado(gravado).norma(norma).tarifa(tarifa).build(); + }, concepto); + return results.isEmpty() ? null : results.get(0); + } + @Override public void recalculateInterestRate(final Loan loan) { final Long loanId = loan.getId(); @@ -4032,15 +4472,113 @@ public void recalculateInterestRate(final Loan loan) { } } - public void persistDailyAccrual(LocalDate localDate) { - List loans = loanRepository.findActiveLoansWithNotYetPostedAccrual(localDate); - loans.forEach(loan -> { + public void persistDailyAccrual(final LocalDate transactionDate) { + final List loans = loanRepository.findActiveLoansWithNotYetPostedAccrual(transactionDate); + for (final Loan loan : loans) { + final MonetaryCurrency currency = loan.getCurrency(); log.info("Persisting daily accrual for loan: {}", loan.getId()); - loan.applyDailyAccruals(localDate); + ExternalId externalIdentifier = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalIdentifier = ExternalId.generate(); + } + final List repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); + BigDecimal dailyAccrualInterest = null; + Integer accrualInstallmentNumber = null; + Money principalLoanBalanceOutstanding = loan.getPrincipal(); + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : repaymentScheduleInstallments) { + if (!transactionDate.isBefore(loanRepaymentScheduleInstallment.getFromDate()) + && !transactionDate.isAfter(loanRepaymentScheduleInstallment.getDueDate().minusDays(1))) { + final BigDecimal totalAccruedInterestForInstallment = loan + .getAccruedInterestForInstallment(loanRepaymentScheduleInstallment.getInstallmentNumber()); + long daysInPeriod = Math.toIntExact(ChronoUnit.DAYS.between(loanRepaymentScheduleInstallment.getFromDate(), + loanRepaymentScheduleInstallment.getDueDate())); + Money interestForInstallment = loanRepaymentScheduleInstallment.getInterestCharged(currency); + // Adjust interest on the last day of the period to make up the difference + if (transactionDate.equals(loanRepaymentScheduleInstallment.getDueDate().minusDays(1))) { + // if the amount is whole number when divided across the days in the period, then do same for + // last + // day + if (interestForInstallment.getAmount().remainder(BigDecimal.valueOf(daysInPeriod)) + .compareTo(BigDecimal.ZERO) == 0) { + dailyAccrualInterest = interestForInstallment.getAmount().divide(BigDecimal.valueOf(daysInPeriod), 2, + RoundingMode.HALF_UP); + } else { + // This will ensure no rounding differences remain + if (interestForInstallment.getAmount().compareTo(totalAccruedInterestForInstallment) > 0) { + dailyAccrualInterest = interestForInstallment.getAmount().subtract(totalAccruedInterestForInstallment); + } else { + dailyAccrualInterest = BigDecimal.ZERO; + } + } + } else { + dailyAccrualInterest = interestForInstallment.getAmount().divide(BigDecimal.valueOf(daysInPeriod), 2, + RoundingMode.HALF_UP); + final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); + final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO); + final LoanTermVariationsDataWrapper loanTermVariationsDataWrapper = loanApplicationTerms.getLoanTermVariations(); + if (loanTermVariationsDataWrapper != null) { + List interestRatesFromInstallment = loanTermVariationsDataWrapper + .getInterestRateFromInstallment(); + if (interestRatesFromInstallment != null && !interestRatesFromInstallment.isEmpty()) { + LocalDate periodStartDate = loanRepaymentScheduleInstallment.getFromDate(); + LocalDate periodEndDate = transactionDate; + final LocalDate periodEndDateFinal = periodEndDate; + interestRatesFromInstallment = interestRatesFromInstallment.stream() + .filter(interestRateVariation -> !DateUtils + .isAfter(interestRateVariation.getTermVariationApplicableFrom(), periodEndDateFinal)) + .toList(); + final List sortedInterestRatesFromInstallment = interestRatesFromInstallment + .stream().sorted(Comparator.comparing(LoanTermVariationsData::getLastModifiedDate, + Comparator.nullsLast(Comparator.reverseOrder()))) + .toList(); + if (CollectionUtils.isNotEmpty(sortedInterestRatesFromInstallment)) { + final LoanTermVariationsData interestRateFromInstallment = sortedInterestRatesFromInstallment.get(0); + final LocalDate termVariationApplicableFromDate = interestRateFromInstallment + .getTermVariationApplicableFrom(); + final BigDecimal interestRate = interestRateFromInstallment.getDecimalValue(); + if (DateUtils.isAfter(termVariationApplicableFromDate, periodStartDate)) { + periodStartDate = termVariationApplicableFromDate; + } + loanApplicationTerms.updateAnnualNominalInterestRate(interestRate); + } + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory + .create(loanApplicationTerms.getLoanScheduleType(), loanApplicationTerms.getInterestMethod()); + final int periodNumber = loanRepaymentScheduleInstallment.getInstallmentNumber(); + if (DateUtils.isEqual(periodStartDate, periodEndDate)) { + periodEndDate = periodEndDate.plusDays(1); + } + final boolean ignoreCurrencyDigitsAfterDecimal = false; + final PrincipalInterest principalInterest = loanScheduleGenerator.calculatePrincipalInterestComponents( + principalLoanBalanceOutstanding, loanApplicationTerms, periodNumber, periodStartDate, periodEndDate, + ignoreCurrencyDigitsAfterDecimal); + final int daysDifference = Math.toIntExact(ChronoUnit.DAYS.between(periodStartDate, periodEndDate)); + dailyAccrualInterest = principalInterest.interest().getAmount().divide(BigDecimal.valueOf(daysDifference), + 2, RoundingMode.HALF_UP); + } + } + } + // Accumulate the daily interest to the installment's accrued interest + dailyAccrualInterest = dailyAccrualInterest.setScale(0, RoundingMode.DOWN); + final BigDecimal accruedInterest = totalAccruedInterestForInstallment.add(dailyAccrualInterest); + loanRepaymentScheduleInstallment.setInterestAccrued(accruedInterest); + accrualInstallmentNumber = loanRepaymentScheduleInstallment.getInstallmentNumber(); + break; + } + principalLoanBalanceOutstanding = principalLoanBalanceOutstanding + .minus(loanRepaymentScheduleInstallment.getPrincipal(currency)); + } + // if (dailyInterest.compareTo(BigDecimal.ZERO) > 0) { + if (dailyAccrualInterest != null && dailyAccrualInterest.compareTo(BigDecimal.ZERO) > 0) { + final Money dailyInterestMoney = Money.of(currency, dailyAccrualInterest); + final LoanTransaction dailyAccrualTransaction = LoanTransaction.accrueDailyInterest(loan.getOffice(), loan, + dailyInterestMoney, transactionDate, externalIdentifier, accrualInstallmentNumber); + loan.addLoanTransaction(dailyAccrualTransaction); + loan.setInterestAccruedTill(transactionDate); + } loanRepository.saveAndFlush(loan); log.info("Daily accrual persisted for loan: {}", loan.getId()); - }); + } } public void persistInstallmentalChargeAccrual(LocalDate localDate) { @@ -4076,14 +4614,17 @@ public void cancelDefaultInsuranceCharges(List Objects.equals(lc.getId(), data.loanChargeId())).findFirst(); if (loanChargeOptional.isPresent()) { loanCharge = loanChargeOptional.get(); - } - BigDecimal cumulative = BigDecimal.ZERO; - cumulative = processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, false); - InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, - data.installment(), incident, currentDate, cumulative); + if ((incident.isMandatory() && loanCharge.isMandatoryInsurance()) + || (incident.isVoluntary() && loanCharge.isVoluntaryInsurance())) { + BigDecimal cumulative = BigDecimal.ZERO; + cumulative = processInsuranceChargeCancellation(cumulative, loan, loanCharge, data, false); + InsuranceIncidentNoveltyNews insuranceIncidentNoveltyNews = InsuranceIncidentNoveltyNews.instance(loan, loanCharge, + data.installment(), incident, currentDate, cumulative); - this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); - saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + this.insuranceIncidentNoveltyNewsRepository.saveAndFlush(insuranceIncidentNoveltyNews); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + } + } } } @@ -4117,8 +4658,8 @@ public void temporarySuspendDefaultInsuranceCharges(List dataValidationErrors = new ArrayList<>(); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loanproductparameterization"); + + // check tha product type exists + String productType = this.fromApiJsonHelper.extractStringNamed(PRODUCT_TYPE, element); + baseDataValidator.reset().parameter(PRODUCT_TYPE).value(productType).notBlank(); + + // validate that billing prefix is not more than 6 characters + if (this.fromApiJsonHelper.parameterExists(BILLING_PREFIX, element)) { + final String billingPrefix = this.fromApiJsonHelper.extractStringNamed(BILLING_PREFIX, element); + baseDataValidator.reset().parameter(BILLING_PREFIX).value(billingPrefix).notNull(); + if (StringUtils.isNotBlank(billingPrefix)) { + baseDataValidator.reset().parameter(BILLING_PREFIX).value(billingPrefix).notExceedingLengthOf(6); + } + } + + // validate that billing resolution number is not more than 50 characters + if (this.fromApiJsonHelper.parameterExists(BILLING_RESOLUTION_NUMBER, element)) { + final Long billingResolutionNumber = this.fromApiJsonHelper.extractLongNamed(BILLING_RESOLUTION_NUMBER, element); + baseDataValidator.reset().parameter(BILLING_RESOLUTION_NUMBER).value(billingResolutionNumber).notNull(); + if (billingResolutionNumber != null) { + baseDataValidator.reset().parameter(BILLING_RESOLUTION_NUMBER).value(billingResolutionNumber).notExceedingLengthOf(50); + } + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + // + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public void validateForUpdate(final String json) { + validateForCreate(json); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/data/LoanProductParameterizationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/data/LoanProductParameterizationData.java index 2af93e960d9..8f7681b5f78 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/data/LoanProductParameterizationData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/data/LoanProductParameterizationData.java @@ -51,4 +51,8 @@ public class LoanProductParameterizationData { private Long lastCreditNoteNumber; private Long lastDebitNoteNumber; + + private String technicalKey; + + private String note; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/domain/LoanProductParameterization.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/domain/LoanProductParameterization.java index 426bddd3c7d..66b2a4273ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/domain/LoanProductParameterization.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/domain/LoanProductParameterization.java @@ -29,6 +29,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductType; import org.apache.fineract.portfolio.loanproductparameterization.data.LoanProductParameterizationData; @@ -70,9 +71,22 @@ public class LoanProductParameterization extends AbstractAuditableWithUTCDateTim @Column(name = "last_debit_note_number") private Long lastDebitNoteNumber; + @Column(name = "clave_tecnica") + private String technicalKey; + + @Column(name = "nota") + private String note; + + @Column(name = "invoice_counter") + private Long invoiceCounter; + + @Column(name = "credit_note_counter") + private Long creditNoteCounter; + public LoanProductParameterizationData toData() { return new LoanProductParameterizationData(getId(), productType, billingPrefix, billingResolutionNumber, generationDate, - expirationDate, rangeStartNumber, rangeEndNumber, lastInvoiceNumber, lastCreditNoteNumber, lastDebitNoteNumber); + expirationDate, rangeStartNumber, rangeEndNumber, lastInvoiceNumber, lastCreditNoteNumber, lastDebitNoteNumber, + technicalKey, note); } public static LoanProductParameterization create(JsonCommand command) { @@ -112,6 +126,12 @@ public void update(JsonCommand command) { if (updatedParameters.getLastDebitNoteNumber() != null) { this.lastDebitNoteNumber = updatedParameters.getLastDebitNoteNumber(); } + if (StringUtils.isNotBlank(updatedParameters.getTechnicalKey())) { + this.technicalKey = updatedParameters.getTechnicalKey(); + } + if (StringUtils.isNotBlank(updatedParameters.getNote())) { + this.note = updatedParameters.getNote(); + } } private static LoanProductParameterization extractParameters(JsonCommand command) { @@ -125,6 +145,8 @@ private static LoanProductParameterization extractParameters(JsonCommand command final Long lastInvoiceNumber = command.longValueOfParameterNamed("lastInvoiceNumber"); final Long lastCreditNoteNumber = command.longValueOfParameterNamed("lastCreditNoteNumber"); final Long lastDebitNoteNumber = command.longValueOfParameterNamed("lastDebitNoteNumber"); + final String note = command.stringValueOfParameterNamed("note"); + final String technicalKey = command.stringValueOfParameterNamed("technicalKey"); // validate that rangeStartNumber is less than rangeEndNumber if (rangeStartNumber > rangeEndNumber) { @@ -140,6 +162,42 @@ private static LoanProductParameterization extractParameters(JsonCommand command } return new LoanProductParameterization(productType, billingPrefix, billingResolutionNumber, generationDate, expirationDate, - rangeStartNumber, rangeEndNumber, lastInvoiceNumber, lastCreditNoteNumber, lastDebitNoteNumber); + rangeStartNumber, rangeEndNumber, lastInvoiceNumber, lastCreditNoteNumber, lastDebitNoteNumber, technicalKey, note, 0L, 0L); + } + + public boolean isInvoiceResolutionExpiring(Long daysPrior) { + LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + LocalDate warningDate = currentDate.plusDays(daysPrior); + // Implement logic to check if the invoice resolution is expiring within the specified days + // Return true if the condition is met, otherwise false + + // Check if the expiration date is within the specified days + return expirationDate != null && (warningDate.isAfter(expirationDate) || warningDate.isEqual(expirationDate)); + } + + public boolean isInvoiceNumberingLimitReached(Long threshold) { + // Implement logic to check if the invoice numbering limit is reached within the specified quantity + // Return true if the condition is met, otherwise false + long maximumInvoiceNumber = rangeEndNumber; + long usedInvoiceNumber = lastInvoiceNumber; + long remainingInvoiceNumber = maximumInvoiceNumber - usedInvoiceNumber; + + // Check if the last invoice number is within the specified quantity + return remainingInvoiceNumber <= threshold; + } + + public Long getInvoiceNumberingRemaining() { + long maximumInvoiceNumber = rangeEndNumber; + long usedInvoiceNumber = lastInvoiceNumber; + return maximumInvoiceNumber - usedInvoiceNumber; + } + + public Long getInvoiceResolutionExpiryDays() { + LocalDate currentDate = DateUtils.getLocalDateOfTenant(); + return DateUtils.getDifferenceInDays(currentDate, expirationDate); + } + + public void validateForCreate(final String json) { + // Implement validation logic for creating a new loan product parameterization } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryConfig.java new file mode 100644 index 00000000000..22c843710f8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryConfig.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproductparameterization.jobs.invoiceexpiryresolution; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +@RequiredArgsConstructor +public class InvoiceResolutionExpiryConfig { + + private final JobRepository jobRepository; + + private final PlatformTransactionManager transactionManager; + private final String jobName = JobName.INVOICE_EXPIRY_RESOLUTION.name(); + + @Bean + protected Step runInvoiceResolutionExpiryStep(InvoiceResolutionExpiryTasklet invoiceResolutionExpiryTasklet) { + return new StepBuilder(jobName, jobRepository).tasklet(invoiceResolutionExpiryTasklet, transactionManager).build(); + } + + @Bean + public Job runInvoiceResolutionExpiryJob(InvoiceResolutionExpiryTasklet runInvoiceResolutionExpiryStep) { + return new JobBuilder(jobName, jobRepository).start(runInvoiceResolutionExpiryStep(runInvoiceResolutionExpiryStep)) + .incrementer(new RunIdIncrementer()).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryTasklet.java new file mode 100644 index 00000000000..f1f6ff9f80d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoiceexpiryresolution/InvoiceResolutionExpiryTasklet.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproductparameterization.jobs.invoiceexpiryresolution; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.loanproductparameterization.service.LoanProductParameterizationWriteService; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class InvoiceResolutionExpiryTasklet implements Tasklet { + + private final LoanProductParameterizationWriteService loanProductParameterizationWriteService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + + List errors = new ArrayList<>(); + try { + loanProductParameterizationWriteService.sendIInvoiceResolutionExpiryNotification(); + } catch (Exception e) { + log.error("Failed to InvoiceResolutionExpiryTasklet ", e); + errors.add(e); + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitConfig.java new file mode 100644 index 00000000000..05946d9a127 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitConfig.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproductparameterization.jobs.invoicenumberinglimit; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +@RequiredArgsConstructor +public class InvoiceNumberingLimitConfig { + + private final JobRepository jobRepository; + + private final PlatformTransactionManager transactionManager; + + @Bean + protected Step runInvoiceNumberingLimitStep(InvoiceNumberingLimitTasklet invoiceNumberingLimitTasklet) { + return new StepBuilder(JobName.INVOICE_NUMBERING_LIMIT.name(), jobRepository) + .tasklet(invoiceNumberingLimitTasklet, transactionManager).build(); + } + + @Bean + public Job runInvoiceNumberingLimitJob(InvoiceNumberingLimitTasklet invoiceNumberingLimitTasklet) { + return new JobBuilder(JobName.INVOICE_NUMBERING_LIMIT.name(), jobRepository) + .start(runInvoiceNumberingLimitStep(invoiceNumberingLimitTasklet)).incrementer(new RunIdIncrementer()).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitTasklet.java new file mode 100644 index 00000000000..4fdf9c98188 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/jobs/invoicenumberinglimit/InvoiceNumberingLimitTasklet.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproductparameterization.jobs.invoicenumberinglimit; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.loanproductparameterization.service.LoanProductParameterizationWriteService; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class InvoiceNumberingLimitTasklet implements Tasklet { + + private final LoanProductParameterizationWriteService loanProductParameterizationWriteService; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + + List errors = new ArrayList<>(); + try { + loanProductParameterizationWriteService.sendInvoiceNumberingLimitNotification(); + + } catch (Exception e) { + log.error("Failed to run InvoiceNumberingLimitTasklet ", e); + errors.add(e); + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteService.java index d4e1869eea7..c0e9d514508 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteService.java @@ -28,4 +28,8 @@ public interface LoanProductParameterizationWriteService { CommandProcessingResult updateProductParameterization(Long parameterId, JsonCommand command); CommandProcessingResult deleteProductParameterization(Long parameterId); + + void sendInvoiceNumberingLimitNotification(); + + void sendIInvoiceResolutionExpiryNotification(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteServiceImpl.java index c3af27e57d3..07d17e10221 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproductparameterization/service/LoanProductParameterizationWriteServiceImpl.java @@ -18,10 +18,15 @@ */ package org.apache.fineract.portfolio.loanproductparameterization.service; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData; +import org.apache.fineract.infrastructure.campaigns.email.service.EmailMessageJobEmailService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanproductparameterization.data.LoanProductParameterDataValidator; import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterization; import org.apache.fineract.portfolio.loanproductparameterization.domain.LoanProductParameterizationRepository; import org.apache.fineract.portfolio.loanproductparameterization.exception.LoanProductParameterizationNotFoundException; @@ -33,9 +38,14 @@ public class LoanProductParameterizationWriteServiceImpl implements LoanProductParameterizationWriteService { private final LoanProductParameterizationRepository productParameterizationRepository; + private final ConfigurationDomainService configurationDomainService; + private final EmailMessageJobEmailService emailMessageJobEmailService; + private final LoanProductParameterDataValidator loanProductParameterDataValidator; @Override public CommandProcessingResult createProductParameterization(JsonCommand command) { + + loanProductParameterDataValidator.validateForCreate(command.json()); LoanProductParameterization productParameterization = LoanProductParameterization.create(command); LoanProductParameterization savedProductParameterization = productParameterizationRepository.save(productParameterization); @@ -45,6 +55,7 @@ public CommandProcessingResult createProductParameterization(JsonCommand command @Override public CommandProcessingResult updateProductParameterization(Long parameterId, JsonCommand command) { LoanProductParameterization productParameterization = findProductParameterization(parameterId); + loanProductParameterDataValidator.validateForUpdate(command.json()); productParameterization.update(command); productParameterizationRepository.save(productParameterization); @@ -58,6 +69,77 @@ public CommandProcessingResult deleteProductParameterization(Long parameterId) { return CommandProcessingResult.empty(); } + @Override + public void sendInvoiceNumberingLimitNotification() { + Long invoiceNumberingLimit = configurationDomainService.retrieveInvoiceThreshold(); + if (invoiceNumberingLimit == null) { + log.warn("Invoice numbering limit is not set , No notification will be sent"); + return; + } + + List emailAddresses = configurationDomainService.retrieveInvoiceJobNotificationEmails(); + if (emailAddresses.isEmpty()) { + log.warn("No email addresses found for invoice numbering limit notification"); + return; + } + + List loanProductParameterizationList = productParameterizationRepository.findAll().stream() + .filter(loanProductParameterization -> loanProductParameterization.isInvoiceNumberingLimitReached(invoiceNumberingLimit)) + .toList(); + + if (loanProductParameterizationList.isEmpty()) { + log.info("No loan product parameterization reached the invoice numbering limit"); + return; + } + loanProductParameterizationList.forEach(i -> log.info( + "Invoice numbering limit reached for loan product parameterization with id: {} with last invoice used as {} and threshold as {}", + i.getId(), i.getLastInvoiceNumber(), invoiceNumberingLimit)); + + final String subject = "Facura electrónica: Agotamiento de numerácion de factura"; + + loanProductParameterizationList.forEach(loanProductParameterization -> { + final String body = String.format("Se notifica que faltan %d Facturas de que se agote la numeración", + loanProductParameterization.getInvoiceNumberingRemaining()); + sendEmail(subject, body, emailAddresses); + }); + + } + + @Override + public void sendIInvoiceResolutionExpiryNotification() { + Long invoiceResolutionExpiryDays = configurationDomainService.retrieveInvoiceResolutionExpiryDays(); + if (invoiceResolutionExpiryDays == null) { + log.warn("Invoice resolution expiry days is not set , No notification will be sent"); + return; + } + + List emailAddresses = configurationDomainService.retrieveInvoiceJobNotificationEmails(); + if (emailAddresses.isEmpty()) { + log.warn("No email addresses found for invoice resolution expiry notification"); + return; + } + + // this is in assumption that the list of loan product parameterization is not huge, if it is huge then we need + // to + // implement a pagination mechanism or use a query to fetch only the required data + List loanProductParameterizationList = productParameterizationRepository.findAll().stream() + .filter(loanProductParameterization -> loanProductParameterization.isInvoiceResolutionExpiring(invoiceResolutionExpiryDays)) + .toList(); + + if (loanProductParameterizationList.isEmpty()) { + log.info("No loan product parameterization reached the invoice resolution expiry"); + return; + } + + final String subject = "Facura electrónica: Vencimiento de la resolución"; + + loanProductParameterizationList.forEach(loanProductParameterization -> { + String body = String.format("Se notifica que estamos a %d días que se venzan la resolución de las facturas", + loanProductParameterization.getInvoiceResolutionExpiryDays()); + sendEmail(subject, body, emailAddresses); + }); + } + private LoanProductParameterization findProductParameterization(Long parameterId) { try { return productParameterizationRepository.findById(parameterId) @@ -67,4 +149,11 @@ private LoanProductParameterization findProductParameterization(Long parameterId throw new LoanProductParameterizationNotFoundException(parameterId); } } + + private void sendEmail(String subject, String body, List emailAddressList) { + EmailMessageWithAttachmentData emailMessageWithAttachmentData = EmailMessageWithAttachmentData.createNew(body, subject, null, + emailAddressList); + emailMessageJobEmailService.sendEmailWithAttachment(emailMessageWithAttachmentData); + } + } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index bbc861cce69..0bb8608c9c4 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -318,6 +318,20 @@ + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240507011009134_SU_477_update_reports_migration_aval_charge.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240507011009134_SU_477_update_reports_migration_aval_charge.xml new file mode 100644 index 00000000000..42ba88efe2c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240507011009134_SU_477_update_reports_migration_aval_charge.xml @@ -0,0 +1,525 @@ + + + + + + + = current_date and mlrs.completed_derived != true + AND fromdate <= current_date + ) due_interest on due_interest.loan_id = ml.id + left join ( + select mlrs.loan_id,sum(mlrs.interest_amount) as intersetAmount + from m_loan_repayment_schedule mlrs + where mlrs.completed_derived != true and duedate <= current_date group by loan_id + ) overdue_interest on overdue_interest.loan_id = ml.id + join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum IN (468, 575, 231) + group by mlc.loan_id + ) insurance_chg ON insurance_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum IN (468, 575, 231) + group by mlc2.loan_id + ) vat_chg ON vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum in (41, 286) + group by mlc.loan_id + ) aval_chg ON aval_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum in (41, 286) + group by mlc2.loan_id + ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) + group by mlc.loan_id + ) other_chg ON other_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) + group by mlc2.loan_id + ) other_vat_chg ON other_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum != 342 + group by mlc.loan_id + ) penalty_chg ON penalty_chg.loan_id = ml.id + LEFT JOIN ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum = 342 + group by mlc.loan_id + ) penalty_vat_chg ON penalty_vat_chg.loan_id = ml.id + WHERE + ml.loan_status_id = 300 + and (CURRENT_DATE - mlaa.overdue_since_date_derived) > (select value from c_configuration where name = 'Dias de mora minimos para reclamar') + and ml.excluded_for_insurance_claim is null and ml.claim_type is null and ml.loan_schedule_type = 'PROGRESSIVE' + ]]> + + report_name='Reporte de reclamación de seguro' + + + + + + = current_date and mlrs.completed_derived != true + AND fromdate <= current_date + ) due_interest on due_interest.loan_id = ml.id + left join ( + select mlrs.loan_id,sum(mlrs.interest_amount) as intersetAmount + from m_loan_repayment_schedule mlrs + where mlrs.completed_derived != true and duedate <= current_date group by loan_id + ) overdue_interest on overdue_interest.loan_id = ml.id + left join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum IN (468, 575, 231) + group by mlc.loan_id + ) insurance_chg ON insurance_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum IN (468, 575, 231) + group by mlc2.loan_id + ) vat_chg ON vat_chg.loan_id = ml.id + join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum in (41, 286) + group by mlc.loan_id + ) aval_chg ON aval_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum in (41, 286) + group by mlc2.loan_id + ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) + group by mlc.loan_id + ) other_chg ON other_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) + group by mlc2.loan_id + ) other_vat_chg ON other_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum != 342 + group by mlc.loan_id + ) penalty_chg ON penalty_chg.loan_id = ml.id + LEFT JOIN ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum = 342 + group by mlc.loan_id + ) penalty_vat_chg ON penalty_vat_chg.loan_id = ml.id + WHERE + ml.loan_status_id = 300 + and (CURRENT_DATE - mlaa.overdue_since_date_derived) > (select value from c_configuration where name = 'Dias de mora minimos para reclamar') + and ml.excluded_for_insurance_claim is null and ml.claim_type is null and ml.loan_schedule_type = 'PROGRESSIVE' + ]]> + + report_name='Reporte de reclamación de Aval' + + + + + + = current_date and mlrs.completed_derived != true + AND fromdate <= current_date + ) due_interest on due_interest.loan_id = ml.id + left join ( + select mlrs.loan_id,sum(mlrs.interest_amount) as intersetAmount + from m_loan_repayment_schedule mlrs + where mlrs.completed_derived != true and duedate <= current_date group by loan_id + ) overdue_interest on overdue_interest.loan_id = ml.id + left join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum IN (468, 575, 231) + group by mlc.loan_id + ) insurance_chg ON insurance_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum IN (468, 575, 231) + group by mlc2.loan_id + ) vat_chg ON vat_chg.loan_id = ml.id + left join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum in (41, 286) + group by mlc.loan_id + ) aval_chg ON aval_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum in (41, 286) + group by mlc2.loan_id + ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) + group by mlc.loan_id + ) other_chg ON other_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) + group by mlc2.loan_id + ) other_vat_chg ON other_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum != 342 + group by mlc.loan_id + ) penalty_chg ON penalty_chg.loan_id = ml.id + LEFT JOIN ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum = 342 + group by mlc.loan_id + ) penalty_vat_chg ON penalty_vat_chg.loan_id = ml.id + WHERE + ml.loan_status_id = 300 + and (CURRENT_DATE - mlaa.overdue_since_date_derived) > (select value from c_configuration where name = 'Dias de mora minimos para reclamar') + and ml.excluded_for_insurance_claim is null and ml.claim_type is null and ml.loan_schedule_type = 'PROGRESSIVE' + ]]> + + report_name='Reporte de Castigo de cartera' + + + + + + = current_date and mlrs.completed_derived != true + AND fromdate <= current_date + ) due_interest on due_interest.loan_id = ml.id + left join ( + select mlrs.loan_id,sum(mlrs.interest_amount) as intersetAmount + from m_loan_repayment_schedule mlrs + where mlrs.completed_derived != true and duedate <= current_date group by loan_id + ) overdue_interest on overdue_interest.loan_id = ml.id + left join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum IN (468, 575, 231) + group by mlc.loan_id + ) insurance_chg ON insurance_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum IN (468, 575, 231) + group by mlc2.loan_id + ) vat_chg ON vat_chg.loan_id = ml.id + left join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum in (41, 286) + group by mlc.loan_id + ) aval_chg ON aval_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum in (41, 286) + group by mlc2.loan_id + ) aval_vat_chg ON aval_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlic.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + where mlc.charge_calculation_enum NOT IN (468, 575, 231, 342, 41, 286) + group by mlc.loan_id + ) other_chg ON other_chg.loan_id = ml.id + LEFT JOIN ( + SELECT sum(mlic.amount_outstanding_derived) outstanding_amount, mlc2.loan_id FROM m_loan_charge mlc2 + inner join m_loan_installment_charge mlic ON mlic.loan_charge_id = mlc2.id + inner join m_loan_repayment_schedule mlrs on mlic.loan_schedule_id = mlrs.id + and mlrs.completed_derived != true + and mlrs.duedate >= (select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc2.loan_id) + and (mlrs.duedate <= current_date or (mlrs.duedate >= current_date and mlrs.fromdate <= current_date)) + JOIN m_charge mc2 ON mc2.id = mlc2.charge_id AND mc2.charge_calculation_enum = 342 + JOIN m_charge parent_charge on parent_charge.id = mc2.parent_charge_id + WHERE parent_charge.charge_calculation_enum NOT IN (468, 575, 231, 41, 286) + group by mlc2.loan_id + ) other_vat_chg ON other_vat_chg.loan_id = ml.id + LEFT join ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum != 342 + group by mlc.loan_id + ) penalty_chg ON penalty_chg.loan_id = ml.id + LEFT JOIN ( + select mlc.loan_id, sum(mlc.amount_outstanding_derived) outstanding_amount from m_loan_charge mlc + inner join m_loan_overdue_installment_charge mloic ON mloic.loan_charge_id = mlc.id + where mlc.due_for_collection_as_of_date >=(select overdue_since_date_derived from m_loan_arrears_aging where loan_id = mlc.loan_id) + and mc2.charge_calculation_enum = 342 + group by mlc.loan_id + ) penalty_vat_chg ON penalty_vat_chg.loan_id = ml.id + WHERE + ml.loan_status_id = 300 + and (CURRENT_DATE - mlaa.overdue_since_date_derived) > (select value from c_configuration where name = 'Dias de mora minimos para reclamar') + and ml.id in ('${loanIds}') + ]]> + + report_name='Reporte de Castigo de cartera' + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240529122200_SU_198_loan-product-parameters-1.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240529122200_SU_198_loan-product-parameters-1.xml new file mode 100644 index 00000000000..21abd5f2d50 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20240529122200_SU_198_loan-product-parameters-1.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20241208121243_SU_485_client_death_blocking_reason.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20241208121243_SU_485_client_death_blocking_reason.xml new file mode 100644 index 00000000000..b33d2e66e85 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20241208121243_SU_485_client_death_blocking_reason.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412101146000000_SU_462_c_client_ally_point_of_sales_UPDATE_CONSTRAINT.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412101146000000_SU_462_c_client_ally_point_of_sales_UPDATE_CONSTRAINT.xml new file mode 100644 index 00000000000..eeed2d5e5f5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412101146000000_SU_462_c_client_ally_point_of_sales_UPDATE_CONSTRAINT.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412120000000100_SU_472_m_permission_INSERT_CLASIFICACION_CONCEPTO_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412120000000100_SU_472_m_permission_INSERT_CLASIFICACION_CONCEPTO_permissions.xml new file mode 100644 index 00000000000..fbf4160003b --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/202412120000000100_SU_472_m_permission_INSERT_CLASIFICACION_CONCEPTO_permissions.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242708011009113_SU-199_invoice_job.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242708011009113_SU-199_invoice_job.xml new file mode 100644 index 00000000000..88553bd6d0a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242708011009113_SU-199_invoice_job.xml @@ -0,0 +1,102 @@ + + + + + + + SELECT COUNT(*) FROM job WHERE name ='Daily Loan Accrual'; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242711011009135_SU_439_add_migration_columns_loan.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242711011009135_SU_439_add_migration_columns_loan.xml new file mode 100644 index 00000000000..76e6f75c571 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20242711011009135_SU_439_add_migration_columns_loan.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009140_SU_316_add_column.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009140_SU_316_add_column.xml new file mode 100644 index 00000000000..fb5b33364ef --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009140_SU_316_add_column.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_200_electronic_invoice.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_200_electronic_invoice.xml new file mode 100644 index 00000000000..3421c8ad056 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_200_electronic_invoice.xml @@ -0,0 +1,446 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM c_configuration WHERE name = 'Dias a partir de los cuales empezar a considerar suspendido' + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_318_add_tabel_collection_house.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_318_add_tabel_collection_house.xml new file mode 100644 index 00000000000..a0e20ffc090 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009141_SU_318_add_tabel_collection_house.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009142_SU_488_update_report._archivo.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009142_SU_488_update_report._archivo.xml new file mode 100644 index 00000000000..70dd301d6f8 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009142_SU_488_update_report._archivo.xml @@ -0,0 +1,87 @@ + + + + + + + + report_name = 'Archivo de cartera' + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_319_add_table_collection_house_history.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_319_add_table_collection_house_history.xml new file mode 100644 index 00000000000..9e3545e0180 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_319_add_table_collection_house_history.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM c_external_service WHERE name = 'COLLECTION_HOUSE_HISTORY_PROVIDER' + + + + + + + + + + SELECT COUNT(*) FROM c_external_service_properties WHERE name = 'COLLECTION_HOUSE_URL' + + + + + + + + + + + + SELECT COUNT(*) FROM c_external_service_properties WHERE name = 'COLLECTION_HOUSE_API_KEY' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_432_update-job-run.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_432_update-job-run.xml new file mode 100644 index 00000000000..f695b9e1a87 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/20243008011009148_SU_432_update-job-run.xml @@ -0,0 +1,46 @@ + + + + + + + Select count(*) from job where name = 'Liquidacion de Recaudos' + + + + + name = 'Liquidacion de Recaudos' + + + + + + + Select count(*) from job where name = 'Liquidacion de Compras' + + + + + name = 'Liquidacion de Compras' + + +