diff --git a/mifosng-db/migrations/core_db/V63__add_sync_disbursement_with_meeting_column_to_loan.sql b/mifosng-db/migrations/core_db/V63__add_sync_disbursement_with_meeting_column_to_loan.sql new file mode 100644 index 000000000..b909e828b --- /dev/null +++ b/mifosng-db/migrations/core_db/V63__add_sync_disbursement_with_meeting_column_to_loan.sql @@ -0,0 +1,4 @@ + + +ALTER TABLE `m_loan` + ADD COLUMN `sync_disbursement_with_meeting` TINYINT(1) NULL AFTER `loan_transaction_strategy_id`; \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java index 47878b4a2..87fa2066a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/domain/CalendarInstanceRepository.java @@ -9,6 +9,8 @@ import java.util.Collection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface CalendarInstanceRepository extends JpaRepository, JpaSpecificationExecutor { @@ -16,4 +18,6 @@ public interface CalendarInstanceRepository extends JpaRepository findByEntityIdAndEntityTypeId(Long entityId, Integer entityTypeId); + @Query("from CalendarInstance ci where ci.entityId = :loanId and ci.entityTypeId = :entityTypeId") + CalendarInstance findCalendarInstaneByLoanId(@Param("loanId") Long loanId, @Param("entityTypeId") Integer entityTypeId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/MeetingFrequencyMismatchException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/MeetingFrequencyMismatchException.java new file mode 100644 index 000000000..36dd1fde0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/exception/MeetingFrequencyMismatchException.java @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.calendar.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * A {@link RuntimeException} thrown when Calendar resources are not found. + */ +public class MeetingFrequencyMismatchException extends AbstractPlatformDomainRuleException { + + public MeetingFrequencyMismatchException(String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { + super("error.msg.calendar." + postFix + ".not.the.same.as.meeting.frequency", defaultUserMessage, defaultUserMessageArgs); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java index 9b7de8f1d..a85640223 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarHelper.java @@ -27,6 +27,8 @@ import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public class CalendarHelper { @@ -35,8 +37,21 @@ public class CalendarHelper { final Recur recur = CalendarHelper.getICalRecur(recurringRule); if (recur == null) { return null; } - - final Date nextRecDate = recur.getNextDate(new Date(seedDate.toDate()), new Date(startDate.toDate())); + + final DateTime periodStart = new DateTime(startDate.toDate()); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded + final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate()); + + Date seed = null; + try { + seed = new Date(seedDateStr, "yyyy-MM-dd"); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + + final Date nextRecDate = recur.getNextDate(seed, periodStart); return nextRecDate == null ? null : new LocalDate(nextRecDate); } @@ -46,10 +61,16 @@ public class CalendarHelper { final LocalDate periodEndDate = (endDate == null) ? DateUtils.getLocalDateOfTenant().plusYears(5) : endDate; return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate); } + + public static Collection getRecurringDatesFrom(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) { + final LocalDate periodStartDate = (startDate == null) ? DateUtils.getLocalDateOfTenant() : startDate; + final LocalDate periodEndDate = DateUtils.getLocalDateOfTenant().plusYears(5); + return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate); + } public static Collection getRecurringDates(final String recurringRule, final LocalDate seedDate, final LocalDate periodStartDate, final LocalDate periodEndDate) { - final int maxCount = 15;// Default number of recurring dates + final int maxCount = 10;// Default number of recurring dates return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate, maxCount); } @@ -209,4 +230,79 @@ public class CalendarHelper { return DayNameEnum.MO;//Default it to Monday } } + + public static PeriodFrequencyType getMeetingPeriodFrequencyType(final String recurringRule){ + final Recur recur = CalendarHelper.getICalRecur(recurringRule); + PeriodFrequencyType meetingFrequencyType = PeriodFrequencyType.INVALID; + if(recur.getFrequency().equals(Recur.DAILY)){ + meetingFrequencyType = PeriodFrequencyType.DAYS; + }else if(recur.getFrequency().equals(Recur.WEEKLY)){ + meetingFrequencyType = PeriodFrequencyType.WEEKS; + }else if(recur.getFrequency().equals(Recur.MONTHLY)){ + meetingFrequencyType = PeriodFrequencyType.MONTHS; + }else if(recur.getFrequency().equals(Recur.YEARLY)){ + meetingFrequencyType = PeriodFrequencyType.YEARS; + } + return meetingFrequencyType; + } + + public static String getMeetingFrequencyFromPeriodFrequencyType(final PeriodFrequencyType periodFrequency){ + String frequency = null; + if(periodFrequency.equals(PeriodFrequencyType.DAYS)){ + frequency = Recur.DAILY; + }else if(periodFrequency.equals(PeriodFrequencyType.WEEKS)){ + frequency = Recur.WEEKLY; + }else if(periodFrequency.equals(PeriodFrequencyType.MONTHS)){ + frequency = Recur.MONTHLY; + }else if(periodFrequency.equals(PeriodFrequencyType.YEARS)){ + frequency = Recur.YEARLY; + } + return frequency; + } + + public static int getInterval(final String recurringRule){ + final Recur recur = CalendarHelper.getICalRecur(recurringRule); + return recur.getInterval(); + } + + public static LocalDate getFirstRepaymentMeetingDate(final Calendar calendar, final LocalDate disbursementDate, final Integer loanRepaymentInterval, final String frequency){ + final Recur recur = CalendarHelper.getICalRecur(calendar.getRecurrence()); + if (recur == null) { return null; } + LocalDate startDate = disbursementDate; + final LocalDate seedDate = calendar.getStartDateLocalDate(); + if(isValidRedurringDate(calendar.getRecurrence(), seedDate, startDate)){ + startDate = startDate.plusDays(1); + } + //Recurring dates should follow loanRepaymentInterval. + //e.g. + // for weekly meeting interval is 1 + //where as for loan product with fortnightly frequency interval is 2 + //to generate currect set of meeting dates reset interval same as loan repayment interval. + recur.setInterval(loanRepaymentInterval); + + //Recurring dates should follow loanRepayment frequency. + //e.g. + // daily meeting frequency should support all loan products with any frequency type. + //to generate currect set of meeting dates reset frequency same as loan repayment frequency. + if(recur.getFrequency().equals(Recur.DAILY)){ + recur.setFrequency(frequency); + } + + final DateTime periodStart = new DateTime(startDate.toDate()); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");//Date format in iCal4J is hard coded + final String seedDateStr = df.format(seedDate.toDateTimeAtStartOfDay().toDate()); + + Date seed = null; + try { + seed = new Date(seedDateStr, "yyyy-MM-dd"); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + final Date nextRecDate = recur.getNextDate(seed, periodStart); + final LocalDate firstRepaymentDate = nextRecDate == null ? null : new LocalDate(nextRecDate); + + return firstRepaymentDate; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index c96d99fa4..43d7abb10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -96,7 +96,7 @@ public class LoansApiResource { "summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors", "meeting", "productOptions", "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", "repaymentFrequencyTypeOptions", "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", - "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions")); + "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions", "syncDisbursementWithMeeting")); private final String resourceNameForPermissions = "LOAN"; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index f1f9378c9..7cb6ab09f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -87,6 +87,7 @@ public class LoanAccountData { private final Integer graceOnInterestCharged; private final LocalDate interestChargedFromDate; private final LocalDate expectedFirstRepaymentOnDate; + private final Boolean syncDisbursementWithMeeting; // timeline private final LoanApplicationTimelineData timeline; @@ -190,6 +191,7 @@ public class LoanAccountData { final Collection loanOfficerOptions = null; final Collection loanPurposeOptions = null; final Collection calendarOptions = null; + final Boolean syncDisbursementWithMeeting = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -200,7 +202,8 @@ public class LoanAccountData { feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + syncDisbursementWithMeeting); } /** @@ -271,6 +274,7 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; + final Boolean syncDisbursementWithMeeting = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -281,7 +285,7 @@ public class LoanAccountData { feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting); } public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { @@ -298,7 +302,7 @@ public class LoanAccountData { acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); + acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, acc.syncDisbursementWithMeeting); } /** @@ -370,7 +374,8 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; - + final Boolean syncDisbursementWithMeeting = null; + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, @@ -380,7 +385,8 @@ public class LoanAccountData { feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + syncDisbursementWithMeeting); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { @@ -397,7 +403,8 @@ public class LoanAccountData { acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); + acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, + acc.syncDisbursementWithMeeting); } public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, @@ -444,6 +451,7 @@ public class LoanAccountData { final LoanSummaryData summary = null; final BigDecimal feeChargesDueAtDisbursementCharged = null; final ChargeData chargeTemplate = null; + final Boolean syncDisbursementWithMeeting = null; final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -474,7 +482,7 @@ public class LoanAccountData { repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, - loanPurposeOptions, loanCollateralOptions, calendarOptions); + loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -520,7 +528,7 @@ public class LoanAccountData { acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, acc.syncDisbursementWithMeeting); } /* @@ -539,7 +547,7 @@ public class LoanAccountData { final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, - final BigDecimal feeChargesDueAtDisbursementCharged) { + final BigDecimal feeChargesDueAtDisbursementCharged, final Boolean syncDisbursementWithMeeting) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -572,7 +580,8 @@ public class LoanAccountData { feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + syncDisbursementWithMeeting); } /* @@ -601,7 +610,8 @@ public class LoanAccountData { acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions, + acc.syncDisbursementWithMeeting); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -626,7 +636,8 @@ public class LoanAccountData { acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, - acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); + acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions, + acc.syncDisbursementWithMeeting); } private LoanAccountData( @@ -672,7 +683,8 @@ public class LoanAccountData { final Collection interestTypeOptions, final Collection interestCalculationPeriodTypeOptions, final Collection fundOptions, final Collection chargeOptions, final ChargeData chargeTemplate, final Collection loanOfficerOptions, final Collection loanPurposeOptions, - final Collection loanCollateralOptions, final Collection calendarOptions) { + final Collection loanCollateralOptions, final Collection calendarOptions, + final Boolean syncDisbursementWithMeeting) { this.id = id; this.accountNo = accountNo; this.status = status; @@ -714,6 +726,7 @@ public class LoanAccountData { this.interestChargedFromDate = interestChargedFromDate; this.timeline = timeline; this.feeChargesAtDisbursementCharged = feeChargesDueAtDisbursementCharged; + this.syncDisbursementWithMeeting = syncDisbursementWithMeeting; // totals this.summary = summary; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 509bc5d4f..594d2c332 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -135,6 +135,9 @@ public class Loan extends AbstractPersistable { @Column(name = "loan_status_id", nullable = false) private Integer loanStatus; + @Column(name = "sync_disbursement_with_meeting", nullable = true) + private Boolean syncDisbursementWithMeeting; + // loan application states @Temporal(TemporalType.DATE) @Column(name = "submittedon_date") @@ -276,31 +279,34 @@ public class Loan extends AbstractPersistable { final Set collateral) { final LoanStatus status = null; final Group group = null; + final Boolean syncDisbursementWithMeeting = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); } public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final LoanTransactionProcessingStrategy transactionProcessingStrategy, - final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Boolean syncDisbursementWithMeeting) { final LoanStatus status = null; final CodeValue loanPurpose = null; final Set collateral = null; final Client client = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); } public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final LoanTransactionProcessingStrategy transactionProcessingStrategy, - final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Boolean syncDisbursementWithMeeting) { final LoanStatus status = null; final CodeValue loanPurpose = null; final Set collateral = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, - loanRepaymentScheduleDetail, status, loanCharges, collateral); + loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting); } protected Loan() { @@ -310,7 +316,7 @@ public class Loan extends AbstractPersistable { private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, - final Set loanCharges, final Set collateral) { + final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting) { this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); @@ -348,6 +354,8 @@ public class Loan extends AbstractPersistable { this.collateral = null; } this.loanOfficerHistory = null; + + this.syncDisbursementWithMeeting = syncDisbursementWithMeeting; } private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) { @@ -815,8 +823,7 @@ public class Loan extends AbstractPersistable { } // add clientId, groupId and loanType changes to actual changes - // FIXME: AA - We may require separate api command to move loan from one - // client to another + final String clientIdParamName = "clientId"; final Long clientId = this.client == null ? null : this.client.getId(); if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { @@ -916,6 +923,13 @@ public class Loan extends AbstractPersistable { } } + final String syncDisbursementParameterName = "syncDisbursementWithMeeting"; + if (command.isChangeInBooleanParameterNamed(syncDisbursementParameterName, isSyncDisbursementWithMeeting())) { + final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(syncDisbursementParameterName); + actualChanges.put(syncDisbursementParameterName, valueAsInput); + this.syncDisbursementWithMeeting = valueAsInput; + } + final String interestChargedFromDateParamName = "interestChargedFromDate"; if (command.isChangeInLocalDateParameterNamed(interestChargedFromDateParamName, getInterestChargedFromDate())) { final String valueAsInput = command.stringValueOfParameterNamed(interestChargedFromDateParamName); @@ -1278,7 +1292,8 @@ public class Loan extends AbstractPersistable { public Map disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, - final List existingReversedTransactionIds, final Map actualChanges, final PaymentDetail paymentDetail) { + final List existingReversedTransactionIds, final Map actualChanges, + final PaymentDetail paymentDetail, final LocalDate firstRepaymentMeetingDate) { updateLoanToPreDisbursalState(); @@ -1304,7 +1319,7 @@ public class Loan extends AbstractPersistable { handleDisbursementTransaction(paymentDetail, actualDisbursementDate); if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { - regenerateRepaymentSchedule(loanScheduleFactory, currency); + regenerateRepaymentSchedule(loanScheduleFactory, currency, firstRepaymentMeetingDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); } else { updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); @@ -1331,7 +1346,7 @@ public class Loan extends AbstractPersistable { * details/state. */ private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, - final ApplicationCurrency applicationCurrency) { + final ApplicationCurrency applicationCurrency, final LocalDate firstRepaymentMeetingDate) { final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); @@ -1341,9 +1356,11 @@ public class Loan extends AbstractPersistable { final Integer loanTermFrequency = this.termFrequency; final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType); - + //If ExpectedFirstRepaymentOnDate is null and repayment is synced with meeting then use firstRepaymentMeetingDate + //to generate repayment schedule dates + final LocalDate firstRepaymentOnDate = (getExpectedFirstRepaymentOnDate() == null) ? firstRepaymentMeetingDate : getExpectedFirstRepaymentOnDate(); LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, - loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), getExpectedFirstRepaymentOnDate(), getInArrearsTolerance(), + loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), firstRepaymentOnDate, getInArrearsTolerance(), this.loanRepaymentScheduleDetail); LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges); @@ -2001,7 +2018,7 @@ public class Loan extends AbstractPersistable { return disbursementDate; } - private LocalDate getExpectedFirstRepaymentOnDate() { + public LocalDate getExpectedFirstRepaymentOnDate() { LocalDate firstRepaymentDate = null; if (this.expectedFirstRepaymentOnDate != null) { firstRepaymentDate = new LocalDate(this.expectedFirstRepaymentOnDate); @@ -2299,4 +2316,9 @@ public class Loan extends AbstractPersistable { this.loanSummaryWrapper = loanSummaryWrapper; this.transactionProcessorFactory = transactionProcessorFactory; } + + public boolean isSyncDisbursementWithMeeting() { + return this.syncDisbursementWithMeeting == null ? false : this.syncDisbursementWithMeeting; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 980fe3554..2b4fac643 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -16,7 +16,13 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; +import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; +import org.mifosplatform.portfolio.calendar.exception.MeetingFrequencyMismatchException; +import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; @@ -45,18 +51,20 @@ public class LoanScheduleAssembler { private final LoanChargeAssembler loanChargeAssembler; private final LoanScheduleGeneratorFactory loanScheduleFactory; private final AprCalculator aprCalculator; + private final CalendarRepository calendarRepository; @Autowired public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final AprCalculator aprCalculator, - final LoanChargeAssembler loanChargeAssembler) { + final LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository) { this.fromApiJsonHelper = fromApiJsonHelper; this.loanProductRepository = loanProductRepository; this.applicationCurrencyRepository = applicationCurrencyRepository; this.loanScheduleFactory = loanScheduleFactory; this.aprCalculator = aprCalculator; this.loanChargeAssembler = loanChargeAssembler; + this.calendarRepository = calendarRepository; } public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { @@ -104,10 +112,41 @@ public class LoanScheduleAssembler { // disbursement details final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); final Money principalMoney = Money.of(currency, principal); - + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); - final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); - + LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); + + final Boolean synchDisbursement = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); + final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element); + Calendar calendar = null; + if ((synchDisbursement != null && synchDisbursement.booleanValue()) || + (calendarId != null && calendarId != 0)) { + calendar = this.calendarRepository.findOne(calendarId); + if (calendar == null) { throw new CalendarNotFoundException(calendarId); } + + //validate repayment frequency and interval with meeting frequency and interval + PeriodFrequencyType meetingPeriodFrequency = CalendarHelper.getMeetingPeriodFrequencyType(calendar.getRecurrence()); + validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(), repaymentFrequencyType, + CalendarHelper.getInterval(calendar.getRecurrence()),repaymentEvery); + } + + if (synchDisbursement != null && synchDisbursement.booleanValue()) { + validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); + } + + //if calendar is attached (not null) then reschedule repayment dates according to meeting + if (null != calendar) { + //TODO : AA - Is it require to reset repaymentsStartingFromDate or set only if it is not provided (or null) + // Currently provided repaymentsStartingFromDate takes precedence over system generated next meeting date + if(repaymentsStartingFromDate == null){ + //FIXME: AA - Possibility of having next meeting date immediately after disbursement date, + //need to have minimum number of days gap between disbursement and first repayment date. + final String frequency = CalendarHelper.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + repaymentsStartingFromDate = CalendarHelper.getFirstRepaymentMeetingDate(calendar, expectedDisbursementDate, repaymentEvery, frequency); + }else{//validate user provided repaymentsStartFromDate + validateRepaymentsStartDateWithMeetingDates(repaymentsStartingFromDate, calendar); + } + } // grace details final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); @@ -125,7 +164,47 @@ public class LoanScheduleAssembler { graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); } - public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { + private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) { + if(repaymentsStartingFromDate != null && !CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repaymentsStartingFromDate)){ + final String errorMessage = "First repayment date '" + repaymentsStartingFromDate + "' do not fall on a meeting date"; + throw new LoanApplicationDateException("first.repayment.date.do.not.match.meeting.date", errorMessage, repaymentsStartingFromDate); + } + } + + private void validateDisbursementDateWithMeetingDates( + final LocalDate expectedDisbursementDate, final Calendar calendar) { + // disbursement date should fall on a meeting date + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), + calendar.getStartDateLocalDate(), expectedDisbursementDate)) { + final String errorMessage = "Expected disbursement date '" + + expectedDisbursementDate + + "' do not fall on a meeting date"; + throw new LoanApplicationDateException( + "disbursement.date.do.not.match.meeting.date", + errorMessage, expectedDisbursementDate); + } + + } + + private void validateRepaymentFrequencyIsSameAsMeetingFrequency( + final Integer meetingFrequency, final Integer repaymentFrequency, + final Integer meetingInterval, final Integer repaymentInterval) { + //meeting with daily frequency should allow loan products with any frequency. + if(!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)){ + //repayment frequency must match with meeting frequency + if(!meetingFrequency.equals(repaymentFrequency)){ + throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency", "Loan repayment frequency period must match that of meeting frequency period", repaymentFrequency); + }else if(meetingFrequency.equals(repaymentFrequency)){//repayment frequency is same as meeting frequency + //repayment interval should be same or multiple of meeting interval + if (repaymentInterval % meetingInterval != 0) { + // throw exception: Loan product frequency/interval + throw new MeetingFrequencyMismatchException("loanapplication.repayment.interval", "Loan repayment repaid every # must equal or multiple of meeting interval", repaymentInterval); + } + } + } + } + + public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); return loanApplicationTerms.toLoanProductRelatedDetail(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index 11619237b..2cfd8b267 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -28,7 +28,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC public LoanScheduleModel calculateLoanSchedule(final JsonQuery query) { this.fromApiJsonDeserializer.validate(query.json()); - + return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson()); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 585cf2186..f3c694572 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -42,7 +42,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { "loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", - "dateFormat", "charges", "collateral")); + "dateFormat", "charges", "collateral", "syncDisbursementWithMeeting")); private final FromJsonHelper fromApiJsonHelper; @@ -179,7 +179,26 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { } } } - + + boolean meetingIdRequired = false; + //validate syncDisbursement + final String syncDisbursementParameterName = "syncDisbursementWithMeeting"; + if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element); + if(syncDisbursement == null){ + baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false); + }else if(syncDisbursement.booleanValue()) { + meetingIdRequired = true; + } + } + + final String calendarIdParameterName = "calendarId"; + //if disbursement is synced then must have a meeting (calendar) + if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element); + baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero(); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index b6be5dddc..c39516181 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -47,7 +47,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { "accountNo", "externalId", "fundId", "loanOfficerId", // optional "loanPurposeId", "inArrearsTolerance", "charges", "collateral", // optional "transactionProcessingStrategyId", // settings - "calendarId" // optional + "calendarId", // optional + "syncDisbursementWithMeeting"//optional )); private final FromJsonHelper fromApiJsonHelper; @@ -230,12 +231,6 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(transactionProcessingStrategyIdParameterName).value(transactionProcessingStrategyId).notNull() .integerGreaterThanZero(); - final String calendarIdParameterName = "calendarId"; - if (fromApiJsonHelper.parameterExists(calendarIdParameterName, element)) { - final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element); - baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).ignoreIfNull().integerGreaterThanZero(); - } - // charges final String chargesParameterName = "charges"; if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) { @@ -302,6 +297,25 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + boolean meetingIdRequired = false; + //validate syncDisbursement + final String syncDisbursementParameterName = "syncDisbursementWithMeeting"; + if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element); + if(syncDisbursement == null){ + baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false); + }else if(syncDisbursement.booleanValue()) { + meetingIdRequired = true; + } + } + + final String calendarIdParameterName = "calendarId"; + //if disbursement is synced then must have a meeting (calendar) + if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element); + baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero(); + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } @@ -517,12 +531,6 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notExceedingLengthOf(500); } - final String calendarIdParameterName = "calendarId"; - if (fromApiJsonHelper.parameterExists(calendarIdParameterName, element)) { - final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element); - baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).ignoreIfNull().integerGreaterThanZero(); - } - // charges final String chargesParameterName = "charges"; if (element.isJsonObject() && fromApiJsonHelper.parameterExists(chargesParameterName, element)) { @@ -591,6 +599,25 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + boolean meetingIdRequired = false; + //validate syncDisbursement + final String syncDisbursementParameterName = "syncDisbursementWithMeeting"; + if (fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Boolean syncDisbursement = fromApiJsonHelper.extractBooleanNamed(syncDisbursementParameterName, element); + if(syncDisbursement == null){ + baseDataValidator.reset().parameter(syncDisbursementParameterName).value(syncDisbursement).trueOrFalseRequired(false); + }else if(syncDisbursement.booleanValue()) { + meetingIdRequired = true; + } + } + + final String calendarIdParameterName = "calendarId"; + //if disbursement is synced then must have a meeting (calendar) + if (meetingIdRequired || fromApiJsonHelper.parameterExists(syncDisbursementParameterName, element)) { + final Long calendarId = fromApiJsonHelper.extractLongNamed(calendarIdParameterName, element); + baseDataValidator.reset().parameter(calendarIdParameterName).value(calendarId).notNull().integerGreaterThanZero(); + } + if (!atLeastOneParameterPassedForUpdate) { final Object forceError = null; baseDataValidator.reset().anyOfNotNull(forceError); @@ -639,4 +666,5 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index 74a8c6733..65d983a2b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -21,6 +21,10 @@ import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.calendar.domain.Calendar; +import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; +import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException; +import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -66,6 +70,23 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + + public void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance) { + if (null != calendarInstance) { + Calendar calendar = calendarInstance.getCalendar(); + if (calendar != null && actualDisbursementDate != null) { + // Disbursement date should fall on a meeting date + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), actualDisbursementDate)) { + final String errorMessage = "Expected disbursement date '" + actualDisbursementDate.toString() + + "' does not fall on a meeting date."; + throw new NotValidRecurringDateException("loan.actual.disbursement.date", errorMessage, actualDisbursementDate.toString(), + calendar.getTitle()); + } + + } + } + } + public void validateTransaction(final String json) { @@ -121,6 +142,22 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + public void validateRepaymentDateWithMeetingDate(LocalDate repaymentDate, final CalendarInstance calendarInstance) { + if (null != calendarInstance) { + Calendar calendar = calendarInstance.getCalendar(); + if (calendar != null && repaymentDate != null) { + // Disbursement date should fall on a meeting date + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repaymentDate)) { + final String errorMessage = "Transaction date '" + repaymentDate.toString() + + "' does not fall on a meeting date."; + throw new NotValidRecurringDateException("loan.transaction.date", errorMessage, repaymentDate.toString(), + calendar.getTitle()); + } + + } + } + } + private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { // Validate all string payment detail fields for max length final Integer paymentTypeId = fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index eab9804bb..8ea99a606 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -12,7 +12,6 @@ import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; -import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.api.JsonQuery; @@ -31,8 +30,6 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; import org.mifosplatform.portfolio.calendar.domain.CalendarRepository; import org.mifosplatform.portfolio.calendar.exception.CalendarNotFoundException; -import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException; -import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.client.domain.AccountNumberGenerator; import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory; import org.mifosplatform.portfolio.client.domain.Client; @@ -48,7 +45,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTrans import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; -import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; @@ -156,16 +152,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } - // validate dates against calendar recurring dates - // get calendar - final Long calendarId = command.longValueOfParameterNamed("calendarId"); - Calendar calendar = null; - if (calendarId != null && calendarId != 0) { - calendar = this.calendarRepository.findOne(calendarId); - if (calendar == null) { throw new CalendarNotFoundException(calendarId); } - validateCalendarDates(command, calendar); - } - final Loan newLoanApplication = loanAssembler.assembleFrom(command, currentUser); this.loanRepository.save(newLoanApplication); @@ -184,12 +170,19 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } // Save calendar instance - if (calendar != null) { + final Long calendarId = command.longValueOfParameterNamed("calendarId"); + Calendar calendar = null; + + if (calendarId != null && calendarId != 0) { + calendar = this.calendarRepository.findOne(calendarId); + if (calendar == null) { throw new CalendarNotFoundException(calendarId); } + CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(), CalendarEntityType.LOANS.getValue()); this.calendarInstanceRepository.save(calendarInstance); } - + + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(newLoanApplication.getId()) // @@ -209,14 +202,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.fromApiJsonDeserializer.validateForModify(command.json()); - final Long calendarId = command.longValueOfParameterNamed("calendarId"); - Calendar calendar = null; - if (calendarId != null && calendarId != 0) { - calendar = this.calendarRepository.findOne(calendarId); - if (calendar == null) { throw new CalendarNotFoundException(calendarId); } - validateCalendarDates(command, calendar); - } - final Loan existingLoanApplication = retrieveLoanBy(loanId); if (!existingLoanApplication.isSubmittedAndPendingApproval()) { throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified( @@ -309,6 +294,13 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.noteRepository.save(note); } + final Long calendarId = command.longValueOfParameterNamed("calendarId"); + Calendar calendar = null; + if (calendarId != null && calendarId != 0) { + calendar = this.calendarRepository.findOne(calendarId); + if (calendar == null) { throw new CalendarNotFoundException(calendarId); } + } + List ciList = (List) this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loanId, CalendarEntityType.LOANS.getValue()); if (calendar != null) { @@ -534,49 +526,4 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa return loan; } - private void validateCalendarDates(final JsonCommand command, final Calendar calendar) { - - final LocalDate edDate = command.localDateValueOfParameterNamed("expectedDisbursementDate"); - - if (edDate != null) { - // Expected disbursement date should not be before Meeting start - // date - if (edDate.isBefore(calendar.getStartDateLocalDate())) { - final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' cannot be before meeting start date '" - + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate, - calendar.getStartDate()); - } - - // Disbursement date should fall on a meeting date - if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), edDate)) { - final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' does not fall on a meeting date."; - throw new NotValidRecurringDateException("loan.expected.disbursement.date", errorMessage, edDate.toString(), - calendar.getTitle()); - } - - } - - final LocalDate repStartingDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate"); - - if (repStartingDate != null) { - - // First repayment date should not be before Meeting date - if (repStartingDate.isBefore(calendar.getStartDateLocalDate())) { - final String errorMessage = "First repayment date '" + repStartingDate.toString() - + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage, - repStartingDate, calendar.getStartDate()); - } - - if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repStartingDate)) { - final String errorMessage = "The First repayment date '" + repStartingDate.toString() - + "' does not fall on a meeting date."; - throw new NotValidRecurringDateException("loan.first.repayment.date", errorMessage, repStartingDate.toString(), - calendar.getTitle()); - } - - } - - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index b8d500a26..221be5b74 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -112,6 +112,7 @@ public class LoanAssembler { final Long loanOfficerId = fromApiJsonHelper.extractLongNamed("loanOfficerId", element); final Long transactionProcessingStrategyId = fromApiJsonHelper.extractLongNamed("transactionProcessingStrategyId", element); final Long loanPurposeId = fromApiJsonHelper.extractLongNamed("loanPurposeId", element); + final Boolean syncDisbursementWithMeeting = fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final LoanProduct loanProduct = this.loanProductRepository.findOne(productId); if (loanProduct == null) { throw new LoanProductNotFoundException(productId); } @@ -154,7 +155,7 @@ public class LoanAssembler { if (group == null) { throw new GroupNotFoundException(groupId); } loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); + loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting); } if (client != null && group != null) { @@ -162,7 +163,7 @@ public class LoanAssembler { if (!group.hasClientAsMember(client)) { throw new ClientNotInGroupException(clientId, groupId); } loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), - loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); + loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting); } final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index a0fc10ec1..142d642a6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -442,7 +442,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " la.fee_charges_overdue_derived as feeChargesOverdue," + " la.penalty_charges_overdue_derived as penaltyChargesOverdue," + " la.total_overdue_derived as totalOverdue," - + " la.overdue_since_date_derived as overdueSinceDate" + + " la.overdue_since_date_derived as overdueSinceDate," + + " l.sync_disbursement_with_meeting as syncDisbursementWithMeeting" + " from m_loan l" // + " join m_product_loan lp on lp.id = l.product_id" // + " join m_currency rc on rc.`code` = l.currency_code" // @@ -584,6 +585,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LocalDate expectedFirstRepaymentOnDate = JdbcSupport.getLocalDate(rs, "expectedFirstRepaymentOnDate"); final LocalDate interestChargedFromDate = JdbcSupport.getLocalDate(rs, "interestChargedFromDate"); + final Boolean syncDisbursementWithMeeting = rs.getBoolean("syncDisbursementWithMeeting"); + final BigDecimal feeChargesDueAtDisbursementCharged = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "feeChargesDueAtDisbursementCharged"); LoanSummaryData loanSummary = null; @@ -652,7 +655,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, - graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); + graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged, + syncDisbursementWithMeeting); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 6b1079adb..317c4167b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -8,7 +8,6 @@ package org.mifosplatform.portfolio.loanaccount.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -29,7 +28,6 @@ import org.mifosplatform.portfolio.calendar.domain.Calendar; import org.mifosplatform.portfolio.calendar.domain.CalendarEntityType; import org.mifosplatform.portfolio.calendar.domain.CalendarInstance; import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository; -import org.mifosplatform.portfolio.calendar.exception.NotValidRecurringDateException; import org.mifosplatform.portfolio.calendar.service.CalendarHelper; import org.mifosplatform.portfolio.charge.domain.Charge; import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; @@ -57,7 +55,6 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository; -import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; @@ -65,6 +62,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFound import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; import org.mifosplatform.portfolio.note.domain.Note; import org.mifosplatform.portfolio.note.domain.NoteRepository; @@ -74,7 +72,6 @@ import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; @Service public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { @@ -139,12 +136,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanEventApiJsonValidator.validateDisbursement(command.json()); final Loan loan = retrieveLoanBy(loanId); - - // validate actual disbursement date against meeting date - Collection calendarInstances = this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loan.getId(), + // validate actual disbursement date against meeting date + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), CalendarEntityType.LOANS.getValue()); - validateDisbursementDate(command.localDateValueOfParameterNamed("actualDisbursementDate"), calendarInstances); - + if(loan.isSyncDisbursementWithMeeting()){ + + final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance); + } + final MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); @@ -155,8 +155,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + //Recalculate first repayment date based in actual disbursement date. + final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); + final LocalDate firstRepaymentMeetingDate = getFirstRepaymentMeetingDate(actualDisbursementDate, loan, calendarInstance); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail); + existingReversedTransactionIds, changes, paymentDetail, firstRepaymentMeetingDate); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -181,36 +185,39 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - private void validateDisbursementDate(final LocalDate disbursementDate, final Collection calendarInstances) { - if (!CollectionUtils.isEmpty(calendarInstances)) { - Calendar calendar = null; - for (CalendarInstance calendarInstance : calendarInstances) { - calendar = calendarInstance.getCalendar(); - break; - } - if (calendar != null && disbursementDate != null) { - - // Actual disbursement date cannot be before Meeting start - // date - if (disbursementDate.isBefore(calendar.getStartDateLocalDate())) { - final String errorMessage = "Actual disbursement date '" + disbursementDate.toString() - + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("actual.disbursement.date.cannot.be.before.meeting.start.date", errorMessage, - disbursementDate.toString(), calendar.getStartDate()); - } - - // Disbursement date should fall on a meeting date - if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), disbursementDate)) { - final String errorMessage = "Expected disbursement date '" + disbursementDate.toString() - + "' does not fall on a meeting date."; - throw new NotValidRecurringDateException("loan.actual.disbursement.date", errorMessage, disbursementDate.toString(), - calendar.getTitle()); - } - - } - } - } + private LocalDate getFirstRepaymentMeetingDate( + final LocalDate actualDisbursementDate, final Loan loan, + final CalendarInstance calendarInstance) { + final Calendar calendar = (calendarInstance == null) ? null + : calendarInstance.getCalendar(); + LocalDate firstRepaymentMeetingDate = null; + if (calendar != null) {// sync repayments + // TODO: AA - user provided first repayment date takes precedence over recalculated meeting date + if (loan.getExpectedFirstRepaymentOnDate() == null) { + // FIXME: AA - Possibility of having next meeting date + // immediately after disbursement date, + // need to have minimum number of days gap between disbursement + // and first repayment date. + final LoanProductRelatedDetail repaymentScheduleDetails = loan + .repaymentScheduleDetail(); + if (repaymentScheduleDetails != null) {// Not expecting to be + // null + final Integer repayEvery = repaymentScheduleDetails + .getRepayEvery(); + final String frequency = CalendarHelper + .getMeetingFrequencyFromPeriodFrequencyType(repaymentScheduleDetails + .getRepaymentPeriodFrequencyType()); + firstRepaymentMeetingDate = CalendarHelper + .getFirstRepaymentMeetingDate(calendar, + actualDisbursementDate, repayEvery, + frequency); + } + } + } + return firstRepaymentMeetingDate; + } + /**** * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base *****/ @@ -235,8 +242,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + // Bulk disbursement should happen on meeting date (mostly from collection sheet). + //FIXME: AA - this should be first meeting date based on disbursement date and next available meeting dates + //assuming repayment schedule won't regenerate because expected disbursement and actual disbursement happens on same date + final LocalDate firstRepaymentOnDate = null; loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, - existingReversedTransactionIds, changes, paymentDetail); + existingReversedTransactionIds, changes, paymentDetail, firstRepaymentOnDate); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -327,7 +338,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private CommandProcessingResultBuilder saveLoanRepayment(final Long loanId, final PaymentDetail paymentDetail, final BigDecimal transactionAmount, final LocalDate transactionDate, final String noteText) { final Loan loan = retrieveLoanBy(loanId); - + + //TODO: Is it required to validate transaction date with meeting dates if repayments is synced with meeting? + /*if(loan.isSyncDisbursementWithMeeting()){ + // validate actual disbursement date against meeting date + CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + this.loanEventApiJsonValidator.validateRepaymentDateWithMeetingDate(transactionDate, calendarInstance); + }*/ + final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList();