Merge branch 'develop' of git://github.com/michaeldk-miagstan/mifosx into michaeldk-miagstan-develop

Conflicts:
	mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanTransactionHelper.java
	mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/PortfolioAccountReadPlatformService.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformService.java
	mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformService.java
This commit is contained in:
Vishwas Babu A J
2014-12-16 18:29:31 -08:00
51 changed files with 2691 additions and 71 deletions
+348 -1
View File
@@ -365,6 +365,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>
@@ -1062,6 +1078,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>
@@ -8914,6 +8938,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>
@@ -9000,6 +9029,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">
@@ -9259,7 +9327,7 @@ Request Body:
</code>
</div>
</div>
<!--Start of guarantors functionality-->
<a id="guarantors" name="guarantors" class="old-syle-anchor">&nbsp;</a>
@@ -25621,6 +25689,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,7 +35,10 @@ 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;
import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.builder.ResponseSpecBuilder;
@@ -66,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() {
@@ -891,6 +896,23 @@ public class ClientLoanIntegrationTest {
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--------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder() //
@@ -992,6 +1014,32 @@ public class ClientLoanIntegrationTest {
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--------------------------");
@@ -3820,4 +3868,539 @@ 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")));
}
private Integer createSavingsProduct(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final String minOpenningBalance) {
System.out.println("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------");
SavingsProductHelper savingsProductHelper = new SavingsProductHelper();
final String savingsProductJSON = savingsProductHelper
//
.withInterestCompoundingPeriodTypeAsDaily()
//
.withInterestPostingPeriodTypeAsMonthly()
//
.withInterestCalculationPeriodTypeAsDailyBalance()
.withMinimumOpenningBalance(minOpenningBalance).build();
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
}
}
@@ -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());
}
}
@@ -36,6 +36,7 @@ public class LoanTransactionHelper {
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash";
public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
this.requestSpec = requestSpec;
@@ -90,9 +91,9 @@ public class LoanTransactionHelper {
final String URL = "/mifosng-provider/api/v1/loans/" + loanID + "?associations=all&" + Utils.TENANT_IDENTIFIER;
return Utils.performServerGet(requestSpec, responseSpec, URL, param);
}
public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanProductId,
final String jsonAttributeToGetBack) {
public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer loanProductId, final String jsonAttributeToGetBack) {
final String URL = "/mifosng-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
return Utils.performServerGet(requestSpec, responseSpec, URL, jsonAttributeToGetBack);
}
@@ -421,6 +422,10 @@ public class LoanTransactionHelper {
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--------------------------------");
final ArrayList<HashMap> repaymentPeriods = getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
@@ -457,4 +462,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;
@@ -85,6 +88,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);
}
}
}
@@ -293,4 +300,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);
}
}
@@ -943,5 +943,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");
}
}
@@ -697,6 +697,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";
@@ -2185,4 +2194,12 @@ public class CommandWrapperBuilder {
this.json = "{}";
return this;
}
public CommandWrapperBuilder refundByTransfer() {
this.actionName = "REFUNDBYTRANSFER";
this.entityName = "ACCOUNTTRANSFER";
this.entityId = null;
this.href = "/refundByTransfer";
return this;
}
}
@@ -130,6 +130,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (wrapper.isAccountTransferResource()) {
if (wrapper.isCreate()) {
handler = this.applicationContext.getBean("createAccountTransferCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isRefundByTransfer()) {
handler = this.applicationContext.getBean("refundByTransferCommandHandler", NewCommandSourceHandler.class);
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
@@ -396,6 +398,10 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = this.applicationContext.getBean("undoWriteOffLoanCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isGuaranteeRecovery()) {
handler = this.applicationContext.getBean("recoverFromGuarantorCommandHandler", NewCommandSourceHandler.class);
} 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());
}
@@ -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);
}
@@ -411,4 +411,114 @@ public class AccountTransfersReadPlatformServiceImpl implements AccountTransfers
this.accountTransfersMapper);
}
@Override
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 };
}
PortfolioAccountDTO portfolioAccountDTO = new PortfolioAccountDTO(mostRelevantFromAccountType, mostRelevantFromClientId,
loanStatus);
fromAccountOptions = this.portfolioAccountReadPlatformService.retrieveAllForLookup(portfolioAccountDTO);
}
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
@@ -398,5 +405,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();
}
}
@@ -17,4 +17,6 @@ public interface PortfolioAccountReadPlatformService {
PortfolioAccountData retrieveOne(Long accountId, Integer accountTypeId, String currencyCode);
Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO);
PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId);
}
@@ -33,12 +33,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
@@ -265,4 +267,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); }
@@ -70,6 +70,7 @@ import org.mifosplatform.portfolio.loanaccount.data.LoanApprovalData;
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;
@@ -465,6 +466,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) {
@@ -510,13 +512,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,
null, null, 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);
@@ -177,6 +177,9 @@ public class LoanAccountData {
private final LoanInterestRecalculationData interestRecalculationData;
private final Boolean createStandingInstructionAtDisbursement;
// Paid In Advance
private final PaidInAdvanceData paidInAdvance;
/**
* Used to produce a {@link LoanAccountData} with only collateral options
* for now.
@@ -275,6 +278,7 @@ public class LoanAccountData {
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final Boolean createStandingInstructionAtDisbursement = 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,
@@ -291,7 +295,8 @@ 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, createStandingInstructionAtDisbursement);
isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement,
paidInAdvance);
}
/**
@@ -392,6 +397,7 @@ public class LoanAccountData {
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final Boolean createStandingInstructionAtDisbursement = 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,
@@ -408,8 +414,8 @@ 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, createStandingInstructionAtDisbursement);
isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement,
paidInAdvance);
}
public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) {
@@ -433,7 +439,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
/**
@@ -535,6 +541,7 @@ public class LoanAccountData {
final LoanInterestRecalculationData interestRecalculationData = null;
final LoanScheduleData originalSchedule = null;
final Boolean createStandingInstructionAtDisbursement = 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,
@@ -551,7 +558,7 @@ public class LoanAccountData {
syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount,
disbursementData, multiDisburseLoan, fixedEmiAmount, maxOutstandingBalance, emiAmountVariations, memberVariations, product,
inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement);
interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance);
}
public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) {
@@ -575,7 +582,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product,
@@ -691,6 +698,7 @@ public class LoanAccountData {
final Boolean isNPA = null;
final LoanScheduleData originalSchedule = null;
final Boolean createStandingInstructionAtDisbursement = 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,
@@ -710,7 +718,7 @@ public class LoanAccountData {
disbursementData, product.getMultiDisburseLoan(), fixedEmi, product.getOutstandingLoanBalance(), emiAmountVariations,
memberVariations, product, inArrears, product.getGraceOnArrearsAgeing(), product.overdueFeeCharges(), isNPA,
product.getDaysInMonthType(), product.getDaysInYearType(), product.isInterestRecalculationEnabled(),
product.toLoanInterestRecalculationData(), originalSchedule, createStandingInstructionAtDisbursement);
product.toLoanInterestRecalculationData(), originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance);
}
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
@@ -736,6 +744,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();
@@ -764,7 +773,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance);
}
/*
@@ -823,6 +832,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,
@@ -839,7 +849,7 @@ public class LoanAccountData {
syncDisbursementWithMeeting, loancounter, loanProductCounter, notes, accountLinkingOptions, linkedAccount,
disbursementData, multiDisburseLoan, fixedEmiAmont, outstandingLoanBalance, emiAmountVariations, memberVariations, product,
inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement);
interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance);
}
/*
@@ -861,7 +871,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,
@@ -881,7 +892,7 @@ public class LoanAccountData {
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.createStandingInstructionAtDisbursement);
acc.createStandingInstructionAtDisbursement, paidInAdvance);
}
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
@@ -893,7 +904,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) {
@@ -916,7 +927,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) {
@@ -976,7 +987,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
@@ -1002,7 +1013,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.createStandingInstructionAtDisbursement);
acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
@@ -1026,7 +1037,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, acc.createStandingInstructionAtDisbursement);
originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance);
}
private LoanAccountData(
@@ -1087,8 +1098,7 @@ public class LoanAccountData {
final Collection<ChargeData> overdueCharges, final Boolean isNPA, final EnumOptionData daysInMonthType,
final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled,
final LoanInterestRecalculationData interestRecalculationData, final LoanScheduleData originalSchedule,
final Boolean createStandingInstructionAtDisbursement) {
final Boolean createStandingInstructionAtDisbursement, final PaidInAdvanceData paidInAdvance) {
this.id = id;
this.accountNo = accountNo;
this.status = status;
@@ -1239,6 +1249,7 @@ public class LoanAccountData {
this.interestRecalculationData = interestRecalculationData;
this.originalSchedule = originalSchedule;
this.createStandingInstructionAtDisbursement = createStandingInstructionAtDisbursement;
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;
}
}
@@ -85,6 +85,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;
@@ -1195,9 +1196,9 @@ public class Loan extends AbstractPersistable<Long> {
if (command.isChangeInBooleanParameterNamed(createSiAtDisbursementParameterName, shouldCreateStandingInstructionAtDisbursement())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(createSiAtDisbursementParameterName);
actualChanges.put(createSiAtDisbursementParameterName, valueAsInput);
this.createStandingInstructionAtDisbursement = valueAsInput;
this.createStandingInstructionAtDisbursement = valueAsInput;
}
final String externalIdParamName = "externalId";
if (command.isChangeInStringParameterNamed(externalIdParamName, this.externalId)) {
final String newValue = command.stringValueOfParameterNamed(externalIdParamName);
@@ -3073,7 +3074,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));
}
}
@@ -3853,15 +3854,13 @@ public class Loan extends AbstractPersistable<Long> {
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) {
// first repayment's from date is same as disbursement date.
/*
* meetingStartDate is used as seedDate Capture the seedDate from
* user and use the seedDate as meetingStart date
* meetingStartDate is used as seedDate Capture the seedDate from user
* and use the seedDate as meetingStart date
*/
LocalDate tmpFromDate = getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery();
@@ -3869,38 +3868,37 @@ public class Loan extends AbstractPersistable<Long> {
LocalDate newRepaymentDate = null;
Boolean isFirstTime = true;
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
if ( oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate) ) {
if(isFirstTime){
if (oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate)) {
if (isFirstTime) {
isFirstTime = false;
newRepaymentDate = newMeetingDate;
}else{
} else {
// tmpFromDate.plusDays(1) is done to make sure
// getNewRepaymentMeetingDate method returns next meeting
// date and not the same as tmpFromDate
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, tmpFromDate, tmpFromDate.plusDays(1),
loanRepaymentInterval, frequency, workingDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
if(!isFirstTime){
if (!isFirstTime) {
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
tmpFromDate = newRepaymentDate;// update with new repayment
// date
} else {
@@ -3911,7 +3909,7 @@ public class Loan extends AbstractPersistable<Long> {
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays) {
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
@@ -3920,18 +3918,17 @@ public class Loan extends AbstractPersistable<Long> {
LocalDate newRepaymentDate = null;
LocalDate seedDate = meetingStartDate;
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.repaymentScheduleInstallments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// FIXME: AA this won't update repayment dates before current date.
if ( oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant()) ) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate,
loanRepaymentInterval, frequency, workingDays);
if (oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant())) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, loanRepaymentInterval,
frequency, workingDays);
final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType,
loanRepaymentInterval, tmpFromDate);
@@ -4167,6 +4164,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;
}
@@ -4372,6 +4374,14 @@ 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;
}
@@ -4979,4 +4989,128 @@ public class Loan extends AbstractPersistable<Long> {
public BigDecimal getGuaranteeAmount() {
return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived;
}
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) {
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;
}
}
@@ -31,6 +31,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
@@ -548,4 +548,45 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
return user;
}
@Override
public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) {
final Loan loan = this.loanAccountAssembler.assembleFrom(accountId);
checkClientOrGroupActive(loan);
final List<Long> existingTransactionIds = new ArrayList<Long>();
final List<Long> existingReversedTransactionIds = new ArrayList<Long>();
AppUser currentUser = getAppUserIfPresent();
final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail,
transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
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));
}
}
@@ -229,6 +229,12 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return applyCharge;
}
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(),
paymentDate, externalId, createdDate, appUser);
}
public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction,
final LoanTransaction newLoanTransaction) {
if (loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
@@ -610,6 +616,15 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
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();
}
public boolean isManuallyAdjustedOrReversed() {
return this.manuallyAdjustedOrReversed;
}
@@ -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,18 +6,20 @@
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;
import org.mifosplatform.infrastructure.core.service.SearchParameters;
import org.mifosplatform.organisation.staff.data.StaffData;
import org.mifosplatform.portfolio.calendar.data.CalendarData;
import org.mifosplatform.infrastructure.core.service.SearchParameters;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanApprovalData;
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.loanschedule.data.LoanScheduleData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
@@ -45,7 +47,7 @@ public interface LoanReadPlatformService {
LoanTransactionData retrieveNewClosureDetails();
LoanTransactionData retrieveDisbursalTemplate(Long loanId, boolean paymentDetailsRequired);
LoanApprovalData retrieveApprovalTemplate(Long loanId);
LoanAccountData retrieveTemplateWithCompleteGroupAndProductDetails(Long groupId, Long productId);
@@ -101,4 +103,10 @@ public interface LoanReadPlatformService {
Collection<LoanSchedulePeriodData> fetchWaiverInterestRepaymentData(Long loanId);
boolean isGuaranteeRequired(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;
@@ -29,6 +30,7 @@ import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.core.service.Page;
import org.mifosplatform.infrastructure.core.service.PaginationHelper;
import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
import org.mifosplatform.infrastructure.core.service.SearchParameters;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
@@ -56,7 +58,6 @@ import org.mifosplatform.portfolio.fund.service.FundReadPlatformService;
import org.mifosplatform.portfolio.group.data.GroupGeneralData;
import org.mifosplatform.portfolio.group.data.GroupRoleData;
import org.mifosplatform.portfolio.group.service.GroupReadPlatformService;
import org.mifosplatform.infrastructure.core.service.SearchParameters;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.LoanAccountData;
import org.mifosplatform.portfolio.loanaccount.data.LoanApplicationTimelineData;
@@ -68,6 +69,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;
@@ -328,8 +330,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Collection<CalendarData> calendarsData = null;
final CalendarData collectionMeetingCalendar = null;
final Collection<GroupRoleData> groupRoles = null;
groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers,
groupRoles, calendarsData, collectionMeetingCalendar);
groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, groupRoles, calendarsData,
collectionMeetingCalendar);
}
final LocalDate expectedDisbursementDate = DateUtils.getLocalDateOfTenant();
@@ -418,8 +420,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final CurrencyData currencyData = applicationCurrency.toData();
final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction(
DateUtils.getLocalDateTimeOfTenant(), currentUser);
final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction(DateUtils.getLocalDateTimeOfTenant(),
currentUser);
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST);
@@ -448,7 +450,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) { throw new LoanNotFoundException(loanId); }
return new LoanApprovalData(loan.getProposedPrincipal(),DateUtils.getLocalDateOfTenant());
return new LoanApprovalData(loan.getProposedPrincipal(), DateUtils.getLocalDateOfTenant());
}
@@ -827,8 +829,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, groupData,
loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName,
loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid, inArrearsTolerance,
termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType,
loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, approvedPrincipal, totalOverpaid,
inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType,
repaymentFrequencyNthDayType, repaymentFrequencyDayOfWeekType, transactionStrategyId, transactionStrategyName,
amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType,
interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment,
@@ -1322,8 +1324,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Collection<CalendarData> calendarsData = null;
final CalendarData collectionMeetingCalendar = null;
final Collection<GroupRoleData> groupRoles = null;
groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers,
groupRoles, calendarsData, collectionMeetingCalendar);
groupAccount = GroupGeneralData.withAssocations(groupAccount, membersOfGroup, activeClientMembers, groupRoles, calendarsData,
collectionMeetingCalendar);
}
return LoanAccountData.groupDefaults(groupAccount, expectedDisbursementDate);
@@ -1785,4 +1787,59 @@ 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, false);
}
}
@@ -86,4 +86,6 @@ public interface LoanWritePlatformService {
void applyMeetingDateChanges(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances);
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.HashSet;
import java.util.LinkedHashMap;
@@ -122,6 +123,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;
@@ -2705,6 +2708,55 @@ 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());
}
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
@@ -364,6 +364,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);