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
@@ -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;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public class LoanApprovedAmountChangedBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanApprovedAmountChangedBusinessEvent";
public LoanApprovedAmountChangedBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -0,0 +1,47 @@
/**
* 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.io.Serializable;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
/**
* Immutable object representing an Approved Amount change operation on a Loan
*
* Note: no getter/setters required as google-gson will produce json from fields of object.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class LoanApprovedAmountHistoryData implements Serializable {
private Long loanId;
private ExternalId externalLoanId;
private BigDecimal newApprovedAmount;
private BigDecimal oldApprovedAmount;
private OffsetDateTime dateOfChange;
}
@@ -0,0 +1,47 @@
/**
* 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.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@Entity
@Table(name = "m_loan_approved_amount_history")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoanApprovedAmountHistory extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "loan_id", nullable = false)
private Long loanId;
@Column(name = "new_approved_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal newApprovedAmount;
@Column(name = "old_approved_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal oldApprovedAmount;
}
@@ -0,0 +1,39 @@
/**
* 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 java.util.List;
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
public interface LoanApprovedAmountHistoryRepository
extends JpaRepository<LoanApprovedAmountHistory, Long>, JpaSpecificationExecutor<LoanApprovedAmountHistory> {
@Query("""
SELECT NEW org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData(
laah.loanId, l.externalId, laah.newApprovedAmount, laah.oldApprovedAmount, laah.createdDate
)
FROM LoanApprovedAmountHistory laah JOIN Loan l on laah.loanId = l.id
WHERE laah.loanId = :loanId
""")
List<LoanApprovedAmountHistoryData> findAllByLoanId(Long loanId, Pageable pageable);
}
@@ -0,0 +1,42 @@
/**
* 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.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.LoanApprovedAmountWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN_APPROVED_AMOUNT", action = "UPDATE")
public class LoanApprovedAmountModificationCommandHandler implements NewCommandSourceHandler {
private final LoanApprovedAmountWritePlatformService loanApprovedAmountWritePlatformService;
@Override
@Transactional
public CommandProcessingResult processCommand(JsonCommand command) {
return loanApprovedAmountWritePlatformService.modifyLoanApprovedAmount(command.getLoanId(), command);
}
}
@@ -0,0 +1,26 @@
/**
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
public interface LoanApprovedAmountValidator {
void validateLoanApprovedAmountModification(JsonCommand command);
}
@@ -0,0 +1,27 @@
/**
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface LoanApprovedAmountWritePlatformService {
CommandProcessingResult modifyLoanApprovedAmount(Long loanId, JsonCommand command);
}
@@ -0,0 +1,76 @@
/**
* 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.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
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.event.business.domain.loan.LoanApprovedAmountChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApprovedAmountValidator;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoanApprovedAmountWritePlatformServiceImpl implements LoanApprovedAmountWritePlatformService {
private final LoanAssembler loanAssembler;
private final LoanApprovedAmountValidator loanApprovedAmountValidator;
private final LoanApprovedAmountHistoryRepository loanApprovedAmountHistoryRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Override
public CommandProcessingResult modifyLoanApprovedAmount(final Long loanId, final JsonCommand command) {
// API rule validations
this.loanApprovedAmountValidator.validateLoanApprovedAmountModification(command);
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("newApprovedAmount", command.stringValueOfParameterNamed(LoanApiConstants.amountParameterName));
changes.put("locale", command.locale());
Loan loan = this.loanAssembler.assembleFrom(loanId);
changes.put("oldApprovedAmount", loan.getApprovedPrincipal());
BigDecimal newApprovedAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.amountParameterName);
LoanApprovedAmountHistory loanApprovedAmountHistory = new LoanApprovedAmountHistory(loan.getId(), newApprovedAmount,
loan.getApprovedPrincipal());
loan.setApprovedPrincipal(newApprovedAmount);
loanApprovedAmountHistoryRepository.saveAndFlush(loanApprovedAmountHistory);
businessEventNotifierService.notifyPostBusinessEvent(new LoanApprovedAmountChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withEntityExternalId(loan.getExternalId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.with(changes) //
.build();
}
}
@@ -86,6 +86,7 @@
<class>org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.Loan</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>