3030import java .time .format .DateTimeFormatter ;
3131import java .util .List ;
3232import java .util .Map ;
33+ import java .util .Optional ;
34+ import java .util .function .Consumer ;
35+ import java .util .function .Predicate ;
3336import lombok .RequiredArgsConstructor ;
3437import lombok .extern .slf4j .Slf4j ;
3538import org .apache .fineract .client .feign .FineractFeignClient ;
39+ import org .apache .fineract .client .feign .util .CallFailedRuntimeException ;
3640import org .apache .fineract .client .models .DelinquencyBucketRequest ;
3741import org .apache .fineract .client .models .MinimumPaymentPeriodAndRule ;
3842import org .apache .fineract .client .models .PostAllowAttributeOverrides ;
@@ -94,17 +98,7 @@ public void createWcDelinquencyBucket(final int frequency, final String frequenc
9498
9599 @ When ("Admin creates WC delinquency reschedule action with minimumPayment {int} and frequency {int} {word}" )
96100 public void createRescheduleAction (final int minimumPayment , final int frequency , final String frequencyType ) {
97- final Long loanId = getLoanId ();
98- final PostWorkingCapitalLoansDelinquencyActionRequest request = buildRescheduleRequest (new BigDecimal (minimumPayment ), frequency ,
99- frequencyType );
100- log .info ("Creating RESCHEDULE action for WC loan {}: minimumPayment={}, frequency={} {}" , loanId , minimumPayment , frequency ,
101- frequencyType );
102-
103- final PostWorkingCapitalLoansDelinquencyActionResponse result = ok (
104- () -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().createDelinquencyAction (loanId , request ));
105- assertThat (result ).isNotNull ();
106- assertThat (result .getResourceId ()).isNotNull ();
107- log .info ("RESCHEDULE action created with id={}" , result .getResourceId ());
101+ createRescheduleActionInternal (new BigDecimal (minimumPayment ), frequency , frequencyType );
108102 }
109103
110104 @ Then ("Admin fails to create WC delinquency reschedule action with minimumPayment {int} and frequency {int} {word}" )
@@ -118,6 +112,22 @@ public void failToCreateRescheduleAction(final int minimumPayment, final int fre
118112 fail (() -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().createDelinquencyAction (loanId , request ));
119113 }
120114
115+ @ Then ("Admin fails to create WC delinquency reschedule action with minimumPayment {int} and frequency {int} {word} with error containing {string}" )
116+ public void failToCreateRescheduleActionWithMessage (final int minimumPayment , final int frequency , final String frequencyType ,
117+ final String expectedMessage ) {
118+ final Long loanId = getLoanId ();
119+ final PostWorkingCapitalLoansDelinquencyActionRequest request = buildRescheduleRequest (new BigDecimal (minimumPayment ), frequency ,
120+ frequencyType );
121+ log .info (
122+ "Attempting to create RESCHEDULE action for WC loan {} (expecting HTTP 400 and message '{}'): minimumPayment={}, frequency={} {}" ,
123+ loanId , expectedMessage , minimumPayment , frequency , frequencyType );
124+
125+ final CallFailedRuntimeException exception = fail (
126+ () -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().createDelinquencyAction (loanId , request ));
127+ assertThat (exception .getStatus ()).as ("HTTP status code" ).isEqualTo (400 );
128+ assertThat (exception .getDeveloperMessage ()).as ("Developer message" ).contains (expectedMessage );
129+ }
130+
121131 @ Then ("WC loan delinquency range schedule has the following periods:" )
122132 public void verifyPeriods (final DataTable table ) {
123133 final Long loanId = getLoanId ();
@@ -128,39 +138,115 @@ public void verifyPeriods(final DataTable table) {
128138 assertThat (periods ).as ("Number of periods" ).hasSize (expectedRows .size ());
129139
130140 for (int i = 0 ; i < expectedRows .size (); i ++) {
131- final Map <String , String > expected = expectedRows .get (i );
132141 final WorkingCapitalLoanDelinquencyRangeScheduleData actual = periods .get (i );
133- final String periodLabel = "Period " + (i + 1 );
134-
135- assertThat (actual .getPeriodNumber ()).as (periodLabel + " periodNumber" )
136- .isEqualTo (Integer .parseInt (expected .get ("periodNumber" )));
137- assertThat (actual .getFromDate ()).as (periodLabel + " fromDate" )
138- .isEqualTo (LocalDate .parse (expected .get ("fromDate" ), DATE_FORMAT ));
139- assertThat (actual .getToDate ()).as (periodLabel + " toDate" ).isEqualTo (LocalDate .parse (expected .get ("toDate" ), DATE_FORMAT ));
140- assertThat (actual .getExpectedAmount ()).as (periodLabel + " expectedAmount" )
141- .isEqualByComparingTo (new BigDecimal (expected .get ("expectedAmount" )));
142- assertThat (actual .getPaidAmount ()).as (periodLabel + " paidAmount" )
143- .isEqualByComparingTo (new BigDecimal (expected .get ("paidAmount" )));
144- assertThat (actual .getOutstandingAmount ()).as (periodLabel + " outstandingAmount" )
145- .isEqualByComparingTo (new BigDecimal (expected .get ("outstandingAmount" )));
146-
147- final String criteriaMetStr = expected .get ("minPaymentCriteriaMet" );
148- if (criteriaMetStr == null || criteriaMetStr .isBlank ()) {
149- assertThat (actual .getMinPaymentCriteriaMet ()).as (periodLabel + " minPaymentCriteriaMet" ).isNull ();
150- } else {
151- assertThat (actual .getMinPaymentCriteriaMet ()).as (periodLabel + " minPaymentCriteriaMet" )
152- .isEqualTo (Boolean .parseBoolean (criteriaMetStr ));
153- }
142+ final int periodNumber = i + 1 ;
143+ expectedRows .get (i ).forEach ((field , value ) -> verifyFullScheduleField (actual , field , value , periodNumber ));
154144 }
155145 }
156146
157147 @ Then ("WC loan delinquency actions contain {int} action(s)" )
158148 public void verifyActionCount (final int count ) {
159149 final Long loanId = getLoanId ();
160- final List <WorkingCapitalLoanDelinquencyActionData > actions = ok (
161- () -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().retrieveDelinquencyActions (loanId ));
150+ assertThat (retrieveDelinquencyActions (loanId )).hasSize (count );
151+ }
152+
153+ @ Then ("WC loan has both PAUSE and RESCHEDULE delinquency actions" )
154+ public void verifyBothPauseAndRescheduleActions () {
155+ final Long loanId = getLoanId ();
156+ final List <WorkingCapitalLoanDelinquencyActionData > actions = retrieveDelinquencyActions (loanId );
157+ assertThat (actions .stream ().map (a -> a .getAction ().name ()).toList ()).as ("Should contain both PAUSE and RESCHEDULE" )
158+ .contains ("PAUSE" , "RESCHEDULE" );
159+ }
160+
161+ @ Then ("WC loan last delinquency action has the following data:" )
162+ public void verifyLastActionContent (final DataTable table ) {
163+ final Long loanId = getLoanId ();
164+ final List <WorkingCapitalLoanDelinquencyActionData > actions = retrieveDelinquencyActions (loanId );
165+ assertThat (actions ).as ("Actions should not be empty" ).isNotEmpty ();
166+
167+ final WorkingCapitalLoanDelinquencyActionData last = actions .get (actions .size () - 1 );
168+ final List <Map <String , String >> rows = table .asMaps ();
169+ assertThat (rows ).as ("Expected exactly 1 data row" ).hasSize (1 );
170+ rows .get (0 ).forEach ((field , value ) -> verifyActionField (last , field , value ));
171+ }
172+
173+ @ Then ("WC loan delinquency range schedule periods have specific data:" )
174+ public void verifySpecificPeriods (final DataTable table ) {
175+ final Long loanId = getLoanId ();
176+ final List <WorkingCapitalLoanDelinquencyRangeScheduleData > periods = ok (
177+ () -> fineractFeignClient .workingCapitalLoanDelinquencyRangeSchedule ().retrieveDelinquencyRangeSchedule (loanId ));
178+
179+ for (final Map <String , String > expected : table .asMaps ()) {
180+ final int periodNumber = Integer .parseInt (expected .get ("periodNumber" ));
181+ final WorkingCapitalLoanDelinquencyRangeScheduleData actual = periods .stream ()
182+ .filter (p -> p .getPeriodNumber ().equals (periodNumber )).findFirst ().orElse (null );
183+ assertThat (actual ).as ("Period %d should exist" , periodNumber ).isNotNull ();
184+ expected .forEach ((field , value ) -> verifyFullScheduleField (actual , field , value , periodNumber ));
185+ }
186+ }
187+
188+ @ When ("Admin creates WC delinquency reschedule action with decimal minimumPayment {string} and frequency {int} {word}" )
189+ public void createRescheduleActionWithDecimal (final String minimumPayment , final int frequency , final String frequencyType ) {
190+ createRescheduleActionInternal (new BigDecimal (minimumPayment ), frequency , frequencyType );
191+ }
192+
193+ private void createRescheduleActionInternal (final BigDecimal minimumPayment , final int frequency , final String frequencyType ) {
194+ final Long loanId = getLoanId ();
195+ final PostWorkingCapitalLoansDelinquencyActionRequest request = buildRescheduleRequest (minimumPayment , frequency , frequencyType );
196+ log .info ("Creating RESCHEDULE action for WC loan {}: minimumPayment={}, frequency={} {}" , loanId , minimumPayment , frequency ,
197+ frequencyType );
198+
199+ final PostWorkingCapitalLoansDelinquencyActionResponse result = ok (
200+ () -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().createDelinquencyAction (loanId , request ));
201+ assertThat (result ).isNotNull ();
202+ assertThat (result .getResourceId ()).isNotNull ();
203+ log .info ("RESCHEDULE action created with id={}" , result .getResourceId ());
204+ }
205+
206+ private List <WorkingCapitalLoanDelinquencyActionData > retrieveDelinquencyActions (final Long loanId ) {
207+ return ok (() -> fineractFeignClient .workingCapitalLoanDelinquencyActions ().retrieveDelinquencyActions (loanId ));
208+ }
209+
210+ private void verifyActionField (final WorkingCapitalLoanDelinquencyActionData actual , final String field , final String expected ) {
211+ switch (field ) {
212+ case "action" -> assertThat (actual .getAction ().name ()).as ("action" ).isEqualTo (expected );
213+ case "startDate" -> assertThat (actual .getStartDate ()).as ("startDate" ).isEqualTo (LocalDate .parse (expected , DATE_FORMAT ));
214+ case "endDate" ->
215+ verifyOptionalField (expected , v -> assertThat (actual .getEndDate ()).as ("endDate" ).isEqualTo (LocalDate .parse (v , DATE_FORMAT )),
216+ () -> assertThat (actual .getEndDate ()).as ("endDate" ).isNull ());
217+ case "minimumPayment" ->
218+ assertThat (actual .getMinimumPayment ()).as ("minimumPayment" ).isEqualByComparingTo (new BigDecimal (expected ));
219+ case "frequency" -> assertThat (actual .getFrequency ()).as ("frequency" ).isEqualTo (Integer .parseInt (expected ));
220+ case "frequencyType" -> assertThat (actual .getFrequencyType ().name ()).as ("frequencyType" ).isEqualTo (expected );
221+ default -> throw new IllegalArgumentException ("Unknown action field: " + field );
222+ }
223+ }
224+
225+ private void verifyFullScheduleField (final WorkingCapitalLoanDelinquencyRangeScheduleData actual , final String field ,
226+ final String expected , final int periodNumber ) {
227+ final String label = "Period " + periodNumber + " " + field ;
228+ switch (field ) {
229+ case "periodNumber" -> assertThat (actual .getPeriodNumber ()).as (label ).isEqualTo (Integer .parseInt (expected ));
230+ case "fromDate" -> assertThat (actual .getFromDate ()).as (label ).isEqualTo (LocalDate .parse (expected , DATE_FORMAT ));
231+ case "toDate" -> assertThat (actual .getToDate ()).as (label ).isEqualTo (LocalDate .parse (expected , DATE_FORMAT ));
232+ case "expectedAmount" -> assertThat (actual .getExpectedAmount ()).as (label ).isEqualByComparingTo (new BigDecimal (expected ));
233+ case "paidAmount" -> assertThat (actual .getPaidAmount ()).as (label ).isEqualByComparingTo (new BigDecimal (expected ));
234+ case "outstandingAmount" -> assertThat (actual .getOutstandingAmount ()).as (label ).isEqualByComparingTo (new BigDecimal (expected ));
235+ case "minPaymentCriteriaMet" -> verifyOptionalField (expected ,
236+ v -> assertThat (actual .getMinPaymentCriteriaMet ()).as (label ).isEqualTo (Boolean .parseBoolean (v )),
237+ () -> assertThat (actual .getMinPaymentCriteriaMet ()).as (label ).isNull ());
238+ case "delinquentDays" ->
239+ verifyOptionalField (expected , v -> assertThat (actual .getDelinquentDays ()).as (label ).isEqualTo (Long .parseLong (v )),
240+ () -> assertThat (actual .getDelinquentDays ()).as (label ).isNull ());
241+ case "delinquentAmount" -> verifyOptionalField (expected ,
242+ v -> assertThat (actual .getDelinquentAmount ()).as (label ).isEqualByComparingTo (new BigDecimal (v )),
243+ () -> assertThat (actual .getDelinquentAmount ()).as (label ).isNull ());
244+ default -> throw new IllegalArgumentException ("Unknown schedule field: " + field );
245+ }
246+ }
162247
163- assertThat (actions ).hasSize (count );
248+ private void verifyOptionalField (final String expected , final Consumer <String > whenPresent , final Runnable whenAbsent ) {
249+ Optional .ofNullable (expected ).filter (Predicate .not (String ::isBlank )).ifPresentOrElse (whenPresent , whenAbsent );
164250 }
165251
166252 private Long getLoanId () {
0 commit comments