MIFOSX-247: add basic lifecycle state to savings account and support some constrains around deposits and withdrawals.

This commit is contained in:
Keith Woodlock
2013-03-25 17:39:01 +00:00
parent 3e36a8a3cb
commit 63eba40115
36 changed files with 809 additions and 221 deletions
+62 -15
View File
@@ -7118,6 +7118,12 @@ Content-Type: application/json
<tr class=alt><td>externalId</td></tr>
<tr><td class=fielddesc>A place to put an external reference for this savings account e.g. The ID another system uses. If provided, it must be unique.</td></tr>
<tr class=alt><td>active</td></tr>
<tr><td class=fielddesc>Indicates whether this savings account should go into active state or not automatically</td></tr>
<tr class=alt><td>activationDate</td></tr>
<tr><td class=fielddesc>If <b>active</b> is true the <i>activationDate</i> must be provided. If the savings account has an entry for <i>minRequiredOpeningBalance</i> then a deposit for that amount with <i>activationDate</i> as the date is automatically entered.</td></tr>
<tr class=alt><td>interestRate</td></tr>
<tr><td class=fielddesc>The default interest rate set when creating savings accounts of this type of product. e.g. <b>5</b>% Per year</td></tr>
@@ -7257,7 +7263,7 @@ Content-Type: application/json
<br/>
<table class=matrixHeading>
<tr class="matrixHeadingBG"><td><div class="mifosXHeading2">Mandatory Fields</div></td></tr>
<tr class=alt><td>clientId or groupId, productId, accountNo, externalId</td></tr>
<tr class=alt><td>clientId or groupId, productId, accountNo, externalId, active, (activationDate if active is true)</td></tr>
</table>
<br/>
<table class=matrixHeading>
@@ -7273,27 +7279,29 @@ Content-Type: application/json
<div class="method-example">
<p>Minimal request: accountNo auto generated, remaining details inherited from savings product.</p>
<code class="method-declaration">POST https://Domain Name/api/v1/savingsaccounts</code>
<code class="method-request">POST savingaccount
Content-Type: application/json
Request Body:
{
"clientId": 1,
"productId": 1
}
</code>
<p>Minimal request: accountNo provided (must be unique), remaining details inherited from savings product.</p>
<code class="method-request">POST savingaccount
<code class="method-request">POST savingsaccount
Content-Type: application/json
Request Body:
{
"clientId": 1,
"productId": 1,
"active": false
}
</code>
<p>Minimal request: accountNo provided (must be unique), remaining details inherited from savings product.</p>
<code class="method-request">POST savingsaccount
Content-Type: application/json
Request Body:
{
"clientId": 1,
"productId": 1,
"active": false,
"accountNo": "SA000023",
"externalId": "SYS-23"
}
</code>
<p>Full request: accountNo provided (must be unique), remaining details override details from savings product (except currency).</p>
<code class="method-request">POST savingaccount
<code class="method-request">POST savingsaccount
Content-Type: application/json
Request Body:
{
@@ -7301,7 +7309,10 @@ Request Body:
"productId": 1,
"accountNo": "SA000023",
"externalId": "SYS-23",
"locale": "en",
"active": true,
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"activationDate": "01 March 2011",
"interestRate": "5.65",
"interestRatePeriodFrequencyType": 2,
"minRequiredOpeningBalance": "1,000",
@@ -7313,6 +7324,35 @@ Request Body:
{
"officeId": 1,
"clientId": 1,
"savingsId": 1,
"resourceId": 1
}
</code>
</div>
</div>
<a id="savingsaccounts_activate" name="savingsaccounts_activate" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h2>Activate a Savings Account</h2>
<p>Activates a savings account.</p>
</div>
<div class="method-example">
<code class="method-declaration">POST https://Domain Name/api/v1/savingsaccounts/{savingsId}?command=activate</code>
<code class="method-request">POST savingsaccount/1?command=activate
Content-Type: application/json
Request Body:
{
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"activationDate": "01 March 2011"
}
</code>
<code class="method-response">
{
"officeId": 1,
"clientId": 1,
"savingsId": 1,
"resourceId": 1
}
</code>
@@ -11436,6 +11476,14 @@ Content-Type: application/json
<td><a href="#savingsaccounts_delete">Delete a Savings Account</a></td>
</tr>
<tr class="alt">
<td></td>
<td>savingsaccounts/{accountId}?command=activate</td>
<td><a href="#savingsaccounts_activate">Activate a Savings Account</a></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#savingsaccounts_transactions">Savings Transactions</a></td>
<td>savingsaccounts/{accountId}/transactions?command=deposit</td>
<td><a href="#savingsaccounts_deposit">Make a Deposit</a></td>
@@ -11443,7 +11491,7 @@ Content-Type: application/json
<td></td>
<td></td>
</tr>
<tr>
<tr class="alt">
<td></td>
<td>savingsaccounts/{accountId}/transactions?command=withdrawal</td>
<td><a href="#savingsaccounts_withdrawal">Make a Withdrawal</a></td>
@@ -11467,7 +11515,6 @@ Content-Type: application/json
<td><a href="#offices_update">Update an Office</a></td>
<td></td>
</tr>
<tr class="alt">
<td><a href="#configs">CURRENCY</a></td>
<td>currencies</td>
@@ -729,6 +729,7 @@ CREATE TABLE `m_savings_product` (
UNIQUE KEY `sp_unq_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `m_savings_account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_no` varchar(20) NOT NULL,
@@ -736,6 +737,8 @@ CREATE TABLE `m_savings_account` (
`client_id` bigint(20) DEFAULT NULL,
`group_id` bigint(20) DEFAULT NULL,
`product_id` bigint(20) DEFAULT NULL,
`status_enum` SMALLINT(5) NOT NULL DEFAULT 300,
`activation_date` DATE DEFAULT NULL,
`currency_code` varchar(3) NOT NULL,
`currency_digits` smallint(5) NOT NULL,
`nominal_interest_rate_per_period` decimal(19,6) NOT NULL,
@@ -744,6 +747,7 @@ CREATE TABLE `m_savings_account` (
`min_required_opening_balance` decimal(19,6) DEFAULT NULL,
`lockin_period_frequency` decimal(19,6) DEFAULT NULL,
`lockin_period_frequency_enum` smallint(5) DEFAULT NULL,
`lockedin_until_date_derived` DATE DEFAULT NULL,
`total_deposits_derived` decimal(19,6) DEFAULT NULL,
`total_withdrawals_derived` decimal(19,6) DEFAULT NULL,
`total_interest_posted_derived` decimal(19,6) DEFAULT NULL,
@@ -17,19 +17,36 @@ INSERT INTO `r_enum_value`
VALUES
('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),
('amortization_method_enum',1,'Equal installments','Equal installments'),
('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),
('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),
('interest_period_frequency_enum',2,'Per month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),
('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),
('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),
('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),
('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('loan_transaction_strategy_id',2,'heavensfamily-strategy','Heavensfamily'),
('loan_transaction_strategy_id',3,'creocore-strategy','Creocore'),('loan_transaction_strategy_id',4,'rbi-india-strategy','RBI (India)'),
('processing_result_enum',0,'invalid','Invalid'),('processing_result_enum',1,'processed','Processed'),
('processing_result_enum',2,'awaiting.approval','Awaiting Approval'),('processing_result_enum',3,'rejected','Rejected'),
('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),
('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),
('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years');
('interest_calculated_in_period_enum',0,'Daily','Daily'),
('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),
('interest_method_enum',0,'Declining Balance','Declining Balance'),
('interest_method_enum',1,'Flat','Flat'),
('interest_period_frequency_enum',2,'Per month','Per month'),
('interest_period_frequency_enum',3,'Per year','Per year'),
('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),
('loan_status_id',200,'Approved','Approved'),
('loan_status_id',300,'Active','Active'),
('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),
('loan_status_id',500,'Rejected','Rejected'),
('loan_status_id',600,'Closed','Closed'),
('loan_status_id',601,'Written-Off','Written-Off'),
('loan_status_id',602,'Rescheduled','Rescheduled'),
('loan_status_id',700,'Overpaid','Overpaid'),
('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),
('loan_transaction_strategy_id',2,'heavensfamily-strategy','Heavensfamily'),
('loan_transaction_strategy_id',3,'creocore-strategy','Creocore'),
('loan_transaction_strategy_id',4,'rbi-india-strategy','RBI (India)'),
('processing_result_enum',0,'invalid','Invalid'),
('processing_result_enum',1,'processed','Processed'),
('processing_result_enum',2,'awaiting.approval','Awaiting Approval'),
('processing_result_enum',3,'rejected','Rejected'),
('repayment_period_frequency_enum',0,'Days','Days'),
('repayment_period_frequency_enum',1,'Weeks','Weeks'),
('repayment_period_frequency_enum',2,'Months','Months'),
('term_period_frequency_enum',0,'Days','Days'),
('term_period_frequency_enum',1,'Weeks','Weeks'),
('term_period_frequency_enum',2,'Months','Months'),
('term_period_frequency_enum',3,'Years','Years');
INSERT INTO `r_enum_value` (`enum_name`, `enum_id`, `enum_message_property`, `enum_value`)
VALUES ('transaction_type_enum', '1', 'Disbursement', 'Disbursement');
@@ -64,7 +81,6 @@ VALUES ('transaction_type_enum', '10', 'Apply Charges', 'Apply Charges');
INSERT INTO `r_enum_value` (`enum_name`, `enum_id`, `enum_message_property`, `enum_value`)
VALUES ('transaction_type_enum', '11', 'Apply Interest', 'Apply Interest');
INSERT INTO `m_currency`
(`id`,`code`,`decimal_places`,`display_symbol`,`name`, `internationalized_name_code`)
VALUES
@@ -263,7 +263,9 @@ INSERT INTO `m_permission`
('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'DEPOSIT', '1'),
('transaction_savings', 'DEPOSIT_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'DEPOSIT', '0'),
('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'WITHDRAWAL', '1'),
('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'WITHDRAWAL', '0');
('transaction_savings', 'WITHDRAWAL_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'WITHDRAWAL', '0'),
('transaction_savings', 'ACTIVATE_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'ACTIVATE', '1'),
('transaction_savings', 'ACTIVATE_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'ACTIVATE', '0');
-- == accounting related permissions
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) VALUES
@@ -0,0 +1,16 @@
ALTER TABLE `m_savings_account`
ADD COLUMN `status_enum` SMALLINT(5) NOT NULL DEFAULT 300 AFTER `product_id` ;
ALTER TABLE `m_savings_account`
ADD COLUMN `activation_date` DATE DEFAULT NULL AFTER `status_enum`;
ALTER TABLE `m_savings_account`
ADD COLUMN `lockedin_until_date_derived` DATE DEFAULT NULL AFTER `lockin_period_frequency_enum`;
ALTER TABLE `m_portfolio_command_source`
ADD COLUMN `savings_id` BIGINT(20) NULL DEFAULT NULL AFTER `loan_id` ;
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('transaction_savings', 'ACTIVATE_SAVINGSACCOUNT', 'SAVINGSACCOUNT', 'ACTIVATE', '1'),
('transaction_savings', 'ACTIVATE_SAVINGSACCOUNT_CHECKER', 'SAVINGSACCOUNT', 'ACTIVATE', '0');
@@ -1,4 +1,17 @@
import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.post;
import static com.jayway.restassured.path.json.JsonPath.from;
import static junit.framework.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import org.junit.Before;
import org.junit.Test;
import com.google.gson.Gson;
import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.builder.ResponseSpecBuilder;
@@ -6,22 +19,11 @@ import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.path.json.JsonPath;
import com.jayway.restassured.specification.RequestSpecification;
import com.jayway.restassured.specification.ResponseSpecification;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.post;
import static com.jayway.restassured.path.json.JsonPath.from;
import static junit.framework.Assert.assertEquals;
/**
* Client Loan Integration Test for checking Loan Application Repayment Schedule.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ClientLoanIntegrationTest {
String basicAuthKey;
ResponseSpecification responseSpec;
@@ -70,7 +72,7 @@ public class ClientLoanIntegrationTest {
return from(json).get("clientId");
}
private void checkClientCreatedOnServer(Integer generatedClientID) {
private void checkClientCreatedOnServer(final Integer generatedClientID) {
System.out.println("------------------------------CHECK CLIENT DETAILS------------------------------------\n");
given().spec(requestSpec)
.expect().spec(responseSpec)
@@ -86,14 +88,14 @@ public class ClientLoanIntegrationTest {
return from(json).get("resourceId");
}
private void applyForLoanApplication(Integer clientID, Integer loanProductID) {
private void applyForLoanApplication(final Integer clientID, final Integer loanProductID) {
System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
given().spec(requestSpec).body(getLoanApplicationBodyAsJSON(clientID.toString(), loanProductID.toString()))
.expect().spec(responseSpec)
.when().post("/mifosng-provider/api/v1/loans?tenantIdentifier=default");
}
private ArrayList getLoanRepaymentSchedule(Integer productID)
private ArrayList getLoanRepaymentSchedule(final Integer productID)
{
System.out.println("---------------------------GETTING LOAN REPAYMENT SCHEDULE--------------------------------");
String json = given().spec(requestSpec).body(getLoanCalculationBodyAsJSON(productID.toString()))
@@ -102,7 +104,7 @@ public class ClientLoanIntegrationTest {
return from(json).get("periods");
}
private void verifyLoanRepaymentSchedule(ArrayList<HashMap> loanSchedule) {
private void verifyLoanRepaymentSchedule(final ArrayList<HashMap> loanSchedule) {
System.out.println("--------------------VERIFYING THE PRINCIPAL DUES,INTEREST DUE AND DUE DATE--------------------------");
assertEquals("Checking for Due Date for 1st Month",new ArrayList<Integer>(Arrays.asList(2011, 10, 20)),loanSchedule.get(1).get("dueDate"));
@@ -156,7 +158,7 @@ public class ClientLoanIntegrationTest {
return new Gson().toJson(map);
}
private String getLoanApplicationBodyAsJSON(String clientID,String productID){
private String getLoanApplicationBodyAsJSON(final String clientID,final String productID){
HashMap<String, String> map = new HashMap<String, String>();
map.put("dateFormat", "dd MMMM yyyy");
map.put("locale", "en_GB");
@@ -179,7 +181,7 @@ public class ClientLoanIntegrationTest {
return new Gson().toJson(map);
}
private String getLoanCalculationBodyAsJSON(String productID){
private String getLoanCalculationBodyAsJSON(final String productID){
HashMap<String, String> map = new HashMap<String, String>();
map.put("dateFormat", "dd MMMM yyyy");
map.put("locale", "en_GB");
@@ -200,15 +202,15 @@ public class ClientLoanIntegrationTest {
return new Gson().toJson(map);
}
public String randomIDGenerator(String prefix, int lenOfRandomSuffix) {
public String randomIDGenerator(final String prefix, final int lenOfRandomSuffix) {
return randomStringGenerator(prefix,lenOfRandomSuffix, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
public String randomNameGenerator(String prefix, int lenOfRandomSuffix) {
public String randomNameGenerator(final String prefix, final int lenOfRandomSuffix) {
return randomStringGenerator(prefix,lenOfRandomSuffix, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
private String randomStringGenerator(String prefix,int len, String sourceSetString) {
private String randomStringGenerator(final String prefix,final int len, final String sourceSetString) {
int lengthOfSource = sourceSetString.length();
Random rnd = new Random();
StringBuilder sb = new StringBuilder(len);
@@ -47,6 +47,10 @@ public class CommandSource extends AbstractPersistable<Long> {
@Column(name = "loan_id")
private Long loanId;
@SuppressWarnings("unused")
@Column(name = "savings_id")
private Long savingsId;
@Column(name = "api_get_url", length = 100)
private String resourceGetUrl;
@@ -165,11 +169,12 @@ public class CommandSource extends AbstractPersistable<Long> {
this.processingResult = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
}
public void updateForAudit(final Long officeId, final Long groupId, final Long clientId, final Long loanId) {
public void updateForAudit(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savignsId) {
this.officeId = officeId;
this.groupId = groupId;
this.clientId = clientId;
this.loanId = loanId;
this.savingsId = savignsId;
}
public String getResourceGetUrl() {
@@ -31,12 +31,12 @@ public class CommandWrapper {
}
public static CommandWrapper fromExistingCommand(final Long commandId, final String actionName, final String entityName,
final Long resourceId, final Long subresourceId, final String resourceGetUrl) {
final Long resourceId, final Long subresourceId, final String resourceGetUrl) {
return new CommandWrapper(commandId, actionName, entityName, resourceId, subresourceId, resourceGetUrl);
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId, final Long subresourceId,
final String resourceGetUrl) {
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
final Long subresourceId, final String resourceGetUrl) {
this.commandId = commandId;
this.officeId = null;
this.groupId = null;
@@ -54,13 +54,12 @@ public class CommandWrapper {
this.href = resourceGetUrl;
this.json = null;
this.transactionId = null;
}
public CommandWrapper(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId,
final String actionName, final String entityName, final Long entityId, final Long subentityId,
final Long codeId, final String supportedEntityType, final Long supportedEntityId, final String href, final String json,
final String transactionId) {
final String actionName, final String entityName, final Long entityId, final Long subentityId, final Long codeId,
final String supportedEntityType, final Long supportedEntityId, final String href, final String json, final String transactionId) {
this.commandId = null;
this.officeId = officeId;
this.groupId = groupId;
@@ -95,7 +94,7 @@ public class CommandWrapper {
public Long resourceId() {
return this.entityId;
}
public Long subresourceId() {
return this.subentityId;
}
@@ -135,7 +134,7 @@ public class CommandWrapper {
public Long getEntityId() {
return this.entityId;
}
public Long getSubentityId() {
return this.subentityId;
}
@@ -354,12 +353,12 @@ public class CommandWrapper {
return this.actionName.equalsIgnoreCase("BULKREASSIGN") && this.entityName.equalsIgnoreCase("LOAN");
}
public boolean isDatatableResource() {
public boolean isDatatableResource() {
return this.href.startsWith("/datatables/");
}
public boolean isDeleteOneToOne() {
/* also covers case of deleting all of a one to many*/
/* also covers case of deleting all of a one to many */
return isDatatableResource() && isDeleteOperation() && this.subentityId == null;
}
@@ -431,6 +430,10 @@ public class CommandWrapper {
return this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
public boolean isSavingsAccountActivation() {
return this.actionName.equalsIgnoreCase("ACTIVATE") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
public boolean isSavingsAccountDeposit() {
return this.actionName.equalsIgnoreCase("DEPOSIT") && this.entityName.equalsIgnoreCase("SAVINGSACCOUNT");
}
@@ -446,8 +449,8 @@ public class CommandWrapper {
public boolean isNoteResource() {
boolean isnoteResource = false;
if (this.entityName.equalsIgnoreCase("CLIENTNOTE") || this.entityName.equalsIgnoreCase("LOANNOTE")
|| this.entityName.equalsIgnoreCase("LOANTRANSACTIONNOTE") || this.entityName.equalsIgnoreCase("DEPOSITNOTE")
|| this.entityName.equalsIgnoreCase("SAVINGNOTE") || this.entityName.equalsIgnoreCase("GROUPNOTE")) {
|| this.entityName.equalsIgnoreCase("LOANTRANSACTIONNOTE") || this.entityName.equalsIgnoreCase("SAVINGNOTE")
|| this.entityName.equalsIgnoreCase("GROUPNOTE")) {
isnoteResource = true;
}
return isnoteResource;
@@ -456,5 +459,4 @@ public class CommandWrapper {
public boolean isGroupResource() {
return this.entityName.equalsIgnoreCase("GROUP");
}
}
@@ -686,10 +686,19 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder savingsAccountActivation(final Long accountId) {
this.actionName = "ACTIVATE";
this.entityName = "SAVINGSACCOUNT";
this.savingsId = accountId;
this.entityId = null;
this.href = "/savingsaccounts/" + accountId + "?command=activate";
return this;
}
public CommandWrapperBuilder savingsAccountDeposit(final Long accountId) {
this.actionName = "DEPOSIT";
this.entityName = "SAVINGSACCOUNT";
this.loanId = accountId;
this.savingsId = accountId;
this.entityId = null;
this.href = "/savingsaccounts/" + accountId + "/transactions";
return this;
@@ -698,7 +707,7 @@ public class CommandWrapperBuilder {
public CommandWrapperBuilder savingsAccountWithdrawal(final Long accountId) {
this.actionName = "WITHDRAWAL";
this.entityName = "SAVINGSACCOUNT";
this.loanId = accountId;
this.savingsId = accountId;
this.entityId = null;
this.href = "/savingsaccounts/" + accountId + "/transactions";
return this;
@@ -69,7 +69,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
commandSourceResult = CommandSource.fullEntryFrom(wrapper, command, maker);
}
commandSourceResult.updateResourceId(result.resourceId());
commandSourceResult.updateForAudit(result.getOfficeId(), result.getGroupId(), result.getClientId(), result.getLoanId());
commandSourceResult.updateForAudit(result.getOfficeId(), result.getGroupId(), result.getClientId(), result.getLoanId(),
result.getSavingsId());
String changesOnlyJson = null;
if (result.hasChanges()) {
@@ -360,6 +361,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
handler = applicationContext.getBean("depositSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isSavingsAccountWithdrawal()) {
handler = applicationContext.getBean("withdrawSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isSavingsAccountActivation()) {
handler = applicationContext.getBean("activateSavingsAccountCommandHandler", NewCommandSourceHandler.class);
} else {
throw new UnsupportedCommandException(wrapper.commandName());
}
@@ -8,6 +8,7 @@ package org.mifosplatform.infrastructure.core.api;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
@@ -377,4 +378,8 @@ public final class JsonCommand {
final PlatformUser dummyPlatformUser = new BasicPasswordEncodablePlatformUser(saltValue, "", passwordPlainText);
return platformPasswordEncoder.encode(dummyPlatformUser);
}
public Locale extractLocale() {
return this.fromApiJsonHelper.extractLocaleParameter(this.parsedCommand.getAsJsonObject());
}
}
@@ -22,12 +22,6 @@ public class ApiGlobalErrorResponse {
*/
private String developerMessage;
/**
* A link to a URL which provides more information on the details of this
* type of error.
*/
private String developerDocLink;
/**
* The HTTP status code of the response.
*/
@@ -57,7 +51,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid authentication details were passed in api request.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authenticated");
globalErrorResponse.setDefaultUserMessage("Unauthenticated. Please login.");
@@ -69,7 +62,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid tenant details were passed in api request.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.invalid.tenant.identifier");
globalErrorResponse.setDefaultUserMessage("Invalide tenant identifier provided with request.");
@@ -81,7 +73,6 @@ public class ApiGlobalErrorResponse {
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse
.setDeveloperMessage("The user associated with credentials passed on this request does not have sufficient privileges to perform this action.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authorized");
globalErrorResponse.setDefaultUserMessage("Insufficient privileges to perform this action.");
@@ -97,7 +88,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("Request was understood but caused a domain rule violation.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("validation.msg.domain.rule.violation");
globalErrorResponse.setDefaultUserMessage("Errors contain reason for domain rule violation.");
@@ -114,7 +104,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("404");
globalErrorResponse.setDeveloperMessage("The requested resource is not available.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.resource.not.found");
globalErrorResponse.setDefaultUserMessage("The requested resource is not available.");
@@ -131,7 +120,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("The request caused a data integrity issue to be fired by the database.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
@@ -149,7 +137,6 @@ public class ApiGlobalErrorResponse {
globalErrorResponse.setHttpStatusCode("400");
globalErrorResponse
.setDeveloperMessage("The request was invalid. This typically will happen due to validation errors which are provided.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
@@ -164,7 +151,6 @@ public class ApiGlobalErrorResponse {
ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("500");
globalErrorResponse.setDeveloperMessage("An unexpected error occured on the platform server.");
globalErrorResponse.setDeveloperDocLink("https://github.com/keithwoodlock/mifosx/wiki/HTTP-API-Error-codes");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.platform.server.side.error");
globalErrorResponse.setDefaultUserMessage("An unexpected error occured on the platform server.");
@@ -224,12 +210,4 @@ public class ApiGlobalErrorResponse {
public void setUserMessageGlobalisationCode(final String userMessageGlobalisationCode) {
this.userMessageGlobalisationCode = userMessageGlobalisationCode;
}
public String getDeveloperDocLink() {
return developerDocLink;
}
public void setDeveloperDocLink(final String developerDocLink) {
this.developerDocLink = developerDocLink;
}
}
@@ -9,14 +9,23 @@ import java.util.List;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
/**
* Exception thrown when problem with an API request to the platform.
*/
public class PlatformApiDataValidationException extends RuntimeException {
private final String globalisationMessageCode;
private final String defaultUserMessage;
private final List<ApiParameterError> errors;
public PlatformApiDataValidationException(final List<ApiParameterError> errors) {
this.globalisationMessageCode = "validation.msg.validation.errors.exist";
this.defaultUserMessage = "Validation errors exist.";
this.errors = errors;
}
public PlatformApiDataValidationException(final String globalisationMessageCode, final String defaultUserMessage,
List<ApiParameterError> errors) {
final List<ApiParameterError> errors) {
this.globalisationMessageCode = globalisationMessageCode;
this.defaultUserMessage = defaultUserMessage;
this.errors = errors;
@@ -33,4 +42,4 @@ public class PlatformApiDataValidationException extends RuntimeException {
public List<ApiParameterError> getErrors() {
return errors;
}
}
}
@@ -29,11 +29,11 @@ import org.springframework.stereotype.Component;
public class PlatformApiDataValidationExceptionMapper implements ExceptionMapper<PlatformApiDataValidationException> {
@Override
public Response toResponse(PlatformApiDataValidationException exception) {
public Response toResponse(final PlatformApiDataValidationException exception) {
ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.badClientRequest(exception.getGlobalisationMessageCode(),
final ApiGlobalErrorResponse dataValidationErrorResponse = ApiGlobalErrorResponse.badClientRequest(exception.getGlobalisationMessageCode(),
exception.getDefaultUserMessage(), exception.getErrors());
return Response.status(Status.BAD_REQUEST).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
return Response.status(Status.BAD_REQUEST).entity(dataValidationErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
}
@@ -10,10 +10,9 @@ import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRul
/**
* Exception thrown when an attempt is made update the parent of a root office.
*/
public class CannotUpdateOfficeWithParentOfficeSameAsSelf extends
AbstractPlatformDomainRuleException {
public class CannotUpdateOfficeWithParentOfficeSameAsSelf extends AbstractPlatformDomainRuleException {
public CannotUpdateOfficeWithParentOfficeSameAsSelf(final Long officeId, final Long parentId) {
super("error.msg.office.parentId.same.as.id", "Cannot update office with parent same as self.", officeId, parentId);
}
public CannotUpdateOfficeWithParentOfficeSameAsSelf(final Long officeId, final Long parentId) {
super("error.msg.office.parentId.same.as.id", "Cannot update office with parent same as self.", officeId, parentId);
}
}
@@ -10,10 +10,9 @@ import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRul
/**
* Exception thrown when an attempt is made update the parent of a root office.
*/
public class RootOfficeParentCannotBeUpdated extends
AbstractPlatformDomainRuleException {
public class RootOfficeParentCannotBeUpdated extends AbstractPlatformDomainRuleException {
public RootOfficeParentCannotBeUpdated() {
super("error.msg.office.cannot.update.parent.office.of.root.office", "The root office must not be set with a parent office.");
}
public RootOfficeParentCannotBeUpdated() {
super("error.msg.office.cannot.update.parent.office.of.root.office", "The root office must not be set with a parent office.");
}
}
@@ -29,24 +29,22 @@ public enum PeriodFrequencyType {
}
public static PeriodFrequencyType fromInt(final Integer frequency) {
PeriodFrequencyType repaymentFrequencyType = PeriodFrequencyType.INVALID;
switch (frequency) {
case 0:
repaymentFrequencyType = PeriodFrequencyType.DAYS;
break;
case 1:
repaymentFrequencyType = PeriodFrequencyType.WEEKS;
break;
case 2:
repaymentFrequencyType = PeriodFrequencyType.MONTHS;
break;
case 3:
repaymentFrequencyType = PeriodFrequencyType.YEARS;
break;
default:
repaymentFrequencyType = PeriodFrequencyType.INVALID;
break;
if (frequency != null) {
switch (frequency) {
case 0:
repaymentFrequencyType = PeriodFrequencyType.DAYS;
break;
case 1:
repaymentFrequencyType = PeriodFrequencyType.WEEKS;
break;
case 2:
repaymentFrequencyType = PeriodFrequencyType.MONTHS;
break;
case 3:
repaymentFrequencyType = PeriodFrequencyType.YEARS;
break;
}
}
return repaymentFrequencyType;
}
@@ -96,7 +96,10 @@ public class SavingsAccountTransactionsApiResource {
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) { throw new UnrecognizedQueryParamException("command", commandParam); }
if (result == null) {
// FIXME - KW - handle blank command param use case
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.toApiJsonSerializer.serialize(result);
}
@@ -23,6 +23,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.commands.domain.CommandWrapper;
import org.mifosplatform.commands.service.CommandWrapperBuilder;
import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService;
@@ -30,6 +31,7 @@ import org.mifosplatform.infrastructure.core.api.ApiParameterHelper;
import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
@@ -183,6 +185,33 @@ public class SavingsAccountsApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String activate(@PathParam("accountId") final Long accountId, @QueryParam("command") final String commandParam,
final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
if (is(commandParam, "activate")) {
final CommandWrapper commandRequest = builder.savingsAccountActivation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) {
// FIXME - KW - handle blank command param use case
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@DELETE
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -21,9 +21,12 @@ public class SavingsApiConstants {
public static final String idParamName = "id";
public static final String accountNoParamName = "accountNo";
public static final String externalIdParamName = "externalId";
public static final String statusParamName = "status";
public static final String clientIdParamName = "clientId";
public static final String groupIdParamName = "groupId";
public static final String productIdParamName = "productId";
public static final String activeParamName = "active";
public static final String activationDateParamName = "activationDate";
public static final String nameParamName = "name";
public static final String descriptionParamName = "description";
public static final String currencyCodeParamName = "currencyCode";
@@ -55,9 +58,9 @@ public class SavingsApiConstants {
"lockinPeriodFrequencyTypeOptions"));
public static final Set<String> SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(localeParamName,
accountNoParamName, externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, interestRateParamName,
interestRatePeriodFrequencyTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName,
lockinPeriodFrequencyTypeParamName));
dateFormatParamName, accountNoParamName, externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName,
activeParamName, activationDateParamName, interestRateParamName, interestRatePeriodFrequencyTypeParamName,
minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName));
/**
* These parameters will match the class level parameters of
@@ -65,14 +68,18 @@ public class SavingsApiConstants {
* parameters to match those of request parameters.
*/
public static final Set<String> SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(idParamName,
accountNoParamName, externalIdParamName, clientIdParamName, "clientName", groupIdParamName, "groupName", "savingsProductId",
"savingsProductName", "currency", interestRateParamName, interestRatePeriodFrequencyTypeParamName, "annualInterestRate",
minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, "summary",
"transactions", "productOptions", "interestRatePeriodFrequencyTypeOptions", "lockinPeriodFrequencyTypeOptions"));
accountNoParamName, externalIdParamName, statusParamName, activationDateParamName, clientIdParamName, "clientName",
groupIdParamName, "groupName", "savingsProductId", "savingsProductName", "currency", interestRateParamName,
interestRatePeriodFrequencyTypeParamName, "annualInterestRate", minRequiredOpeningBalanceParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, "summary", "transactions", "productOptions",
"interestRatePeriodFrequencyTypeOptions", "lockinPeriodFrequencyTypeOptions"));
public static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(
localeParamName, dateFormatParamName, transactionDateParamName, transactionAmountParamName));
public static final Set<String> SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(idParamName,
"accountId", accountNoParamName, "currency", "amount"));
public static final Set<String> SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(localeParamName,
dateFormatParamName, activationDateParamName));
}
@@ -8,6 +8,7 @@ package org.mifosplatform.portfolio.savings.data;
import java.math.BigDecimal;
import java.util.Collection;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
@@ -20,6 +21,8 @@ public class SavingsAccountData {
private final Long id;
private final String accountNo;
private final String externalId;
private final SavingsAccountStatusEnumData status;
private final LocalDate activationDate;
private final Long groupId;
private final String groupName;
private final Long clientId;
@@ -43,10 +46,11 @@ public class SavingsAccountData {
private final Collection<EnumOptionData> interestRatePeriodFrequencyTypeOptions;
private final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions;
public static SavingsAccountData instance(final Long id, final String accountNo, final String externalId, final Long groupId,
final String groupName, final Long clientId, final String clientName, final Long productId, final String productName,
final CurrencyData currency, final BigDecimal interestRate, final EnumOptionData interestRatePeriodFrequencyType,
final BigDecimal annualInterestRate, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
public static SavingsAccountData instance(final Long id, final String accountNo, final String externalId,
final SavingsAccountStatusEnumData status, final LocalDate activationDate, final Long groupId, final String groupName,
final Long clientId, final String clientName, final Long productId, final String productName, final CurrencyData currency,
final BigDecimal interestRate, final EnumOptionData interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
final EnumOptionData lockinPeriodFrequencyType, final SavingsAccountSummaryData summary) {
final Collection<SavingsProductData> productOptions = null;
@@ -54,21 +58,22 @@ public class SavingsAccountData {
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions = null;
final Collection<SavingsAccountTransactionData> transactions = null;
return new SavingsAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance,
lockinPeriodFrequency, lockinPeriodFrequencyType, summary, productOptions, interestRatePeriodFrequencyTypeOptions,
lockinPeriodFrequencyTypeOptions, transactions);
return new SavingsAccountData(id, accountNo, externalId, status, activationDate, groupId, groupName, clientId, clientName,
productId, productName, currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate,
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, summary, productOptions,
interestRatePeriodFrequencyTypeOptions, lockinPeriodFrequencyTypeOptions, transactions);
}
public static SavingsAccountData withTemplateOptions(final SavingsAccountData account,
final Collection<SavingsProductData> productOptions, final Collection<EnumOptionData> interestRatePeriodFrequencyTypeOptions,
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, final Collection<SavingsAccountTransactionData> transactions) {
return new SavingsAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.savingsProductId, account.savingsProductName, account.currency,
account.interestRate, account.interestRatePeriodFrequencyType, account.annualInterestRate,
account.minRequiredOpeningBalance, account.lockinPeriodFrequency, account.lockinPeriodFrequencyType, account.summary,
productOptions, interestRatePeriodFrequencyTypeOptions, lockinPeriodFrequencyTypeOptions, transactions);
return new SavingsAccountData(account.id, account.accountNo, account.externalId, account.status, account.activationDate,
account.groupId, account.groupName, account.clientId, account.clientName, account.savingsProductId,
account.savingsProductName, account.currency, account.interestRate, account.interestRatePeriodFrequencyType,
account.annualInterestRate, account.minRequiredOpeningBalance, account.lockinPeriodFrequency,
account.lockinPeriodFrequencyType, account.summary, productOptions, interestRatePeriodFrequencyTypeOptions,
lockinPeriodFrequencyTypeOptions, transactions);
}
public static SavingsAccountData withClientTemplate(final Long clientId, final String clientName, final Long groupId,
@@ -77,6 +82,8 @@ public class SavingsAccountData {
final Long id = null;
final String accountNo = null;
final String externalId = null;
final SavingsAccountStatusEnumData status = null;
final LocalDate activationDate = null;
final Long productId = null;
final String productName = null;
final CurrencyData currency = null;
@@ -93,15 +100,16 @@ public class SavingsAccountData {
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions = null;
final Collection<SavingsAccountTransactionData> transactions = null;
return new SavingsAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance,
lockinPeriodFrequency, lockinPeriodFrequencyType, summary, productOptions, interestRatePeriodFrequencyTypeOptions,
lockinPeriodFrequencyTypeOptions, transactions);
return new SavingsAccountData(id, accountNo, externalId, status, activationDate, groupId, groupName, clientId, clientName,
productId, productName, currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate,
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, summary, productOptions,
interestRatePeriodFrequencyTypeOptions, lockinPeriodFrequencyTypeOptions, transactions);
}
private SavingsAccountData(final Long id, final String accountNo, final String externalId, final Long groupId, final String groupName,
final Long clientId, final String clientName, final Long productId, final String productName, final CurrencyData currency,
final BigDecimal interestRate, final EnumOptionData interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
private SavingsAccountData(final Long id, final String accountNo, final String externalId, final SavingsAccountStatusEnumData status,
final LocalDate activationDate, final Long groupId, final String groupName, final Long clientId, final String clientName,
final Long productId, final String productName, final CurrencyData currency, final BigDecimal interestRate,
final EnumOptionData interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
final EnumOptionData lockinPeriodFrequencyType, final SavingsAccountSummaryData summary,
final Collection<SavingsProductData> productOptions, final Collection<EnumOptionData> interestRatePeriodFrequencyTypeOptions,
@@ -109,6 +117,8 @@ public class SavingsAccountData {
this.id = id;
this.accountNo = accountNo;
this.externalId = externalId;
this.status = status;
this.activationDate = activationDate;
this.groupId = groupId;
this.groupName = groupName;
this.clientId = clientId;
@@ -1,6 +1,8 @@
package org.mifosplatform.portfolio.savings.data;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.accountNoParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.activationDateParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.activeParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.clientIdParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.externalIdParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.groupIdParamName;
@@ -18,6 +20,7 @@ import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
@@ -69,6 +72,15 @@ public class SavingsAccountDataValidator {
final Long productId = fromApiJsonHelper.extractLongNamed(productIdParamName, element);
baseDataValidator.reset().parameter(productIdParamName).value(productId).notNull().integerGreaterThanZero();
final Boolean active = fromApiJsonHelper.extractBooleanNamed(activeParamName, element);
baseDataValidator.reset().parameter(activeParamName).value(active).notNull();
LocalDate activationDate = null;
if (active != null && active.booleanValue()) {
activationDate = fromApiJsonHelper.extractLocalDateNamed(activationDateParamName, element);
baseDataValidator.reset().parameter(activationDateParamName).value(activationDate).notNull();
}
if (fromApiJsonHelper.parameterExists(accountNoParamName, element)) {
final String accountNo = fromApiJsonHelper.extractStringNamed(accountNoParamName, element);
baseDataValidator.reset().parameter(accountNoParamName).value(accountNo).notBlank().notExceedingLengthOf(20);
@@ -0,0 +1,35 @@
/**
* 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.savings.data;
/**
* Immutable data object represent savings account status enumerations.
*/
public class SavingsAccountStatusEnumData {
@SuppressWarnings("unused")
private final Long id;
@SuppressWarnings("unused")
private final String code;
@SuppressWarnings("unused")
private final String value;
@SuppressWarnings("unused")
private final boolean unactivated;
@SuppressWarnings("unused")
private final boolean active;
@SuppressWarnings("unused")
private final boolean closed;
public SavingsAccountStatusEnumData(final Long id, final String code, final String value, final boolean unactivated,
final boolean active, final boolean closed) {
this.id = id;
this.code = code;
this.value = value;
this.unactivated = unactivated;
this.active = active;
this.closed = closed;
}
}
@@ -1,5 +1,6 @@
package org.mifosplatform.portfolio.savings.data;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.activationDateParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.transactionAmountParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.transactionDateParamName;
@@ -54,7 +55,28 @@ public class SavingsAccountTransactionDataValidator {
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().zeroOrPositiveAmount();
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateActivation(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsApiConstants.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate activationDate = fromApiJsonHelper.extractLocalDateNamed(activationDateParamName, element);
baseDataValidator.reset().parameter(activationDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -7,6 +7,8 @@ package org.mifosplatform.portfolio.savings.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -19,6 +21,8 @@ import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
@@ -26,7 +30,10 @@ import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
@@ -36,7 +43,6 @@ import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.savings.api.SavingsApiConstants;
import org.mifosplatform.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.mifosplatform.portfolio.savings.exception.InvalidSavingsAccountTransactionException;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@@ -47,7 +53,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
@Column(name = "account_no", length = 20, unique = true, nullable = false)
private String accountNumber;
@Column(name = "external_id")
@Column(name = "external_id", nullable = true)
private String externalId;
@ManyToOne(optional = true)
@@ -62,6 +68,13 @@ public class SavingsAccount extends AbstractPersistable<Long> {
@JoinColumn(name = "product_id", nullable = false)
private SavingsProduct product;
@Column(name = "status_enum", nullable = false)
private Integer status;
@Temporal(TemporalType.DATE)
@Column(name = "activation_date", nullable = true)
private Date activationDate;
@Embedded
private MonetaryCurrency currency;
@@ -84,6 +97,15 @@ public class SavingsAccount extends AbstractPersistable<Long> {
@Column(name = "lockin_period_frequency_enum", nullable = false)
private Integer lockinPeriodFrequencyType;
/**
* When account is becomes <code>active</code> this field is derived if
* <code>lockinPeriodFrequency</code> and
* <code>lockinPeriodFrequencyType</code> details are present.
*/
@Temporal(TemporalType.DATE)
@Column(name = "lockedin_until_date_derived", nullable = true)
private Date lockedInUntilDate;
@Embedded
private SavingsAccountSummary summary;
@@ -102,18 +124,25 @@ public class SavingsAccount extends AbstractPersistable<Long> {
}
public static SavingsAccount createNewAccount(final Client client, final Group group, final SavingsProduct product,
final String accountNo, final String externalId, final BigDecimal interestRate,
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
final String accountNo, final String externalId, final boolean active, final LocalDate activationDate,
final BigDecimal interestRate, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
final PeriodFrequencyType lockinPeriodFrequencyType) {
return new SavingsAccount(client, group, product, accountNo, externalId, interestRate, interestRatePeriodFrequencyType,
annualInterestRate, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType);
SavingsAccountStatusType status = SavingsAccountStatusType.UNACTIVATED;
if (active) {
status = SavingsAccountStatusType.ACTIVE;
}
return new SavingsAccount(client, group, product, accountNo, externalId, status, activationDate, interestRate,
interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance, lockinPeriodFrequency,
lockinPeriodFrequencyType);
}
private SavingsAccount(final Client client, final Group group, final SavingsProduct product, final String accountNo,
final String externalId, final BigDecimal interestRate, final PeriodFrequencyType interestRatePeriodFrequencyType,
final BigDecimal annualInterestRate, final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
final String externalId, final SavingsAccountStatusType status, final LocalDate activationDate, final BigDecimal interestRate,
final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualInterestRate,
final BigDecimal minRequiredOpeningBalance, final Integer lockinPeriodFrequency,
final PeriodFrequencyType lockinPeriodFrequencyType) {
this.client = client;
this.group = group;
@@ -126,6 +155,10 @@ public class SavingsAccount extends AbstractPersistable<Long> {
}
this.currency = product.currency();
this.externalId = externalId;
this.status = status.getValue();
if (activationDate != null) {
this.activationDate = activationDate.toDate();
}
this.interestRate = interestRate;
this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType.getValue();
this.annualInterestRate = annualInterestRate;
@@ -140,24 +173,121 @@ public class SavingsAccount extends AbstractPersistable<Long> {
/**
* Used after fetching/hydrating a {@link SavingsAccount} object to inject
* helper services/components used for update summary details after
* events/transactions on a loan.
* events/transactions on a {@link SavingsAccount}.
*/
public void setHelpers(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper) {
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
}
public SavingsAccountTransaction deposit(final LocalDate transactionDate, final BigDecimal transactionAmount) {
public boolean isNotActive() {
return !isActive();
}
final Money amount = Money.of(this.currency, transactionAmount);
public boolean isActive() {
return SavingsAccountStatusType.fromInt(this.status).isActive();
}
// FIXME - kw - check transactionDate is not before savings account
// activation date.
public void activate(final DateTimeFormatter formatter, final LocalDate activationDate) {
if (isTransactionInTheFuture(transactionDate)) {
final String defaultUserMessage = "Transaction date cannot be in the future.";
throw new InvalidSavingsAccountTransactionException("in.the.future", defaultUserMessage, "transactionDate", transactionDate);
if (isActive()) {
final String defaultUserMessage = "Cannot activate account. Account is already active.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.already.active",
defaultUserMessage, "activationDate", activationDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (isDateInTheFuture(activationDate)) {
final String defaultUserMessage = "Activation date cannot be in the future.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future",
defaultUserMessage, "activationDate", activationDate);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
this.activationDate = activationDate.toDate();
this.status = SavingsAccountStatusType.ACTIVE.getValue();
this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate());
// auto enter deposit for minimum required opening balance when
// activating account.
final Money minRequiredOpeningBalance = Money.of(this.currency, this.minRequiredOpeningBalance);
if (minRequiredOpeningBalance.isGreaterThanZero()) {
deposit(formatter, activationDate, minRequiredOpeningBalance.getAmount());
}
}
private Date calculateDateAccountIsLockedUntil(final LocalDate activationLocalDate) {
Date lockedInUntilLocalDate = null;
final PeriodFrequencyType lockinPeriodFrequencyType = PeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType);
switch (lockinPeriodFrequencyType) {
case INVALID:
break;
case DAYS:
lockedInUntilLocalDate = activationLocalDate.plusDays(this.lockinPeriodFrequency).toDate();
break;
case WEEKS:
lockedInUntilLocalDate = activationLocalDate.plusWeeks(this.lockinPeriodFrequency).toDate();
break;
case MONTHS:
lockedInUntilLocalDate = activationLocalDate.plusMonths(this.lockinPeriodFrequency).toDate();
break;
case YEARS:
lockedInUntilLocalDate = activationLocalDate.plusYears(this.lockinPeriodFrequency).toDate();
break;
}
return lockedInUntilLocalDate;
}
public SavingsAccountTransaction deposit(final DateTimeFormatter formatter, final LocalDate transactionDate,
final BigDecimal transactionAmount) {
if (isNotActive()) {
final String defaultUserMessage = "Transaction is not allowed. Account is not active.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active",
defaultUserMessage, "transactionDate", transactionDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (isDateInTheFuture(transactionDate)) {
final String defaultUserMessage = "Transaction date cannot be in the future.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future",
defaultUserMessage, "transactionDate", transactionDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (transactionDate.isBefore(getActivationLocalDate())) {
final Object[] defaultUserArgs = Arrays.asList(transactionDate.toString(formatter),
getActivationLocalDate().toString(formatter)).toArray();
final String defaultUserMessage = "Transaction date cannot be before accounts activation date.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.before.activation.date",
defaultUserMessage, "transactionDate", defaultUserArgs);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
final Money amount = Money.of(this.currency, transactionAmount);
final SavingsAccountTransaction transaction = SavingsAccountTransaction.deposit(this, transactionDate, amount);
this.transactions.add(transaction);
@@ -166,19 +296,69 @@ public class SavingsAccount extends AbstractPersistable<Long> {
return transaction;
}
public SavingsAccountTransaction withdrawal(final LocalDate transactionDate, final BigDecimal transactionAmount) {
private LocalDate getActivationLocalDate() {
LocalDate activationLocalDate = null;
if (this.activationDate != null) {
activationLocalDate = new LocalDate(this.activationDate);
}
return activationLocalDate;
}
public SavingsAccountTransaction withdraw(final DateTimeFormatter formatter, final LocalDate transactionDate,
final BigDecimal transactionAmount) {
if (isNotActive()) {
final String defaultUserMessage = "Transaction is not allowed. Account is not active.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.account.is.not.active",
defaultUserMessage, "transactionDate", transactionDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (isDateInTheFuture(transactionDate)) {
final String defaultUserMessage = "Transaction date cannot be in the future.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.in.the.future",
defaultUserMessage, "transactionDate", transactionDate.toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (transactionDate.isBefore(getActivationLocalDate())) {
final Object[] defaultUserArgs = Arrays.asList(transactionDate.toString(formatter),
getActivationLocalDate().toString(formatter)).toArray();
final String defaultUserMessage = "Transaction date cannot be before accounts activation date.";
final ApiParameterError error = ApiParameterError.parameterError("error.msg.savingsaccount.transaction.before.activation.date",
defaultUserMessage, "transactionDate", defaultUserArgs);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if (isAccountLocked(transactionDate)) {
final String defaultUserMessage = "Withdrawal is not allowed. No withdrawals are allowed until after "
+ getLockedInUntilLocalDate().toString(formatter);
final ApiParameterError error = ApiParameterError.parameterError(
"error.msg.savingsaccount.transaction.withdrawals.blocked.during.lockin.period", defaultUserMessage, "transactionDate",
transactionDate.toString(formatter), getLockedInUntilLocalDate().toString(formatter));
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
final Money amount = Money.of(this.currency, transactionAmount);
// FIXME - kw - check transactionDate is not before savings account
// activation date.
if (isTransactionInTheFuture(transactionDate)) {
final String defaultUserMessage = "Transaction date cannot be in the future.";
throw new InvalidSavingsAccountTransactionException("in.the.future", defaultUserMessage, "transactionDate", transactionDate);
}
if (isNotAllowedToWithdraw(amount)) {
if (isNotEnoughFundsToWithdraw(amount)) {
//
throw new InsufficientAccountBalanceException("transactionAmount", getAccountBalance(), transactionAmount);
}
@@ -191,7 +371,24 @@ public class SavingsAccount extends AbstractPersistable<Long> {
return transaction;
}
private boolean isTransactionInTheFuture(final LocalDate transactionDate) {
private boolean isAccountLocked(final LocalDate transactionDate) {
boolean isLocked = false;
final boolean accountHasLockedInSetting = this.lockedInUntilDate != null;
if (accountHasLockedInSetting) {
isLocked = getLockedInUntilLocalDate().isAfter(transactionDate);
}
return isLocked;
}
private LocalDate getLockedInUntilLocalDate() {
LocalDate lockedInUntilLocalDate = null;
if (this.lockedInUntilDate != null) {
lockedInUntilLocalDate = new LocalDate(this.lockedInUntilDate);
}
return lockedInUntilLocalDate;
}
private boolean isDateInTheFuture(final LocalDate transactionDate) {
return transactionDate.isAfter(DateUtils.getLocalDateOfTenant());
}
@@ -199,7 +396,11 @@ public class SavingsAccount extends AbstractPersistable<Long> {
return this.summary.getAccountBalance(this.currency).getAmount();
}
private boolean isNotAllowedToWithdraw(final Money amount) {
private boolean isNotEnoughFundsToWithdraw(final Money amount) {
return !isEnoughFundsToWithdraw(amount);
}
private boolean isEnoughFundsToWithdraw(final Money amount) {
return this.summary.isLessThanOrEqualToAccountBalance(amount);
}
@@ -5,6 +5,8 @@
*/
package org.mifosplatform.portfolio.savings.domain;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.activationDateParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.activeParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.interestRateParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.interestRatePeriodFrequencyTypeParamName;
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.lockinPeriodFrequencyParamName;
@@ -12,7 +14,11 @@ import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.lockin
import static org.mifosplatform.portfolio.savings.api.SavingsApiConstants.minRequiredOpeningBalanceParamName;
import java.math.BigDecimal;
import java.util.Locale;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.client.domain.Client;
@@ -90,6 +96,9 @@ public class SavingsAccountAssembler {
interestRate = product.interestRate();
}
final boolean active = fromApiJsonHelper.extractBooleanNamed(activeParamName, element);
final LocalDate activationDate = fromApiJsonHelper.extractLocalDateNamed(activationDateParamName, element);
PeriodFrequencyType interestRatePeriodFrequencyType = null;
Integer interestRatePeriodFrequencyTypeValue = null;
if (command.parameterExists(interestRatePeriodFrequencyTypeParamName)) {
@@ -128,11 +137,17 @@ public class SavingsAccountAssembler {
lockinPeriodFrequencyType = product.lockinPeriodFrequencyType();
}
final SavingsAccount account = SavingsAccount.createNewAccount(client, group, product, accountNo, externalId, interestRate,
interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance, lockinPeriodFrequency,
lockinPeriodFrequencyType);
final SavingsAccount account = SavingsAccount.createNewAccount(client, group, product, accountNo, externalId, active,
activationDate, interestRate, interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance,
lockinPeriodFrequency, lockinPeriodFrequencyType);
account.setHelpers(this.savingsAccountTransactionSummaryWrapper);
if (account.isActive()) {
final Locale locale = command.extractLocale();
final DateTimeFormatter formatter = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
account.activate(formatter, activationDate);
}
return account;
}
}
@@ -0,0 +1,66 @@
/**
* 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.savings.domain;
/**
* Enum representation of {@link SavingsAccount} status states.
*/
public enum SavingsAccountStatusType {
INVALID(0, "savingsAccountStatusType.invalid"), //
UNACTIVATED(100, "savingsAccountStatusType.unactivated"), //
ACTIVE(300, "savingsAccountStatusType.active"), //
CLOSED(600, "savingsAccountStatusType.closed");
private final Integer value;
private final String code;
public static SavingsAccountStatusType fromInt(final Integer type) {
SavingsAccountStatusType enumeration = SavingsAccountStatusType.INVALID;
switch (type) {
case 100:
enumeration = SavingsAccountStatusType.UNACTIVATED;
break;
case 300:
enumeration = SavingsAccountStatusType.ACTIVE;
break;
case 600:
enumeration = SavingsAccountStatusType.CLOSED;
break;
}
return enumeration;
}
private SavingsAccountStatusType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public boolean hasStateOf(final SavingsAccountStatusType state) {
return this.value.equals(state.getValue());
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return code;
}
public boolean isUnactivated() {
return this.value.equals(SavingsAccountStatusType.UNACTIVATED.getValue());
}
public boolean isActive() {
return this.value.equals(SavingsAccountStatusType.ACTIVE.getValue());
}
public boolean isClosed() {
return this.value.equals(SavingsAccountStatusType.CLOSED.getValue());
}
}
@@ -45,7 +45,7 @@ public final class SavingsAccountSummary {
public boolean isLessThanOrEqualToAccountBalance(final Money amount) {
final Money accountBalance = getAccountBalance(amount.getCurrency());
return accountBalance.isGreaterThanOrEqualTo(accountBalance);
return accountBalance.isGreaterThanOrEqualTo(amount);
}
public Money getAccountBalance(final MonetaryCurrency currency) {
@@ -1,15 +0,0 @@
package org.mifosplatform.portfolio.savings.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* Thrown when an attempt is made to withdraw money that is greater than the
* account balance.
*/
public class InvalidSavingsAccountTransactionException extends AbstractPlatformDomainRuleException {
public InvalidSavingsAccountTransactionException(final String errorCode, final String defaultErrorMessage, final String paramName,
final Object... defaultUserMessageArgs) {
super("error.msg.savingsaccount.transaction." + errorCode, defaultErrorMessage, paramName, defaultUserMessageArgs);
}
}
@@ -0,0 +1,26 @@
package org.mifosplatform.portfolio.savings.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.savings.service.SavingsAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ActivateSavingsAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService savingAccountWritePlatformService;
@Autowired
public ActivateSavingsAccountCommandHandler(final SavingsAccountWritePlatformService savingAccountWritePlatformService) {
this.savingAccountWritePlatformService = savingAccountWritePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.savingAccountWritePlatformService.activate(command.getSavingsId(), command);
}
}
@@ -21,6 +21,6 @@ public class DepositSavingsAccountCommandHandler implements NewCommandSourceHand
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.savingAccountWritePlatformService.deposit(command.getLoanId(), command);
return this.savingAccountWritePlatformService.deposit(command.getSavingsId(), command);
}
}
@@ -21,6 +21,6 @@ public class WithdrawSavingsAccountCommandHandler implements NewCommandSourceHan
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.savingAccountWritePlatformService.withdrawal(command.getLoanId(), command);
return this.savingAccountWritePlatformService.withdrawal(command.getSavingsId(), command);
}
}
@@ -23,6 +23,7 @@ import org.mifosplatform.portfolio.group.data.GroupData;
import org.mifosplatform.portfolio.group.service.GroupReadPlatformService;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.savings.data.SavingsAccountData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountSummaryData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionEnumData;
@@ -92,6 +93,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
public SavingAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, ");
sqlBuilder.append("sa.status_enum as statusEnum, sa.activation_date as activationDate, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.name as groupName, ");
sqlBuilder.append("sp.id as productId, sp.name as productName, ");
@@ -128,9 +130,14 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = rs.getLong("groupId");
final Integer statusEnum = JdbcSupport.getInteger(rs, "statusEnum");
final SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusEnum);
final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = rs.getLong("clientId");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
@@ -167,9 +174,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
final SavingsAccountSummaryData summary = new SavingsAccountSummaryData(currency, totalDeposits, totalWithdrawals,
totalInterestPosted, accountBalance);
return SavingsAccountData.instance(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance,
lockinPeriodFrequency, lockinPeriodFrequencyType, summary);
return SavingsAccountData.instance(id, accountNo, externalId, status, activationDate, groupId, groupName, clientId, clientName,
productId, productName, currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate,
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, summary);
}
}
@@ -413,10 +420,12 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
groupName = group.getName();
}
final SavingsAccountStatusEnumData status = null;
final LocalDate activationDate = null;
final SavingsAccountSummaryData summary = null;
return SavingsAccountData.instance(null, null, null, groupId, groupName, clientId, clientName, productId, productName,
currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate, minRequiredOpeningBalance,
lockinPeriodFrequency, lockinPeriodFrequencyType, summary);
return SavingsAccountData.instance(null, null, null, status, activationDate, groupId, groupName, clientId, clientName,
productId, productName, currency, interestRate, interestRatePeriodFrequencyType, annualInterestRate,
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, summary);
}
}
}
@@ -16,6 +16,8 @@ public interface SavingsAccountWritePlatformService {
CommandProcessingResult deleteSavingAccount(Long accountId);
CommandProcessingResult activate(Long savingsId, JsonCommand command);
CommandProcessingResult deposit(Long savingsId, JsonCommand command);
CommandProcessingResult withdrawal(Long savingsId, JsonCommand command);
@@ -7,9 +7,12 @@ package org.mifosplatform.portfolio.savings.service;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -61,8 +64,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
public SavingsAccountWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final SavingsAccountRepositoryWrapper savingAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final SavingsAccountAssembler savingAccountAssembler,
final SavingsAccountDataValidator savingsAccountDataValidator,
final SavingsAccountAssembler savingAccountAssembler, final SavingsAccountDataValidator savingsAccountDataValidator,
final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator,
final AccountNumberGeneratorFactory accountIdentifierGeneratorFactory, final ClientRepository clientRepository,
final GroupRepository groupRepository, final SavingsProductRepository savingsProductRepository) {
@@ -124,30 +126,31 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
this.savingAccountRepository.save(account);
}
final Long savingsId = account.getId();
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(account.getId()) //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
} catch (DataAccessException dve) {
handleDataIntegrityIssues(command, dve);
return new CommandProcessingResult(Long.valueOf(-1));
}
}
@Transactional
@Override
public CommandProcessingResult updateSavingAccount(final Long accountId, final JsonCommand command) {
public CommandProcessingResult updateSavingAccount(final Long savingsId, final JsonCommand command) {
try {
this.context.authenticatedUser();
this.savingsAccountDataValidator.validateForUpdate(command.json());
final Map<String, Object> changes = new LinkedHashMap<String, Object>(20);
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(accountId);
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId);
account.update(command, changes);
@@ -190,10 +193,11 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(account.getId()) //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.with(changes) //
.build();
} catch (DataAccessException dve) {
@@ -204,19 +208,46 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult deleteSavingAccount(final Long accountId) {
public CommandProcessingResult deleteSavingAccount(final Long savingsId) {
this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(accountId);
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId);
this.savingAccountRepository.delete(account);
return new CommandProcessingResultBuilder() //
.withEntityId(accountId) //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@Transactional
@Override
public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
this.savingsAccountTransactionDataValidator.validateActivation(command);
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate");
account.activate(fmt, activationDate);
this.savingAccountRepository.save(account);
return new CommandProcessingResultBuilder() //
.withEntityId(savingsId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.build();
}
@@ -230,10 +261,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final SavingsAccountTransaction deposit = account.deposit(transactionDate, transactionAmount);
final SavingsAccountTransaction deposit = account.deposit(fmt, transactionDate, transactionAmount);
final Long transactionId = saveTransactionToGenerateTransactionId(deposit);
this.savingAccountRepository.save(account);
@@ -258,12 +292,17 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
this.context.authenticatedUser();
this.savingsAccountTransactionDataValidator.validate(command);
final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId);
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final SavingsAccountTransaction withdrawal = account.withdrawal(transactionDate, transactionAmount);
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale);
final SavingsAccountTransaction withdrawal = account.withdraw(fmt, transactionDate, transactionAmount);
final Long transactionId = saveTransactionToGenerateTransactionId(withdrawal);
this.savingAccountRepository.save(account);
@@ -7,7 +7,9 @@ package org.mifosplatform.portfolio.savings.service;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionEnumData;
import org.mifosplatform.portfolio.savings.domain.SavingsAccountStatusType;
import org.mifosplatform.portfolio.savings.domain.SavingsAccountTransactionType;
public class SavingsEnumerations {
@@ -103,4 +105,35 @@ public class SavingsEnumerations {
}
return optionData;
}
public static SavingsAccountStatusEnumData status(final Integer statusEnum) {
return status(SavingsAccountStatusType.fromInt(statusEnum));
}
public static SavingsAccountStatusEnumData status(final SavingsAccountStatusType type) {
SavingsAccountStatusEnumData optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.INVALID.getValue().longValue(),
SavingsAccountStatusType.INVALID.getCode(), "Invalid", false, false, false);
switch (type) {
case INVALID:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.INVALID.getValue().longValue(),
SavingsAccountStatusType.INVALID.getCode(), "Invalid", type.isUnactivated(), type.isActive(), type.isClosed());
break;
case UNACTIVATED:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.UNACTIVATED.getValue().longValue(),
SavingsAccountStatusType.UNACTIVATED.getCode(), "Unactivated", type.isUnactivated(), type.isActive(),
type.isClosed());
break;
case ACTIVE:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.ACTIVE.getValue().longValue(),
SavingsAccountStatusType.ACTIVE.getCode(), "Active", type.isUnactivated(), type.isActive(), type.isClosed());
break;
case CLOSED:
optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.CLOSED.getValue().longValue(),
SavingsAccountStatusType.CLOSED.getCode(), "Closed", type.isUnactivated(), type.isActive(), type.isClosed());
break;
}
return optionData;
}
}