FINERACT-2317: Add support for modifying loan approved amounts with validation and history tracking

This commit is contained in:
mark.vituska
2025-07-21 17:13:51 +02:00
committed by Adam Saghy
parent c84ba089f0
commit 6d65f07924
30 changed files with 1348 additions and 12 deletions
@@ -127,12 +127,14 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.GlimRepaymentTemplate;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -176,6 +178,8 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@@ -303,6 +307,7 @@ public class LoansApiResource {
private final LoanTermVariationsRepository loanTermVariationsRepository;
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
private final LoanApprovedAmountHistoryRepository loanApprovedAmountHistoryRepository;
/*
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
@@ -872,6 +877,55 @@ public class LoansApiResource {
return createLoanDelinquencyAction(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
}
@PUT
@Path("{loanId}/approved-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Modifies the approved amount of the loan", description = "")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
public CommandProcessingResult modifyLoanApprovedAmount(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return modifyLoanApprovedAmount(loanId, ExternalId.empty(), apiRequestBodyAsJson);
}
@PUT
@Path("external-id/{loanExternalId}/approved-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Modifies the approved amount of the loan", description = "")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
public CommandProcessingResult modifyLoanApprovedAmount(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
@Context final UriInfo uriInfo, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return modifyLoanApprovedAmount(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
}
@GET
@Path("{loanId}/approved-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Collects and returns the approved amount modification history for a given loan", description = "")
public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, @Context final UriInfo uriInfo) {
return getLoanApprovedAmountHistory(loanId, ExternalId.empty());
}
@GET
@Path("external-id/{loanExternalId}/approved-amount")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Collects and returns the approved amount modification history for a given loan", description = "")
public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
@Context final UriInfo uriInfo) {
return getLoanApprovedAmountHistory(null, ExternalIdFactory.produce(loanExternalId));
}
private String retrieveApprovalTemplate(final Long loanId, final String loanExternalIdStr, final String templateType,
final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
@@ -1294,4 +1348,19 @@ public class LoansApiResource {
return delinquencyActionSerializer.serialize(result);
}
private CommandProcessingResult modifyLoanApprovedAmount(Long loanId, ExternalId loanExternalId, String apiRequestBodyAsJson) {
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandWrapper commandRequest = builder.updateLoanApprovedAmount(resolvedLoanId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
private List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(Long loanId, ExternalId loanExternalId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
Pageable sortedByCreationDate = Pageable.unpaged(Sort.by("createdDate").ascending());
return loanApprovedAmountHistoryRepository.findAllByLoanId(resolvedLoanId, sortedByCreationDate);
}
}
@@ -1771,4 +1771,47 @@ final class LoansApiResourceSwagger {
@Schema(description = "PostLoansLoanIdChanges")
public PostLoansLoanIdChanges changes;
}
@Schema(description = "PutLoansApprovedAmountRequest")
public static final class PutLoansApprovedAmountRequest {
private PutLoansApprovedAmountRequest() {}
@Schema(example = "1000")
public BigDecimal amount;
@Schema(example = "en")
public String locale;
}
@Schema(description = "PutLoansApprovedAmountResponse")
public static final class PutLoansApprovedAmountResponse {
private PutLoansApprovedAmountResponse() {}
static final class PutLoansApprovedAmountChanges {
private PutLoansApprovedAmountChanges() {}
@Schema(example = "1000")
public BigDecimal oldApprovedAmount;
@Schema(example = "1000")
public BigDecimal newApprovedAmount;
@Schema(example = "en_GB")
public String locale;
}
@Schema(example = "3")
public Long resourceId;
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
public String resourceExternalId;
@Schema(example = "2")
public Long officeId;
@Schema(example = "6")
public Long clientId;
@Schema(example = "10")
public Long groupId;
@Schema(description = "PutLoansApprovedAmountChanges")
public PutLoansApprovedAmountChanges changes;
}
}
@@ -2174,12 +2174,33 @@ public final class LoanApplicationValidator {
public BigDecimal getOverAppliedMax(Loan loan) {
LoanProduct loanProduct = loan.getLoanProduct();
// Check if overapplied calculation type and number are properly configured
if (loanProduct.getOverAppliedCalculationType() == null || loanProduct.getOverAppliedNumber() == null) {
// If overapplied calculation is not configured, return proposed principal (original behavior)
return loan.getProposedPrincipal();
}
// For loans with approved amount modifications, use proposed principal as base to allow
// disbursement up to the originally requested amount regardless of the reduced approved amount
boolean hasApprovedAmountModification = loan.getApprovedPrincipal() != null && loan.getProposedPrincipal() != null
&& loan.getApprovedPrincipal().compareTo(loan.getProposedPrincipal()) != 0;
BigDecimal basePrincipal;
if (hasApprovedAmountModification) {
// Use proposed principal for loans with approved amount modifications
basePrincipal = loan.getProposedPrincipal();
} else {
// Use approved principal for normal loans
basePrincipal = loan.getApprovedPrincipal() != null ? loan.getApprovedPrincipal() : loan.getProposedPrincipal();
}
if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) {
BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber());
BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100)));
return loan.getProposedPrincipal().multiply(totalPercentage);
return basePrincipal.multiply(totalPercentage);
} else {
return loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
return basePrincipal.add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
}
}
@@ -0,0 +1,104 @@
/**
* 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.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public final class LoanApprovedAmountValidatorImpl implements LoanApprovedAmountValidator {
private static final Set<LoanStatus> INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION = Set.of(LoanStatus.INVALID,
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.REJECTED);
private final FromJsonHelper fromApiJsonHelper;
private final LoanRepository loanRepository;
private final LoanApplicationValidator loanApplicationValidator;
@Override
public void validateLoanApprovedAmountModification(JsonCommand command) {
String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> supportedParameters = new HashSet<>(
Arrays.asList(LoanApiConstants.amountParameterName, LoanApiConstants.localeParameterName));
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final BigDecimal newApprovedAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.amountParameterName,
element);
Validator.validateOrThrow("loan.approved.amount", baseDataValidator -> {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).notNull();
});
Validator.validateOrThrowDomainViolation("loan.approved.amount", baseDataValidator -> {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).positiveAmount();
final Long loanId = command.getLoanId();
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
if (INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION.contains(loan.getStatus())) {
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("loan.status.not.valid.for.approved.amount.modification");
}
BigDecimal maximumThresholdForApprovedAmount;
if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
maximumThresholdForApprovedAmount = loanApplicationValidator.getOverAppliedMax(loan);
} else {
maximumThresholdForApprovedAmount = loan.getProposedPrincipal();
}
if (MathUtil.isGreaterThan(newApprovedAmount, maximumThresholdForApprovedAmount)) {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
.failWithCode("can't.be.greater.than.maximum.applied.loan.amount.calculation");
}
BigDecimal totalPrincipalOnLoan = loan.getSummary().getTotalPrincipal();
if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan)) {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
.failWithCode("less.than.disbursed.principal.and.capitalized.income");
}
});
}
}
@@ -44,7 +44,7 @@ public final class LoanDisbursementValidator {
} else {
if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0)
|| (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) {
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
final String errorMsg = "Loan can't be disbursed, disburse amount is exceeding approved principal.";
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
loan.getApprovedPrincipal());
}
@@ -158,7 +158,7 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
validateLoanClientIsActive(loan);
validateLoanGroupIsActive(loan);
final BigDecimal disbursedAmount = loan.getDisbursedAmount();
final BigDecimal disbursedAmount = loan.getSummary().getTotalPrincipalDisbursed();
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, principal, disbursedAmount);
if (loan.isChargedOff()) {
@@ -161,6 +161,7 @@ public class LoanDisbursementService {
} else {
loan.getLoanRepaymentScheduleDetail()
.setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount());
totalAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
}
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount);
}
@@ -432,7 +432,7 @@ public class LoanAccountConfiguration {
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
LoanTransactionService loanTransactionService, BuyDownFeePlatformService buyDownFeePlatformService) {
LoanTransactionService loanTransactionService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -209,4 +209,6 @@
<include file="parts/0188_create_loan_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0189_add_loan_buydown_fee_event.xml" relativeToChangelogFile="true" />
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanApprovedAmountChangedBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<createTable tableName="m_loan_approved_amount_history">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_approved_amount_history"/>
</column>
<column name="loan_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="new_approved_amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="old_approved_amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="timestamp with time zone"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="timestamp with time zone"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="2" context="mysql">
<createTable tableName="m_loan_approved_amount_history">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_approved_amount_history"/>
</column>
<column name="loan_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="new_approved_amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="old_approved_amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="DATETIME"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="DATETIME"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="3">
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan_approved_amount_history"
constraintName="FK_loan_approved_amount_history_created_by" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan_approved_amount_history"
constraintName="FK_loan_approved_amount_history_last_modified_by" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="loan_id" baseTableName="m_loan_approved_amount_history"
constraintName="FK_loan_approved_amount_history_loan_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_loan" validate="true"/>
</changeSet>
</databaseChangeLog>
@@ -112,7 +112,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -206,7 +206,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));