Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,24 @@ public CommandWrapperBuilder discountFeeAdjustmentWorkingCapitalLoanTransaction(
return this;
}

public CommandWrapperBuilder discountFeeWorkingCapitalLoanTransaction(final Long loanId, final Long transactionId) {
this.actionName = ACTION_DISCOUNTFEE;
this.entityName = ENTITY_WORKINGCAPITALLOAN;
this.entityId = transactionId;
this.loanId = loanId;
this.href = "/working-capital-loans/" + loanId + "/transactions/" + transactionId + "?command=discountFee";
return this;
}

public CommandWrapperBuilder discountFeeAdjustmentWorkingCapitalLoanTransaction(final Long loanId, final Long transactionId) {
this.actionName = ACTION_DISCOUNTFEEADJUSTMENT;
this.entityName = ENTITY_WORKINGCAPITALLOAN;
this.entityId = transactionId;
this.loanId = loanId;
this.href = "/working-capital-loans/" + loanId + "/transactions/" + transactionId + "?command=discountFeeAdjustment";
return this;
}

public CommandWrapperBuilder undoWorkingCapitalLoanTransaction(final Long loanId, final Long transactionId) {
this.actionName = ACTION_UNDO;
this.entityName = ENTITY_WORKINGCAPITALLOANTRANSACTION;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The scope of this document includes:
A `DISCOUNT_FEE` transaction can be created in two ways:

* Automatically during disbursement when the resolved discount amount is greater than zero.
* Explicitly after disbursement using `POST /transactions?command=discountFee`, if no Discount Fee was created for the related disbursement.
* Explicitly after disbursement using `POST /transactions?command=discountFee`, or by naming the disbursement in the path (`/transactions/{transactionId}` or `/transactions/external-id/{transactionExternalId}`), if no Discount Fee was created for the related disbursement.

Each Discount Fee transaction is linked to its disbursement transaction through `m_wc_loan_transaction_relation` with relation type `RELATED`.

Expand Down Expand Up @@ -63,7 +63,7 @@ A `DISCOUNT_FEE_ADJUSTMENT` transaction reduces the loan-level discount that was
=== Applicability

* Loan must be ACTIVE.
* `relatedResourceId` must reference an active, non-reversed `DISCOUNT_FEE` transaction.
* The related Discount Fee transaction (`relatedResourceId`, or the transaction in the path) must be an active, non-reversed `DISCOUNT_FEE` transaction.
* Multiple adjustments are allowed against the same Discount Fee transaction until the remaining adjustable amount reaches zero.

=== Transaction Behavior
Expand Down Expand Up @@ -101,7 +101,7 @@ On Discount Fee Adjustment:
| Adjustment is allowed only for ACTIVE loans

| Parent transaction
| `relatedResourceId` must reference a non-reversed Discount Fee transaction
| `relatedResourceId`, or the transaction in the path, must reference a non-reversed Discount Fee transaction
|===

=== Allocation
Expand Down Expand Up @@ -134,6 +134,10 @@ When disbursement is reversed:
----
POST /v1/working-capital-loans/{loanId}/transactions?command=discountFee
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions?command=discountFee
POST /v1/working-capital-loans/{loanId}/transactions/{transactionId}?command=discountFee
POST /v1/working-capital-loans/{loanId}/transactions/external-id/{transactionExternalId}?command=discountFee
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions/{transactionId}?command=discountFee
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions/external-id/{transactionExternalId}?command=discountFee
----

[source,json]
Expand All @@ -157,7 +161,7 @@ POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions?command
----

* `transactionAmount` is optional. If omitted, the service defaults to the loan's current discount.
* `relatedResourceId` is mandatory and must be the related disbursement transaction ID.
* On `/transactions?command=discountFee`, `relatedResourceId` is mandatory and must be the related disbursement transaction ID. On `/transactions/{transactionId}` and `/transactions/external-id/{transactionExternalId}` the path names the disbursement and `relatedResourceId` is not accepted. An unknown transaction external id, or one that belongs to another loan, returns 404.
* `classificationId` is optional and must reference `working_capital_loan_discount_fee_classification` when provided.

=== Create Discount Fee Adjustment
Expand All @@ -166,6 +170,10 @@ POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions?command
----
POST /v1/working-capital-loans/{loanId}/transactions?command=discountFeeAdjustment
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions?command=discountFeeAdjustment
POST /v1/working-capital-loans/{loanId}/transactions/{transactionId}?command=discountFeeAdjustment
POST /v1/working-capital-loans/{loanId}/transactions/external-id/{transactionExternalId}?command=discountFeeAdjustment
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions/{transactionId}?command=discountFeeAdjustment
POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions/external-id/{transactionExternalId}?command=discountFeeAdjustment
----

[source,json]
Expand All @@ -182,7 +190,7 @@ POST /v1/working-capital-loans/external-id/{loanExternalId}/transactions?command
}
----

* `relatedResourceId` is mandatory and must be the Discount Fee transaction ID.
* On `/transactions?command=discountFeeAdjustment`, `relatedResourceId` is mandatory and must be the Discount Fee transaction ID. On `/transactions/{transactionId}` and `/transactions/external-id/{transactionExternalId}` the path names the Discount Fee transaction and `relatedResourceId` is not accepted. An unknown transaction external id, or one that belongs to another loan, returns 404.
* `transactionAmount` is mandatory and capped by the remaining adjustable discount.
* `classificationId` is optional and must reference `working_capital_loan_discount_fee_classification` when provided.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.fineract.client.models.DelinquencyBucketRequest;
import org.apache.fineract.client.models.DelinquencyBucketResponse;
import org.apache.fineract.client.models.DelinquencyRangeResponse;
import org.apache.fineract.client.models.ExecuteWorkingCapitalLoanTransactionCommandRequest;
import org.apache.fineract.client.models.MinimumPaymentPeriodAndRule;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostAllowAttributeOverrides;
Expand Down Expand Up @@ -325,6 +326,12 @@ public PostWorkingCapitalLoanTransactionsRequest defaultWorkingCapitalLoanRepaym
.locale(LOCALE_EN);
}

public ExecuteWorkingCapitalLoanTransactionCommandRequest defaultWorkingCapitalLoanTransactionCommandRequest() {
return new ExecuteWorkingCapitalLoanTransactionCommandRequest() //
.dateFormat(DATE_FORMAT) //
.locale(LOCALE_EN);
}

