4040import org .apache .fineract .infrastructure .core .service .MathUtil ;
4141import org .apache .fineract .organisation .monetary .data .CurrencyData ;
4242import org .apache .fineract .organisation .monetary .domain .Money ;
43+ import org .apache .fineract .organisation .monetary .domain .MoneyHelper ;
4344import org .apache .fineract .portfolio .common .domain .DaysInMonthType ;
4445import org .apache .fineract .portfolio .common .domain .DaysInYearCustomStrategyType ;
4546import org .apache .fineract .portfolio .common .domain .DaysInYearType ;
4647import org .apache .fineract .portfolio .common .domain .PeriodFrequencyType ;
4748import org .apache .fineract .portfolio .loanaccount .data .LoanTermVariationsData ;
4849import org .apache .fineract .portfolio .loanaccount .domain .LoanRepaymentScheduleInstallment ;
4950import org .apache .fineract .portfolio .loanaccount .domain .LoanTermVariationType ;
51+ import org .apache .fineract .portfolio .loanaccount .domain .LoanTransaction ;
52+ import org .apache .fineract .portfolio .loanaccount .domain .reaging .LoanReAgeParameter ;
53+ import org .apache .fineract .portfolio .loanaccount .loanschedule .domain .LoanApplicationTerms ;
5054import org .apache .fineract .portfolio .loanaccount .loanschedule .domain .LoanScheduleModelRepaymentPeriod ;
55+ import org .apache .fineract .portfolio .loanaccount .loanschedule .domain .ScheduledDateGenerator ;
5156import org .apache .fineract .portfolio .loanproduct .calc .data .EmiAdjustment ;
5257import org .apache .fineract .portfolio .loanproduct .calc .data .EmiChangeOperation ;
5358import org .apache .fineract .portfolio .loanproduct .calc .data .InterestPeriod ;
@@ -66,6 +71,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
6671 private static final BigDecimal DIVISOR_100 = new BigDecimal ("100" );
6772 private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal .valueOf (7 );
6873
74+ private final ScheduledDateGenerator scheduledDateGenerator ;
75+
6976 @ Override
7077 @ NotNull
7178 public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel (@ NotNull List <LoanScheduleModelRepaymentPeriod > periods ,
@@ -559,6 +566,129 @@ private void calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(fi
559566 }
560567 }
561568
569+ @ Override
570+ public void updateModelRepaymentPeriodsDuringReAge (final ProgressiveLoanInterestScheduleModel scheduleModel ,
571+ final LoanTransaction loanTransaction , final LoanApplicationTerms loanApplicationTerms , final MathContext mc ) {
572+ final LoanReAgeParameter loanReAgeParameter = loanTransaction .getLoanReAgeParameter ();
573+ final LocalDate reAgingStartDate = loanReAgeParameter .getStartDate ();
574+ final LocalDate transactionDate = loanTransaction .getTransactionDate ();
575+ final List <RepaymentPeriod > existingRepaymentPeriods = scheduleModel .repaymentPeriods ();
576+
577+ moveOutstandingAmountsFromPeriodsBeforeReAging (existingRepaymentPeriods , reAgingStartDate );
578+
579+ final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate (loanReAgeParameter );
580+
581+ final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel (loanApplicationTerms ,
582+ mc , periodStartDate , transactionDate );
583+
584+ mergeNewInterestScheduleModelWithExistingOne (scheduleModel , temporaryReAgedScheduleModel , loanTransaction );
585+ }
586+
587+ /**
588+ * * Merging the new temporary model of re-aged repayment periods and existing one together. After that recalculate
589+ * the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
590+ * significantly from other periods.
591+ */
592+ private void mergeNewInterestScheduleModelWithExistingOne (final ProgressiveLoanInterestScheduleModel scheduleModel ,
593+ final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel , final LoanTransaction loanTransaction ) {
594+ final List <RepaymentPeriod > newPeriods = temporaryReAgedScheduleModel .repaymentPeriods ();
595+
596+ if (newPeriods .isEmpty ()) {
597+ return ;
598+ }
599+
600+ final List <RepaymentPeriod > existingRepaymentPeriods = scheduleModel .repaymentPeriods ();
601+ final LocalDate reAgingStartDate = loanTransaction .getLoanReAgeParameter ().getStartDate ();
602+
603+ final Optional <RepaymentPeriod > firstExistingRepaymentPeriodOpt = existingRepaymentPeriods .stream ()
604+ .filter (period -> period .getDueDate ().equals (reAgingStartDate )).findFirst ();
605+
606+ for (final RepaymentPeriod newPeriod : newPeriods ) {
607+ final Optional <RepaymentPeriod > existingRepaymentPeriodOpt = existingRepaymentPeriods .stream ().filter (
608+ period -> period .getFromDate ().equals (newPeriod .getFromDate ()) && period .getDueDate ().equals (newPeriod .getDueDate ()))
609+ .findFirst ();
610+ Optional <RepaymentPeriod > previousExistingRepaymentPeriodOpt = Optional .empty ();
611+ if (existingRepaymentPeriodOpt .isPresent () && firstExistingRepaymentPeriodOpt .isPresent ()
612+ && existingRepaymentPeriodOpt .get ().equals (firstExistingRepaymentPeriodOpt .get ())) {
613+ previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt .get ().getPrevious ();
614+ }
615+
616+ final Money newPrincipal = newPeriod .getDuePrincipal ();
617+ final Money newInterest = newPeriod .getDueInterest ();
618+
619+ final RepaymentPeriod rp = RepaymentPeriod .create (
620+ previousExistingRepaymentPeriodOpt .orElseGet (existingRepaymentPeriods ::getLast ), newPeriod .getFromDate (),
621+ newPeriod .getDueDate (), newPrincipal .add (newInterest ), MoneyHelper .getMathContext (),
622+ loanTransaction .getLoan ().getLoanProductRelatedDetail ());
623+ rp .setTotalDisbursedAmount (scheduleModel .repaymentPeriods ().getFirst ().getTotalDisbursedAmount ());
624+
625+ existingRepaymentPeriodOpt .ifPresent (existingRepaymentPeriods ::remove );
626+ existingRepaymentPeriods .add (rp );
627+ calculateRateFactorForRepaymentPeriod (rp , scheduleModel );
628+ }
629+
630+ final RepaymentPeriod lastReAgedInstallment = newPeriods .getLast ();
631+ final List <RepaymentPeriod > reAgedRepaymentPeriods = existingRepaymentPeriods .stream ()
632+ .filter (repaymentPeriod -> (!repaymentPeriod .getFromDate ().isBefore (reAgingStartDate )
633+ || repaymentPeriod .getDueDate ().isEqual (reAgingStartDate ))
634+ && !repaymentPeriod .getDueDate ().isAfter (lastReAgedInstallment .getDueDate ()))
635+ .toList ();
636+
637+ calculateOutstandingBalance (scheduleModel );
638+ calculateLastUnpaidRepaymentPeriodEMI (scheduleModel , loanTransaction .getTransactionDate ());
639+ checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods (scheduleModel , reAgedRepaymentPeriods );
640+ }
641+
642+ /**
643+ * * Generates temporary interestScheduleModel with re-aged repayment periods
644+ */
645+ @ NotNull
646+ private ProgressiveLoanInterestScheduleModel generateTemporaryReAgedScheduleModel (final LoanApplicationTerms loanApplicationTerms ,
647+ final MathContext mc , final LocalDate periodStartDate , final LocalDate transactionDate ) {
648+ final List <LoanScheduleModelRepaymentPeriod > expectedRepaymentPeriods = scheduledDateGenerator .generateRepaymentPeriods (mc ,
649+ periodStartDate , loanApplicationTerms , null );
650+ final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel (
651+ expectedRepaymentPeriods , loanApplicationTerms .toLoanProductRelatedDetailMinimumData (), null ,
652+ loanApplicationTerms .getInstallmentAmountInMultiplesOf (), mc );
653+
654+ addDisbursement (temporaryReAgedScheduleModel , EmiChangeOperation .disburse (transactionDate , loanApplicationTerms .getPrincipal ()));
655+ return temporaryReAgedScheduleModel ;
656+ }
657+
658+ /**
659+ * * Based on the re-aging start date and frequency data calculates start date for the first re-aged period, which
660+ * is used to generate re-aged repayment periods
661+ */
662+ @ NotNull
663+ private static LocalDate calculateFirstReAgedPeriodStartDate (final LoanReAgeParameter loanReAgeParameter ) {
664+ final LocalDate reAgingStartDate = loanReAgeParameter .getStartDate ();
665+ return switch (loanReAgeParameter .getFrequencyType ()) {
666+ case DAYS -> reAgingStartDate .minusDays (loanReAgeParameter .getFrequencyNumber ());
667+ case WEEKS -> reAgingStartDate .minusWeeks (loanReAgeParameter .getFrequencyNumber ());
668+ case MONTHS -> reAgingStartDate .minusMonths (loanReAgeParameter .getFrequencyNumber ());
669+ case YEARS -> reAgingStartDate .minusYears (loanReAgeParameter .getFrequencyNumber ());
670+ case WHOLE_TERM -> throw new IllegalStateException ("Unexpected RecalculationFrequencyType: WHOLE_TERM" );
671+ case INVALID -> throw new IllegalStateException ("Unexpected RecalculationFrequencyType: INVALID" );
672+ };
673+ }
674+
675+ /**
676+ * * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases
677+ * the balance correction amount (added during interest recalculation for the business date) by the amount of the
678+ * principal that was moved.
679+ */
680+ private static void moveOutstandingAmountsFromPeriodsBeforeReAging (final List <RepaymentPeriod > existingRepaymentPeriods ,
681+ final LocalDate reAgingStartDate ) {
682+ final List <RepaymentPeriod > periodsBeforeReAging = existingRepaymentPeriods .stream ()
683+ .filter (rp -> rp .getFromDate ().isBefore (reAgingStartDate ) && !rp .isFullyPaid ()).toList ();
684+
685+ periodsBeforeReAging .forEach (rp -> {
686+ final InterestPeriod lastInterestPeriod = rp .getInterestPeriods ().getLast ();
687+ lastInterestPeriod .addBalanceCorrectionAmount (rp .getOutstandingPrincipal ().negated ());
688+ rp .setEmi (rp .getTotalPaidAmount ());
689+ });
690+ }
691+
562692 private void calculateLastUnpaidRepaymentPeriodEMI (ProgressiveLoanInterestScheduleModel scheduleModel , LocalDate tillDate ) {
563693 Optional <RepaymentPeriod > findLastUnpaidRepaymentPeriod = scheduleModel .repaymentPeriods ().stream ().filter (rp -> !rp .isFullyPaid ())
564694 .reduce ((first , second ) -> second );
0 commit comments