Skip to content

Commit 7893d4e

Browse files
committed
FINERACT-2317: Add support for modifying loan approved amounts with validation and history tracking
1 parent 4c3da5b commit 7893d4e

22 files changed

Lines changed: 903 additions & 8 deletions

File tree

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3858,4 +3858,13 @@ public CommandWrapperBuilder undoContractTermination(final Long loanId) {
38583858
this.href = "/loans/" + loanId;
38593859
return this;
38603860
}
3861+
3862+
public CommandWrapperBuilder updateLoanApprovedAmount(final Long loanId) {
3863+
this.actionName = "UPDATE";
3864+
this.entityName = "LOAN_APPROVED_AMOUNT";
3865+
this.entityId = loanId;
3866+
this.loanId = loanId;
3867+
this.href = "/loans/" + loanId;
3868+
return this;
3869+
}
38613870
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.infrastructure.core.exception;
20+
21+
import java.util.List;
22+
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
23+
24+
public class PlatformApiDomainRuleValidationException extends AbstractPlatformDomainRuleException {
25+
26+
private final List<ApiParameterError> errors;
27+
28+
public PlatformApiDomainRuleValidationException(String globalisationMessageCode, String defaultUserMessage,
29+
List<ApiParameterError> errors) {
30+
super(globalisationMessageCode, defaultUserMessage);
31+
this.errors = errors;
32+
}
33+
34+
protected PlatformApiDomainRuleValidationException(String globalisationMessageCode, String defaultUserMessage,
35+
Object... defaultUserMessageArgs) {
36+
super(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs);
37+
this.errors = null;
38+
}
39+
40+
public List<ApiParameterError> getErrors() {
41+
return this.errors;
42+
}
43+
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
2828
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
2929
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
30+
import org.apache.fineract.infrastructure.core.exception.PlatformApiDomainRuleValidationException;
3031
import org.springframework.context.annotation.Scope;
3132
import org.springframework.stereotype.Component;
3233

@@ -46,8 +47,14 @@ public class PlatformDomainRuleExceptionMapper implements FineractExceptionMappe
4647
@Override
4748
public Response toResponse(final AbstractPlatformDomainRuleException exception) {
4849
log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception));
49-
final ApiGlobalErrorResponse notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation(
50-
exception.getGlobalisationMessageCode(), exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
50+
final ApiGlobalErrorResponse notFoundErrorResponse;
51+
if (exception instanceof PlatformApiDomainRuleValidationException validationException) {
52+
notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation(validationException.getGlobalisationMessageCode(),
53+
validationException.getDefaultUserMessage(), validationException.getErrors());
54+
} else {
55+
notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation(exception.getGlobalisationMessageCode(),
56+
exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
57+
}
5158
// request understood but not carried out due to it violating some
5259
// domain/business logic
5360
return Response.status(Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();

fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,35 @@
2424
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
2525
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
2626
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
27+
import org.apache.fineract.infrastructure.core.exception.PlatformApiDomainRuleValidationException;
2728

2829
public final class Validator {
2930

3031
private Validator() {}
3132

3233
public static void validateOrThrow(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
33-
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
34-
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource);
35-
36-
baseDataValidator.accept(dataValidatorBuilder);
34+
final List<ApiParameterError> dataValidationErrors = getApiParameterErrors(resource, baseDataValidator);
3735

3836
if (!dataValidationErrors.isEmpty()) {
3937
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
4038
dataValidationErrors);
4139
}
4240
}
41+
42+
public static void validateOrThrowDomainViolation(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
43+
final List<ApiParameterError> dataValidationErrors = getApiParameterErrors(resource, baseDataValidator);
44+
45+
if (!dataValidationErrors.isEmpty()) {
46+
throw new PlatformApiDomainRuleValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
47+
dataValidationErrors);
48+
}
49+
}
50+
51+
private static List<ApiParameterError> getApiParameterErrors(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
52+
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
53+
final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource);
54+
55+
baseDataValidator.accept(dataValidatorBuilder);
56+
return dataValidationErrors;
57+
}
4358
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.data;
20+
21+
import java.io.Serializable;
22+
import java.math.BigDecimal;
23+
import java.time.OffsetDateTime;
24+
import lombok.Data;
25+
import lombok.NoArgsConstructor;
26+
import lombok.experimental.Accessors;
27+
import org.apache.fineract.infrastructure.core.domain.ExternalId;
28+
29+
/**
30+
* Immutable object representing an Approved Amount change operation on a Loan
31+
*
32+
* Note: no getter/setters required as google-gson will produce json from fields of object.
33+
*/
34+
35+
@Data
36+
@NoArgsConstructor
37+
@Accessors(chain = true)
38+
public class LoanApprovedAmountHistoryData implements Serializable {
39+
40+
private Long loanId;
41+
private ExternalId externalLoanId;
42+
private BigDecimal newApprovedAmount;
43+
private BigDecimal oldApprovedAmount;
44+
private OffsetDateTime dateOfChange;
45+
46+
public LoanApprovedAmountHistoryData(final Long loanId, final ExternalId externalLoanId, final BigDecimal newApprovedAmount,
47+
final BigDecimal oldApprovedAmount, final OffsetDateTime dateOfChange) {
48+
this.loanId = loanId;
49+
this.externalLoanId = externalLoanId;
50+
this.newApprovedAmount = newApprovedAmount;
51+
this.oldApprovedAmount = oldApprovedAmount;
52+
this.dateOfChange = dateOfChange;
53+
}
54+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.data;
20+
21+
import java.io.Serializable;
22+
import java.util.List;
23+
import lombok.Data;
24+
import lombok.NoArgsConstructor;
25+
import lombok.experimental.Accessors;
26+
27+
/**
28+
* Immutable object representing the Approved Amount change history of a Loan
29+
*
30+
* Note: no getter/setters required as google-gson will produce json from fields of object.
31+
*/
32+
33+
@Data
34+
@NoArgsConstructor
35+
@Accessors(chain = true)
36+
public class LoanApprovedAmountHistoryResponse implements Serializable {
37+
38+
private List<LoanApprovedAmountHistoryData> approvedAmountHistory;
39+
40+
public LoanApprovedAmountHistoryResponse(final List<LoanApprovedAmountHistoryData> approvedAmountHistory) {
41+
this.approvedAmountHistory = approvedAmountHistory;
42+
}
43+
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,12 @@ public LocalDate determineExpectedMaturityDate() {
756756
return maturityDate;
757757
}
758758

759+
public List<LoanDisbursementDetails> getExpectedLoanDisbursementDetails() {
760+
return getDisbursementDetails().stream() //
761+
.filter(it -> it.actualDisbursementDate() == null) //
762+
.collect(Collectors.toList());
763+
}
764+
759765
public List<LoanDisbursementDetails> getDisbursedLoanDisbursementDetails() {
760766
return getDisbursementDetails().stream() //
761767
.filter(it -> it.actualDisbursementDate() != null) //
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.domain;
20+
21+
import jakarta.persistence.Column;
22+
import jakarta.persistence.Entity;
23+
import jakarta.persistence.JoinColumn;
24+
import jakarta.persistence.ManyToOne;
25+
import jakarta.persistence.Table;
26+
import java.math.BigDecimal;
27+
import lombok.AllArgsConstructor;
28+
import lombok.Getter;
29+
import lombok.NoArgsConstructor;
30+
import lombok.Setter;
31+
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
32+
33+
@Entity
34+
@Table(name = "m_loan_approved_amount_history")
35+
@Getter
36+
@Setter
37+
@NoArgsConstructor
38+
@AllArgsConstructor
39+
public class LoanApprovedAmountHistory extends AbstractAuditableWithUTCDateTimeCustom<Long> {
40+
41+
@ManyToOne
42+
@JoinColumn(name = "loan_id", nullable = false)
43+
private Loan loan;
44+
45+
@Column(name = "new_approved_amount", scale = 6, precision = 19, nullable = false)
46+
private BigDecimal newApprovedAmount;
47+
48+
@Column(name = "old_approved_amount", scale = 6, precision = 19, nullable = false)
49+
private BigDecimal oldApprovedAmount;
50+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.domain;
20+
21+
import java.util.List;
22+
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
23+
import org.springframework.data.jpa.repository.JpaRepository;
24+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
25+
import org.springframework.data.jpa.repository.Query;
26+
27+
public interface LoanApprovedAmountHistoryRepository
28+
extends JpaRepository<LoanApprovedAmountHistory, Long>, JpaSpecificationExecutor<LoanApprovedAmountHistory> {
29+
30+
@Query("""
31+
SELECT NEW org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData(
32+
laah.loan.id, laah.loan.externalId, laah.newApprovedAmount, laah.oldApprovedAmount, laah.createdDate
33+
)
34+
FROM LoanApprovedAmountHistory laah
35+
WHERE laah.loan.id = :loanId
36+
ORDER BY laah.createdDate ASC
37+
""")
38+
List<LoanApprovedAmountHistoryData> findAllByLoanId(Long loanId);
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "LOAN_APPROVED_AMOUNT", action = "UPDATE")
33+
public class LoanApprovedAmountModificationCommandHandler implements NewCommandSourceHandler {
34+
35+
private final LoanWritePlatformService loanWritePlatformService;
36+
37+
@Override
38+
@Transactional
39+
public CommandProcessingResult processCommand(JsonCommand command) {
40+
return loanWritePlatformService.modifyLoanApprovedAmount(command.getLoanId(), command);
41+
}
42+
}

0 commit comments

Comments
 (0)