private Long getWCBreachIdByName(String breachName) {
try {
List<WorkingCapitalBreachData> breaches = fineractClient.workingCapitalBreaches().retrieveAllWorkingCapitalBreaches(Map.of());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.apache.fineract.client.models.Header;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostWorkingCapitalLoanTransactionsRequest;
import org.apache.fineract.client.models.PostWorkingCapitalLoanTransactionsResponse;
import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest;
import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdResponse;
import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest;
Expand Down Expand Up @@ -79,6 +80,7 @@ public class WorkingCapitalBatchApiStepDef extends AbstractStepDef {
private static final String COMMAND_REJECT = "?command=reject";
private static final String COMMAND_DISBURSE = "?command=disburse";
private static final String COMMAND_DISCOUNT = "?command=discountFee";
private static final String COMMAND_DISCOUNT_ADJUSTMENT = "?command=discountFeeAdjustment";
private static final String WCL_TRANSACTIONS_PATH = "/transactions";

private final FineractFeignClient fineractFeignClient;
Expand Down Expand Up @@ -231,6 +233,33 @@ public void batchApiAddDiscountFee(String amount) throws IOException {
testContext().set(TestContextKey.BATCH_API_CALL_RESPONSE, responses);
}

@When("Batch API adds discount fee with {string} amount referencing the disbursement external-id in relatedResourceId on the working capital loan")
public void batchApiAddDiscountFeeWithDisbursementExternalIdInRelatedResourceId(final String amount) throws IOException {
final PostWorkingCapitalLoansLoanIdResponse disburseResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE);
batchApiDiscountCommandWithStringRelatedResourceId(COMMAND_DISCOUNT, amount, disburseResponse.getResourceExternalId());
}

@When("Batch API adds discount fee adjustment with {string} amount referencing the discount fee external-id in relatedResourceId on the working capital loan")
public void batchApiAddDiscountFeeAdjustmentWithDiscountFeeExternalIdInRelatedResourceId(final String amount) throws IOException {
final List<BatchResponse> discountFeeResponses = testContext().get(TestContextKey.BATCH_API_CALL_RESPONSE);
final String discountFeeExternalId = fromJson(discountFeeResponses.getFirst().getBody(),
PostWorkingCapitalLoanTransactionsResponse.class).getResourceExternalId();
batchApiDiscountCommandWithStringRelatedResourceId(COMMAND_DISCOUNT_ADJUSTMENT, amount, discountFeeExternalId);
}

private void batchApiDiscountCommandWithStringRelatedResourceId(final String command, final String amount,
final String relatedResourceId) throws IOException {
final PostWorkingCapitalLoanTransactionsRequest request = new PostWorkingCapitalLoanTransactionsRequest()
.transactionDate("01 January 2026").transactionAmount(new BigDecimal(amount))
.locale(WorkingCapitalLoanRequestFactory.DEFAULT_LOCALE).dateFormat(WorkingCapitalLoanRequestFactory.DATE_FORMAT);
final String serialized = GSON.toJson(request);
final String body = serialized.substring(0, serialized.lastIndexOf('}')) + ",\"relatedResourceId\":\"" + relatedResourceId + "\"}";
final String url = resolveLoanUrlForGet() + WCL_TRANSACTIONS_PATH + command;
final BatchRequest batchRequest = buildBatchRequest(1L, null, url, BATCH_API_METHOD_POST, body);
final List<BatchResponse> responses = handleBatchRequests(List.of(batchRequest), false);
testContext().set(TestContextKey.BATCH_API_CALL_RESPONSE, responses);
}

@When("Batch API fetches working capital loan details by loan ID")
public void batchApiGetWCLoanById() throws IOException {
final BatchRequest batchRequest = buildBatchRequest(1L, null, resolveLoanUrlForGet(), BATCH_API_METHOD_GET, BODY_GET_REQUEST);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,32 @@ public void addDiscountFeeWCLoanDisbursementWithRandomExternalId(final String di
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE, response);
}

@When("Admin adds Discount fee with {string} amount, a random externalId, {string} classification and {string} payment type by loan and disbursement external-ids on Working Capital loan account")
public void addDiscountFeeByLoanAndDisbursementExternalIds(final String discountAmount, final String classificationCodeValueName,
final String paymentTypeName) {
final PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE);
Assertions.assertNotNull(lastDisbursementResponse);
final PostWorkingCapitalLoansLoanIdRequest lastDisbursementRequest = testContext().get(TestContextKey.LOAN_DISBURSE_REQUEST);
final String randomExternalId = Utils.randomStringGenerator("TestDiscountFeeExtId_", 10);
testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED, randomExternalId);

final String loanExternalId = retrieveLoanExternalId(getCreatedLoanId());
final Long classificationId = getClassificationCodeValueId(CodeNames.WORKING_CAPITAL_DISCOUNT_FEE_CLASSIFICATION.getValue(),
classificationCodeValueName);
final long paymentTypeId = paymentTypeResolver.resolve(DefaultPaymentType.valueOf(paymentTypeName));
final ExecuteWorkingCapitalLoanTransactionCommandRequest request = workingCapitalProductRequestFactory
.defaultWorkingCapitalLoanTransactionCommandRequest().transactionDate(lastDisbursementRequest.getActualDisbursementDate())
.transactionAmount(new BigDecimal(discountAmount)).note("Discount applied").externalId(randomExternalId)
.classificationId(classificationId)
.paymentDetails(new PostWorkingCapitalLoanTransactionsPaymentDetailRequest().paymentTypeId(paymentTypeId));

ok(() -> fineractClient.workingCapitalLoanTransactions()
.executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionExternalId(loanExternalId,
lastDisbursementResponse.getResourceExternalId(), "discountFee", request));
rememberLastWorkingCapitalTransaction(TransactionType.DISCOUNT_FEE.getValue(), lastDisbursementRequest.getActualDisbursementDate(),
request.getTransactionAmount());
}

