-
Notifications
You must be signed in to change notification settings - Fork 2.2k
FINERACT-2317: Add support for modifying loan approved amounts with validation and history tracking #4828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FINERACT-2317: Add support for modifying loan approved amounts with validation and history tracking #4828
Conversation
7893d4e to
687fb7f
Compare
| import java.util.List; | ||
| import org.apache.fineract.infrastructure.core.data.ApiParameterError; | ||
|
|
||
| public class PlatformApiDomainRuleValidationException extends AbstractPlatformDomainRuleException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should GeneralPlatformDomainRuleException be used instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to create a new exception, that will be mapped to http status code 403 and behaves similarily to PlatformApiDataValidationException so that I can add multiple validation error messages to the exception. I can expand this exception instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... to me it looks like the love child of PlatformApiDataValidationException and GeneralPlatformDomainRuleException.
The difference is
- GeneralPlatformDomainRuleException -> HTTP 403 and to be used if any domain rule violated like "You cannot set a future date", "You cannot reverse an already reversed transaction" etc.
PlatformApiDataValidationException -> HTTP 400 and to be used if the request contains unknown fields, or a mandatory field were not passed, or the value is incorrect, etc...
| log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception)); | ||
| final ApiGlobalErrorResponse notFoundErrorResponse = ApiGlobalErrorResponse.domainRuleViolation( | ||
| exception.getGlobalisationMessageCode(), exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs()); | ||
| final ApiGlobalErrorResponse notFoundErrorResponse; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the problem here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is a validation exception I want to include the error messages that were generated during validation. These validation messages are stored in the errors list in the exception, not in the defaultUserMessageArgs. I did it this way to keep the new PlatformApiDomainRuleValidationException consistent with PlatformApiDataValidationException. But I can change it.
I will replace the PlatformApiDomainRuleValidationException with the GeneralPlatformDomainRuleException mentioned in the other comment and remove this logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PlatformApiDomainRuleValidationException is mapped HTTP 403 exception for domain rule violations
PlatformApiDataValidationException is mapped to HTTP 400 exception for request data validation violations
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public final class LoanValidatorImpl implements LoanValidator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems not a LoanValidator to me... We are validating whether approved amount can be changed....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to create a validator that in the future can be used to group validation logics relating to the loan entity itself. Like for example the LoanTransactionValidator for different loan transaction validation logics. I can rename it so it only relates to this use case.
| Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); | ||
| BigDecimal approvedPrincipal = loan.getApprovedPrincipal(); | ||
|
|
||
| if (MathUtil.isGreaterThan(newApprovedAmount, approvedPrincipal)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should allow to increase approved amount if:
- Applied amount + configured threshold equals or higher than "new approved" amount
- new approved amount cannot be lower than already disbursed amount + total capitalized income
- new approved amount cannot be lower than total "expected disbursement" + total capitalized income
| } | ||
|
|
||
| BigDecimal reservedPrincipal = BigDecimal.ZERO; | ||
| if (!loan.loanProduct().isDisallowExpectedDisbursements()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can simplify this, by removing this condition and aggregate the principal of loan disbursement details (except the reversed=true entries). It will contains all already disbursed (actualDisbursementDate != null) and not yet disbursed but expected (actualDisbursementDate == null).
|
|
||
| reservedPrincipal = reservedPrincipal.add(loan.getSummary().getTotalPrincipal()); | ||
|
|
||
| if (MathUtil.isLessThan(newApprovedAmount, reservedPrincipal)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not fond of "reserved principal" definition. Would "total already disbursed and expected disbursement amount is higher than new approved amount"?
adamsaghy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kindly see my review!
ddb8a0f to
bec1239
Compare
bec1239 to
b328b63
Compare
adamsaghy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
799bc86 to
1034f4a
Compare
1034f4a to
54130c0
Compare
fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java
Show resolved
Hide resolved
| } | ||
|
|
||
| public static String updateApprovedLoanExceedReservedPrincipalFailure() { | ||
| return "Failed data validation due to: less.than.reserved.principal."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is reserved principal?
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "loan_id", nullable = false) | ||
| private Loan loan; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We dont need to fetch the Loan entity here... you can use Long type instead ;)
| ) | ||
| FROM LoanApprovedAmountHistory laah | ||
| WHERE laah.loan.id = :loanId | ||
| ORDER BY laah.createdDate ASC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of hardcoding the order by you might wanna provide Pageable to this method and caller service can decide which order by to be used ;)
| CommandWrapper commandRequest = builder.updateLoanApprovedAmount(resolvedLoanId).build(); | ||
|
|
||
| final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); | ||
| return this.toApiJsonSerializer.serialize(result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can skip the serialization and return directly the CommandProcessingResult type.
...t-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
Show resolved
Hide resolved
| context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); | ||
| Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); | ||
| List<LoanApprovedAmountHistoryData> loanApprovedAmountHistory = loanApprovedAmountHistoryRepository.findAllByLoanId(resolvedLoanId); | ||
| return new LoanApprovedAmountHistoryResponse(loanApprovedAmountHistory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can return directly the List<LoanApprovedAmountHistoryData>, no need extra serialization.
| @Consumes(MediaType.APPLICATION_JSON) | ||
| @Produces(MediaType.APPLICATION_JSON) | ||
| @Operation(summary = "Collects and returns the approved amount modification history for a given loan", description = "") | ||
| @ApiResponses({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
| || (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) { | ||
| 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, | ||
| final String errorMsg = "Loan can't be disbursed, disburse amount is exceeding proposed principal."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we were checking the approved amount i think we should not change the exception message!
| .build(); | ||
| } | ||
|
|
||
| @Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LoanWritePlatformService getting bloated. Would you consider to move this into its own service?
54130c0 to
e5f30be
Compare
e5f30be to
699c865
Compare
0bba32d to
07c3b32
Compare
| private BigDecimal oldApprovedAmount; | ||
| private OffsetDateTime dateOfChange; | ||
|
|
||
| public LoanApprovedAmountHistoryData(final Long loanId, final ExternalId externalLoanId, final BigDecimal newApprovedAmount, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AllArgConstructor?
|
|
||
| private List<LoanApprovedAmountHistoryData> approvedAmountHistory; | ||
|
|
||
| public LoanApprovedAmountHistoryResponse(final List<LoanApprovedAmountHistoryData> approvedAmountHistory) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AllArgConstructor?
| </changeSet> | ||
| <changeSet author="fineract" id="4" runInTransaction="false" context="postgresql"> | ||
| <sql> | ||
| create index concurrently if not exists m_loan_approved_amount_history_idx_1 on m_loan_approved_amount_history(loan_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We dont need index if the column is already a foreign key!
adamsaghy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please kindly see my review!
348752b to
1e5d9b2
Compare
…alidation and history tracking
54d4118 to
b431f6e
Compare
…proved amounts with validation and history tracking
b431f6e to
59a62ca
Compare
adamsaghy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Description
Added two new endpoints to handle loan approved amount modifications. Users can now lower the approved amount on loans, if the new approved amount is not lower than the actual and expected disbursements and capitalized income transactions. The history of these changes can also be examined by calling the new get endpoint with a loan id.
Ignore if these details are present on the associated Apache Fineract JIRA ticket.
Checklist
Please make sure these boxes are checked before submitting your pull request - thanks!
FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.