[MIFOSX-1514] Refund for Active Loans

This commit is contained in:
Michael Duku-Kaakyire
2014-11-01 02:10:14 +02:00
parent c7cd28c340
commit ed03a3078a
51 changed files with 2657 additions and 26 deletions
+348 -1
View File
@@ -341,6 +341,22 @@
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>accounttransfers/templateRefundByTransfer</td>
<td></td>
<td><a href="#accounttransfers_retrieve_template_refund_by_transfer">Retrieve Refund of an Active Loan by Transfer Template</a></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>accounttransfers/refundByTransfer</td>
<td><a href="#accounttransfers_refund_by_transfer">Refund an active loan by transfer</a></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
@@ -1022,6 +1038,14 @@
<td></td>
</tr>
<tr>
<td></td>
<td>loans/{loanId}/transactions?command=refundByCash</td>
<td><a href="#loans_transaction_refund_by_cash">Refund an Active Loan by Cash</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#loans_charges">Loan Charges</a></td>
<td>loans/{loanId}/charges</td>
<td><a href="#loans_charges_create">Add a Loan Charge</a></td>
@@ -8538,6 +8562,11 @@ No Request Body:
outstanding. The amount that can be waived is limited to the
loan's inArrearsTolerance
</dd>
<dd>
<b>'refundbycash'</b><br> "date" is set to the current date <br>
"total" "amount" is set
to the total amount paid in advance.
</dd>
</dl>
<p>Example Request:</p>
<div class=apiClick>loans/1/transactions/template?command=repayment</div>
@@ -8624,6 +8653,45 @@ Request Body:
</code>
</div>
</div>
<a id="loans_transaction_refund_by_cash" name="loans_transaction_refund_by_cash" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Make a Refund of an Active Loan by Cash </h4>
</div>
<div class="method-example">
<code class="method-declaration">
POST https://DomainName/api/v1/loans/{loanId}/transactions?command=refundByCash
</code>
<code class="method-request">
POST loans/5/transactions?command=refundByCash
Content-Type: application/json
Request Body:
{
"dateFormat": "dd MMMM yyyy",
"locale": "en",
"transactionDate": "14 May 2013",
"transactionAmount": "500.00",
"paymentTypeId": "12"
}
</code>
<code class="method-response">
{
"officeId": 1,
"clientId": 1,
"loanId": 5,
"resourceId": 564,
"changes": {
"transactionDate": "14 May 2013",
"transactionAmount": "500.00",
"locale": "en",
"dateFormat": "dd MMMM yyyy
}
}
</code>
</div>
</div>
<a id="loans_transaction_waiveinterest" name="loans_transaction_waiveinterest" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
@@ -8882,7 +8950,7 @@ Request Body:
</code>
</div>
</div>
<!--Start of guarantors functionality-->
<a id="guarantors" name="guarantors" class="old-syle-anchor">&nbsp;</a>
@@ -24780,6 +24848,285 @@ No Request Body:
"id": 1,
"accountNo": "000000001"
}
}
</code>
</div>
</div>
<a id="accounttransfers_retrieve_template_refund_by_transfer" name="accounttransfers_retrieve_template_refund_by_transfer" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h2>Retrieve Refund of an Active Loan by Transfer Template:</h2>
<p>Example Requests : </p>
<div class=apiClick>accounttransfers/templateRefundByTransfer?fromAccountId=2&fromAccountType=1&
toAccountId=1&toAccountType=2&toClientId=1&toOfficeId=1</div>
<br>
</div>
<div class="method-example">
<code class="method-declaration">GET https://DomainName/api/v1/accounttransfers/templateRefundByTransfer</code>
<code class="method-response">
{
   "currency": {
      "code": "USD",
      "name": "US Dollar",
      "decimalPlaces": 0,
      "inMultiplesOf": 0,
      "displaySymbol": "$",
      "nameCode": "currency.USD",
      "displayLabel": "US Dollar ($)"
   },
   "transferAmount": 130,
   "transferDate": [
      2014,
      11,
      1
   ],
   "fromOffice": {
      "id": 1,
      "name": "Head Office",
      "nameDecorated": "Head Office",
      "externalId": "1",
      "openingDate": [
         2009,
         1,
         1
      ],
      "hierarchy": "."
   },
   "fromClient": {
      "id": 1,
      "accountNo": "000000001",
      "status": {
         "id": 300,
         "code": "clientStatusType.active",
         "value": "Active"
      },
      "active": true,
      "activationDate": [
         2012,
         2,
         1
      ],
      "firstname": "Daniel",
      "lastname": "Owusu",
      "displayName": "Daniel Owusu",
      "gender": {},
      "clientType": {},
      "clientClassification": {},
      "officeId": 1,
      "officeName": "Head Office",
      "timeline": {
         "submittedOnDate": [
            2012,
            2,
            1
         ],
         "submittedByUsername": "mifos",
         "submittedByFirstname": "App",
         "submittedByLastname": "Administrator",
         "activatedOnDate": [
            2012,
            2,
            1
         ],
         "activatedByUsername": "mifos",
         "activatedByFirstname": "App",
         "activatedByLastname": "Administrator"
      },
      "groups": []
   },
   "fromAccountType": {
      "id": 1,
      "code": "accountType.loan",
      "value": "Loan Account"
   },
   "fromAccount": {
      "id": 2,
      "accountNo": "000000002",
      "clientId": 1,
      "clientName": "Daniel Owusu",
      "productId": 1,
      "productName": "CTRL",
      "fieldOfficerId": 0,
      "currency": {
         "code": "USD",
         "name": "US Dollar",
         "decimalPlaces": 0,
         "inMultiplesOf": 0,
         "displaySymbol": "$",
         "nameCode": "currency.USD",
         "displayLabel": "US Dollar ($)"
      },
      "amtForTransfer": 130
   },
   "toOffice": {
      "id": 1,
      "name": "Head Office",
      "nameDecorated": "Head Office",
      "externalId": "1",
      "openingDate": [
         2009,
         1,
         1
      ],
      "hierarchy": "."
   },
   "toClient": {
      "id": 1,
      "displayName": "Daniel Owusu",
      "officeId": 1,
      "officeName": "Head Office"
   },
   "toAccountType": {
      "id": 2,
      "code": "accountType.savings",
      "value": "Savings Account"
   },
   "toAccount": {
      "id": 1,
      "accountNo": "000000001",
      "clientId": 1,
      "clientName": "Daniel Owusu",
      "productId": 1,
      "productName": "TEST",
      "fieldOfficerId": 0,
      "currency": {
         "code": "USD",
         "name": "US Dollar",
         "decimalPlaces": 0,
         "inMultiplesOf": 0,
         "displaySymbol": "$",
         "nameCode": "currency.USD",
         "displayLabel": "US Dollar ($)"
      }
   },
   "fromOfficeOptions": [
      {
         "id": 1,
         "name": "Head Office",
         "nameDecorated": "Head Office"
      }
   ],
   "fromClientOptions": [
      {
         "id": 1,
         "displayName": "Daniel Owusu",
         "officeId": 1,
         "officeName": "Head Office"
      }
   ],
   "fromAccountTypeOptions": [
      {
         "id": 2,
         "code": "accountType.savings",
         "value": "Savings Account"
      },
      {
         "id": 1,
         "code": "accountType.loan",
         "value": "Loan Account"
      }
   ],
   "fromAccountOptions": [
      {
         "id": 2,
         "accountNo": "000000002",
         "clientId": 1,
         "clientName": "Daniel Owusu",
         "productId": 1,
         "productName": "CTRL",
         "fieldOfficerId": 0,
         "currency": {
            "code": "USD",
            "name": "US Dollar",
            "decimalPlaces": 0,
            "inMultiplesOf": 0,
            "displaySymbol": "$",
            "nameCode": "currency.USD",
            "displayLabel": "US Dollar ($)"
         }
      }
   ],
   "toOfficeOptions": [
      {
         "id": 1,
         "name": "Head Office",
         "nameDecorated": "Head Office"
      }
   ],
   "toClientOptions": [
      {
         "id": 1,
         "displayName": "Daniel Owusu",
         "officeId": 1,
         "officeName": "Head Office"
      }
   ],
   "toAccountTypeOptions": [
      {
         "id": 2,
         "code": "accountType.savings",
         "value": "Savings Account"
      }
   ],
   "toAccountOptions": [
      {
         "id": 1,
         "accountNo": "000000001",
         "clientId": 1,
         "clientName": "Daniel Owusu",
         "productId": 1,
         "productName": "TEST",
         "fieldOfficerId": 0,
         "currency": {
            "code": "USD",
            "name": "US Dollar",
            "decimalPlaces": 0,
            "inMultiplesOf": 0,
            "displaySymbol": "$",
            "nameCode": "currency.USD",
            "displayLabel": "US Dollar ($)"
         }
      }
   ]
}
</code>
</div>
</div>
<a id="accounttransfers_refund_by_transfer" name="accounttransfers_refund_by_transfer" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h2>Refund of an Active Loan by Transfer</h2>
<p>Ability to refund an active loan by transferring to a savings account.</p>
</div>
<div class="method-example">
<code class="method-declaration">POST https://Domain Name/api/v1/accounttransfers</code>
<code class="method-request">POST refundByTransfer/
Content-Type: application/json
No Request Body:
{
"fromAccountId": "2",
"fromAccountType": 1,
"toOfficeId": 1,
"toClientId": 1,
"toAccountType": 2,
"toAccountId": 1,
"transferAmount": 130,
"transferDate": "31 October 2014",
"transferDescription": "Transfer refund to my savings account",
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"fromClientId": 1,
"fromOfficeId": 1
}
</code>
<code class="method-response">
{
"savingsId": 1,
"resourceId": 1
}
</code>
</div>
@@ -35,6 +35,7 @@ import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilde
import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder;
import org.mifosplatform.integrationtests.common.loans.LoanStatusChecker;
import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper;
import org.mifosplatform.integrationtests.common.savings.AccountTransferHelper;
import org.mifosplatform.integrationtests.common.savings.SavingsAccountHelper;
import org.mifosplatform.integrationtests.common.savings.SavingsProductHelper;
import org.mifosplatform.integrationtests.common.savings.SavingsStatusChecker;
@@ -68,6 +69,8 @@ public class ClientLoanIntegrationTest {
private AccountHelper accountHelper;
private SchedulerJobHelper schedulerJobHelper;
private PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper;
private SavingsAccountHelper savingsAccountHelper;
private AccountTransferHelper accountTransferHelper;
@Before
public void setup() {
@@ -888,6 +891,23 @@ public class ClientLoanIntegrationTest {
.currencyDetails(digitsAfterDecimal, inMultiplesOf).build(null);
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
}
private Integer createLoanProduct(final String inMultiplesOf, final String digitsAfterDecimal, final String repaymentStrategy,
final String accountingRule, final Account... accounts) {
System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder() //
.withPrincipal("10000000.00") //
.withNumberOfRepayments("24") //
.withRepaymentAfterEvery("1") //
.withRepaymentTypeAsMonth() //
.withinterestRatePerPeriod("2") //
.withInterestRateFrequencyTypeAsMonths() //
.withRepaymentStrategy(repaymentStrategy) //
.withAmortizationTypeAsEqualPrincipalPayment() //
.withInterestTypeAsDecliningBalance() //
.currencyDetails(digitsAfterDecimal, inMultiplesOf).withAccounting(accountingRule, accounts).build(null);
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
}
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String graceOnPrincipalPayment) {
System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
@@ -989,6 +1009,32 @@ public class ClientLoanIntegrationTest {
.withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId);
return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
}
private Integer applyForLoanApplicationWithPaymentStrategyAndPastMonth(final Integer clientID, final Integer loanProductID, List<HashMap> charges,
final String savingsId, String principal, final String repaymentStrategy, final int month) {
System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
Calendar fourMonthsfromNowCalendar = Calendar.getInstance();
fourMonthsfromNowCalendar.add(Calendar.MONTH, month);
String fourMonthsfromNow = new SimpleDateFormat("dd MMMMMM yyyy").format(fourMonthsfromNowCalendar.getTime());
final String loanApplicationJSON = new LoanApplicationTestBuilder() //
.withPrincipal(principal) //
.withLoanTermFrequency("6") //
.withLoanTermFrequencyAsMonths() //
.withNumberOfRepayments("6") //
.withRepaymentEveryAfter("1") //
.withRepaymentFrequencyTypeAsMonths() //
.withInterestRatePerPeriod("2") //
.withAmortizationTypeAsEqualInstallments() //
.withInterestTypeAsFlatBalance() //
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
.withExpectedDisbursementDate(fourMonthsfromNow) //
.withSubmittedOnDate(fourMonthsfromNow) //
.withwithRepaymentStrategy(repaymentStrategy) //
.withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId);
return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
}
private void verifyLoanRepaymentSchedule(final ArrayList<HashMap> loanSchedule) {
System.out.println("--------------------VERIFYING THE PRINCIPAL DUES,INTEREST DUE AND DUE DATE--------------------------");
@@ -3756,4 +3802,543 @@ public class ClientLoanIntegrationTest {
index++;
}
}
/***
* Test case to verify Mifos Style payment strategy
*/
@Test
public void testLoanRefundByCashCashBasedAccounting() {
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
Calendar fourMonthsfromNowCalendar = Calendar.getInstance();
fourMonthsfromNowCalendar.add(Calendar.MONTH, -4);
String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
/***
* Create loan product with MIFOS STYLE strategy
*/
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, CASH_BASED,
assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
Assert.assertNotNull(loanProductID);
/***
* Apply for loan application and verify loan status
*/
final String savingsId = null;
final String principal = "12,000.00";
// Add charges with payment mode regular
List<HashMap> charges = new ArrayList<>();
Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
addCharges(charges, flatInstallmentFee, "50", null);
final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, savingsId, principal,
LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4);
Assert.assertNotNull(loanID);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
final JournalEntry[] assetAccountInitialEntry = {
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) };
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry);
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap firstInstallment = loanSchedule.get(1);
validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #1
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID);
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
firstInstallment = loanSchedule.get(1);
validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #2
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow,
new JournalEntry(Float.valueOf("2290"), JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("2000"),
JournalEntry.TransactionType.CREDIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, twoMonthsfromNow,
new JournalEntry(Float.valueOf("50"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("240"), JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
Map secondInstallment = loanSchedule.get(2);
validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #3
//Pay 2290 more than expected
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow,
new JournalEntry(Float.valueOf("4580"), JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("4000"),
JournalEntry.TransactionType.CREDIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, oneMonthfromNow,
new JournalEntry(Float.valueOf("100"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap thirdInstallment = loanSchedule.get(3);
validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod")));
// Make refund of 20
//max 2290 to refund. Pay 20 means only principal
//mifos style refund order(principal, interest, fees and penalties
//paid: principal 2000, interest 240, fees 50, penalty 0
//refund 20 means paid: principal 1980, interest 240, fees 50, penalty 0
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("20"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"),
JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
// Make refund of 2000
//max 2270 to refund. Pay 2000 means only principal
//paid: principal 1980, interest 240, fees 50, penalty 0
//refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0
this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("2000"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("2000"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"),
JournalEntry.TransactionType.DEBIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
}
/***
* Test case to verify Mifos style payment strategy
*/
@Test
public void testLoanRefundByCashAccrualBasedAccounting() {
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
Calendar fourMonthsfromNowCalendar = Calendar.getInstance();
fourMonthsfromNowCalendar.add(Calendar.MONTH, -4);
String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
/***
* Create loan product with MIFOS STYLE strategy
*/
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, ACCRUAL_UPFRONT,
assetAccount, incomeAccount, expenseAccount, overpaymentAccount);//, LoanProductTestBuilder.EQUAL_INSTALLMENTS,
//LoanProductTestBuilder.FLAT_BALANCE);
Assert.assertNotNull(loanProductID);
/***
* Apply for loan application and verify loan status
*/
final String savingsId = null;
final String principal = "12,000.00";
// Add charges with payment mode regular
List<HashMap> charges = new ArrayList<>();
Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
addCharges(charges, flatInstallmentFee, "50", null);
final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, savingsId, principal,
LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4);
Assert.assertNotNull(loanID);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
final JournalEntry[] assetAccountInitialEntry = {
new JournalEntry(Float.valueOf("1440"), JournalEntry.TransactionType.DEBIT),
new JournalEntry(Float.valueOf("300.00"), JournalEntry.TransactionType.DEBIT),
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) };
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry);
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap firstInstallment = loanSchedule.get(1);
validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #1
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID);
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
firstInstallment = loanSchedule.get(1);
validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #2
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow,
new JournalEntry(Float.valueOf("2290"), JournalEntry.TransactionType.DEBIT),
new JournalEntry(Float.valueOf("50"), JournalEntry.TransactionType.CREDIT),new JournalEntry(Float.valueOf("240"),
JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("2000"),
JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
Map secondInstallment = loanSchedule.get(2);
validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #3
//Pay 2290 more than expected
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow,
new JournalEntry(Float.valueOf("4580"), JournalEntry.TransactionType.DEBIT),
new JournalEntry(Float.valueOf("100"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("4000"),JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap thirdInstallment = loanSchedule.get(3);
validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod")));
// Make refund of 20
//max 2290 to refund. Pay 20 means only principal
//mifos style refund order(principal, interest, fees and penalties
//paid: principal 2000, interest 240, fees 50, penalty 0
//refund 20 means paid: principal 1980, interest 240, fees 50, penalty 0
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("20"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"),
JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
// Make refund of 2000
//max 2270 to refund. Pay 2000 means only principal
//paid: principal 1980, interest 240, fees 50, penalty 0
//refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0
this.loanTransactionHelper.makeRefundByCash(now, Float.valueOf("2000"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("2000"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"),
JournalEntry.TransactionType.DEBIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
}
@Test
public void testLoanRefundByTransferCashBasedAccounting() {
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
this.accountTransferHelper = new AccountTransferHelper(this.requestSpec, this.responseSpec);
Calendar fourMonthsfromNowCalendar = Calendar.getInstance();
fourMonthsfromNowCalendar.add(Calendar.MONTH, -4);
String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, MINIMUM_OPENING_BALANCE);
Assert.assertNotNull(savingsProductID);
final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL);
Assert.assertNotNull(savingsProductID);
HashMap modifications = this.savingsAccountHelper.updateSavingsAccount(clientID, savingsProductID, savingsId,
ACCOUNT_TYPE_INDIVIDUAL);
Assert.assertTrue(modifications.containsKey("submittedOnDate"));
HashMap savingsStatusHashMap = SavingsStatusChecker.getStatusOfSavings(this.requestSpec, this.responseSpec, savingsId);
SavingsStatusChecker.verifySavingsIsPending(savingsStatusHashMap);
savingsStatusHashMap = this.savingsAccountHelper.approveSavings(savingsId);
SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId);
SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
/***
* Create loan product with MIFOS STYLE strategy
*/
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.MIFOS_STANDARD_STRATEGY, CASH_BASED,
assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
Assert.assertNotNull(loanProductID);
/***
* Apply for loan application and verify loan status
*/
final String principal = "12,000.00";
// Add charges with payment mode regular
List<HashMap> charges = new ArrayList<>();
Integer flatInstallmentFee = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
addCharges(charges, flatInstallmentFee, "50", null);
final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, null, principal,
LoanApplicationTestBuilder.MIFOS_STANDARD_STRATEGY, -4);
Assert.assertNotNull(loanID);
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
System.out.println("-----------------------------------APPROVE LOAN-----------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.approveLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
System.out.println("-------------------------------DISBURSE LOAN-------------------------------------------");
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(fourMonthsfromNow, loanID);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
final JournalEntry[] assetAccountInitialEntry = {
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("12000.00"), JournalEntry.TransactionType.DEBIT) };
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, fourMonthsfromNow, assetAccountInitialEntry);
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap firstInstallment = loanSchedule.get(1);
validateNumberForEqual("2290", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #1
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String threeMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(threeMonthsfromNow, Float.valueOf("2290"), loanID);
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
firstInstallment = loanSchedule.get(1);
validateNumberForEqual("0.00", String.valueOf(firstInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #2
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String twoMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(twoMonthsfromNow, Float.valueOf("2290"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, twoMonthsfromNow,
new JournalEntry(Float.valueOf("2290"), JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("2000"),
JournalEntry.TransactionType.CREDIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, twoMonthsfromNow,
new JournalEntry(Float.valueOf("50"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("240"), JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
Map secondInstallment = loanSchedule.get(2);
validateNumberForEqual("0.00", String.valueOf(secondInstallment.get("totalOutstandingForPeriod")));
// Make payment for installment #3
//Pay 2290 more than expected
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String oneMonthfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
this.loanTransactionHelper.makeRepayment(oneMonthfromNow, Float.valueOf("4580"), loanID);
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, oneMonthfromNow,
new JournalEntry(Float.valueOf("4580"), JournalEntry.TransactionType.DEBIT), new JournalEntry(Float.valueOf("4000"),
JournalEntry.TransactionType.CREDIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, oneMonthfromNow,
new JournalEntry(Float.valueOf("100"), JournalEntry.TransactionType.CREDIT),
new JournalEntry(Float.valueOf("480"), JournalEntry.TransactionType.CREDIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap thirdInstallment = loanSchedule.get(3);
validateNumberForEqual("0.00", String.valueOf(thirdInstallment.get("totalOutstandingForPeriod")));
// Make refund of 20
//max 2290 to refund. Pay 20 means only principal
//mifos style refund order(principal, interest, fees and penalties
//paid: principal 2000, interest 240, fees 50, penalty 0
//refund 20 means paid: principal 1980, interest 240, fees 50, penalty 0
Float TRANSFER_AMOUNT = 20f;
fourMonthsfromNowCalendar.add(Calendar.MONTH, 1);
final String now = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
final String FROM_LOAN_ACCOUNT_TYPE = "1";
final String TO_SAVINGS_ACCOUNT_TYPE = "2";
this.accountTransferHelper.refundLoanByTransfer(now, clientID, loanID, clientID, savingsId, FROM_LOAN_ACCOUNT_TYPE,
TO_SAVINGS_ACCOUNT_TYPE, TRANSFER_AMOUNT.toString());
Float toSavingsBalance = new Float(MINIMUM_OPENING_BALANCE);
HashMap toSavingsSummaryAfter = this.savingsAccountHelper.getSavingsSummary(savingsId);
toSavingsBalance += TRANSFER_AMOUNT;
// Verifying toSavings Account Balance after Account Transfer
assertEquals("Verifying From Savings Account Balance after Account Transfer", toSavingsBalance,
toSavingsSummaryAfter.get("accountBalance"));
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("20"),
JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
HashMap fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
// Make refund of 2000
//max 2270 to refund. Pay 2000 means only principal
//paid: principal 1980, interest 240, fees 50, penalty 0
//refund 2000 means paid: principal 0, interest 220, fees 50, penalty 0
//final String now = Utils.convertDate(fourMonthsfromNowCalendar);
TRANSFER_AMOUNT = 2000f;
this.accountTransferHelper.refundLoanByTransfer(now, clientID, loanID, clientID, savingsId, FROM_LOAN_ACCOUNT_TYPE,
TO_SAVINGS_ACCOUNT_TYPE, TRANSFER_AMOUNT.toString());
toSavingsSummaryAfter = this.savingsAccountHelper.getSavingsSummary(savingsId);
toSavingsBalance += TRANSFER_AMOUNT;
// Verifying toSavings Account Balance after Account Transfer
assertEquals("Verifying From Savings Account Balance after Account Transfer", toSavingsBalance,
toSavingsSummaryAfter.get("accountBalance"));
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, now,
new JournalEntry(Float.valueOf("2000"), JournalEntry.TransactionType.CREDIT), new JournalEntry(Float.valueOf("1980"),
JournalEntry.TransactionType.DEBIT));
this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, now,
new JournalEntry(Float.valueOf("20"), JournalEntry.TransactionType.DEBIT));
loanSchedule.clear();
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
fourthInstallment = loanSchedule.get(4);
validateNumberForEqual("2020.00", String.valueOf(fourthInstallment.get("totalOutstandingForPeriod")));
validateNumberForEqual("2000.00", String.valueOf(fourthInstallment.get("principalOutstanding")));
validateNumberForEqual("20.00", String.valueOf(fourthInstallment.get("interestOutstanding")));
validateNumberForEqual("0.00", String.valueOf(fourthInstallment.get("feeChargesOutstanding")));
}
}
@@ -13,6 +13,7 @@ import static org.junit.Assert.fail;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Random;
@@ -115,5 +116,9 @@ public class Utils {
public static String randomNameGenerator(final String prefix, final int lenOfRandomSuffix) {
return randomStringGenerator(prefix, lenOfRandomSuffix);
}
public static String convertDateToURLFormat(final Calendar dateToBeConvert) {
return new SimpleDateFormat("dd MMMMMM yyyy").format(dateToBeConvert.getTime());
}
}
@@ -35,6 +35,7 @@ public class LoanTransactionHelper {
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash";
public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
this.requestSpec = requestSpec;
@@ -352,6 +353,10 @@ public class LoanTransactionHelper {
final HashMap response = Utils.performServerGet(requestSpec, responseSpec, URL, "");
return response;
}
private String createLoanRefundTransferURL() {
return "/mifosng-provider/api/v1/accounttransfers/refundByTransfer?tenantIdentifier=default";
}
public void verifyRepaymentScheduleEntryFor(final int repaymentNumber, final float expectedPrincipalOutstanding, final Integer loanID) {
System.out.println("---------------------------GETTING LOAN REPAYMENT SCHEDULE--------------------------------");
@@ -389,4 +394,44 @@ public class LoanTransactionHelper {
assertTrue("No Accrual entries are posted", isTransactionFound);
}
public HashMap makeRefundByCash(final String date, final Float amountToBeRefunded, final Integer loanID) {
return performLoanTransaction(createLoanTransactionURL(MAKE_REFUND_BY_CASH_COMMAND, loanID),
getRefundByCashBodyAsJSON(date, amountToBeRefunded));
}
public HashMap makeRefundByTransfer(final Integer fromAccountId, final Integer toClientId, final Integer toAccountId,
final Integer fromClientId, final String date, final Float amountToBeRefunded) {
return performLoanTransaction(createLoanRefundTransferURL(),
getRefundByTransferBodyAsJSON(fromAccountId, toClientId, toAccountId, fromClientId, date, amountToBeRefunded));
}
private String getRefundByCashBodyAsJSON(final String transactionDate, final Float transactionAmount) {
final HashMap<String, String> map = new HashMap<>();
map.put("locale", "en");
map.put("dateFormat", "dd MMMM yyyy");
map.put("transactionDate", transactionDate);
map.put("transactionAmount", transactionAmount.toString());
map.put("note", "Refund Made!!!");
return new Gson().toJson(map);
}
private String getRefundByTransferBodyAsJSON(final Integer fromAccountId, final Integer toClientId, final Integer toAccountId,
final Integer fromClientId, final String transactionDate, final Float transactionAmount) {
final HashMap<String, String> map = new HashMap<>();
map.put("fromAccountId", fromAccountId.toString());
map.put("fromAccountType", "1");
map.put("toOfficeId", "1");
map.put("toClientId", toClientId.toString());
map.put("toAccountType", "2");
map.put("toAccountId", toAccountId.toString());
map.put("dateFormat", "dd MMMM yyyy");
map.put("transferDate", transactionDate);
map.put("transferAmount", transactionAmount.toString());
map.put("transferDescription", "Refund Made!!!");
map.put("fromClientId", fromClientId.toString());
map.put("fromOfficeId", "1");
map.put("locale", "en");
return new Gson().toJson(map);
}
}
@@ -16,6 +16,7 @@ import com.jayway.restassured.specification.ResponseSpecification;
public class AccountTransferHelper {
private static final String ACCOUNT_TRANSFER_URL = "/mifosng-provider/api/v1/accounttransfers";
private static final String LOAN_REFUND_BY_TRANSFER_URL = "/mifosng-provider/api/v1/accounttransfers/refundByTransfer";
private static final String LOCALE = "en_GB";
private static final String OFFICE_ID = "1";
private static final String TRANSFER_DESCRIPTION = "Transfer";
@@ -70,4 +71,15 @@ public class AccountTransferHelper {
return Utils.performServerPost(this.requestSpec, this.responseSpec, ACCOUNT_TRANSFER_URL + "?" + Utils.TENANT_IDENTIFIER,
accountTransferJSON, "savingsId");
}
public Integer refundLoanByTransfer(final String date, final Integer fromClientId, final Integer fromAccountId, final Integer toClientId,
final Integer toAccountId, final String fromAccountType, final String toAccountType, final String transferAmount) {
System.out.println("--------------------------------ACCOUNT TRANSFER--------------------------------");
final String accountTransferJSON = new AccountTransferHelper(this.requestSpec, this.responseSpec) //
.withTransferOnDate(date) //
.build(fromAccountId.toString(), fromClientId.toString(), toAccountId.toString(), toClientId.toString(), fromAccountType,
toAccountType, transferAmount);
return Utils.performServerPost(this.requestSpec, this.responseSpec, LOAN_REFUND_BY_TRANSFER_URL + "?" + Utils.TENANT_IDENTIFIER,
accountTransferJSON, "savingsId");
}
}
@@ -6,12 +6,15 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.FINANCIAL_ACTIVITY;
import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO;
import org.mifosplatform.organisation.office.domain.Office;
@@ -72,6 +75,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
.getTransactionType().isWaiveCharges())) {
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false);
}
/** Logic for Refunds of Active Loans **/
else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) {
createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office);
}
}
}
@@ -339,4 +347,75 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
paymentTypeId, loanId, transactionId, transactionDate, refundAmount, isReversal);
}
}
private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
// TODO Auto-generated method stub
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
BigDecimal totalDebitAmount = new BigDecimal(0);
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO,
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalAmount, !isReversal);
}
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS,
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, interestAmount, !isReversal);
}
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
List<ChargePaymentDTO> chargePaymentDTOs = new ArrayList<>();
for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getFeePayments()) {
chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(),
chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() ));
}
this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount,
!isReversal, chargePaymentDTOs);
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
List<ChargePaymentDTO> chargePaymentDTOs = new ArrayList<>();
for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getPenaltyPayments()) {
chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(),
chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() ));
}
this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(), loanProductId, loanId, transactionId, transactionDate,
penaltiesAmount, !isReversal, chargePaymentDTOs);
}
if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, !isReversal);
}
/*** create a single debit entry (or reversal) for the entire amount **/
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE.getValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, !isReversal);
}
}
@@ -6,11 +6,14 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.FINANCIAL_ACTIVITY;
import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO;
import org.mifosplatform.organisation.office.domain.Office;
@@ -82,6 +85,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
|| loanTransactionDTO.getTransactionType().isWithdrawTransfer()) {
createJournalEntriesForTransfers(loanDTO, loanTransactionDTO, office);
}
/** Logic for Refunds of Active Loans **/
else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) {
createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office);
}
}
}
@@ -290,4 +297,76 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
null, loanId, transactionId, transactionDate, principalAmount, isReversal);
}
}
private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
// TODO Auto-generated method stub
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final Date transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
BigDecimal totalDebitAmount = new BigDecimal(0);
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.LOAN_PORTFOLIO,
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalAmount, !isReversal);
}
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.INTEREST_ON_LOANS,
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, interestAmount, !isReversal);
}
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
List<ChargePaymentDTO> chargePaymentDTOs = new ArrayList<>();
for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getFeePayments()) {
chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(),
chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() ));
}
this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount,
!isReversal, chargePaymentDTOs);
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
List<ChargePaymentDTO> chargePaymentDTOs = new ArrayList<>();
for(ChargePaymentDTO chargePaymentDTO : loanTransactionDTO.getPenaltyPayments()) {
chargePaymentDTOs.add(new ChargePaymentDTO(chargePaymentDTO.getChargeId(), chargePaymentDTO.getLoanChargeId(),
chargePaymentDTO.getAmount().floatValue() < 0 ? chargePaymentDTO.getAmount().multiply(new BigDecimal(-1)):chargePaymentDTO.getAmount() ));
}
this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode,
CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(), loanProductId, loanId, transactionId, transactionDate,
penaltiesAmount, !isReversal, chargePaymentDTOs);
}
if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.OVERPAYMENT, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, !isReversal);
}
/*** create a single debit entry (or reversal) for the entire amount **/
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CASH_ACCOUNTS_FOR_LOAN.FUND_SOURCE.getValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, !isReversal);
}
}
@@ -920,5 +920,17 @@ public class CommandWrapper {
public boolean isInactivateSavingsAccountCharge() {
return this.actionName.equalsIgnoreCase("INACTIVATE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNTCHARGE");
}
public boolean isRefundByTransfer() {
return this.actionName.equalsIgnoreCase("REFUNDBYTRANSFER");
}
public boolean isLoanRefundByCash() {
return this.actionName.equalsIgnoreCase("REFUNDBYCASH") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isUndoLoanRefund() {
return this.actionName.equalsIgnoreCase("UNDOREFUND") && this.entityName.equalsIgnoreCase("LOAN");
}
}
@@ -658,6 +658,15 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/transactions/" + transactionId;
return this;
}
public CommandWrapperBuilder refundLoanTransactionByCash(final Long loanId) {
this.actionName = "REFUNDBYCASH";
this.entityName = "LOAN";
this.entityId = null;
this.loanId = loanId;
this.href = "/loans/" + loanId + "/transactions/template?command=refundbycash";
return this;
}
public CommandWrapperBuilder createLoanApplication() {
this.actionName = "CREATE";
@@ -2112,4 +2121,12 @@ public class CommandWrapperBuilder {
this.href = "/rescheduleloans/" + requestId + "?command=reject";
return this;
}
public CommandWrapperBuilder refundByTransfer() {
this.actionName = "REFUNDBYTRANSFER";
this.entityName = "ACCOUNTTRANSFER";
this.entityId = null;
this.href = "/refundByTransfer";
return this;
}
}
@@ -131,7 +131,11 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (wrapper.isAccountTransferResource()) {
if (wrapper.isCreate()) {
handler = this.applicationContext.getBean("createAccountTransferCommandHandler", NewCommandSourceHandler.class);
} else {
}
else if (wrapper.isRefundByTransfer()) {
handler = this.applicationContext.getBean("refundByTransferCommandHandler", NewCommandSourceHandler.class);
}
else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else if (wrapper.isConfigurationResource()) {
@@ -389,7 +393,14 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("loanApplicationDeletionCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUndoLoanWriteOff()) {
handler = this.applicationContext.getBean("undoWriteOffLoanCommandHandler", NewCommandSourceHandler.class);
} else {
}
else if (wrapper.isLoanRefundByCash()) {
handler = this.applicationContext.getBean("loanRefundByCashCommandHandler", NewCommandSourceHandler.class);
}
else if (wrapper.isUndoLoanRefund()) {
handler = this.applicationContext.getBean("loanRefundAdjustmentCommandHandler", NewCommandSourceHandler.class);
}
else {
throw new UnsupportedCommandException(wrapper.commandName());
}
} else if (wrapper.isLoanChargeResource()) {
@@ -119,4 +119,36 @@ public class AccountTransfersApiResource {
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, transfer, AccountTransfersApiConstants.RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("templateRefundByTransfer")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String templateRefundByTransfer(@QueryParam("fromOfficeId") final Long fromOfficeId, @QueryParam("fromClientId") final Long fromClientId,
@QueryParam("fromAccountId") final Long fromAccountId, @QueryParam("fromAccountType") final Integer fromAccountType,
@QueryParam("toOfficeId") final Long toOfficeId, @QueryParam("toClientId") final Long toClientId,
@QueryParam("toAccountId") final Long toAccountId, @QueryParam("toAccountType") final Integer toAccountType,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(AccountTransfersApiConstants.ACCOUNT_TRANSFER_RESOURCE_NAME);
final AccountTransferData transferData = this.accountTransfersReadPlatformService.retrieveRefundByTransferTemplate(fromOfficeId, fromClientId,
fromAccountId, fromAccountType, toOfficeId, toClientId, toAccountId, toAccountType);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, transferData, AccountTransfersApiConstants.RESPONSE_DATA_PARAMETERS);
}
@POST
@Path("refundByTransfer")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String templateRefundByTransferPost(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().refundByTransfer().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
}
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.account.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.portfolio.account.service.AccountTransfersWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class RefundByTransferCommandHandler implements NewCommandSourceHandler {
private final AccountTransfersWritePlatformService writePlatformService;
@Autowired
public RefundByTransferCommandHandler(final AccountTransfersWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.refundByTransfer(command);
}
}
@@ -26,4 +26,7 @@ public interface AccountTransfersReadPlatformService {
Page<AccountTransferData> retrieveByStandingInstruction(Long id, SearchParameters searchParameters);
Collection<Long> fetchPostInterestTransactionIds(Long accountId);
AccountTransferData retrieveRefundByTransferTemplate(Long fromOfficeId, Long fromClientId, Long fromAccountId, Integer fromAccountType,
Long toOfficeId, Long toClientId, Long toAccountId, Integer toAccountType);
}
@@ -406,5 +406,114 @@ public class AccountTransfersReadPlatformServiceImpl implements AccountTransfers
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), finalObjectArray,
this.accountTransfersMapper);
}
public AccountTransferData retrieveRefundByTransferTemplate(final Long fromOfficeId, final Long fromClientId, final Long fromAccountId,
final Integer fromAccountType, final Long toOfficeId, final Long toClientId, final Long toAccountId, final Integer toAccountType) {
// TODO Auto-generated method stub
final EnumOptionData loanAccountType = AccountTransferEnumerations.accountType(PortfolioAccountType.LOAN);
final EnumOptionData savingsAccountType = AccountTransferEnumerations.accountType(PortfolioAccountType.SAVINGS);
final Integer mostRelevantFromAccountType = fromAccountType;
final Collection<EnumOptionData> fromAccountTypeOptions = Arrays.asList(savingsAccountType, loanAccountType);
final Collection<EnumOptionData> toAccountTypeOptions;
if (mostRelevantFromAccountType == 1) {
// overpaid loan amt transfer to savings account
toAccountTypeOptions = Arrays.asList(savingsAccountType);
} else {
toAccountTypeOptions = Arrays.asList(loanAccountType, savingsAccountType);
}
final Integer mostRelevantToAccountType = toAccountType;
final EnumOptionData fromAccountTypeData = AccountTransferEnumerations.accountType(mostRelevantFromAccountType);
final EnumOptionData toAccountTypeData = AccountTransferEnumerations.accountType(mostRelevantToAccountType);
// from settings
OfficeData fromOffice = null;
ClientData fromClient = null;
PortfolioAccountData fromAccount = null;
OfficeData toOffice = null;
ClientData toClient = null;
PortfolioAccountData toAccount = null;
// template
Collection<PortfolioAccountData> fromAccountOptions = null;
Collection<PortfolioAccountData> toAccountOptions = null;
Long mostRelevantFromOfficeId = fromOfficeId;
Long mostRelevantFromClientId = fromClientId;
Long mostRelevantToOfficeId = toOfficeId;
Long mostRelevantToClientId = toClientId;
if (fromAccountId != null) {
Integer accountType;
if (mostRelevantFromAccountType == 1) {
accountType = PortfolioAccountType.LOAN.getValue();
} else {
accountType = PortfolioAccountType.SAVINGS.getValue();
}
fromAccount = this.portfolioAccountReadPlatformService.retrieveOneByPaidInAdvance(fromAccountId, accountType);
// override provided fromClient with client of account
mostRelevantFromClientId = fromAccount.clientId();
}
if (mostRelevantFromClientId != null) {
fromClient = this.clientReadPlatformService.retrieveOne(mostRelevantFromClientId);
mostRelevantFromOfficeId = fromClient.officeId();
long[] loanStatus = null;
if (mostRelevantFromAccountType == 1) {
loanStatus = new long[] { 300, 700 };
}
fromAccountOptions = this.portfolioAccountReadPlatformService.retrieveAllForLookup(mostRelevantFromAccountType,
mostRelevantFromClientId, loanStatus);
}
Collection<OfficeData> fromOfficeOptions = null;
Collection<ClientData> fromClientOptions = null;
if (mostRelevantFromOfficeId != null) {
fromOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantFromOfficeId);
fromOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown();
fromClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantFromOfficeId);
}
// defaults
final LocalDate transferDate = DateUtils.getLocalDateOfTenant();
Collection<OfficeData> toOfficeOptions = fromOfficeOptions;
Collection<ClientData> toClientOptions = null;
if (toAccountId != null && fromAccount != null) {
toAccount = this.portfolioAccountReadPlatformService.retrieveOne(toAccountId, mostRelevantToAccountType,
fromAccount.currencyCode());
mostRelevantToClientId = toAccount.clientId();
}
if (mostRelevantToClientId != null) {
toClient = this.clientReadPlatformService.retrieveOne(mostRelevantToClientId);
mostRelevantToOfficeId = toClient.officeId();
toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId);
toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId);
}
if (mostRelevantToOfficeId != null) {
toOffice = this.officeReadPlatformService.retrieveOffice(mostRelevantToOfficeId);
toOfficeOptions = this.officeReadPlatformService.retrieveAllOfficesForDropdown();
toClientOptions = this.clientReadPlatformService.retrieveAllForLookupByOfficeId(mostRelevantToOfficeId);
if (toClientOptions != null && toClientOptions.size() == 1) {
toClient = new ArrayList<>(toClientOptions).get(0);
toAccountOptions = retrieveToAccounts(fromAccount, mostRelevantToAccountType, mostRelevantToClientId);
}
}
return AccountTransferData.template(fromOffice, fromClient, fromAccountTypeData, fromAccount, transferDate, toOffice, toClient,
toAccountTypeData, toAccount, fromOfficeOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions,
toOfficeOptions, toClientOptions, toAccountTypeOptions, toAccountOptions);
}
}
@@ -22,4 +22,6 @@ public interface AccountTransfersWritePlatformService {
void reverseAllTransactions(Long accountId, PortfolioAccountType accountTypeId);
void updateLoanTransaction(Long loanTransactionId, LoanTransaction newLoanTransaction);
CommandProcessingResult refundByTransfer(JsonCommand command);
}
@@ -13,6 +13,7 @@ import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstan
import static org.mifosplatform.portfolio.account.api.AccountTransfersApiConstants.transferDateParamName;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -35,7 +36,10 @@ import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler;
import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService;
import org.mifosplatform.portfolio.paymentdetail.domain.PaymentDetail;
import org.mifosplatform.portfolio.savings.SavingsTransactionBooleanValues;
import org.mifosplatform.portfolio.savings.domain.SavingsAccount;
@@ -59,6 +63,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private final LoanAccountDomainService loanAccountDomainService;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final LoanReadPlatformService loanReadPlatformService;
@Autowired
public AccountTransfersWritePlatformServiceImpl(final AccountTransfersDataValidator accountTransfersDataValidator,
@@ -66,7 +71,8 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
final SavingsAccountAssembler savingsAccountAssembler, final SavingsAccountDomainService savingsAccountDomainService,
final LoanAssembler loanAssembler, final LoanAccountDomainService loanAccountDomainService,
final SavingsAccountWritePlatformService savingsAccountWritePlatformService,
final AccountTransferDetailRepository accountTransferDetailRepository) {
final AccountTransferDetailRepository accountTransferDetailRepository,
final LoanReadPlatformService loanReadPlatformService) {
this.accountTransfersDataValidator = accountTransfersDataValidator;
this.accountTransferAssembler = accountTransferAssembler;
this.accountTransferRepository = accountTransferRepository;
@@ -76,6 +82,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
this.loanAccountDomainService = loanAccountDomainService;
this.savingsAccountWritePlatformService = savingsAccountWritePlatformService;
this.accountTransferDetailRepository = accountTransferDetailRepository;
this.loanReadPlatformService = loanReadPlatformService;
}
@Transactional
@@ -385,5 +392,54 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
@Override
@Transactional
public CommandProcessingResult refundByTransfer(JsonCommand command) {
// TODO Auto-generated method stub
this.accountTransfersDataValidator.validate(command);
final LocalDate transactionDate = command.localDateValueOfParameterNamed(transferDateParamName);
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(transferAmountParamName);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final PaymentDetail paymentDetail = null;
Long transferTransactionId = null;
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(fromLoanAccountId).getPaidInAdvance();
if (overpaid == null || overpaid.equals(BigDecimal.ZERO) || transactionAmount.floatValue() > overpaid.floatValue()) {
if(overpaid == null)
overpaid = BigDecimal.ZERO;
throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString());
}
final LoanTransaction loanRefundTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(fromLoanAccountId,
new CommandProcessingResultBuilder(), transactionDate, transactionAmount, paymentDetail, null, null);
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId);
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
transactionAmount, paymentDetail, true, true);
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = accountTransferDetails.getId();
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(transferTransactionId);
// if (fromAccountType.isSavingsAccount()) {
builder.withSavingsId(toSavingsAccountId);
// }
return builder.build();
}
}
@@ -18,4 +18,6 @@ public interface PortfolioAccountReadPlatformService {
Collection<PortfolioAccountData> retrieveAllForLookup(Integer toAccountType, Long toClientId, long[] accountStatus);
Collection<PortfolioAccountData> retrieveAllForLookup(Integer toAccountType, Long toClientId, String currencyCode, long[] accountStatus, Integer depositType);
PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId);
}
@@ -32,12 +32,14 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
// mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
@Autowired
public PortfolioAccountReadPlatformServiceImpl(final RoutingDataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper();
}
@Override
@@ -267,4 +269,107 @@ public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccount
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
public PortfolioLoanAccountRefundByTransferMapper() {
final StringBuilder amountQueryString = new StringBuilder(400);
amountQueryString.append("(select (SUM(ifnull(mr.principal_completed_derived, 0)) +");
amountQueryString.append("SUM(ifnull(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(ifnull(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append(" SUM(ifnull(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id");
amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300");
amountQueryString.append(" and mr.duedate >= CURDATE() group by ml.id having");
amountQueryString.append(" (SUM(ifnull(mr.principal_completed_derived, 0)) + ");
amountQueryString.append(" SUM(ifnull(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(ifnull(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append(amountQueryString.toString());
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf,
currencyDisplaySymbol, currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
@Override
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub
Object[] sqlParams = new Object[] { accountId , accountId};
PortfolioAccountData accountData = null;
//String currencyCode = null;
try {
String sql = null;
//final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?";
/* if (currencyCode != null) {
sql += " and la.currency_code = ?";
sqlParams = new Object[] {accountId , accountId,currencyCode };
}*/
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId);
}
return accountData;
}
}
@@ -96,7 +96,14 @@ public class LoanTransactionsApiResource {
transactionData = this.loanReadPlatformService.retrieveRecoveryPaymentTemplate(loanId);
} else if (is(commandParam, "prepayLoan")) {
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(loanId);
} else {
}
else if (is(commandParam, "refundbycash")) {
transactionData = this.loanReadPlatformService.retrieveRefundByCashTemplate(loanId);
}
else if (is(commandParam, "refundbytransfer")) {
transactionData = this.loanReadPlatformService.retrieveDisbursalTemplate(loanId, true);
}
else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -155,6 +162,10 @@ public class LoanTransactionsApiResource {
final CommandWrapper commandRequest = builder.loanRecoveryPaymentTransaction(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
else if (is(commandParam, "refundByCash")) {
final CommandWrapper commandRequest = builder.refundLoanTransactionByCash(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
@@ -68,6 +68,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData;
import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTermVariationType;
import org.mifosplatform.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException;
@@ -426,6 +427,7 @@ public class LoansApiResource {
Collection<CodeValueData> loanCollateralOptions = null;
Collection<CalendarData> calendarOptions = null;
Collection<PortfolioAccountData> accountLinkingOptions = null;
PaidInAdvanceData paidInAdvanceTemplate = null;
final boolean template = ApiParameterHelper.template(uriInfo.getQueryParameters());
if (template) {
@@ -470,13 +472,15 @@ public class LoansApiResource {
Collection<ChargeData> overdueCharges = this.chargeReadPlatformService.retrieveLoanProductCharges(loanBasicDetails.loanProductId(),
ChargeTimeType.OVERDUE_INSTALLMENT);
paidInAdvanceTemplate = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId);
final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments,
charges, collateral, guarantors, meeting, productOptions, loanTermFrequencyTypeOptions, repaymentFrequencyTypeOptions,
repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions,
interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions,
loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations,
overdueCharges);
overdueCharges, paidInAdvanceTemplate);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters);
@@ -169,6 +169,9 @@ public class LoanAccountData {
private final boolean isInterestRecalculationEnabled;
private final LoanInterestRecalculationData interestRecalculationData;
//Paid In Advance
private final PaidInAdvanceData paidInAdvance;
/**
* Used to produce a {@link LoanAccountData} with only collateral options
@@ -262,6 +265,7 @@ public class LoanAccountData {
final boolean isInterestRecalculationEnabled = false;
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final PaidInAdvanceData paidInAdvance = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -277,7 +281,7 @@ public class LoanAccountData {
syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount,
disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingLoanBalance, emiAmountVariations, memberVariations,
product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, originalSchedule);
isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, paidInAdvance);
}
/**
@@ -372,6 +376,7 @@ public class LoanAccountData {
final boolean isInterestRecalculationEnabled = false;
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final PaidInAdvanceData paidInAdvance = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -386,7 +391,7 @@ public class LoanAccountData {
loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter,
notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount,
maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges,
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule);
isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, paidInAdvance);
}
public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) {
@@ -409,7 +414,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
acc.originalSchedule);
acc.originalSchedule, acc.paidInAdvance);
}
/**
@@ -505,6 +510,7 @@ public class LoanAccountData {
final boolean isInterestRecalculationEnabled = false;
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final PaidInAdvanceData paidInAdvance = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -519,7 +525,7 @@ public class LoanAccountData {
loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter,
notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingBalance,
emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType,
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule);
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, paidInAdvance);
}
public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) {
@@ -541,7 +547,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
acc.originalSchedule);
acc.originalSchedule, acc.paidInAdvance);
}
public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product,
@@ -650,6 +656,7 @@ public class LoanAccountData {
final Boolean inArrears = null;
final Boolean isNPA = null;
final LoanScheduleData originalSchedule = null;
final PaidInAdvanceData paidInAdvance = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType,
product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), loanPurposeId,
@@ -668,7 +675,7 @@ public class LoanAccountData {
product.getOutstandingLoanBalance(), emiAmountVariations, memberVariations, product, inArrears,
product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), isNPA, product.getDaysInMonthType(),
product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(),
originalSchedule);
originalSchedule, paidInAdvance);
}
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
@@ -694,6 +701,7 @@ public class LoanAccountData {
final Collection<CodeValueData> loanPurposeOptions = null;
final Collection<CodeValueData> loanCollateralOptions = null;
final Collection<CalendarData> calendarOptions = null;
final PaidInAdvanceData paidInAdvance = null;
final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery();
final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType();
@@ -722,7 +730,7 @@ public class LoanAccountData {
product.getOutstandingLoanBalance(), acc.emiAmountVariations, acc.memberVariations, product, acc.inArrears,
product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), acc.isNPA, product.getDaysInMonthType(),
product.getDaysInYearType(), product.isInterestRecalculationEnabled(), product.toLoanInterestRecalculationData(),
acc.originalSchedule);
acc.originalSchedule, paidInAdvance);
}
/*
@@ -778,6 +786,7 @@ public class LoanAccountData {
final LoanProductData product = null;
final Collection<ChargeData> overdueCharges = null;
final LoanScheduleData originalSchedule = null;
final PaidInAdvanceData paidInAdvance = null;
return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId,
loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName,
@@ -792,7 +801,7 @@ public class LoanAccountData {
loanPurposeOptions, loanCollateralOptions, calendarOptions, syncDisbursementWithMeeting, loancounter, loanProductCounter,
notes, accountLinkingOptions, linkedAccount, disbursementData, multiDisburseLoan, fixedEmiAmont, outstandingLoanBalance,
emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType,
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule);
daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, paidInAdvance);
}
/*
@@ -812,7 +821,8 @@ public class LoanAccountData {
final Collection<CodeValueData> loanCollateralOptions, final Collection<CalendarData> calendarOptions,
final Collection<NoteData> notes, final Collection<PortfolioAccountData> accountLinkingOptions,
final PortfolioAccountData linkedAccount, final Collection<DisbursementData> disbursementDetails,
final Collection<LoanTermVariationsData> emiAmountVariations, final Collection<ChargeData> overdueCharges) {
final Collection<LoanTermVariationsData> emiAmountVariations, final Collection<ChargeData> overdueCharges,
final PaidInAdvanceData paidInAdvance) {
return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId,
acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName,
@@ -830,7 +840,7 @@ public class LoanAccountData {
accountLinkingOptions, linkedAccount, disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount,
acc.maxOutstandingLoanBalance, emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears,
acc.graceOnArrearsAgeing, overdueCharges, acc.isNPA, acc.daysInMonthType, acc.daysInYearType,
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule);
acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, paidInAdvance);
}
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
@@ -841,7 +851,7 @@ public class LoanAccountData {
acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions,
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions,
acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges);
acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance);
}
public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) {
@@ -862,7 +872,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
acc.originalSchedule);
acc.originalSchedule, acc.paidInAdvance);
}
public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) {
@@ -920,7 +930,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
acc.originalSchedule);
acc.originalSchedule, acc.paidInAdvance);
}
public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
@@ -944,7 +954,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, interestRecalculationData,
acc.originalSchedule);
acc.originalSchedule, acc.paidInAdvance);
}
public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
@@ -966,7 +976,7 @@ public class LoanAccountData {
acc.linkedAccount, acc.disbursementDetails, acc.multiDisburseLoan, acc.fixedEmiAmount, acc.maxOutstandingLoanBalance,
acc.emiAmountVariations, acc.memberVariations, acc.product, acc.inArrears, acc.graceOnArrearsAgeing, acc.overdueCharges,
acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData,
originalSchedule);
originalSchedule, acc.paidInAdvance);
}
private LoanAccountData(
@@ -1022,7 +1032,8 @@ public class LoanAccountData {
final LoanProductData product, final Boolean inArrears, final Integer graceOnArrearsAgeing,
final Collection<ChargeData> overdueCharges, final Boolean isNPA, final EnumOptionData daysInMonthType,
final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled,
final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule) {
final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule,
final PaidInAdvanceData paidInAdvance) {
this.id = id;
this.accountNo = accountNo;
this.status = status;
@@ -1167,6 +1178,7 @@ public class LoanAccountData {
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
this.interestRecalculationData = interestRecalculationData;
this.originalSchedule = originalSchedule;
this.paidInAdvance = paidInAdvance;
}
public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData() {
@@ -30,6 +30,7 @@ public class LoanTransactionEnumData {
private final boolean rejectTransfer;
private final boolean chargePayment;
private final boolean refund;
private final boolean refundForActiveLoans;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -50,6 +51,7 @@ public class LoanTransactionEnumData {
this.rejectTransfer = Long.valueOf(15).equals(this.id);
this.refund = Long.valueOf(16).equals(this.id);
this.chargePayment = Long.valueOf(17).equals(this.id);
this.refundForActiveLoans = Long.valueOf(18).equals(this.id);
}
public Long id() {
@@ -130,5 +132,9 @@ public class LoanTransactionEnumData {
public boolean isRefund() {
return this.refund;
}
public boolean isRefundForActiveLoans() {
return this.refundForActiveLoans;
}
}
@@ -0,0 +1,23 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.data;
import java.math.BigDecimal;
public class PaidInAdvanceData {
private final BigDecimal paidInAdvance;
public PaidInAdvanceData(final BigDecimal paidInAdvance) {
this.paidInAdvance = paidInAdvance;
}
public BigDecimal getPaidInAdvance() {
return paidInAdvance;
}
}
@@ -82,6 +82,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanR
import org.mifosplatform.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
@@ -2955,7 +2956,7 @@ public class Loan extends AbstractPersistable<Long> {
}
for (final LoanTransaction loanTransaction : this.loanTransactions) {
if (loanTransaction.isRefund() && !loanTransaction.isReversed()) {
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) && !loanTransaction.isReversed()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
}
}
@@ -3972,6 +3973,11 @@ public class Loan extends AbstractPersistable<Long> {
action = "charge.payment";
postfix = "cannot.be.made.before.client.transfer.date";
break;
case LOAN_REFUND:
errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office";
action = "refund";
postfix = "cannot.be.made.before.client.transfer.date";
break;
default:
break;
}
@@ -4177,6 +4183,15 @@ public class Loan extends AbstractPersistable<Long> {
dataValidationErrors.add(error);
}
break;
case LOAN_REFUND:
if (!isOpen()) {
final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError(
"error.msg.loan.refund.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
default:
break;
}
@@ -4724,4 +4739,129 @@ public class Loan extends AbstractPersistable<Long> {
}
return isEnabled;
}
public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
validateAccountStatus(LoanEvent.LOAN_REFUND);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REFUND, loanTransaction.getTransactionDate());
validateRefundDateIsAfterLastRepayment(loanTransaction.getTransactionDate());
validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRefundTransaction(loanTransaction, loanLifecycleStateMachine, null);
return changedTransactionDetail;
}
private void validateRefundDateIsAfterLastRepayment(final LocalDate refundTransactionDate) {
// TODO Auto-generated method stub
final LocalDate possibleNextRefundDate = possibleNextRefundDate();
if (possibleNextRefundDate == null || refundTransactionDate.isBefore(possibleNextRefundDate)) { throw new InvalidRefundDateException(
refundTransactionDate.toString()); }
}
private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
ChangedTransactionDetail changedTransactionDetail = null;
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
// final boolean isTransactionChronologicallyLatest =
// isChronologicallyLatestRefund(loanTransaction,
// this.loanTransactions);
if (status().isOverpaid() || status().isClosed()) {
final String errorMessage = "This refund option is only for active loans ";
throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, this.totalOverpaid,
loanTransaction.getAmount(getCurrency()).getAmount());
} else if (this.getTotalPaidInRepayments().isZero()) {
final String errorMessage = "Cannot refund when no payment has been made";
throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage);
}
if (loanTransaction.isNotZero(loanCurrency())) {
this.loanTransactions.add(loanTransaction);
}
if (loanTransaction.isNotRefundForActiveLoan()) {
final String errorMessage = "A transaction of type refund was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage);
}
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
loanTransactionDate, getDisbursementDate());
}
if (loanTransactionDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) {
BigDecimal totalDisbursed = getDisbursedAmount();
if (totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction cannot be done before the loan disbursement: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage);
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
// If is a refund
if (adjustedTransaction == null) {
loanRepaymentScheduleTransactionProcessor.handleRefund(loanTransaction, getCurrency(), this.repaymentScheduleInstallments,
charges());
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, charges(), null);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
}
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
return changedTransactionDetail;
}
public LocalDate possibleNextRefundDate() {
final LocalDate now = new LocalDate();
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if ((transaction.isRepayment() || transaction.isRefundForActiveLoan()) && transaction.isNonZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
return lastTransactionDate == null ? now : lastTransactionDate;
}
}
@@ -30,6 +30,10 @@ public interface LoanAccountDomainService {
PaymentDetail paymentDetail, String noteText, String txnExternalId);
LocalDate getCalculatedRepaymentsStartingFromDate(LocalDate actualDisbursementDate, Loan loan, CalendarInstance calendarInstance);
LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult,
LocalDate transactionDate, BigDecimal transactionAmount,
PaymentDetail paymentDetail, String noteText, String txnExternalId);
/**
* This method is to recalculate and accrue the income till the last accrued
@@ -500,5 +500,46 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.accountTransferRepository.save(transferTransaction);
}
}
@Override
public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult,
LocalDate transactionDate, BigDecimal transactionAmount,
PaymentDetail paymentDetail, String noteText, String txnExternalId) {
// TODO Auto-generated method stub
final Loan loan = this.loanAccountAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail, transactionDate,
txnExternalId);
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(),
transactionDate.toDate(), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
final ChangedTransactionDetail changedTransactionDetail = loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds,
allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
this.loanTransactionRepository.save(newRefundTransaction);
this.loanRepository.save(loan);
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, newRefundTransaction, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
recalculateAccruals(loan);
builderResult.withEntityId(newRefundTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()); //
return newRefundTransaction;
}
}
@@ -883,4 +883,48 @@ public class LoanCharge extends AbstractPersistable<Long> {
public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() {
return this.overdueInstallmentCharge;
}
public Money undoPaidOrPartiallyAmountBy(final Money incrementBy,final Integer installmentNumber, final Money feeAmount) {
Money processAmount = Money.zero(incrementBy.getCurrency());
if(isInstalmentFee()){
if(installmentNumber == null){
processAmount = getLastPaidOrPartiallyPaidInstallmentLoanCharge(incrementBy.getCurrency()).undoPaidAmountBy(incrementBy, feeAmount);
}else{
processAmount = getInstallmentLoanCharge(installmentNumber).undoPaidAmountBy(incrementBy, feeAmount);
}
}else{
processAmount = incrementBy;
}
Money amountPaidToDate = Money.of(processAmount.getCurrency(), this.amountPaid);
Money amountDeductedOnThisCharge = Money.zero(processAmount.getCurrency());
if (processAmount.isGreaterThanOrEqualTo(amountPaidToDate)) {
amountDeductedOnThisCharge = amountPaidToDate;
amountPaidToDate = Money.zero(processAmount.getCurrency());
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = this.amount;
this.paid = false;
} else {
amountDeductedOnThisCharge = processAmount;
amountPaidToDate = amountPaidToDate.minus(processAmount);
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency());
}
return amountDeductedOnThisCharge;
}
public LoanInstallmentCharge getLastPaidOrPartiallyPaidInstallmentLoanCharge(MonetaryCurrency currency) {
LoanInstallmentCharge paidChargePerInstallment = null;
for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) {
Money outstanding = Money.of(currency, loanChargePerInstallment.getAmountOutstanding());
final boolean partiallyPaid = outstanding.isGreaterThanZero() && outstanding.isLessThan(loanChargePerInstallment.getAmount(currency));
if ((partiallyPaid || loanChargePerInstallment.isPaid())
&& (paidChargePerInstallment == null || paidChargePerInstallment.getRepaymentInstallment().getDueDate()
.isBefore(loanChargePerInstallment.getRepaymentInstallment().getDueDate()))) {
paidChargePerInstallment = loanChargePerInstallment;
}
}
return paidChargePerInstallment;
}
}
@@ -27,5 +27,6 @@ public enum LoanEvent {
LOAN_OVERPAYMENT, //
LOAN_CHARGE_PAYMENT, //
LOAN_CLOSED, //
LOAN_EDIT_MULTI_DISBURSE_DATE;
LOAN_EDIT_MULTI_DISBURSE_DATE, //
LOAN_REFUND;
}
@@ -252,5 +252,27 @@ public class LoanInstallmentCharge extends AbstractPersistable<Long> {
public void updateInstallment(LoanRepaymentScheduleInstallment installment) {
this.installment = installment;
}
public Money undoPaidAmountBy(final Money incrementBy, final Money feeAmount) {
Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid);
Money amountToDeductOnThisCharge = Money.zero(incrementBy.getCurrency());
if (incrementBy.isGreaterThanOrEqualTo(amountPaidToDate)) {
amountToDeductOnThisCharge = amountPaidToDate;
amountPaidToDate = Money.zero(incrementBy.getCurrency());
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = this.amount;
} else {
amountToDeductOnThisCharge = incrementBy;
amountPaidToDate = amountPaidToDate.minus(incrementBy);
this.amountPaid = amountPaidToDate.getAmount();
this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency());
}
this.amountThroughChargePayment = feeAmount.getAmount();
this.paid = determineIfFullyPaid();
return amountToDeductOnThisCharge;
}
}
@@ -563,6 +563,9 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
if (this.obligationsMet) {
this.obligationsMetOnDate = transactionDate.toDate();
}
else {
this.obligationsMetOnDate = null;
}
}
public void updateDueDate(final LocalDate newDueDate) {
@@ -645,4 +648,113 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
return obligationsMetOnDate;
}
/********** UNPAY COMPONENTS ****/
public Money unpayPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money penaltyPortionOfTransactionDeducted = Money.zero(currency);
final Money penaltyChargesCompleted = getPenaltyChargesPaid(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(penaltyChargesCompleted)) {
this.penaltyChargesPaid = Money.zero(currency).getAmount();
penaltyPortionOfTransactionDeducted = penaltyChargesCompleted;
} else {
this.penaltyChargesPaid = penaltyChargesCompleted.minus(transactionAmountRemaining).getAmount();
penaltyPortionOfTransactionDeducted = transactionAmountRemaining;
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return penaltyPortionOfTransactionDeducted;
}
public Money unpayFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money feePortionOfTransactionDeducted = Money.zero(currency);
final Money feeChargesCompleted = getFeeChargesPaid(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(feeChargesCompleted)) {
this.feeChargesPaid = Money.zero(currency).getAmount();
feePortionOfTransactionDeducted = feeChargesCompleted;
} else {
this.feeChargesPaid = feeChargesCompleted.minus(transactionAmountRemaining).getAmount();
feePortionOfTransactionDeducted = transactionAmountRemaining;
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, feePortionOfTransactionDeducted);
return feePortionOfTransactionDeducted;
}
public Money unpayInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money interestPortionOfTransactionDeducted = Money.zero(currency);
final Money interestCompleted = getInterestPaid(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestCompleted)) {
this.interestPaid = Money.zero(currency).getAmount();
interestPortionOfTransactionDeducted = interestCompleted;
} else {
this.interestPaid = interestCompleted.minus(transactionAmountRemaining).getAmount();
interestPortionOfTransactionDeducted = transactionAmountRemaining;
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, interestPortionOfTransactionDeducted);
return interestPortionOfTransactionDeducted;
}
public Money unpayPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money principalPortionOfTransactionDeducted = Money.zero(currency);
final Money principalCompleted = getPrincipalCompleted(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalCompleted)) {
this.principalCompleted = Money.zero(currency).getAmount();
principalPortionOfTransactionDeducted = principalCompleted;
} else {
this.principalCompleted = principalCompleted.minus(transactionAmountRemaining).getAmount();
principalPortionOfTransactionDeducted = transactionAmountRemaining;
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
reduceAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, principalPortionOfTransactionDeducted);
return principalPortionOfTransactionDeducted;
}
private void reduceAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency,
final Money amountDeductedInRepaymentPeriod) {
if (isInAdvance(transactionDate)) {
Money mTotalPaidInAdvance = Money.of(currency,this.totalPaidInAdvance);
if(mTotalPaidInAdvance.isLessThan(amountDeductedInRepaymentPeriod) || mTotalPaidInAdvance.isEqualTo(amountDeductedInRepaymentPeriod))
this.totalPaidInAdvance = Money.zero(currency).getAmount();
else
this.totalPaidInAdvance = mTotalPaidInAdvance.minus(amountDeductedInRepaymentPeriod).getAmount();
} else if (isLatePayment(transactionDate)) {
Money mTotalPaidLate = Money.of(currency,this.totalPaidLate);
if(mTotalPaidLate.isLessThan(amountDeductedInRepaymentPeriod) || mTotalPaidLate.isEqualTo(amountDeductedInRepaymentPeriod))
this.totalPaidLate = Money.zero(currency).getAmount();
else
this.totalPaidLate = mTotalPaidLate.minus(amountDeductedInRepaymentPeriod).getAmount();
}
}
public Money getDue(MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterestCharged(currency)).plus(getFeeChargesCharged(currency)).plus(getPenaltyChargesCharged(currency));
}
}
@@ -204,6 +204,11 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
return applyCharge;
}
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId) {
return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(), paymentDate, externalId);
}
public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction,
final LoanTransaction newLoanTransaction) {
@@ -577,5 +582,14 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
this.outstandingLoanBalance = outstandingLoanBalance;
}
public boolean isNotRefundForActiveLoan() {
// TODO Auto-generated method stub
return !isRefundForActiveLoan();
}
public boolean isRefundForActiveLoan() {
return LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.equals(getTypeOf()) && isNotReversed();
}
}
@@ -37,7 +37,8 @@ public enum LoanTransactionType {
WITHDRAW_TRANSFER(14, "loanTransactionType.withdrawTransfer"), //
REJECT_TRANSFER(15, "loanTransactionType.rejectTransfer"), //
REFUND(16, "loanTransactionType.refund"), //
CHARGE_PAYMENT(17, "loanTransactionType.chargePayment");
CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"), //
REFUND_FOR_ACTIVE_LOAN(18, "loanTransactionType.refund");
private final Integer value;
private final String code;
@@ -109,6 +110,9 @@ public enum LoanTransactionType {
case 17:
loanTransactionType = LoanTransactionType.CHARGE_PAYMENT;
break;
case 18:
loanTransactionType = LoanTransactionType.REFUND_FOR_ACTIVE_LOAN;
break;
default:
loanTransactionType = LoanTransactionType.INVALID;
break;
@@ -151,4 +155,8 @@ public enum LoanTransactionType {
public boolean isChargePayment() {
return this.value.equals(LoanTransactionType.CHARGE_PAYMENT.getValue());
}
public boolean isRefundForActiveLoan() {
return this.value.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue());
}
}
@@ -5,7 +5,10 @@
*/
package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -22,6 +25,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
@@ -136,6 +140,16 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
for (final LoanTransaction loanTransaction : transactionstoBeProcessed) {
if(!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments,byDate);
}
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions
@@ -170,6 +184,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.resetDerivedComponents();
handleWriteOff(loanTransaction, currency, installments);
}
else if(loanTransaction.isRefundForActiveLoan()) {
loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments,charges);
}
}
return changedTransactionDetail;
}
@@ -510,6 +529,104 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
@Override
public void handleRefund(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
final Set<LoanCharge> charges) {
// TODO Auto-generated method stub
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1,
LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments, Collections.reverseOrder(byDate));
Money transactionAmountUnprocessed = loanTransaction
.getAmount(currency);
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
Money outstanding = currentInstallment
.getTotalOutstanding(currency);
Money due = currentInstallment.getDue(currency);
if (outstanding.isLessThan(due)) {
transactionAmountUnprocessed = handleRefundTransactionPaymentOfInstallment(
currentInstallment, loanTransaction,
transactionAmountUnprocessed);
}
if (transactionAmountUnprocessed.isZero())
break;
}
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees,
installmentNumber);
}
final Money penaltyCharges = loanTransaction
.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
undoChargesPaidAmountBy(loanTransaction, penaltyCharges,
loanPenalties, installmentNumber);
}
}
/**
* Invoked when a there is a refund of an active loan or undo of an active
* loan
*
* Undoes principal, interest, fees and charges of this transaction based on
* the repayment strategy
*
*/
protected abstract Money handleRefundTransactionPaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed);
private void undoChargesPaidAmountBy(final LoanTransaction loanTransaction,
final Money feeCharges, final Set<LoanCharge> charges,
final Integer installmentNumber) {
Money amountRemaining = feeCharges;
while (amountRemaining.isGreaterThanZero()) {
final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(
charges, feeCharges.getCurrency());
if (paidCharge != null) {
Money feeAmount = feeCharges.zero();
final Money amountDeductedTowardsCharge = paidCharge
.undoPaidOrPartiallyAmountBy(amountRemaining,
installmentNumber, feeAmount);
if (amountDeductedTowardsCharge.isGreaterThanZero()) {
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(
loanTransaction, paidCharge,
amountDeductedTowardsCharge.getAmount().multiply(
new BigDecimal(-1)), null);
loanTransaction.getLoanChargesPaid().add(loanChargePaidBy);
amountRemaining = amountRemaining
.minus(amountDeductedTowardsCharge);
}
}
}
}
@Override
public ChangedTransactionDetail populateDerivedFeildsWithoutReprocess(final LocalDate disbursementDate,
@@ -519,5 +636,48 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return handleTransaction(disbursementDate, transactionsPostDisbursement, currency, installments, charges, recalculateChargesFrom,
reprocessCharges);
}
private LoanCharge findLatestPaidChargeFromUnOrderedSet(
final Set<LoanCharge> charges, MonetaryCurrency currency) {
LoanCharge latestPaidCharge = null;
LoanCharge installemntCharge = null;
LoanInstallmentCharge chargePerInstallment = null;
for (final LoanCharge loanCharge : charges) {
boolean isPaidOrPartiallyPaid = loanCharge
.isPaidOrPartiallyPaid(currency);
if (isPaidOrPartiallyPaid && !loanCharge.isDueAtDisbursement()) {
if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge paidLoanChargePerInstallment = loanCharge
.getLastPaidOrPartiallyPaidInstallmentLoanCharge(currency);
if (chargePerInstallment == null
|| (paidLoanChargePerInstallment != null && chargePerInstallment
.getRepaymentInstallment()
.getDueDate()
.isBefore(
paidLoanChargePerInstallment
.getRepaymentInstallment()
.getDueDate()))) {
installemntCharge = loanCharge;
chargePerInstallment = paidLoanChargePerInstallment;
}
} else if (latestPaidCharge == null
|| (loanCharge.isPaidOrPartiallyPaid(currency))
&& loanCharge.getDueLocalDate().isAfter(
latestPaidCharge.getDueLocalDate())) {
latestPaidCharge = loanCharge;
}
}
}
if (latestPaidCharge == null
|| (chargePerInstallment != null && latestPaidCharge
.getDueLocalDate().isAfter(
chargePerInstallment.getRepaymentInstallment()
.getDueDate()))) {
latestPaidCharge = installemntCharge;
}
return latestPaidCharge;
}
}
@@ -45,5 +45,10 @@ public interface LoanRepaymentScheduleTransactionProcessor {
ChangedTransactionDetail populateDerivedFeildsWithoutReprocess(LocalDate disbursementDate,
List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, LocalDate recalculateChargesFrom);
void handleRefund(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
final Set<LoanCharge> charges);
}
@@ -129,4 +129,37 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
if(transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
if(transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if(transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -137,5 +137,42 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
return transactionAmountRemaining;
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -181,4 +181,41 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -104,4 +104,40 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
return transactionAmountRemaining;
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -123,4 +123,38 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends Abstrac
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// TODO - KW - dont do anything with loan over-payment for now
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -104,4 +104,42 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
return transactionAmountRemaining;
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -209,4 +209,41 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
return true;
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (transactionAmountRemaining.isGreaterThanZero()) {
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
}
if (transactionAmountRemaining.isGreaterThanZero()) {
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
}
@@ -0,0 +1,21 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* {@link AbstractPlatformDomainRuleException} thrown an action to transition a
* loan from one state to another violates a domain rule.
*/
public class InvalidPaidInAdvanceAmountException extends AbstractPlatformDomainRuleException {
public InvalidPaidInAdvanceAmountException(final String refundAmountString) {
super("error.msg.loan.refund.amount.invalid", "The refund amount `" + refundAmountString + "`"
+ "` is invalid or loan is not paid in advance.", new Object[] { refundAmountString});
}
}
@@ -0,0 +1,20 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class InvalidRefundDateException extends AbstractPlatformDomainRuleException {
public InvalidRefundDateException(final String refundDateAsString) {
super("error.msg.loan.refund.failed", "The refund date`" + refundDateAsString + "`"
+ "` cannot be before the smallest repayment transaction date", new Object[] { refundDateAsString});
}
public InvalidRefundDateException(final String defaultUserMessage,final String entity,final Object... defaultUserMessageArgs) {
super("error.msg.loan." + entity , defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LoanRefundByCashCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
@Autowired
public LoanRefundByCashCommandHandler(final LoanWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.makeLoanRefund(command.getLoanId(), command);
}
}
@@ -388,5 +388,33 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateNewRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Set<String> transactionParameters = new HashSet<String>(Arrays.asList("transactionDate", "transactionAmount", "externalId",
"note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber",
"bankNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
}
@@ -6,6 +6,7 @@
package org.mifosplatform.portfolio.loanaccount.service;
import java.util.Collection;
import java.util.Date;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.service.Page;
@@ -17,8 +18,8 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData;
import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
@@ -97,4 +98,10 @@ public interface LoanReadPlatformService {
Collection<LoanTransactionData> retrieveWaiverLoanTransactions(Long loanId);
Collection<LoanSchedulePeriodData> fetchWaiverInterestRepaymentData(Long loanId);
Date retrieveMinimumDateOfRepaymentTransaction(Long loanId);
PaidInAdvanceData retrieveTotalPaidInAdvance(Long loanId);
LoanTransactionData retrieveRefundByCashTemplate(Long loanId);
}
@@ -11,6 +11,7 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -67,6 +68,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanSummaryData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTermVariationsData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.mifosplatform.portfolio.loanaccount.data.PaidInAdvanceData;
import org.mifosplatform.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -1744,5 +1746,60 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
}
@Override
public Date retrieveMinimumDateOfRepaymentTransaction(Long loanId) {
// TODO Auto-generated method stub
Date date = this.jdbcTemplate.queryForObject(
"select min(transaction_date) from m_loan_transaction where loan_id=? and transaction_type_enum=2",
new Object[] { loanId }, Date.class);
return date;
}
@Override
public PaidInAdvanceData retrieveTotalPaidInAdvance(Long loanId) {
// TODO Auto-generated method stub
try {
final String sql = " select (SUM(ifnull(mr.principal_completed_derived, 0)) +"
+ " + SUM(ifnull(mr.interest_completed_derived, 0)) " + " + SUM(ifnull(mr.fee_charges_completed_derived, 0)) "
+ " + SUM(ifnull(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived "
+ " from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "
+ " where ml.id=? and mr.duedate >= CURDATE() group by ml.id having "
+ " (SUM(ifnull(mr.principal_completed_derived, 0)) " + " + SUM(ifnull(mr.interest_completed_derived, 0)) "
+ " + SUM(ifnull(mr.fee_charges_completed_derived, 0)) "
+ "+ SUM(ifnull(mr.penalty_charges_completed_derived, 0))) > 0";
BigDecimal bigDecimal = this.jdbcTemplate.queryForObject(sql, BigDecimal.class, loanId);
return new PaidInAdvanceData(bigDecimal);
} catch (DataAccessException e) {
// TODO Auto-generated catch block
return new PaidInAdvanceData(new BigDecimal(0));
}
}
@Override
public LoanTransactionData retrieveRefundByCashTemplate(Long loanId) {
// TODO Auto-generated method stub
this.context.authenticatedUser();
// TODO - KW - OPTIMIZE - write simple sql query to fetch back date of
// possible next transaction date.
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) { throw new LoanNotFoundException(loanId); }
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CurrencyData currencyData = applicationCurrency.toData();
final LocalDate earliestUnpaidInstallmentDate = new LocalDate();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN);
final Collection<CodeValueData> paymentOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(PaymentDetailConstants.paymentTypeCodeName);
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), null, null, null, null, null, null, paymentOptions, null, null,
null, null);
}
}
@@ -81,4 +81,6 @@ public interface LoanWritePlatformService {
void recalculateInterest();
CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command);
}
@@ -9,6 +9,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -109,6 +110,8 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException;
import org.mifosplatform.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanDisbursalException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException;
@@ -2557,5 +2560,54 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
return installments;
}
@Override
@Transactional
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
// TODO Auto-generated method stub
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
//checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate);
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
checkIfLoanIsPaidInAdvance(loanId, transactionAmount);
final Map<String, Object> changes = new LinkedHashMap<String, Object>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
}
final PaymentDetail paymentDetail = null;
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, transactionDate, transactionAmount,
paymentDetail, noteText, null);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
private void checkIfLoanIsPaidInAdvance(final Long loanId,final BigDecimal transactionAmount) {
BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId).getPaidInAdvance();
if(overpaid == null || overpaid.equals(new BigDecimal(0)) || transactionAmount.floatValue() > overpaid.floatValue()) {
if(overpaid == null)
overpaid = BigDecimal.ZERO;
throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString());
}
}
}
@@ -319,6 +319,10 @@ public class LoanEnumerations {
optionData = new LoanTransactionEnumData(LoanTransactionType.CHARGE_PAYMENT.getValue().longValue(),
LoanTransactionType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case REFUND_FOR_ACTIVE_LOAN:
optionData = new LoanTransactionEnumData(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue().longValue(),
LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getCode(), "Refund");
break;
default:
break;
}
@@ -0,0 +1,16 @@
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('transaction_savings', 'REFUNDBYTRANSFER_ACCOUNTTRANSFER_CHECKER', 'ACCOUNTTRANSFER', 'REFUNDBYTRANSFER', 0);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
( 'transaction_savings', 'REFUNDBYTRANSFER_ACCOUNTTRANSFER', 'ACCOUNTTRANSFER', 'REFUNDBYTRANSFER', 1);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('transaction_loan', 'REFUNDBYCASH_LOAN', 'LOAN', 'REFUNDBYCASH', 1);
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('transaction_loan', 'REFUNDBYCASH_LOAN_CHECKER', 'LOAN', 'REFUNDBYCASH', 0);