@Then("Adding Discount fee with {string} amount reusing the previously shared externalId on Working Capital loan account for last disbursement results an error with the following data:")
public void addDiscountFeeReusingSharedExternalIdResultsAnError(final String discountAmount, final DataTable table) {
final PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE);
Expand Down Expand Up @@ -2225,6 +2251,17 @@ public void addDiscountFeeAdjustmentWCLoan(final String adjustmentAmount) {
executeDiscountFeeAdjustmentById(getCreatedLoanId(), request);
}

@When("Admin adds Discount fee adjustment with {string} amount by loan and discount fee external-ids on Working Capital loan account")
public void addDiscountFeeAdjustmentByLoanAndDiscountFeeExternalIds(final String adjustmentAmount) {
final String loanExternalId = retrieveLoanExternalId(getCreatedLoanId());
final ExecuteWorkingCapitalLoanTransactionCommandRequest request = workingCapitalProductRequestFactory
.defaultWorkingCapitalLoanTransactionCommandRequest().transactionAmount(new BigDecimal(adjustmentAmount));

ok(() -> fineractClient.workingCapitalLoanTransactions()
.executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionExternalId(loanExternalId,
latestActiveTransactionOfType(TransactionType.DISCOUNT_FEE).getExternalId(), "discountFeeAdjustment", request));
}

@And("Admin adds Discount fee adjustment with {string} amount on transaction date {string} on Working Capital loan account for last discount")
public void addDiscountFeeAdjustmentWCLoanWithTransactionDate(final String adjustmentAmount, final String transactionDate) {
final PostWorkingCapitalLoanTransactionsResponse lastDiscountResponse = testContext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ Feature: Working Capital Batch API
| WCLP_DISCOUNT | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 |
And Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026"
And Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount
When Batch API adds discount fee with "12" amount referencing the disbursement external-id in relatedResourceId on the working capital loan
Then Verify that WCL step 1 throws an error with error code 400 and message "relatedResourceId.not.a.number"
When Batch API adds discount fee with "12" amount on the working capital loan
Then Admin checks that all steps result 200OK
When Batch API adds discount fee adjustment with "5" amount referencing the discount fee external-id in relatedResourceId on the working capital loan
Then Verify that WCL step 1 throws an error with error code 400 and message "relatedResourceId.not.a.number"
And Working Capital Loan has transactions:
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
| 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,33 @@ Feature: Working Capital Discount
Then In Working Capital Loan Transactions all transactions have non-blank external-id
Then Active Discount Fee transactions contain the user-generated externalId from DISCOUNTFEE

Scenario: Discount fee and its adjustment added by loan and transaction external ids, the discount fee externalId, classification and payment type are persisted as sent
When Admin sets the business date to "01 January 2026"
And Admin creates a client with random data
And Admin creates a working capital loan with the following data:
| LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount |
| WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | |
Then Working capital loan creation was successful
Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026"
Then Working capital loan approval was successful
Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount
Then Working Capital loan status will be "ACTIVE"
Then Verify Working Capital loan disbursement was successful
Then Admin adds Discount fee with "12" amount, a random externalId, "working_capital_loan_discount_fee_classification_value" classification and "AUTOPAY" payment type by loan and disbursement external-ids on Working Capital loan account
And Working Capital Loan has transactions:
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
| 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false |
| 01 January 2026 | Discount Fee | 12.0 | 12.0 | 0.0 | 0.0 | false |
Then Active Discount Fee transactions contain the user-generated externalId from DISCOUNTFEE
And Working Capital Loan has a "DISCOUNT_FEE" transaction with date "01 January 2026" which has classification code value "working_capital_loan_discount_fee_classification_value"
And Working Capital loan transaction with type "DISCOUNT_FEE" has payment type "AUTOPAY"
When Admin adds Discount fee adjustment with "5" amount by loan and discount fee external-ids on Working Capital loan account
Then Working Capital Loan has transactions:
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
| 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false |
| 01 January 2026 | Discount Fee | 12.0 | 12.0 | 0.0 | 0.0 | false |
| 01 January 2026 | Discount Fee Adjustment | 5.0 | 5.0 | 0.0 | 0.0 | false |

@TestRailId:C83041
Scenario: Discount provided during disbursement without externalId gets an auto-generated externalId
When Admin sets the business date to "01 January 2026"
Expand Down
Loading
Loading