FINERACT-1734: New business events and refactorings

This commit is contained in:
Arnold Galovics
2022-09-11 16:56:03 +02:00
committed by Arnold Gálovics
parent d06c3655af
commit 130d9c0c3d
23 changed files with 505 additions and 356 deletions
@@ -524,17 +524,6 @@
"org.apache.fineract.avro.portfolio.v1.PortfolioAccountDataV1"
]
},
{
"default": null,
"name": "accountLinkingOptions",
"type": [
"null",
{
"type": "array",
"items": "org.apache.fineract.avro.portfolio.v1.PortfolioAccountDataV1"
}
]
},
{
"default": null,
"name": "multiDisburseLoan",
@@ -637,6 +626,14 @@
"bigdecimal"
]
},
{
"default": null,
"name": "product",
"type": [
"null",
"org.apache.fineract.avro.loan.v1.LoanProductDataV1"
]
},
{
"default": null,
"name": "inArrears",
@@ -0,0 +1,23 @@
{
"name": "LoanChargeDeletedV1",
"namespace": "org.apache.fineract.avro.loan.v1",
"type": "record",
"fields": [
{
"default": null,
"name": "id",
"type": [
"null",
"long"
]
},
{
"default": null,
"name": "chargeId",
"type": [
"null",
"long"
]
}
]
}
@@ -0,0 +1,23 @@
{
"name": "LoanTransactionWithLoanV1",
"namespace": "org.apache.fineract.avro.loan.v1",
"type": "record",
"fields": [
{
"default": null,
"name": "loan",
"type": [
"null",
"org.apache.fineract.avro.loan.v1.LoanAccountDataV1"
]
},
{
"default": null,
"name": "loanTransaction",
"type": [
"null",
"org.apache.fineract.avro.loan.v1.LoanTransactionDataV1"
]
}
]
}
@@ -29,3 +29,5 @@ include::technology.adoc[leveloffset=+1]
include::modules.adoc[leveloffset=+1]
include::business-date.adoc[leveloffset=+1]
include::reliable-event-framework.adoc[leveloffset=+1]
@@ -0,0 +1,33 @@
/**
* 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 LoanRescheduledDueCalendarChangeBusinessEvent extends LoanBusinessEvent {
public LoanRescheduledDueCalendarChangeBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanRescheduledDueCalendarChangeBusinessEvent";
}
}
@@ -0,0 +1,33 @@
/**
* 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 LoanRescheduledDueHolidayBusinessEvent extends LoanBusinessEvent {
public LoanRescheduledDueHolidayBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanRescheduledDueHolidayBusinessEvent";
}
}
@@ -0,0 +1,33 @@
/**
* 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 LoanScheduleVariationsAddedBusinessEvent extends LoanBusinessEvent {
public LoanScheduleVariationsAddedBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanScheduleVariationsAddedBusinessEvent";
}
}
@@ -0,0 +1,33 @@
/**
* 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 LoanScheduleVariationsDeletedBusinessEvent extends LoanBusinessEvent {
public LoanScheduleVariationsDeletedBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanScheduleVariationsDeletedBusinessEvent";
}
}
@@ -0,0 +1,33 @@
/**
* 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 LoanStatusChangedBusinessEvent extends LoanBusinessEvent {
public LoanStatusChangedBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanStatusChangedBusinessEvent";
}
}
@@ -0,0 +1,33 @@
/**
* 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 LoanUpdateDisbursementDataBusinessEvent extends LoanBusinessEvent {
public LoanUpdateDisbursementDataBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return "LoanUpdateDisbursementDataBusinessEvent";
}
}
@@ -19,6 +19,8 @@
package org.apache.fineract.infrastructure.event.external.service;
import java.io.IOException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.external.repository.ExternalEventRepository;
@@ -38,6 +40,8 @@ public class ExternalEventService {
private final ExternalEventIdempotencyKeyGenerator idempotencyKeyGenerator;
private final BusinessEventSerializerFactory serializerFactory;
private EntityManager entityManager;
public <T> void postEvent(BusinessEvent<T> event) {
if (event == null) {
throw new IllegalArgumentException("event cannot be null");
@@ -48,6 +52,7 @@ public class ExternalEventService {
try {
BusinessEventSerializer serializer = serializerFactory.create(event);
String schema = serializer.getSupportedSchema().getName();
flushChangesBeforeSerialization();
byte[] data = serializer.serialize(event);
ExternalEvent externalEvent = new ExternalEvent(eventType, schema, data, idempotencyKey);
@@ -57,4 +62,13 @@ public class ExternalEventService {
}
}
private void flushChangesBeforeSerialization() {
entityManager.flush();
}
@PersistenceContext
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}
@@ -0,0 +1,62 @@
/**
* 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.external.service.serialization.serializer.loan;
import java.io.IOException;
import java.nio.ByteBuffer;
import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.loan.v1.LoanChargeDeletedV1;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanDeleteChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
import org.apache.fineract.infrastructure.event.external.service.support.ByteBufferConverter;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class LoanChargeDeletedBusinessEventSerializer implements BusinessEventSerializer {
private final ByteBufferConverter byteBufferConverter;
@Override
public <T> boolean canSerialize(BusinessEvent<T> event) {
return event instanceof LoanDeleteChargeBusinessEvent;
}
@Override
public <T> byte[] serialize(BusinessEvent<T> rawEvent) throws IOException {
LoanDeleteChargeBusinessEvent event = (LoanDeleteChargeBusinessEvent) rawEvent;
LoanCharge loanCharge = event.get();
Long id = loanCharge.getId();
Long chargeId = loanCharge.getCharge().getId();
LoanChargeDeletedV1 avroDto = new LoanChargeDeletedV1(id, chargeId);
ByteBuffer buffer = avroDto.toByteBuffer();
return byteBufferConverter.convert(buffer);
}
@Override
public Class<? extends GenericContainer> getSupportedSchema() {
return LoanChargeDeletedV1.class;
}
}
@@ -22,12 +22,16 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.loan.v1.LoanAccountDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionWithLoanV1;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanAccountDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanTransactionDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
import org.apache.fineract.infrastructure.event.external.service.support.ByteBufferConverter;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.stereotype.Component;
@@ -37,7 +41,8 @@ import org.springframework.stereotype.Component;
public class LoanTransactionBusinessEventSerializer implements BusinessEventSerializer {
private final LoanReadPlatformService service;
private final LoanTransactionDataMapper mapper;
private final LoanAccountDataMapper loanMapper;
private final LoanTransactionDataMapper loanTransactionMapper;
private final ByteBufferConverter byteBufferConverter;
@Override
@@ -48,8 +53,13 @@ public class LoanTransactionBusinessEventSerializer implements BusinessEventSeri
@Override
public <T> byte[] serialize(BusinessEvent<T> rawEvent) throws IOException {
LoanTransactionBusinessEvent event = (LoanTransactionBusinessEvent) rawEvent;
LoanTransactionData data = service.retrieveLoanTransaction(event.get().getLoan().getId(), event.get().getId());
LoanTransactionDataV1 avroDto = mapper.map(data);
Long loanId = event.get().getLoan().getId();
LoanAccountData loanData = service.retrieveOne(loanId);
LoanAccountDataV1 loanAvroDto = loanMapper.map(loanData);
Long loanTransactionId = event.get().getId();
LoanTransactionData transactionData = service.retrieveLoanTransaction(loanId, loanTransactionId);
LoanTransactionDataV1 loanTransactionAvroDto = loanTransactionMapper.map(transactionData);
LoanTransactionWithLoanV1 avroDto = new LoanTransactionWithLoanV1(loanAvroDto, loanTransactionAvroDto);
ByteBuffer buffer = avroDto.toByteBuffer();
return byteBufferConverter.convert(buffer);
}
@@ -18,13 +18,22 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
// TODO: introduce tests for the state machine
@RequiredArgsConstructor
public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine {
private final List<LoanStatus> allowedLoanStatuses;
private final BusinessEventNotifierService businessEventNotifierService;
public DefaultLoanLifecycleStateMachine(LoanStatus[] allowedLoanStatuses, BusinessEventNotifierService businessEventNotifierService) {
this(Arrays.asList(allowedLoanStatuses), businessEventNotifierService);
}
@Override
public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) {
@@ -35,6 +44,15 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
public void transition(final LoanEvent loanEvent, final Loan loan) {
LoanStatus newState = getNextState(loanEvent, loan);
loan.setLoanStatus(newState.getValue());
if (isNotLoanCreation(loanEvent)) {
// in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan));
}
}
private boolean isNotLoanCreation(LoanEvent loanEvent) {
return !LoanEvent.LOAN_CREATED.equals(loanEvent);
}
private LoanStatus getNextState(LoanEvent loanEvent, Loan loan) {
@@ -122,12 +122,10 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementData
import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
@@ -2406,12 +2404,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) {
this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this);
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
this.disbursedBy = currentUser;
updateLoanScheduleDependentDerivedFields();
@@ -2446,7 +2440,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
addLoanTransaction(interestAppliedTransaction);
}
return reprocessTransactionForDisbursement();
ChangedTransactionDetail result = reprocessTransactionForDisbursement();
this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this);
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
return result;
}
@@ -4597,82 +4594,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
// startDate
}
public void applyHolidayToRepaymentScheduleDates(final Holiday holiday, final LoanUtilService loanUtilService) {
final DefaultScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(this, holiday.getFromDateLocalDate());
final LoanApplicationTerms loanApplicationTerms = this.constructLoanApplicationTerms(scheduleGeneratorDTO);
// LocalDate rescheduleFromDate = holiday.getFromDateLocalDate();
LocalDate adjustedRescheduleToDate = null;
if (holiday.getReScheduleType().isResheduleToNextRepaymentDate()) {
final LocalDate rescheduleToDate = holiday.getToDateLocalDate();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.getRepaymentScheduleInstallments()) {
if (rescheduleToDate.isEqual(loanRepaymentScheduleInstallment.getDueDate())) {
adjustedRescheduleToDate = rescheduleToDate;
break;
} else {
// Account for a Bimonthly Loan Product
// if (loanApplicationTerms.getRepaymentPeriodFrequencyType().isSemiMonthly()) {
// if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
// && rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(15))) {
// adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
// break;
// }
// } else {
// // Once Bimonthly product implemented, Replace the `Standard Monthly Loan Holiday check` below
// HERE
// if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
// && rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(30))) {
// adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
// break;
// }
// }
// Standard Monthly Loan Holiday check
if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
&& rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(30))) {
adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
break;
}
}
}
} else {
adjustedRescheduleToDate = holiday.getRepaymentsRescheduledToLocalDate();
}
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = getDisbursementDate();
// Loop through all loanRepayments
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
if (adjustedRescheduleToDate == null) {
break;
}
final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// update from date if it's not same as previous installament's due
// date.
if (!loanRepaymentScheduleInstallment.getFromDate().isEqual(tmpFromDate)) {
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.isAfter(holiday.getFromDateLocalDate())) {
// FIXME: AA do we need to apply non-working days.
// Assuming holiday's repayment reschedule to date cannot be
// created on a non-working day.
adjustedRescheduleToDate = scheduledDateGenerator.generateNextRepaymentDate(adjustedRescheduleToDate, loanApplicationTerms,
false);
loanRepaymentScheduleInstallment.updateDueDate(adjustedRescheduleToDate);
}
tmpFromDate = loanRepaymentScheduleInstallment.getDueDate();
}
}
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay) {
if (!WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())) {
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
@@ -425,8 +424,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
private void checkClientOrGroupActive(final Loan loan) {
@@ -18,11 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applyholidaystoloans;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
@@ -40,28 +36,15 @@ public class ApplyHolidaysToLoansConfig {
@Autowired
private StepBuilderFactory steps;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private HolidayRepositoryWrapper holidayRepository;
@Autowired
private LoanRepositoryWrapper loanRepositoryWrapper;
@Autowired
private LoanUtilService loanUtilService;
@Bean
protected Step applyHolidaysToLoansStep() {
return steps.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).tasklet(applyHolidaysToLoansTasklet()).build();
protected Step applyHolidaysToLoansStep(ApplyHolidaysToLoansTasklet applyHolidaysToLoansTasklet) {
return steps.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).tasklet(applyHolidaysToLoansTasklet).build();
}
@Bean
public Job applyHolidaysToLoansJob() {
return jobs.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).start(applyHolidaysToLoansStep()).incrementer(new RunIdIncrementer())
.build();
}
@Bean
public ApplyHolidaysToLoansTasklet applyHolidaysToLoansTasklet() {
return new ApplyHolidaysToLoansTasklet(configurationDomainService, holidayRepository, loanRepositoryWrapper, loanUtilService);
public Job applyHolidaysToLoansJob(ApplyHolidaysToLoansTasklet applyHolidaysToLoansTasklet) {
return jobs.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).start(applyHolidaysToLoansStep(applyHolidaysToLoansTasklet))
.incrementer(new RunIdIncrementer()).build();
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applyholidaystoloans;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -26,26 +27,35 @@ import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRescheduledDueHolidayBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class ApplyHolidaysToLoansTasklet implements Tasklet {
private final ConfigurationDomainService configurationDomainService;
private final HolidayRepositoryWrapper holidayRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanUtilService loanUtilService;
private final BusinessEventNotifierService businessEventNotifierService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -72,7 +82,7 @@ public class ApplyHolidaysToLoansTasklet implements Tasklet {
loans.addAll(loanRepositoryWrapper.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (final Loan loan : loans) {
loan.applyHolidayToRepaymentScheduleDates(holiday, loanUtilService);
applyHolidayToRepaymentScheduleDates(loan, holiday);
}
loanRepositoryWrapper.save(loans);
holiday.processed();
@@ -80,4 +90,80 @@ public class ApplyHolidaysToLoansTasklet implements Tasklet {
holidayRepository.save(holidays);
return RepeatStatus.FINISHED;
}
public void applyHolidayToRepaymentScheduleDates(Loan loan, Holiday holiday) {
LocalDate adjustedRescheduleToDate = null;
if (holiday.getReScheduleType().isResheduleToNextRepaymentDate()) {
adjustedRescheduleToDate = getNextRepaymentDate(loan, holiday);
} else {
adjustedRescheduleToDate = holiday.getRepaymentsRescheduledToLocalDate();
}
if (isRepaymentScheduleAdjustmentNeeded(adjustedRescheduleToDate)) {
adjustRepaymentSchedules(loan, holiday, adjustedRescheduleToDate);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueHolidayBusinessEvent(loan));
}
}
private boolean isRepaymentScheduleAdjustmentNeeded(LocalDate adjustedRescheduleToDate) {
return adjustedRescheduleToDate != null;
}
private void adjustRepaymentSchedules(Loan loan, Holiday holiday, LocalDate adjustedRescheduleToDate) {
final DefaultScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, holiday.getFromDateLocalDate());
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = loan.getDisbursementDate();
// Loop through all loanRepayments
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// update from date if it's not same as previous installment's due
// date.
if (!loanRepaymentScheduleInstallment.getFromDate().isEqual(tmpFromDate)) {
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.isAfter(holiday.getFromDateLocalDate())) {
// FIXME: AA do we need to apply non-working days.
// Assuming holiday's repayment reschedule to date cannot be
// created on a non-working day.
adjustedRescheduleToDate = scheduledDateGenerator.generateNextRepaymentDate(adjustedRescheduleToDate, loanApplicationTerms,
false);
loanRepaymentScheduleInstallment.updateDueDate(adjustedRescheduleToDate);
}
tmpFromDate = loanRepaymentScheduleInstallment.getDueDate();
}
}
private LocalDate getNextRepaymentDate(Loan loan, Holiday holiday) {
LocalDate adjustedRescheduleToDate = null;
final LocalDate rescheduleToDate = holiday.getToDateLocalDate();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) {
if (rescheduleToDate.isEqual(loanRepaymentScheduleInstallment.getDueDate())) {
adjustedRescheduleToDate = rescheduleToDate;
break;
} else {
adjustedRescheduleToDate = doStandardMonthlyCheck(adjustedRescheduleToDate, rescheduleToDate,
loanRepaymentScheduleInstallment);
}
}
return adjustedRescheduleToDate;
}
private LocalDate doStandardMonthlyCheck(LocalDate adjustedRescheduleToDate, LocalDate rescheduleToDate,
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) {
// Standard Monthly Loan Holiday check
if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
&& rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(30))) {
adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
}
return adjustedRescheduleToDate;
}
}
@@ -23,10 +23,13 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanScheduleVariationsAddedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanScheduleVariationsDeletedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -34,41 +37,30 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
@RequiredArgsConstructor
public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePlatformService {
private final LoanAccountDomainService loanAccountDomainService;
private final LoanAssembler loanAssembler;
private final LoanScheduleAssembler loanScheduleAssembler;
private final PlatformSecurityContext context;
private final LoanUtilService loanUtilService;
@Autowired
public LoanScheduleWritePlatformServiceImpl(final LoanAccountDomainService loanAccountDomainService,
final LoanScheduleAssembler loanScheduleAssembler, final LoanAssembler loanAssembler, final PlatformSecurityContext context,
final LoanUtilService loanUtilService) {
this.loanAccountDomainService = loanAccountDomainService;
this.loanScheduleAssembler = loanScheduleAssembler;
this.loanAssembler = loanAssembler;
this.context = context;
this.loanUtilService = loanUtilService;
}
private final BusinessEventNotifierService businessEventNotifierService;
@Override
public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final Loan loan = loanAssembler.assembleFrom(loanId);
Map<Long, LoanTermVariations> loanTermVariations = new HashMap<>();
for (LoanTermVariations termVariations : loan.getLoanTermVariations()) {
loanTermVariations.put(termVariations.getId(), termVariations);
}
this.loanScheduleAssembler.assempleVariableScheduleFrom(loan, command.json());
loanScheduleAssembler.assempleVariableScheduleFrom(loan, command.json());
this.loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
final Map<String, Object> changes = new HashMap<>();
List<LoanTermVariationsData> newVariationsData = new ArrayList<>();
List<LoanTermVariations> modifiedVariations = loan.getLoanTermVariations();
@@ -83,6 +75,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
changes.put("removedVariations", loanTermVariations.keySet());
}
changes.put("loanTermVariations", newVariationsData);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsAddedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withLoanId(loanId) //
@@ -92,7 +85,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
@Override
public CommandProcessingResult deleteLoanScheduleVariations(final Long loanId) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final Loan loan = loanAssembler.assembleFrom(loanId);
List<LoanTermVariations> variations = loan.getLoanTermVariations();
List<Long> deletedVariations = new ArrayList<>(variations.size());
for (LoanTermVariations loanTermVariations : variations) {
@@ -102,9 +95,10 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
changes.put("removedEntityIds", deletedVariations);
loan.getLoanTermVariations().clear();
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
this.loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withLoanId(loanId) //
.with(changes) //
@@ -25,13 +25,14 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.PersistenceException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
@@ -87,7 +88,6 @@ import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement;
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepository;
import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.fund.domain.Fund;
@@ -104,10 +104,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonit
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagementRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
@@ -144,9 +142,6 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
@@ -154,10 +149,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Service
@Slf4j
@RequiredArgsConstructor
public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements LoanApplicationWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(LoanApplicationWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final FromJsonHelper fromJsonHelper;
private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator;
@@ -181,7 +176,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final SavingsAccountAssembler savingsAccountAssembler;
private final AccountAssociationsRepository accountAssociationsRepository;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository;
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final ConfigurationDomainService configurationDomainService;
@@ -190,7 +184,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final CalendarReadPlatformService calendarReadPlatformService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
private final FineractEntityToEntityMappingRepository repository;
private final FineractEntityToEntityMappingRepository entityMappingRepository;
private final FineractEntityRelationRepository fineractEntityRelationRepository;
private final LoanProductReadPlatformService loanProductReadPlatformService;
@@ -199,84 +193,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final GLIMAccountInfoRepository glimRepository;
private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanCollateralManagementRepository loanCollateralManagementRepository;
private final ClientCollateralManagementRepository clientCollateralManagementRepository;
@Autowired
public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper,
final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator,
final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer,
final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator,
final LoanAssembler loanAssembler, final LoanChargeAssembler loanChargeAssembler,
final LoanCollateralAssembler loanCollateralAssembler, final LoanRepositoryWrapper loanRepositoryWrapper,
final NoteRepository noteRepository, final LoanScheduleCalculationPlatformService calculationPlatformService,
final ClientRepositoryWrapper clientRepository, final LoanProductRepository loanProductRepository,
final AccountNumberGenerator accountNumberGenerator, final LoanSummaryWrapper loanSummaryWrapper,
final GroupRepositoryWrapper groupRepository,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final CalendarRepository calendarRepository, final CalendarInstanceRepository calendarInstanceRepository,
final SavingsAccountAssembler savingsAccountAssembler, final AccountAssociationsRepository accountAssociationsRepository,
final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository,
final LoanReadPlatformService loanReadPlatformService, final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
final BusinessEventNotifierService businessEventNotifierService, final ConfigurationDomainService configurationDomainService,
final LoanScheduleAssembler loanScheduleAssembler, final LoanUtilService loanUtilService,
final CalendarReadPlatformService calendarReadPlatformService,
final GlobalConfigurationRepositoryWrapper globalConfigurationRepository,
final FineractEntityToEntityMappingRepository repository,
final FineractEntityRelationRepository fineractEntityRelationRepository,
final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService,
final GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, final GLIMAccountInfoRepository glimRepository,
final LoanRepository loanRepository, final GSIMReadPlatformService gsimReadPlatformService, final RateAssembler rateAssembler,
final LoanProductReadPlatformService loanProductReadPlatformService,
final LoanCollateralManagementRepository loanCollateralManagementRepository,
final ClientCollateralManagementRepository clientCollateralManagementRepository) {
this.context = context;
this.fromJsonHelper = fromJsonHelper;
this.loanApplicationTransitionApiJsonValidator = loanApplicationTransitionApiJsonValidator;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanProductCommandFromApiJsonDeserializer = loanProductCommandFromApiJsonDeserializer;
this.aprCalculator = aprCalculator;
this.loanAssembler = loanAssembler;
this.loanChargeAssembler = loanChargeAssembler;
this.loanCollateralAssembler = loanCollateralAssembler;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.noteRepository = noteRepository;
this.calculationPlatformService = calculationPlatformService;
this.clientRepository = clientRepository;
this.loanProductRepository = loanProductRepository;
this.accountNumberGenerator = accountNumberGenerator;
this.loanSummaryWrapper = loanSummaryWrapper;
this.groupRepository = groupRepository;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.calendarRepository = calendarRepository;
this.calendarInstanceRepository = calendarInstanceRepository;
this.savingsAccountAssembler = savingsAccountAssembler;
this.accountAssociationsRepository = accountAssociationsRepository;
this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository;
this.loanReadPlatformService = loanReadPlatformService;
this.accountNumberFormatRepository = accountNumberFormatRepository;
this.businessEventNotifierService = businessEventNotifierService;
this.configurationDomainService = configurationDomainService;
this.loanScheduleAssembler = loanScheduleAssembler;
this.loanUtilService = loanUtilService;
this.calendarReadPlatformService = calendarReadPlatformService;
this.entityDatatableChecksWritePlatformService = entityDatatableChecksWritePlatformService;
this.globalConfigurationRepository = globalConfigurationRepository;
this.repository = repository;
this.fineractEntityRelationRepository = fineractEntityRelationRepository;
this.loanProductReadPlatformService = loanProductReadPlatformService;
this.rateAssembler = rateAssembler;
this.glimAccountInfoWritePlatformService = glimAccountInfoWritePlatformService;
this.glimRepository = glimRepository;
this.loanRepository = loanRepository;
this.gsimReadPlatformService = gsimReadPlatformService;
this.loanCollateralManagementRepository = loanCollateralManagementRepository;
this.clientCollateralManagementRepository = clientCollateralManagementRepository;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
@Transactional
@@ -1287,7 +1206,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
}
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
LOG.error("Error occured.", dve);
log.error("Error occured.", dve);
}
@Transactional
@@ -1775,8 +1694,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
if (restrictToUserOfficeProperty.isEnabled()) {
FineractEntityRelation fineractEntityRelation = fineractEntityRelationRepository
.findOneByCodeName(FineractEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS.toStr());
FineractEntityToEntityMapping officeToLoanProductMappingList = this.repository.findListByProductId(fineractEntityRelation,
productId, officeId);
FineractEntityToEntityMapping officeToLoanProductMappingList = this.entityMappingRepository
.findListByProductId(fineractEntityRelation, productId, officeId);
if (officeToLoanProductMappingList == null) {
throw new NotOfficeSpecificProductException(productId, officeId);
}
@@ -22,11 +22,11 @@ import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
@@ -87,10 +88,10 @@ import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequired
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.rate.service.RateAssembler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoanAssembler {
private final FromJsonHelper fromApiJsonHelper;
@@ -112,38 +113,7 @@ public class LoanAssembler {
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final LoanUtilService loanUtilService;
private final RateAssembler rateAssembler;
@Autowired
public LoanAssembler(final FromJsonHelper fromApiJsonHelper, final LoanRepositoryWrapper loanRepository,
final LoanProductRepository loanProductRepository, final ClientRepositoryWrapper clientRepository,
final GroupRepository groupRepository, final FundRepository fundRepository,
final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository,
final StaffRepository staffRepository, final CodeValueRepositoryWrapper codeValueRepository,
final LoanScheduleAssembler loanScheduleAssembler, final LoanChargeAssembler loanChargeAssembler,
final LoanCollateralAssembler collateralAssembler, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService,
final WorkingDaysRepositoryWrapper workingDaysRepository, final LoanUtilService loanUtilService, RateAssembler rateAssembler) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanRepository = loanRepository;
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
this.groupRepository = groupRepository;
this.fundRepository = fundRepository;
this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository;
this.staffRepository = staffRepository;
this.codeValueRepository = codeValueRepository;
this.loanScheduleAssembler = loanScheduleAssembler;
this.loanChargeAssembler = loanChargeAssembler;
this.collateralAssembler = collateralAssembler;
this.loanSummaryWrapper = loanSummaryWrapper;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.holidayRepository = holidayRepository;
this.configurationDomainService = configurationDomainService;
this.workingDaysRepository = workingDaysRepository;
this.loanUtilService = loanUtilService;
this.rateAssembler = rateAssembler;
}
private final BusinessEventNotifierService businessEventNotifierService;
public Loan assembleFrom(final Long accountId) {
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true);
@@ -354,8 +324,7 @@ public class LoanAssembler {
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) {
@@ -66,15 +66,17 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanInteres
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanReassignOfficerBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRejectTransferBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRemoveOfficerBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRescheduledDueCalendarChangeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoDisbursalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoLastDisbursalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUpdateDisbursementDataBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdrawTransferBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanAddChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanDeleteChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWrittenOffPostBusinessEvent;
@@ -269,8 +271,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
@Transactional
@@ -795,8 +796,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
@Override
public CommandProcessingResult undoGLIMLoanDisbursal(final Long loanId, final JsonCommand command) {
// GroupLoanIndividualMonitoringAccount
// glimAccount=glimRepository.findOne(loanId);
final Long parentLoanId = loanId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow();
List<Loan> childLoans = this.loanRepository.findByGlimId(loanId);
@@ -1727,7 +1726,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newChargeRefundTxn));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeRefundBusinessEvent(newChargeRefundTxn));
return result;
}
@@ -2196,7 +2195,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.withSavingsId(portfolioAccountData.getId()).build();
}
public void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) {
private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) {
final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
@@ -2211,7 +2210,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getTopupLoanDetails().setTopupAmount(amount);
}
public void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) {
private void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) {
final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
@@ -2518,6 +2517,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueCalendarChangeBusinessEvent(loan));
}
}
}
@@ -2971,7 +2971,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.loanProduct().maxTrancheCount(), loan.getDisbursementDetails().size());
}
LoanDisbursementDetails updateDetails = null;
return processLoanDisbursementDetail(loan, loanId, command, updateDetails);
CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, updateDetails);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan));
return result;
}
@@ -3040,85 +3042,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
return processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan));
return result;
}
public LoanTransaction disburseLoanAmountToSavings(final Long loanId, Long loanChargeId, final JsonCommand command,
final boolean isChargeIdIncludedInJson) {
LoanTransaction transaction = null;
this.loanEventApiJsonValidator.validateChargePaymentTransaction(command.json(), isChargeIdIncludedInJson);
if (isChargeIdIncludedInJson) {
loanChargeId = command.longValueOfParameterNamed("chargeId");
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
// Charges may be waived only when the loan associated with them are
// active
if (!loan.status().isActive()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.LOAN_INACTIVE, loanCharge.getId());
}
// validate loan charge is not already paid or waived
if (loanCharge.isWaived()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_WAIVED, loanCharge.getId());
} else if (loanCharge.isPaid()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_PAID, loanCharge.getId());
}
if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.CHARGE_NOT_ACCOUNT_TRANSFER, loanCharge.getId());
}
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
Integer loanInstallmentNumber = null;
BigDecimal amount = loanCharge.amountOutstanding();
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge chargePerInstallment = null;
final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate");
final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber");
if (dueDate != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate);
} else if (installmentNumber != null) {
chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber);
}
if (chargePerInstallment == null) {
chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
}
if (chargePerInstallment.isWaived()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_WAIVED, loanCharge.getId());
} else if (chargePerInstallment.isPaid()) {
throw new LoanChargeCannotBePayedException(LoanChargeCannotBePayedReason.ALREADY_PAID, loanCharge.getId());
}
loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber();
amount = chargePerInstallment.getAmountOutstanding();
}
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId);
if (portfolioAccountData == null) {
final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment";
throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId);
}
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS,
PortfolioAccountType.LOAN, portfolioAccountData.getId(), loanId, "Loan Charge Payment", locale, fmt, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
return transaction;
}
@Transactional
@Override
public void recalculateInterest(final long loanId) {
@@ -29,6 +29,7 @@ import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManager;
import org.apache.fineract.avro.loan.v1.LoanAccountDataV1;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -43,7 +44,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -57,12 +57,15 @@ class ExternalEventServiceTest {
private ExternalEventIdempotencyKeyGenerator idempotencyKeyGenerator;
@Mock
private BusinessEventSerializerFactory serializerFactory;
@Mock
private EntityManager entityManager;
@InjectMocks
private ExternalEventService underTest;
@BeforeEach
public void setUp() {
underTest = new ExternalEventService(repository, idempotencyKeyGenerator, serializerFactory);
underTest.setEntityManager(entityManager);
FineractPlatformTenant tenant = new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null);
ThreadLocalContextUtil.setTenant(tenant);
ThreadLocalContextUtil