FINERACT-2406: Set COMMIT flush mode during transaction processing
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.core.annotation;
|
||||||
|
|
||||||
|
import jakarta.persistence.FlushModeType;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to specify the flush mode for a method or class. When applied to a class, all public methods will use the
|
||||||
|
* specified flush mode. When applied to a method, it overrides any class-level annotation.
|
||||||
|
*/
|
||||||
|
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface WithFlushMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flush mode to be used for the annotated method or class methods.
|
||||||
|
*
|
||||||
|
* @return the flush mode
|
||||||
|
*/
|
||||||
|
FlushModeType value() default FlushModeType.AUTO;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.core.aop;
|
||||||
|
|
||||||
|
import jakarta.persistence.FlushModeType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.annotation.WithFlushMode;
|
||||||
|
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aspect that handles the @WithFlushMode annotation to manage JPA flush mode around method execution.
|
||||||
|
* <p>
|
||||||
|
* This aspect is ordered to run after the @Transactional aspect (Ordered.LOWEST_PRECEDENCE - 1) to ensure proper
|
||||||
|
* transaction management. It will only modify the flush mode if there is an active transaction.
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FlushModeAspect {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FlushModeAspect.class);
|
||||||
|
private final FlushModeHandler flushModeHandler;
|
||||||
|
|
||||||
|
@Around("@within(withFlushMode) || @annotation(withFlushMode)")
|
||||||
|
public Object manageFlushMode(ProceedingJoinPoint joinPoint, WithFlushMode withFlushMode) {
|
||||||
|
// Get the effective annotation (method level takes precedence over class level)
|
||||||
|
WithFlushMode effectiveAnnotation = getEffectiveAnnotation(joinPoint, withFlushMode);
|
||||||
|
if (effectiveAnnotation == null) {
|
||||||
|
return jointPointProceed(joinPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushModeType flushMode = effectiveAnnotation.value();
|
||||||
|
|
||||||
|
// Check if we're in an active transaction
|
||||||
|
boolean hasActiveTransaction = TransactionSynchronizationManager.isActualTransactionActive();
|
||||||
|
|
||||||
|
if (!hasActiveTransaction) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.warn("No active transaction found for @WithFlushMode on {}.{}", joinPoint.getSignature().getDeclaringTypeName(),
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
}
|
||||||
|
return jointPointProceed(joinPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Setting flush mode to {} for {}.{}", flushMode, joinPoint.getSignature().getDeclaringTypeName(),
|
||||||
|
joinPoint.getSignature().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use FlushModeHandler to manage the flush mode around method execution
|
||||||
|
return flushModeHandler.withFlushMode(flushMode, () -> jointPointProceed(joinPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object jointPointProceed(ProceedingJoinPoint joinPoint) {
|
||||||
|
try {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException("Error in method with @WithFlushMode", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WithFlushMode getEffectiveAnnotation(ProceedingJoinPoint joinPoint, WithFlushMode annotation) {
|
||||||
|
// If the annotation is already present on the method, use it
|
||||||
|
if (annotation != null && joinPoint.getSignature() instanceof MethodSignature) {
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try to get the class-level annotation
|
||||||
|
Class<?> targetClass = ClassUtils.getUserClass(joinPoint.getTarget().getClass());
|
||||||
|
return AnnotationUtils.findAnnotation(targetClass, WithFlushMode.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.core.persistence;
|
|||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.FlushModeType;
|
import jakarta.persistence.FlushModeType;
|
||||||
import jakarta.persistence.PersistenceContext;
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -30,10 +31,28 @@ public class FlushModeHandler {
|
|||||||
private EntityManager entityManager;
|
private EntityManager entityManager;
|
||||||
|
|
||||||
public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
|
public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
|
||||||
|
withFlushMode(flushMode, () -> {
|
||||||
|
runnable.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the provided supplier with the specified flush mode, then restores the original flush mode.
|
||||||
|
*
|
||||||
|
* @param flushMode
|
||||||
|
* the flush mode to set
|
||||||
|
* @param supplier
|
||||||
|
* the code to execute
|
||||||
|
* @param <T>
|
||||||
|
* the type of the result
|
||||||
|
* @return the result of the supplier
|
||||||
|
*/
|
||||||
|
public <T> T withFlushMode(FlushModeType flushMode, Supplier<T> supplier) {
|
||||||
FlushModeType original = entityManager.getFlushMode();
|
FlushModeType original = entityManager.getFlushMode();
|
||||||
try {
|
try {
|
||||||
entityManager.setFlushMode(flushMode);
|
entityManager.setFlushMode(flushMode);
|
||||||
runnable.run();
|
return supplier.get();
|
||||||
} finally {
|
} finally {
|
||||||
entityManager.setFlushMode(original);
|
entityManager.setFlushMode(original);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -418,6 +418,33 @@ public class LoanStepDef extends AbstractStepDef {
|
|||||||
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
|
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@When("Admin makes {string} transaction with {string} payment type on {string} with {double} EUR transaction amount and self-generated external-id")
|
||||||
|
public void createTransactionWithExternalId(String transactionTypeInput, String transactionPaymentType, String transactionDate,
|
||||||
|
double transactionAmount) throws IOException, InterruptedException {
|
||||||
|
eventStore.reset();
|
||||||
|
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||||
|
long loanId = loanResponse.body().getLoanId();
|
||||||
|
String externalId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
TransactionType transactionType = TransactionType.valueOf(transactionTypeInput);
|
||||||
|
String transactionTypeValue = transactionType.getValue();
|
||||||
|
DefaultPaymentType paymentType = DefaultPaymentType.valueOf(transactionPaymentType);
|
||||||
|
Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsRequest paymentTransactionRequest = LoanRequestFactory.defaultPaymentTransactionRequest()
|
||||||
|
.transactionDate(transactionDate).transactionAmount(transactionAmount).paymentTypeId(paymentTypeValue)
|
||||||
|
.externalId(externalId);
|
||||||
|
|
||||||
|
Response<PostLoansLoanIdTransactionsResponse> paymentTransactionResponse = loanTransactionsApi
|
||||||
|
.executeLoanTransaction(loanId, paymentTransactionRequest, transactionTypeValue).execute();
|
||||||
|
testContext().set(TestContextKey.LOAN_PAYMENT_TRANSACTION_RESPONSE, paymentTransactionResponse);
|
||||||
|
ErrorHelper.checkSuccessfulApiCall(paymentTransactionResponse);
|
||||||
|
assertThat(paymentTransactionResponse.body().getResourceExternalId()).as("External id is not correct").isEqualTo(externalId);
|
||||||
|
|
||||||
|
eventCheckHelper.transactionEventCheck(paymentTransactionResponse, transactionType, null);
|
||||||
|
eventCheckHelper.loanBalanceChangedEventCheck(loanId);
|
||||||
|
}
|
||||||
|
|
||||||
@When("Customer makes {string} transaction with {string} payment type on {string} with {double} EUR transaction amount and system-generated Idempotency key")
|
@When("Customer makes {string} transaction with {string} payment type on {string} with {double} EUR transaction amount and system-generated Idempotency key")
|
||||||
public void createTransactionWithAutoIdempotencyKey(String transactionTypeInput, String transactionPaymentType, String transactionDate,
|
public void createTransactionWithAutoIdempotencyKey(String transactionTypeInput, String transactionPaymentType, String transactionDate,
|
||||||
double transactionAmount) throws IOException {
|
double transactionAmount) throws IOException {
|
||||||
|
|||||||
+56
@@ -690,3 +690,59 @@ Feature: LoanInterestWaiver
|
|||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | | 260.0 |
|
| INCOME | 404001 | Interest Income Charge Off | | 260.0 |
|
||||||
| INCOME | 404000 | Interest Income | 260.0 | |
|
| INCOME | 404000 | Interest Income | 260.0 | |
|
||||||
|
|
||||||
|
|
||||||
|
@TestRailId:C4200
|
||||||
|
Scenario: Verify Interest Payment Waiver transaction - UC12: IPW after Charge-off
|
||||||
|
When Admin sets the business date to "23 October 2025"
|
||||||
|
And Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_ZERO_CHARGE_OFF | 25 October 2021 | 678.03 | 9.5129 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 24 | MONTHS | 1 | MONTHS | 24 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "25 October 2021" with "678.03" amount and expected disbursement date on "01 January 2024"
|
||||||
|
And Admin successfully disburse the loan on "25 October 2021" with "678.03" EUR transaction amount
|
||||||
|
And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "29 October 2021" with 10 EUR transaction amount and self-generated Idempotency key
|
||||||
|
And Customer makes "AUTOPAY" repayment on "26 August 2022" with 186.84 EUR transaction amount
|
||||||
|
And Admin does charge-off the loan on "24 September 2022"
|
||||||
|
When Admin makes "INTEREST_PAYMENT_WAIVER" transaction with "AUTOPAY" payment type on "24 September 2022" with 46.56 EUR transaction amount and self-generated external-id
|
||||||
|
Then Loan Repayment schedule has 24 periods, with the following data for periods:
|
||||||
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| | | 25 October 2021 | | 678.03 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||||
|
| 1 | 31 | 25 November 2021 | 26 August 2022 | 652.2 | 25.83 | 5.31 | 0.0 | 0.0 | 31.14 | 31.14 | 0.01 | 31.13 | 0.0 |
|
||||||
|
| 2 | 30 | 25 December 2021 | 26 August 2022 | 626.36 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 3 | 31 | 25 January 2022 | 26 August 2022 | 600.52 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 4 | 31 | 25 February 2022 | 26 August 2022 | 574.68 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 5 | 28 | 25 March 2022 | 26 August 2022 | 548.84 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 6 | 31 | 25 April 2022 | 26 August 2022 | 523.0 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 7 | 30 | 25 May 2022 | 24 September 2022 | 497.16 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 31.14 | 0.0 | 31.14 | 0.0 |
|
||||||
|
| 8 | 31 | 25 June 2022 | | 471.32 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 15.43 | 0.0 | 15.43 | 15.71 |
|
||||||
|
| 9 | 30 | 25 July 2022 | | 445.48 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 10 | 31 | 25 August 2022 | | 419.64 | 25.84 | 5.3 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 11 | 31 | 25 September 2022 | | 392.48 | 27.16 | 3.98 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 12 | 30 | 25 October 2022 | | 361.34 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 13 | 31 | 25 November 2022 | | 330.2 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 14 | 30 | 25 December 2022 | | 299.06 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 15 | 31 | 25 January 2023 | | 267.92 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 16 | 31 | 25 February 2023 | | 236.78 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 17 | 28 | 25 March 2023 | | 205.64 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 18 | 31 | 25 April 2023 | | 174.5 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 19 | 30 | 25 May 2023 | | 143.36 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 20 | 31 | 25 June 2023 | | 112.22 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 21 | 30 | 25 July 2023 | | 81.08 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 22 | 31 | 25 August 2023 | | 49.94 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 23 | 31 | 25 September 2023 | | 18.8 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 | 0.0 | 0.0 | 0.0 | 31.14 |
|
||||||
|
| 24 | 30 | 25 October 2023 | | 0.0 | 18.8 | 0.0 | 0.0 | 0.0 | 18.8 | 10.0 | 10.0 | 0.0 | 8.8 |
|
||||||
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
|
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
| 678.03 | 56.99 | 0.0 | 0.0 | 735.02 | 243.41 | 10.01 | 233.40 | 491.61 |
|
||||||
|
Then Loan Transactions tab has the following data:
|
||||||
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||||
|
| 25 October 2021 | Disbursement | 678.03 | 0.0 | 0.0 | 0.0 | 0.0 | 678.03 |
|
||||||
|
| 29 October 2021 | Merchant Issued Refund | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 668.03 |
|
||||||
|
| 29 October 2021 | Interest Refund | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 668.03 |
|
||||||
|
| 26 August 2022 | Repayment | 186.84 | 155.03 | 31.81 | 0.0 | 0.0 | 513.0 |
|
||||||
|
| 24 September 2022 | Accrual | 56.99 | 0.0 | 56.99 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 24 September 2022 | Charge-off | 538.17 | 513.0 | 25.17 | 0.0 | 0.0 | 0.0 |
|
||||||
|
| 24 September 2022 | Interest Payment Waiver | 46.56 | 35.97 | 10.59 | 0.0 | 0.0 | 477.03 |
|
||||||
|
And Customer makes "AUTOPAY" repayment on "24 September 2022" with 491.61 EUR transaction amount
|
||||||
|
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
|
||||||
|
|||||||
+17
-1
@@ -475,7 +475,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
final Loan loan = loanTransaction.getLoan();
|
final Loan loan = loanTransaction.getLoan();
|
||||||
final LoanTransaction chargeOffTransaction = loan.getLoanTransactions().stream().filter(t -> t.isChargeOff() && t.isNotReversed())
|
final LoanTransaction chargeOffTransaction = loan.getLoanTransactions().stream().filter(t -> t.isChargeOff() && t.isNotReversed())
|
||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
if (loan.isChargedOff() && chargeOffTransaction != null) {
|
boolean chargeOffInEffect = chargeOffIsInEffect(ctx, chargeOffTransaction, loanTransaction);
|
||||||
|
if (chargeOffInEffect) {
|
||||||
final LoanChargeOffBehaviour chargeOffBehaviour = loanTransaction.getLoan().getLoanProductRelatedDetail()
|
final LoanChargeOffBehaviour chargeOffBehaviour = loanTransaction.getLoan().getLoanProductRelatedDetail()
|
||||||
.getChargeOffBehaviour();
|
.getChargeOffBehaviour();
|
||||||
if (loan.isProgressiveSchedule() && !LoanChargeOffBehaviour.REGULAR.equals(chargeOffBehaviour)) {
|
if (loan.isProgressiveSchedule() && !LoanChargeOffBehaviour.REGULAR.equals(chargeOffBehaviour)) {
|
||||||
@@ -510,6 +511,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
handleRepayment(loanTransaction, ctx);
|
handleRepayment(loanTransaction, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean chargeOffIsInEffect(TransactionCtx ctx, LoanTransaction chargeOffTransaction, LoanTransaction loanTransaction) {
|
||||||
|
if (ctx instanceof ProgressiveTransactionCtx progressiveCtx && progressiveCtx.isChargedOff()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (chargeOffTransaction == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<LoanTransaction> orderedTransactions = new ArrayList<>();
|
||||||
|
orderedTransactions.add(chargeOffTransaction);
|
||||||
|
orderedTransactions.add(loanTransaction);
|
||||||
|
orderedTransactions.sort(LoanTransactionComparator.INSTANCE);
|
||||||
|
|
||||||
|
return orderedTransactions.getFirst().isChargeOff();
|
||||||
|
}
|
||||||
|
|
||||||
private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() //
|
List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() //
|
||||||
|
|||||||
+2
@@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactio
|
|||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Profile(FineractProfiles.TEST)
|
@Profile(FineractProfiles.TEST)
|
||||||
@Component
|
@Component
|
||||||
@@ -110,6 +111,7 @@ public class InternalCOBApiResource implements InitializingBean {
|
|||||||
@POST
|
@POST
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Path("loan-reprocess/{loanId}")
|
@Path("loan-reprocess/{loanId}")
|
||||||
|
@Transactional
|
||||||
public void loanReprocess(@Context final UriInfo uriInfo, @PathParam("loanId") long loanId) {
|
public void loanReprocess(@Context final UriInfo uriInfo, @PathParam("loanId") long loanId) {
|
||||||
reprocessLoanTransactionsService.reprocessTransactions(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId));
|
reprocessLoanTransactionsService.reprocessTransactions(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -109,8 +109,8 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recalculateAccrualActivityTransaction(Loan loan, ChangedTransactionDetail changedTransactionDetail) {
|
public void recalculateAccrualActivityTransaction(Loan loan, ChangedTransactionDetail changedTransactionDetail) {
|
||||||
List<LoanTransaction> accrualActivities = loanTransactionRepository.findNonReversedByLoanAndType(loan,
|
List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream()
|
||||||
LoanTransactionType.ACCRUAL_ACTIVITY);
|
.filter(lt -> lt.isNotReversed() && lt.isAccrualActivity()).toList();
|
||||||
accrualActivities.forEach(accrualActivity -> {
|
accrualActivities.forEach(accrualActivity -> {
|
||||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(accrualActivity);
|
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(accrualActivity);
|
||||||
|
|
||||||
|
|||||||
+46
-43
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
|
import jakarta.persistence.FlushModeType;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,6 +27,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.fineract.infrastructure.core.annotation.WithFlushMode;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -57,6 +59,7 @@ import org.springframework.util.ObjectUtils;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@WithFlushMode(FlushModeType.COMMIT)
|
||||||
public class LoanTransactionProcessingServiceImpl implements LoanTransactionProcessingService {
|
public class LoanTransactionProcessingServiceImpl implements LoanTransactionProcessingService {
|
||||||
|
|
||||||
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
||||||
@@ -91,36 +94,6 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
|
|||||||
&& currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(loanTransaction.getAmount(loan.getCurrency()));
|
&& currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(loanTransaction.getAmount(loan.getCurrency()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChangedTransactionDetail processLatestTransactionProgressiveInterestRecalculation(
|
|
||||||
AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) {
|
|
||||||
Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan,
|
|
||||||
loanTransaction.getTransactionDate());
|
|
||||||
ProgressiveLoanInterestScheduleModel model = savedModel
|
|
||||||
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
|
|
||||||
|
|
||||||
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
|
|
||||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
|
|
||||||
new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan));
|
|
||||||
progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
|
|
||||||
progressiveContext.setChargedOff(loan.isChargedOff());
|
|
||||||
progressiveContext.setWrittenOff(loan.isClosedWrittenOff());
|
|
||||||
progressiveContext.setContractTerminated(loan.isContractTermination());
|
|
||||||
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
|
|
||||||
if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
|
|
||||||
modelRepository.writeInterestScheduleModel(loan, model);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Money getTotalRefundInterestAmount(Loan loan) {
|
|
||||||
List<LoanTransactionType> supportedInterestRefundTransactionTypes = loan.getSupportedInterestRefundTransactionTypes();
|
|
||||||
if (supportedInterestRefundTransactionTypes != null && supportedInterestRefundTransactionTypes.isEmpty()) {
|
|
||||||
return Money.zero(loan.getCurrency());
|
|
||||||
}
|
|
||||||
return loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed).filter(LoanTransaction::isInterestRefund)
|
|
||||||
.map(t -> t.getAmount(loan.getCurrency())).reduce(Money.zero(loan.getCurrency()), Money::add);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction,
|
public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction,
|
||||||
TransactionCtx ctx) {
|
TransactionCtx ctx) {
|
||||||
@@ -133,19 +106,6 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
|
|||||||
return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx);
|
return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Loan getLoan(List<LoanTransaction> loanTransactions, List<LoanRepaymentScheduleInstallment> installments,
|
|
||||||
Set<LoanCharge> charges) {
|
|
||||||
if (!ObjectUtils.isEmpty(loanTransactions)) {
|
|
||||||
return loanTransactions.getFirst().getLoan();
|
|
||||||
} else if (!ObjectUtils.isEmpty(installments)) {
|
|
||||||
return installments.getFirst().getLoan();
|
|
||||||
} else if (!ObjectUtils.isEmpty(charges)) {
|
|
||||||
return charges.iterator().next().getLoan();
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("No loan found for the given transactions, installments or charges");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate,
|
public ChangedTransactionDetail reprocessLoanTransactions(String transactionProcessingStrategyCode, LocalDate disbursementDate,
|
||||||
List<LoanTransaction> loanTransactions, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
List<LoanTransaction> loanTransactions, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||||
@@ -254,4 +214,47 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
|
|||||||
return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest)
|
return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest)
|
||||||
.feeCharges(feeCharges).penaltyCharges(penaltyCharges);
|
.feeCharges(feeCharges).penaltyCharges(penaltyCharges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Loan getLoan(List<LoanTransaction> loanTransactions, List<LoanRepaymentScheduleInstallment> installments,
|
||||||
|
Set<LoanCharge> charges) {
|
||||||
|
if (!ObjectUtils.isEmpty(loanTransactions)) {
|
||||||
|
return loanTransactions.getFirst().getLoan();
|
||||||
|
} else if (!ObjectUtils.isEmpty(installments)) {
|
||||||
|
return installments.getFirst().getLoan();
|
||||||
|
} else if (!ObjectUtils.isEmpty(charges)) {
|
||||||
|
return charges.iterator().next().getLoan();
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("No loan found for the given transactions, installments or charges");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChangedTransactionDetail processLatestTransactionProgressiveInterestRecalculation(
|
||||||
|
AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) {
|
||||||
|
Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan,
|
||||||
|
loanTransaction.getTransactionDate());
|
||||||
|
ProgressiveLoanInterestScheduleModel model = savedModel
|
||||||
|
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
|
||||||
|
|
||||||
|
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
|
||||||
|
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
|
||||||
|
new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan));
|
||||||
|
progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
|
||||||
|
progressiveContext.setChargedOff(loan.isChargedOff());
|
||||||
|
progressiveContext.setWrittenOff(loan.isClosedWrittenOff());
|
||||||
|
progressiveContext.setContractTerminated(loan.isContractTermination());
|
||||||
|
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
|
||||||
|
if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
|
||||||
|
modelRepository.writeInterestScheduleModel(loan, model);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Money getTotalRefundInterestAmount(Loan loan) {
|
||||||
|
List<LoanTransactionType> supportedInterestRefundTransactionTypes = loan.getSupportedInterestRefundTransactionTypes();
|
||||||
|
if (supportedInterestRefundTransactionTypes != null && supportedInterestRefundTransactionTypes.isEmpty()) {
|
||||||
|
return Money.zero(loan.getCurrency());
|
||||||
|
}
|
||||||
|
return loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed).filter(LoanTransaction::isInterestRefund)
|
||||||
|
.map(t -> t.getAmount(loan.getCurrency())).reduce(Money.zero(loan.getCurrency()), Money::add);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -18,11 +18,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
|
import jakarta.persistence.FlushModeType;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.annotation.WithFlushMode;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
|
||||||
@@ -50,6 +52,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@WithFlushMode(FlushModeType.COMMIT)
|
||||||
public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransactionsService {
|
public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransactionsService {
|
||||||
|
|
||||||
private final LoanAccountService loanAccountService;
|
private final LoanAccountService loanAccountService;
|
||||||
|
|||||||
+421
@@ -18,6 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.integrationtests;
|
package org.apache.fineract.integrationtests;
|
||||||
|
|
||||||
|
import static com.jayway.jsonpath.internal.path.PathCompiler.fail;
|
||||||
|
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||||
import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.DEFAULT_STRATEGY;
|
import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.DEFAULT_STRATEGY;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
@@ -25,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
import io.restassured.builder.RequestSpecBuilder;
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
import io.restassured.builder.ResponseSpecBuilder;
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
import io.restassured.http.ContentType;
|
import io.restassured.http.ContentType;
|
||||||
@@ -35,6 +38,7 @@ import java.time.LocalDate;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
|
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
|
||||||
import org.apache.fineract.batch.domain.BatchRequest;
|
import org.apache.fineract.batch.domain.BatchRequest;
|
||||||
@@ -95,6 +99,7 @@ public class LoanTransactionInterestPaymentWaiverTest extends BaseLoanIntegratio
|
|||||||
private static PostClientsResponse client;
|
private static PostClientsResponse client;
|
||||||
private static LoanRescheduleRequestHelper loanRescheduleRequestHelper;
|
private static LoanRescheduleRequestHelper loanRescheduleRequestHelper;
|
||||||
private static ChargesHelper chargesHelper;
|
private static ChargesHelper chargesHelper;
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void setup() {
|
public static void setup() {
|
||||||
@@ -1617,6 +1622,422 @@ public class LoanTransactionInterestPaymentWaiverTest extends BaseLoanIntegratio
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInterestPaymentWaiverBatchExternalIdOnChargedOffLoan() {
|
||||||
|
Long[] loanIdContainer = new Long[1];
|
||||||
|
String[] loanExternalIdContainer = new String[1];
|
||||||
|
|
||||||
|
runAt("01 January 2025", () -> {
|
||||||
|
PostLoanProductsRequest loanProductRequest = create4IProgressiveWithChargeOffBehaviour();
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductRequest);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
assertNotNull(loanProductId);
|
||||||
|
|
||||||
|
PostClientsResponse clientResponse = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
Long clientId = clientResponse.getClientId();
|
||||||
|
assertNotNull(clientId);
|
||||||
|
|
||||||
|
String loanExternalId = UUID.randomUUID().toString();
|
||||||
|
Long createdLoanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2022", 1500.0, 3,
|
||||||
|
req -> req.numberOfRepayments(3).loanTermFrequency(3).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(9.99))
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).externalId(loanExternalId)
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||||
|
disburseLoan(createdLoanId, BigDecimal.valueOf(1500.0), "01 January 2022");
|
||||||
|
|
||||||
|
Long chargeOffTransactionId = chargeOffLoan(createdLoanId, "15 June 2022");
|
||||||
|
assertNotNull(chargeOffTransactionId);
|
||||||
|
|
||||||
|
loanIdContainer[0] = createdLoanId;
|
||||||
|
loanExternalIdContainer[0] = loanExternalId;
|
||||||
|
});
|
||||||
|
|
||||||
|
Long loanId = loanIdContainer[0];
|
||||||
|
String loanExternalId = loanExternalIdContainer[0];
|
||||||
|
|
||||||
|
runAt("01 January 2025", () -> {
|
||||||
|
String transactionExternalId = UUID.randomUUID().toString();
|
||||||
|
LocalDate waiverDate = LocalDate.of(2022, 9, 24);
|
||||||
|
BigDecimal waiverAmount = new BigDecimal("46.56");
|
||||||
|
|
||||||
|
String waiverBodyJson = GSON.toJson(Map.of("transactionDate", waiverDate.toString(), "dateFormat", "yyyy-MM-dd", "locale",
|
||||||
|
"de_DE", "transactionAmount", waiverAmount.toString(), "externalId", transactionExternalId));
|
||||||
|
|
||||||
|
BatchRequest waiverRequest = new BatchRequest();
|
||||||
|
waiverRequest.setRequestId(1L);
|
||||||
|
waiverRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions?command=interestPaymentWaiver");
|
||||||
|
waiverRequest.setMethod("POST");
|
||||||
|
waiverRequest.setBody(waiverBodyJson);
|
||||||
|
|
||||||
|
BatchRequest getRequest = new BatchRequest();
|
||||||
|
getRequest.setRequestId(2L);
|
||||||
|
getRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions/external-id/$.resourceExternalId");
|
||||||
|
getRequest.setMethod("GET");
|
||||||
|
getRequest.setReference(1L);
|
||||||
|
|
||||||
|
List<BatchRequest> batchRequests = new ArrayList<>();
|
||||||
|
batchRequests.add(waiverRequest);
|
||||||
|
batchRequests.add(getRequest);
|
||||||
|
|
||||||
|
List<BatchResponse> responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec,
|
||||||
|
BatchHelper.toJsonString(batchRequests));
|
||||||
|
|
||||||
|
assertEquals(2, responses.size());
|
||||||
|
|
||||||
|
BatchResponse waiverResponse = responses.get(0);
|
||||||
|
assertEquals(200, waiverResponse.getStatusCode());
|
||||||
|
assertNotNull(waiverResponse.getBody());
|
||||||
|
|
||||||
|
Map<String, Object> waiverResponseBody = GSON.fromJson(waiverResponse.getBody(), Map.class);
|
||||||
|
Object resourceExternalId = waiverResponseBody.get("resourceExternalId");
|
||||||
|
|
||||||
|
BatchResponse getResponse = responses.get(1);
|
||||||
|
|
||||||
|
if (resourceExternalId == null) {
|
||||||
|
fail("POST response missing resourceExternalId field. GET Response: " + getResponse.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getResponse.getStatusCode() != 200) {
|
||||||
|
fail(String.format(
|
||||||
|
"GET transaction by external ID failed. Status: %d, Expected externalId: %s, "
|
||||||
|
+ "Actual resourceExternalId: %s, GET Response: %s",
|
||||||
|
getResponse.getStatusCode(), transactionExternalId, resourceExternalId, getResponse.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(getResponse.getBody());
|
||||||
|
Map<String, Object> getResponseBody = GSON.fromJson(getResponse.getBody(), Map.class);
|
||||||
|
Object retrievedExternalId = getResponseBody.get("externalId");
|
||||||
|
assertEquals(transactionExternalId, retrievedExternalId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test case that reproduces backdated charge-off followed by backdated interest waiver.
|
||||||
|
*
|
||||||
|
* This is the CRITICAL scenario from production: "backbook migrations" where transactions are created TODAY but
|
||||||
|
* with backdated transaction dates. This triggers reverse-replays and reprocessing that causes the external ID
|
||||||
|
* clearing bug.
|
||||||
|
*
|
||||||
|
* Key difference from forward-dated scenario: - All transactions created in PRESENT (today) - But with PAST
|
||||||
|
* transaction dates (backdated) - This triggers different reprocessing logic - Charge-off creates missing accruals
|
||||||
|
* → config query → premature flush
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testInterestPaymentWaiverBackbookBatchExternalId() {
|
||||||
|
runAt("01 January 2025", () -> {
|
||||||
|
PostLoanProductsRequest loanProductRequest = create4IProgressiveWithChargeOffBehaviour();
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductRequest);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
assertNotNull(loanProductId);
|
||||||
|
|
||||||
|
PostClientsResponse clientResponse = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
Long clientId = clientResponse.getClientId();
|
||||||
|
assertNotNull(clientId);
|
||||||
|
|
||||||
|
String loanExternalId = UUID.randomUUID().toString();
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "18 January 2022", 431.98, 3,
|
||||||
|
req -> req.numberOfRepayments(3).loanTermFrequency(3).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(9.99))
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).externalId(loanExternalId)
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(431.98), "18 January 2022");
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment("28 February 2022", 19.83f, loanId.intValue());
|
||||||
|
PostLoansLoanIdTransactionsResponse txn2 = loanTransactionHelper.makeLoanRepayment("18 March 2022", 19.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn2.getResourceId().intValue(), "18 March 2022");
|
||||||
|
|
||||||
|
Long chargeOffTxnId = chargeOffLoan(loanId, "16 September 2022");
|
||||||
|
assertNotNull(chargeOffTxnId);
|
||||||
|
|
||||||
|
String transactionExternalId = UUID.randomUUID().toString();
|
||||||
|
LocalDate waiverDate = LocalDate.of(2022, 9, 24);
|
||||||
|
BigDecimal waiverAmount = new BigDecimal("46.56");
|
||||||
|
|
||||||
|
String waiverBodyJson = GSON.toJson(Map.of("transactionDate", waiverDate.toString(), "dateFormat", "yyyy-MM-dd", "locale",
|
||||||
|
"de_DE", "transactionAmount", waiverAmount.toString(), "externalId", transactionExternalId));
|
||||||
|
|
||||||
|
BatchRequest waiverRequest = new BatchRequest();
|
||||||
|
waiverRequest.setRequestId(1L);
|
||||||
|
waiverRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions?command=interestPaymentWaiver");
|
||||||
|
waiverRequest.setMethod("POST");
|
||||||
|
waiverRequest.setBody(waiverBodyJson);
|
||||||
|
|
||||||
|
BatchRequest getRequest = new BatchRequest();
|
||||||
|
getRequest.setRequestId(2L);
|
||||||
|
getRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions/external-id/$.resourceExternalId");
|
||||||
|
getRequest.setMethod("GET");
|
||||||
|
getRequest.setReference(1L);
|
||||||
|
|
||||||
|
List<BatchRequest> batchRequests = new ArrayList<>();
|
||||||
|
batchRequests.add(waiverRequest);
|
||||||
|
batchRequests.add(getRequest);
|
||||||
|
|
||||||
|
List<BatchResponse> responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec,
|
||||||
|
BatchHelper.toJsonString(batchRequests));
|
||||||
|
|
||||||
|
assertEquals(2, responses.size());
|
||||||
|
|
||||||
|
BatchResponse waiverResponse = responses.get(0);
|
||||||
|
assertEquals(200, waiverResponse.getStatusCode());
|
||||||
|
assertNotNull(waiverResponse.getBody());
|
||||||
|
|
||||||
|
Map<String, Object> waiverResponseBody = GSON.fromJson(waiverResponse.getBody(), Map.class);
|
||||||
|
Object resourceExternalId = waiverResponseBody.get("resourceExternalId");
|
||||||
|
|
||||||
|
BatchResponse getResponse = responses.get(1);
|
||||||
|
|
||||||
|
if (resourceExternalId == null) {
|
||||||
|
fail("POST response missing resourceExternalId with backbook scenario. GET Response: " + getResponse.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getResponse.getStatusCode() != 200) {
|
||||||
|
fail(String.format("GET failed. Status: %d, Expected externalId: %s, Actual resourceExternalId: %s, GET Response: %s",
|
||||||
|
getResponse.getStatusCode(), transactionExternalId, resourceExternalId, getResponse.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(getResponse.getBody());
|
||||||
|
Map<String, Object> getResponseBody = GSON.fromJson(getResponse.getBody(), Map.class);
|
||||||
|
Object retrievedExternalId = getResponseBody.get("externalId");
|
||||||
|
assertEquals(transactionExternalId, retrievedExternalId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInterestPaymentWaiverComplexTransactionHistoryBatchExternalId() {
|
||||||
|
Long[] loanIdContainer = new Long[1];
|
||||||
|
String[] loanExternalIdContainer = new String[1];
|
||||||
|
|
||||||
|
runAt("18 January 2022", () -> {
|
||||||
|
PostLoanProductsRequest loanProductRequest = create4IProgressiveWithChargeOffBehaviour();
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductRequest);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
assertNotNull(loanProductId);
|
||||||
|
|
||||||
|
PostClientsResponse clientResponse = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
Long clientId = clientResponse.getClientId();
|
||||||
|
assertNotNull(clientId);
|
||||||
|
|
||||||
|
String loanExternalId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
Long createdLoanId = applyAndApproveLoan(clientId, loanProductId, "18 January 2022", 431.98, 3,
|
||||||
|
req -> req.numberOfRepayments(3).loanTermFrequency(3).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(9.99))
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).externalId(loanExternalId)
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||||
|
disburseLoan(createdLoanId, BigDecimal.valueOf(431.98), "18 January 2022");
|
||||||
|
loanIdContainer[0] = createdLoanId;
|
||||||
|
loanExternalIdContainer[0] = loanExternalId;
|
||||||
|
});
|
||||||
|
|
||||||
|
Long loanId = loanIdContainer[0];
|
||||||
|
String loanExternalId = loanExternalIdContainer[0];
|
||||||
|
|
||||||
|
runAt("28 February 2022", () -> {
|
||||||
|
loanTransactionHelper.makeLoanRepayment("28 February 2022", 19.83f, loanId.intValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("18 March 2022", () -> {
|
||||||
|
PostLoansLoanIdTransactionsResponse txn = loanTransactionHelper.makeLoanRepayment("18 March 2022", 19.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn.getResourceId().intValue(), "18 March 2022");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("31 March 2022", () -> {
|
||||||
|
PostLoansLoanIdTransactionsResponse txn = loanTransactionHelper.makeLoanRepayment("31 March 2022", 19.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn.getResourceId().intValue(), "31 March 2022");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("18 April 2022", () -> {
|
||||||
|
PostLoansLoanIdTransactionsResponse txn = loanTransactionHelper.makeLoanRepayment("18 April 2022", 39.66f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn.getResourceId().intValue(), "18 April 2022");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("16 September 2022", () -> {
|
||||||
|
Long chargeOffTransactionId = chargeOffLoan(loanId, "16 September 2022");
|
||||||
|
assertNotNull(chargeOffTransactionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("24 September 2022", () -> {
|
||||||
|
String transactionExternalId = UUID.randomUUID().toString();
|
||||||
|
LocalDate waiverDate = LocalDate.of(2022, 9, 24);
|
||||||
|
BigDecimal waiverAmount = new BigDecimal("46.56");
|
||||||
|
|
||||||
|
String waiverBodyJson = GSON.toJson(Map.of("transactionDate", waiverDate.toString(), "dateFormat", "yyyy-MM-dd", "locale",
|
||||||
|
"de_DE", "transactionAmount", waiverAmount.toString(), "externalId", transactionExternalId));
|
||||||
|
|
||||||
|
BatchRequest waiverRequest = new BatchRequest();
|
||||||
|
waiverRequest.setRequestId(1L);
|
||||||
|
waiverRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions?command=interestPaymentWaiver");
|
||||||
|
waiverRequest.setMethod("POST");
|
||||||
|
waiverRequest.setBody(waiverBodyJson);
|
||||||
|
|
||||||
|
BatchRequest getRequest = new BatchRequest();
|
||||||
|
getRequest.setRequestId(2L);
|
||||||
|
getRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions/external-id/$.resourceExternalId");
|
||||||
|
getRequest.setMethod("GET");
|
||||||
|
getRequest.setReference(1L);
|
||||||
|
|
||||||
|
List<BatchRequest> batchRequests = new ArrayList<>();
|
||||||
|
batchRequests.add(waiverRequest);
|
||||||
|
batchRequests.add(getRequest);
|
||||||
|
|
||||||
|
List<BatchResponse> responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec,
|
||||||
|
BatchHelper.toJsonString(batchRequests));
|
||||||
|
|
||||||
|
assertEquals(2, responses.size());
|
||||||
|
|
||||||
|
BatchResponse waiverResponse = responses.get(0);
|
||||||
|
assertEquals(200, waiverResponse.getStatusCode());
|
||||||
|
assertNotNull(waiverResponse.getBody());
|
||||||
|
|
||||||
|
Map<String, Object> waiverResponseBody = GSON.fromJson(waiverResponse.getBody(), Map.class);
|
||||||
|
Object resourceExternalId = waiverResponseBody.get("resourceExternalId");
|
||||||
|
|
||||||
|
BatchResponse getResponse = responses.get(1);
|
||||||
|
|
||||||
|
if (resourceExternalId == null) {
|
||||||
|
fail("POST response missing resourceExternalId with complex scenario. GET Response: " + getResponse.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getResponse.getStatusCode() != 200) {
|
||||||
|
fail(String.format("GET failed. Status: %d, Expected externalId: %s, Actual resourceExternalId: %s, GET Response: %s",
|
||||||
|
getResponse.getStatusCode(), transactionExternalId, resourceExternalId, getResponse.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(getResponse.getBody());
|
||||||
|
Map<String, Object> getResponseBody = GSON.fromJson(getResponse.getBody(), Map.class);
|
||||||
|
Object retrievedExternalId = getResponseBody.get("externalId");
|
||||||
|
assertEquals(transactionExternalId, retrievedExternalId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInterestPaymentWaiverProductionScenarioBatchExternalId() {
|
||||||
|
runAt("01 January 2025", () -> {
|
||||||
|
PostLoanProductsRequest loanProductRequest = create4IProgressiveWithChargeOffBehaviour();
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductRequest);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
assertNotNull(loanProductId);
|
||||||
|
|
||||||
|
PostClientsResponse clientResponse = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
Long clientId = clientResponse.getClientId();
|
||||||
|
assertNotNull(clientId);
|
||||||
|
|
||||||
|
String loanExternalId = UUID.randomUUID().toString();
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "18 January 2022", 431.98, 3,
|
||||||
|
req -> req.numberOfRepayments(3).loanTermFrequency(3).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(9.99))
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).externalId(loanExternalId)
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(431.98), "18 January 2022");
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment("28 February 2022", 19.83f, loanId.intValue());
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn2 = loanTransactionHelper.makeLoanRepayment("18 March 2022", 19.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn2.getResourceId().intValue(), "18 March 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn3 = loanTransactionHelper.makeLoanRepayment("31 March 2022", 19.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn3.getResourceId().intValue(), "31 March 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn4 = loanTransactionHelper.makeLoanRepayment("18 April 2022", 39.66f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn4.getResourceId().intValue(), "18 April 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn5 = loanTransactionHelper.makeLoanRepayment("18 May 2022", 59.49f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn5.getResourceId().intValue(), "18 May 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn6 = loanTransactionHelper.makeLoanRepayment("18 June 2022", 64.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn6.getResourceId().intValue(), "18 June 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn7 = loanTransactionHelper.makeLoanRepayment("18 July 2022", 65.32f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn7.getResourceId().intValue(), "18 July 2022");
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse txn8 = loanTransactionHelper.makeLoanRepayment("18 August 2022", 65.83f, loanId.intValue());
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), txn8.getResourceId().intValue(), "18 August 2022");
|
||||||
|
|
||||||
|
Long chargeOffTxnId = chargeOffLoan(loanId, "16 September 2022");
|
||||||
|
assertNotNull(chargeOffTxnId);
|
||||||
|
|
||||||
|
String transactionExternalId = UUID.randomUUID().toString();
|
||||||
|
LocalDate waiverDate = LocalDate.of(2022, 9, 24);
|
||||||
|
String waiverAmount = "46,56";
|
||||||
|
|
||||||
|
String waiverBodyJson = GSON.toJson(Map.of("transactionDate", waiverDate.toString(), "dateFormat", "yyyy-MM-dd", "locale",
|
||||||
|
"de_DE", "transactionAmount", waiverAmount, "externalId", transactionExternalId));
|
||||||
|
|
||||||
|
BatchRequest waiverRequest = new BatchRequest();
|
||||||
|
waiverRequest.setRequestId(1L);
|
||||||
|
waiverRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions?command=interestPaymentWaiver");
|
||||||
|
waiverRequest.setMethod("POST");
|
||||||
|
waiverRequest.setBody(waiverBodyJson);
|
||||||
|
|
||||||
|
BatchRequest getRequest = new BatchRequest();
|
||||||
|
getRequest.setRequestId(2L);
|
||||||
|
getRequest.setRelativeUrl("loans/external-id/" + loanExternalId + "/transactions/external-id/$.resourceExternalId");
|
||||||
|
getRequest.setMethod("GET");
|
||||||
|
getRequest.setReference(1L);
|
||||||
|
|
||||||
|
List<BatchRequest> batchRequests = new ArrayList<>();
|
||||||
|
batchRequests.add(waiverRequest);
|
||||||
|
batchRequests.add(getRequest);
|
||||||
|
|
||||||
|
List<BatchResponse> responses = BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec,
|
||||||
|
BatchHelper.toJsonString(batchRequests));
|
||||||
|
|
||||||
|
if (responses.size() != 2) {
|
||||||
|
fail("Batch API returned " + responses.size() + " responses instead of 2.");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(2, responses.size());
|
||||||
|
|
||||||
|
BatchResponse waiverResponse = responses.get(0);
|
||||||
|
assertEquals(200, waiverResponse.getStatusCode());
|
||||||
|
|
||||||
|
Map<String, Object> waiverResponseBody = GSON.fromJson(waiverResponse.getBody(), Map.class);
|
||||||
|
Object resourceExternalId = waiverResponseBody.get("resourceExternalId");
|
||||||
|
|
||||||
|
if (resourceExternalId == null) {
|
||||||
|
fail("POST response missing resourceExternalId with production scenario.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchResponse getResponse = responses.get(1);
|
||||||
|
if (getResponse.getStatusCode() != 200) {
|
||||||
|
fail(String.format("GET failed. Status: %d, Expected externalId: %s, Actual resourceExternalId: %s, GET Response: %s",
|
||||||
|
getResponse.getStatusCode(), transactionExternalId, resourceExternalId, getResponse.getBody()));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(getResponse.getBody());
|
||||||
|
Map<String, Object> getResponseBody = GSON.fromJson(getResponse.getBody(), Map.class);
|
||||||
|
Object retrievedExternalId = getResponseBody.get("externalId");
|
||||||
|
assertEquals(transactionExternalId, retrievedExternalId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private PostLoanProductsRequest create4IProgressiveWithChargeOffBehaviour() {
|
||||||
|
return create4IProgressive().principal(1500.0) // Production uses 1500, not 1000
|
||||||
|
.minPrincipal(1.0) // Production min
|
||||||
|
.maxPrincipal(10000.0) // Keep same
|
||||||
|
.numberOfRepayments(3) // Production uses 3, not 4
|
||||||
|
.minNumberOfRepayments(3) // Production min
|
||||||
|
.maxNumberOfRepayments(24) // Production max
|
||||||
|
.daysInMonthType(1) // ACTUAL, not 30 - matches production
|
||||||
|
.daysInYearType(1) // ACTUAL, not 360 - matches production
|
||||||
|
.enableAccrualActivityPosting(true) // CRITICAL: enables accrual transaction generation
|
||||||
|
.chargeOffBehaviour("ZERO_INTEREST").enableInstallmentLevelDelinquency(true).interestRecognitionOnDisbursementDate(true)
|
||||||
|
.daysInYearCustomStrategy(DaysInYearCustomStrategy.FEB_29_PERIOD_ONLY).disallowInterestCalculationOnPastDue(true)
|
||||||
|
.supportedInterestRefundTypes(List.of("MERCHANT_ISSUED_REFUND", "PAYOUT_REFUND"))
|
||||||
|
.paymentAllocation(List.of(createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"),
|
||||||
|
createPaymentAllocation("REPAYMENT", "NEXT_INSTALLMENT"),
|
||||||
|
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "LAST_INSTALLMENT"),
|
||||||
|
createPaymentAllocation("PAYOUT_REFUND", "LAST_INSTALLMENT"),
|
||||||
|
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"),
|
||||||
|
createPaymentAllocation("INTEREST_PAYMENT_WAIVER", "NEXT_INSTALLMENT")));
|
||||||
|
}
|
||||||
|
|
||||||
private void chargeFee(Long loanId, Double amount, String dueDate) {
|
private void chargeFee(Long loanId, Double amount, String dueDate) {
|
||||||
PostChargesResponse feeCharge = chargesHelper.createCharges(new ChargeRequest().penalty(false).amount(9.0)
|
PostChargesResponse feeCharge = chargesHelper.createCharges(new ChargeRequest().penalty(false).amount(9.0)
|
||||||
.chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
|
.chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
|
||||||
|
|||||||
Reference in New Issue
Block a user