Skip to content

Commit 81be71d

Browse files
committed
FINERACT-2015: Fix saving account with withdrawal fee
1 parent f5951f1 commit 81be71d

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-2
lines changed

fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,11 +1346,13 @@ private void payWithdrawalFee(final BigDecimal transactionAmount, final LocalDat
13461346

13471347
if (charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction to
13481348
// specific paymentType
1349-
if (paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) {
1349+
if (paymentDetail != null && paymentDetail.getPaymentType() != null
1350+
&& paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) {
13501351
resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, refNo);
13511352
}
13521353
} else if (charge.isEnablePaymentType()) { // normal charge-transaction to specific paymentType
1353-
if (paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) {
1354+
if (paymentDetail != null && paymentDetail.getPaymentType() != null
1355+
&& paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) {
13541356
charge.updateWithdralFeeAmount(transactionAmount);
13551357
this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, backdatedTxnsAllowedTill,
13561358
refNo);
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.integrationtests.savings;
20+
21+
import static io.restassured.RestAssured.given;
22+
23+
import java.math.BigDecimal;
24+
import org.apache.fineract.client.models.AccountTransferRequest;
25+
import org.apache.fineract.client.models.ChargeRequest;
26+
import org.apache.fineract.client.models.PaymentTypeRequest;
27+
import org.apache.fineract.client.models.PostChargesResponse;
28+
import org.apache.fineract.client.models.PostPaymentTypesResponse;
29+
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
30+
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesRequest;
31+
import org.apache.fineract.client.models.PostSavingsAccountsSavingsAccountIdChargesResponse;
32+
import org.apache.fineract.client.models.PostSavingsProductsRequest;
33+
import org.apache.fineract.client.models.PostSavingsProductsResponse;
34+
import org.apache.fineract.client.models.SavingsAccountData;
35+
import org.apache.fineract.integrationtests.common.ClientHelper;
36+
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
37+
import org.apache.fineract.integrationtests.common.Utils;
38+
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
39+
import org.apache.fineract.integrationtests.savings.base.BaseSavingsIntegrationTest;
40+
import org.junit.jupiter.api.Assertions;
41+
import org.junit.jupiter.api.Test;
42+
43+
public class AccountTransferWithdrawalFeeTest extends BaseSavingsIntegrationTest {
44+
45+
private static final String ACCOUNT_TRANSFER_AMOUNT = "15000.0";
46+
private static final String TRANSFER_DATE = "01 March 2013";
47+
48+
@Test
49+
public void testFromSavingsToSavingsAccountTransferWithWithdrawalFee() {
50+
// Create withdrawal fee charge (standard withdrawal fee without payment type binding)
51+
final ChargesHelper chargesHelper = new ChargesHelper();
52+
final PostChargesResponse withdrawalCharge = chargesHelper
53+
.createCharges(new ChargeRequest().active(true).name(Utils.uniqueRandomStringGenerator("Charge_Savings_", 6))
54+
.currencyCode("USD").amount(100.0d).chargeAppliesTo(2).chargeTimeType(5).chargeCalculationType(1).locale("en"));
55+
Assertions.assertNotNull(withdrawalCharge.getResourceId());
56+
57+
// Create clients
58+
final Long fromClientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
59+
final Long toClientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
60+
61+
// Create savings product with withdrawalFeeForTransfers enabled
62+
final PostSavingsProductsResponse savingsProduct = createProduct(savingsProduct());
63+
final Long productId = savingsProduct.getResourceId();
64+
65+
// Create FROM savings account and enable withdrawalFeeForTransfers on it
66+
final PostSavingsAccountsResponse fromSavingsResponse = applySavingsAccount(
67+
applySavingsRequest(fromClientId, productId, "01 January 2013"));
68+
final Long fromSavingsId = fromSavingsResponse.getSavingsId();
69+
enableWithdrawalFeeForTransfers(fromSavingsId);
70+
71+
approveSavingsAccount(fromSavingsId, "01 January 2013");
72+
activateSavingsAccount(fromSavingsId, "01 January 2013");
73+
deposit(fromSavingsId, "01 January 2013", BigDecimal.valueOf(30000));
74+
75+
// Add withdrawal fee charge to FROM savings account
76+
final PostSavingsAccountsSavingsAccountIdChargesResponse chargeResponse = ok(fineractClient().savingsAccountCharges
77+
.addSavingsAccountCharge(fromSavingsId, new PostSavingsAccountsSavingsAccountIdChargesRequest()
78+
.chargeId(withdrawalCharge.getResourceId()).amount(100.0f).locale("en")));
79+
Assertions.assertNotNull(chargeResponse.getResourceId());
80+
81+
// Create TO savings account
82+
final PostSavingsAccountsResponse toSavingsResponse = applySavingsAccount(
83+
applySavingsRequest(toClientId, productId, "01 January 2013"));
84+
final Long toSavingsId = toSavingsResponse.getSavingsId();
85+
86+
approveSavingsAccount(toSavingsId, "01 January 2013");
87+
activateSavingsAccount(toSavingsId, "01 January 2013");
88+
89+
// Perform transfer - without null-checks in SavingsAccount.payWithdrawalFee(),
90+
// this would trigger NPE because paymentDetail is null during account transfers
91+
ok(fineractClient().accountTransfers.create4(new AccountTransferRequest().fromClientId(String.valueOf(fromClientId))
92+
.fromAccountId(String.valueOf(fromSavingsId)).fromAccountType("2").fromOfficeId("1").toClientId(String.valueOf(toClientId))
93+
.toAccountId(String.valueOf(toSavingsId)).toAccountType("2").toOfficeId("1").transferDate(TRANSFER_DATE)
94+
.transferAmount(ACCOUNT_TRANSFER_AMOUNT).transferDescription("Transfer").dateFormat(DATETIME_PATTERN).locale("en_GB")));
95+
96+
// Verify balances: charge has no payment type binding, so it applies as normal withdrawal fee
97+
// from = 30000 - 15000 (transfer) - 100 (withdrawal fee) = 14900
98+
SavingsAccountData fromSavingsAccount = ok(fineractClient().savingsAccounts.retrieveOne26(fromSavingsId, false, null, "summary"));
99+
Assertions.assertEquals(BigDecimal.valueOf(14900).setScale(0), fromSavingsAccount.getSummary().getAccountBalance().setScale(0),
100+
"Verifying From Savings Account Balance after Account Transfer with Withdrawal Fee");
101+
102+
// to = 0 + 15000 (transfer) = 15000
103+
SavingsAccountData toSavingsAccount = ok(fineractClient().savingsAccounts.retrieveOne26(toSavingsId, false, null, "summary"));
104+
Assertions.assertEquals(BigDecimal.valueOf(15000).setScale(0), toSavingsAccount.getSummary().getAccountBalance().setScale(0),
105+
"Verifying To Savings Account Balance after Account Transfer");
106+
}
107+
108+
@Test
109+
public void testFromSavingsToSavingsAccountTransferWithPaymentTypeWithdrawalFee() {
110+
// Create Payment Type
111+
final PaymentTypeHelper paymentTypeHelper = new PaymentTypeHelper();
112+
final PostPaymentTypesResponse paymentTypeResponse = paymentTypeHelper
113+
.createPaymentType(new PaymentTypeRequest().name("Bank Transfer " + System.currentTimeMillis())
114+
.description("Payment via bank transfer").isCashPayment(false).position(1));
115+
final Long paymentTypeId = paymentTypeResponse.getResourceId();
116+
117+
// Create withdrawal fee charge WITH payment type binding
118+
final ChargesHelper chargesHelper = new ChargesHelper();
119+
final PostChargesResponse withdrawalCharge = chargesHelper.createCharges(new ChargeRequest().active(true)
120+
.name(Utils.uniqueRandomStringGenerator("Charge_Savings_", 6)).currencyCode("USD").amount(100.0d).chargeAppliesTo(2)
121+
.chargeTimeType(5).chargeCalculationType(1).locale("en").enablePaymentType(true).paymentTypeId(paymentTypeId));
122+
Assertions.assertNotNull(withdrawalCharge.getResourceId());
123+
124+
// Create clients
125+
final Long fromClientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
126+
final Long toClientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
127+
128+
// Create savings product
129+
final PostSavingsProductsResponse savingsProduct = createProduct(savingsProduct());
130+
final Long productId = savingsProduct.getResourceId();
131+
132+
// Create FROM savings account with withdrawal fee enabled for transfers
133+
final PostSavingsAccountsResponse fromSavingsResponse = applySavingsAccount(
134+
applySavingsRequest(fromClientId, productId, "01 January 2013"));
135+
final Long fromSavingsId = fromSavingsResponse.getSavingsId();
136+
enableWithdrawalFeeForTransfers(fromSavingsId);
137+
138+
approveSavingsAccount(fromSavingsId, "01 January 2013");
139+
activateSavingsAccount(fromSavingsId, "01 January 2013");
140+
deposit(fromSavingsId, "01 January 2013", BigDecimal.valueOf(30000));
141+
142+
// Add payment-type withdrawal fee charge to FROM savings account
143+
final PostSavingsAccountsSavingsAccountIdChargesResponse chargeResponse = ok(fineractClient().savingsAccountCharges
144+
.addSavingsAccountCharge(fromSavingsId, new PostSavingsAccountsSavingsAccountIdChargesRequest()
145+
.chargeId(withdrawalCharge.getResourceId()).amount(100.0f).locale("en")));
146+
Assertions.assertNotNull(chargeResponse.getResourceId());
147+
148+
// Create TO savings account
149+
final PostSavingsAccountsResponse toSavingsResponse = applySavingsAccount(
150+
applySavingsRequest(toClientId, productId, "01 January 2013"));
151+
final Long toSavingsId = toSavingsResponse.getSavingsId();
152+
153+
approveSavingsAccount(toSavingsId, "01 January 2013");
154+
activateSavingsAccount(toSavingsId, "01 January 2013");
155+
156+
// Perform transfer - without null-checks in SavingsAccount.payWithdrawalFee(),
157+
// this throws NPE because paymentDetail is null and code tries to call
158+
// paymentDetail.getPaymentType().getName()
159+
ok(fineractClient().accountTransfers.create4(new AccountTransferRequest().fromClientId(String.valueOf(fromClientId))
160+
.fromAccountId(String.valueOf(fromSavingsId)).fromAccountType("2").fromOfficeId("1").toClientId(String.valueOf(toClientId))
161+
.toAccountId(String.valueOf(toSavingsId)).toAccountType("2").toOfficeId("1").transferDate(TRANSFER_DATE)
162+
.transferAmount(ACCOUNT_TRANSFER_AMOUNT).transferDescription("Transfer").dateFormat(DATETIME_PATTERN).locale("en_GB")));
163+
164+
// The payment-type fee is not applied because paymentDetail is null during transfer
165+
// and the null-check skips the payment type matching
166+
// from = 30000 - 15000 = 15000
167+
SavingsAccountData fromSavingsAccount = ok(fineractClient().savingsAccounts.retrieveOne26(fromSavingsId, false, null, "summary"));
168+
Assertions.assertEquals(BigDecimal.valueOf(15000).setScale(0), fromSavingsAccount.getSummary().getAccountBalance().setScale(0),
169+
"Verifying From Savings Account Balance after Account Transfer with Payment Type Withdrawal Fee");
170+
171+
// to = 0 + 15000 = 15000
172+
SavingsAccountData toSavingsAccount = ok(fineractClient().savingsAccounts.retrieveOne26(toSavingsId, false, null, "summary"));
173+
Assertions.assertEquals(BigDecimal.valueOf(15000).setScale(0), toSavingsAccount.getSummary().getAccountBalance().setScale(0),
174+
"Verifying To Savings Account Balance after Account Transfer");
175+
}
176+
177+
private void enableWithdrawalFeeForTransfers(Long savingsId) {
178+
given().spec(requestSpec).body("{\"withdrawalFeeForTransfers\": true}").expect().spec(responseSpec).log().ifError().when()
179+
.put("/fineract-provider/api/v1/savingsaccounts/" + savingsId + "?" + Utils.TENANT_IDENTIFIER);
180+
}
181+
182+
private PostSavingsProductsRequest savingsProduct() {
183+
return new PostSavingsProductsRequest().locale("en").name(Utils.uniqueRandomStringGenerator("SAVINGS_PRODUCT_", 6))
184+
.shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Savings product for withdrawal fee test")
185+
.currencyCode("USD").digitsAfterDecimal(2).inMultiplesOf(0).nominalAnnualInterestRate(10.0)
186+
.interestCompoundingPeriodType(InterestPeriodType.DAILY).interestPostingPeriodType(InterestPeriodType.MONTHLY)
187+
.interestCalculationType(InterestCalculationType.DAILY_BALANCE).interestCalculationDaysInYearType(DaysInYearType.DAYS_365)
188+
.accountingRule(1).withdrawalFeeForTransfers(true).allowOverdraft(false).enforceMinRequiredBalance(false).withHoldTax(false)
189+
.isDormancyTrackingActive(false);
190+
}
191+
}

integration-tests/src/test/java/org/apache/fineract/integrationtests/savings/base/BaseSavingsIntegrationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ protected PostSavingsProductsRequest dailyInterestPostingProduct() {
115115
.description("Daily interest posting product") //
116116
.nominalAnnualInterestRate(10.0) //
117117
.digitsAfterDecimal(0) //
118+
.inMultiplesOf(0) //
118119
.currencyCode("EUR") //
119120
.accountingRule(1) // none
120121
.interestCalculationDaysInYearType(DaysInYearType.DAYS_365).interestCompoundingPeriodType(InterestPeriodType.DAILY)

0 commit comments

Comments
 (0)