Skip to content
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-2152: API Create and retrieve interest pause #4222

Conversation

kulminsky
Copy link
Contributor

@kulminsky kulminsky commented Dec 13, 2024

Description

API Create and retrieve interest pause

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!

  • Write the commit message as per https://github.com/apache/fineract/#pull-requests

  • Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.

  • Create/update unit or integration tests for verifying the changes made.

  • Follow coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions.

  • Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes

  • Submission is not a "code dump". (Large changes can be made "in repository" via a branch. Ask on the developer mailing list for guidance, if required.)

FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.

@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanInterestPauseApiResourceSwagger.LoanInterestPauseRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanInterestPauseApiResourceSwagger.LoanInterestPauseResponse.class))) })
public String createInterestPause(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can return the DTO object. WE dont need explicit serialization. Also, if you return the DTO, we dont need to create Swagger schema either, it will do automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still returning String...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommandProcessingResult

/**
* Swagger for LoanInterestPause API.
*/
final class LoanInterestPauseApiResourceSwagger {
Copy link
Contributor

@adamsaghy adamsaghy Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you start using DTO as response, some of these classes are not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final ExternalId loanExternalId = command.getLoanExternalId();
final String startDate = command.stringValueOfParameterNamed("startDate");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetch it as LocalDate instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

public CommandProcessingResult processCommand(final JsonCommand command) {
final ExternalId loanExternalId = command.getLoanExternalId();
final String startDate = command.stringValueOfParameterNamed("startDate");
final String endDate = command.stringValueOfParameterNamed("endDate");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetch it as LocalDate instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

public class InterestPauseData {

private final Long id;
private final String startDate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalDate would be better!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

private List<InterestPauseData> mapToInterestPauseData(List<LoanTermVariationsData> variations) {
return variations.stream()
.map(variation -> new InterestPauseData(variation.getId(), variation.getTermVariationApplicableFrom().toString(),
variation.getDateValue().toString(), DateUtils.DEFAULT_DATE_FORMAT, "en"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded locale is incorrect. We might not needed at all. Return the system locale. If we change the response field to return LocalDate we can use the date formatter from the converter: LocalDateJsonConverter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat).withLocale(new Locale(locale));
return LocalDate.parse(date, formatter);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the DateUtils.parseLocalDate() method and it will handle the exceptions properly!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

if (!endDate.isAfter(startDate)) {
throw new IllegalArgumentException("Interest pause end date must be later than the interest pause start date.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather use GeneralPlatformDomainRuleException!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

if (endDate.isAfter(loan.getMaturityDate())) {
throw new IllegalArgumentException("Interest pause end date cannot be later than loan maturity date.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather use GeneralPlatformDomainRuleException!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

LocalDate endDate = parseAndValidateDate(endDateStr, dateFormat, locale);

if (startDate.isBefore(loan.getSubmittedOnDate())) {
throw new IllegalArgumentException("Interest pause start date cannot be earlier than loan start date.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather use GeneralPlatformDomainRuleException!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


private void validateInterestPauseDates(Loan loan, String startDateStr, String endDateStr, String dateFormat, String locale) {
if (startDateStr == null || endDateStr == null) {
throw new IllegalArgumentException("Both `startDate` and `endDate` parameters are mandatory.");
Copy link
Contributor

@adamsaghy adamsaghy Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateOrThrow("interest-pause", baseDataValidator -> {baseDataValidator.reset().parameter(<parameter_name>).value(<parameter_value>).notNull(); 

might be better here!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final Long loanId = command.getLoanId();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These field values would be better in the service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


Long loanId = loanRepository.findIdByExternalId(loanExternalId);
if (loanId == null) {
throw new IllegalArgumentException("Loan not found with External ID: " + loanExternalId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoanNotFoundException would be better.. but if you inject LoanRepositoryWrapper, it has method to handle not found case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

public Long createInterestPause(ExternalId loanExternalId, String startDate, String endDate, String dateFormat, String locale) {
this.context.authenticatedUser();

Long loanId = loanRepository.findIdByExternalId(loanExternalId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. Fetching id here and 3 lines later fetching the whole entity is weird. Fetch the entity and handle the not found case (see below how)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no changes...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId)

throw new IllegalArgumentException("Loan not found with External ID: " + loanExternalId);
}

Loan loan = loanRepository.findById(loanId).orElseThrow(() -> new IllegalArgumentException("Loan not found with ID: " + loanId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above how to handle not found case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale);

LocalDate startLocalDate = parseAndValidateDate(startDate, dateFormat, locale);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can parse from command the LocalDate or by using the DateUtils.parseLocalDate()`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

endLocalDate, false, loan);

LoanTermVariations savedVariation = loanTermVariationsRepository.save(variation);
loanTermVariationsRepository.flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can call saveAndFlush in one step...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

LoanTermVariations savedVariation = loanTermVariationsRepository.save(variation);
loanTermVariationsRepository.flush();

return savedVariation.getId();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build the CommandResult object here... not in the handler

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

public Long createInterestPauseByLoanId(Long loanId, String startDate, String endDate, String dateFormat, String locale) {
this.context.authenticatedUser();

final Loan loan = loanRepository.findById(loanId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -30,7 +30,8 @@ public enum LoanTermVariationType {
GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), //
GRACE_ON_PRINCIPAL(8, "loanTermType.graceOnPrincipal"), //
EXTEND_REPAYMENT_PERIOD(9, "loanTermType.extendRepaymentPeriod"), //
INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); //
INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"), //
INTEREST_PAUSE(11, "loanTermType.interestPause"); //
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the new type to the fromInt method!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@kulminsky kulminsky force-pushed the FINERACT-2152/API_Create_and_retrieve_interest_pause branch 4 times, most recently from 8ff5a07 to 3f9c84e Compare December 18, 2024 07:49
@kulminsky kulminsky marked this pull request as ready for review December 18, 2024 07:49
@kulminsky kulminsky requested a review from adamsaghy December 18, 2024 09:10
@kulminsky kulminsky force-pushed the FINERACT-2152/API_Create_and_retrieve_interest_pause branch from 3f9c84e to d983244 Compare December 18, 2024 10:02

final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);

return this.toApiJsonSerializer.serialize(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this. Just return the DTO AS-IS. Explicit serialization is not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);

return this.interestPauseReadPlatformService.retrieveInterestPausesByExternalId(loanExternalId).stream()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont need we need to map InterestPauseResponseDto to InterestPauseResponseDto :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


@Test
public void testCreateInterestPauseByLoanId_validRequest_shouldSucceed() {
Long loanId = 1L;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on a hard coded loan is not correct. Please create your own loan application and use that as part of the testing!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

private final RequestSpecification requestSpec;
private final ResponseSpecification defaultResponseSpec;

public InterestPauseHelper(final RequestSpecification requestSpec, final ResponseSpecification defaultResponseSpec) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the fineract-client API instead!
Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper#makeLoanRepayment(java.lang.Long, org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

@adamsaghy adamsaghy left a 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!

@kulminsky kulminsky force-pushed the FINERACT-2152/API_Create_and_retrieve_interest_pause branch from d983244 to 65137ab Compare December 18, 2024 15:03
final ExternalId loanExternalId = command.getLoanExternalId();
result = interestPauseService.createInterestPause(loanExternalId, startDate, endDate, dateFormat, locale);
} else {
throw new IllegalArgumentException("Either loanId or loanExternalId must be provided.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please throw PlatformApiDataValidationException. IllegalArgumentExceptions are not covered by any Exception handler, so it will fallback to HTTP 500 - Internal server error which is not good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should consider using Spring bean configuration, so it is easier to customize (override) this service.
Example:
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl and org.apache.fineract.portfolio.loanaccount.starter.LoanAccountConfiguration#loanReadPlatformService

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@Override
@Transactional(readOnly = true)
public List<InterestPauseResponseDto> retrieveInterestPauses(Long loanId) {
this.context.authenticatedUser();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authenticated user and permission is already checked on API layer. We dont need to do here again!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@Override
@Transactional(readOnly = true)
public List<InterestPauseResponseDto> retrieveInterestPausesByExternalId(String loanExternalId) {
this.context.authenticatedUser();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authenticated user and permission is already checked on API layer. We dont need to do here again!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


@Override
@Transactional
public CommandProcessingResult createInterestPause(ExternalId loanExternalId, LocalDate startDate, LocalDate endDate, String dateFormat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write operation should not be in a Read service. Would you mind to extract them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to InterestPauseWritePlatformService

private CommandProcessingResult processInterestPause(Supplier<Loan> loanSupplier, LocalDate startDate, LocalDate endDate,
String dateFormat, String locale) {

this.context.authenticatedUser();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Permissions were already checked in the Command processor, we dont need to do it again here!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@Transactional
public CommandProcessingResult createInterestPauseByLoanId(Long loanId, LocalDate startDate, LocalDate endDate, String dateFormat,
String locale) {
return processInterestPause(() -> loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), startDate, endDate, dateFormat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write operation should not be in a Read service. Would you mind to extract them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to InterestPauseWritePlatformService

public CommandProcessingResult createInterestPause(ExternalId loanExternalId, LocalDate startDate, LocalDate endDate, String dateFormat,
String locale) {
return processInterestPause(() -> {
Long loanId = loanRepositoryWrapper.findIdByExternalId(loanExternalId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed:
loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); is covering the use case if the loan cannot be found!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId)


@Override
@Transactional
public CommandProcessingResult createInterestPauseByLoanId(Long loanId, LocalDate startDate, LocalDate endDate, String dateFormat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we need the "ByLoanId" postfix, createInterestPause would be enough...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


@Override
@Transactional(readOnly = true)
public List<InterestPauseResponseDto> retrieveInterestPausesByExternalId(String loanExternalId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we need the "ByExternalId" postfix. retrieveInterestPauses would be enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

private final LoanRepositoryWrapper loanRepositoryWrapper;

@Override
@Transactional(readOnly = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move the @Transactional(readOnly = true) on class level. All methods should be read-only in a Read service!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public CommandProcessingResult processCommand(final JsonCommand command) {
CommandProcessingResult result;

final LocalDate startDate = LocalDate.parse(command.stringValueOfParameterNamed("startDate"));
Copy link
Contributor

@adamsaghy adamsaghy Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect. You might wanna move the conversion of the fields into the service layer (i like to keep the validation and conversions at the same place, so anything that could fail is in one place, we dont need to figure out where exceptions might occurred...) and also first we should validate the incoming parameters, and just after we should try to convert them (or maybe doing in one shot the validation and conversion...that is fine). Also the conversion should happen based on the dateformat and locale... so to parse the incoming "startDate" as string into LocalDate, we need to use the "startDate + locale + dateformat" combo...

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved validation and parse to InterestPauseWritePlatformService

endDate, loan.getMaturityDate());
}

if (!endDate.isAfter(startDate)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Start date and End date can be the same, which means for 1 day the interest is paused!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

@adamsaghy adamsaghy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kindly check my updated review!

@kulminsky kulminsky force-pushed the FINERACT-2152/API_Create_and_retrieve_interest_pause branch 5 times, most recently from 19c4770 to 08dd456 Compare December 19, 2024 12:11
@kulminsky kulminsky force-pushed the FINERACT-2152/API_Create_and_retrieve_interest_pause branch from 08dd456 to d7f0956 Compare December 19, 2024 15:30
@kulminsky kulminsky requested a review from adamsaghy December 19, 2024 15:32
Copy link
Contributor

@adamsaghy adamsaghy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@adamsaghy adamsaghy merged commit c2ec015 into apache:develop Dec 19, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants