From 424bd0884e7f21cc9c945fbbfd130be6423fbdbe Mon Sep 17 00:00:00 2001 From: ShruthiRajaram Date: Tue, 17 Jul 2018 17:05:55 +0530 Subject: [PATCH] FINERACT-628 Savings account APIs for Self Service App --- api-docs/apiLive.htm | 470 +++++++++++++++++- .../savings/api/SelfSavingsApiResource.java | 59 ++- .../data/SelfSavingsAccountConstants.java | 28 ++ .../data/SelfSavingsDataValidator.java | 44 ++ 4 files changed, 599 insertions(+), 2 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsAccountConstants.java diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 9a87171ccca..052ba098c02 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -3667,10 +3667,26 @@

Self Service

Savings + self/savingsaccounts/template?clientId={clientId}&productId={productId} + + Retrieve Savings Application Template + + + + + + self/savingsaccounts + Submit new Savings Application + + + + + + self/savingsaccounts/{accountId} Retrieve a Savings Account - + Update Savings Account @@ -46695,6 +46711,100 @@

Retrieve a Loan Charge

+ +   +
+
+

Submit new savings application

+
+ + + +
Mandatory Fields
clientId, productId, submittedOnDate
+
+ + + +
Optional Fields
accountNo, externalId, fieldOfficerId
+
+ + + + +
Inherited from Product (if not provided)
nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeForTransfers, allowOverdraft, overdraftLimit, withHoldTax
+
+
+
+

Minimal request: accountNo auto generated, remaining details inherited from savings product.

+ POST https://Domain Name/api/v1/self/savingsaccounts + POST self/savingsaccount +Content-Type: application/json +Request Body: +{ + "productId":3, + "clientId":"14", + "locale":"en", + "dateFormat":"dd MMMM yyyy", + "submittedOnDate":"09 July 2018" +} + + +{ + "officeId":1, + "clientId":14, + "savingsId":10, + "resourceId":10 +} + + +

Full request:Below details override details from savings product (except currency).

+ POST self/savingsaccount +Content-Type: application/json +Request Body: +{ + "productId":1, + "nominalAnnualInterestRate":9.5, + "minRequiredOpeningBalance":1000, + "lockinPeriodFrequency":1, + "withdrawalFeeForTransfers":true, + "allowOverdraft":true, + "overdraftLimit":"10000", + "nominalAnnualInterestRateOverdraft":"5", + "minOverdraftForInterestCalculation":"1000", + "enforceMinRequiredBalance":false, + "withHoldTax":false, + "interestCompoundingPeriodType":1, + "interestPostingPeriodType":4, + "interestCalculationType":1, + "interestCalculationDaysInYearType":365, + "lockinPeriodFrequencyType":1, + "fieldOfficerId":1, + "externalId":"126", + "submittedOnDate":"11 July 2018", + "locale":"en", + "dateFormat":"dd MMMM yyyy", + "monthDayFormat":"dd MMM", + "charges":[ + { + "chargeId":3, + "amount":2 + } + ], + "clientId":"14" +} + + +{ + "officeId":1, + "clientId":14, + "savingsId":9, + "resourceId":9 +} + +
+
+ +  
@@ -46785,6 +46895,364 @@

Arguments

+ +   +
+
+

Modify a savings application

+

Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc

+
+
+ PUT https://Domain Name/api/v1/self/savingsaccounts/{accountsId} + PUT self/savingsaccounts/1 +Content-Type: application/json +No Request Body: +{ + "productId":1, + "clientId":14 +} + + +{ + "officeId": 1, + "clientId": 14, + "savingsId": 11, + "resourceId": 11, + "changes": { + "productId": 1 + } +} + +
+
+ +   +
+
+

Retrieve Savings Account Template:

+

Arguments

+
+
clientId
+
Integer mandatory
+
productId
+
Integer optional
+
If entered, productId, productName and selectedProduct fields are returned.
+
+

Example Requests:

+
self/savingsaccounts/template?clientId=14
+

+
self/savingsaccounts/template?clientId=14&productId=2
+
+
+ GET https://Domain Name/api/v1/self/savingsaccounts/template?clientId={clientId} + +{ + "clientId": 14, + "clientName": "Bheem", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false, + "isDormancyTrackingActive": false, + "productOptions": [ + { + "id": 1, + "name": "Voluntary savings", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + }, + { + "id": 2, + "name": "Savings", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + }, + { + "id": 3, + "name": "Savings Product", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + } + ], + "chargeOptions": [ + { + "id": 3, + "name": "Saving Accounts Processing Fee", + "active": true, + "penalty": false, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 2, + "chargeTimeType": { + "id": 3, + "code": "chargeTimeType.savingsActivation", + "value": "Savings Activation" + }, + "chargeAppliesTo": { + "id": 2, + "code": "chargeAppliesTo.savings", + "value": "Savings" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "chargePaymentMode": { + "id": 0, + "code": "chargepaymentmode.regular", + "value": "Regular" + } + } + ] +} + +
+ +
+ GET https://Domain Name/api/v1/self/savingsaccounts/template?clientId={clientId}&productId={productId} + +{ + "clientId": 14, + "clientName": "Bheem", + "savingsProductId": 2, + "savingsProductName": "Savings", + "timeline": {}, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "inMultiplesOf": 1, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "nominalAnnualInterestRate": 5, + "interestCompoundingPeriodType": { + "id": 1, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.daily", + "value": "Daily" + }, + "interestPostingPeriodType": { + "id": 4, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.monthly", + "value": "Monthly" + }, + "interestCalculationType": { + "id": 1, + "code": "savingsInterestCalculationType.dailybalance", + "value": "Daily Balance" + }, + "interestCalculationDaysInYearType": { + "id": 365, + "code": "savingsInterestCalculationDaysInYearType.days365", + "value": "365 Days" + }, + "minRequiredOpeningBalance": 1000, + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false, + "isDormancyTrackingActive": false, + "charges": [], + "productOptions": [ + { + "id": 1, + "name": "Voluntary savings", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + }, + { + "id": 2, + "name": "Savings", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + }, + { + "id": 3, + "name": "Savings Product", + "withdrawalFeeForTransfers": false, + "allowOverdraft": false, + "enforceMinRequiredBalance": false, + "withHoldTax": false + } + ], + "fieldOfficerOptions": [ + { + "id": 1, + "firstname": "Aliya", + "lastname": "A", + "displayName": "A, Aliya", + "officeId": 1, + "officeName": "Head Office", + "isLoanOfficer": true, + "isActive": true + } + ], + "interestCompoundingPeriodTypeOptions": [ + { + "id": 1, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.daily", + "value": "Daily" + }, + { + "id": 4, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.monthly", + "value": "Monthly" + }, + { + "id": 5, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.quarterly", + "value": "Quarterly" + }, + { + "id": 6, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.biannual", + "value": "Semi-Annual" + }, + { + "id": 7, + "code": "savings.interest.period.savingsCompoundingInterestPeriodType.annual", + "value": "Annually" + } + ], + "interestPostingPeriodTypeOptions": [ + { + "id": 4, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.monthly", + "value": "Monthly" + }, + { + "id": 5, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.quarterly", + "value": "Quarterly" + }, + { + "id": 6, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.biannual", + "value": "BiAnnual" + }, + { + "id": 7, + "code": "savings.interest.posting.period.savingsPostingInterestPeriodType.annual", + "value": "Annually" + } + ], + "interestCalculationTypeOptions": [ + { + "id": 1, + "code": "savingsInterestCalculationType.dailybalance", + "value": "Daily Balance" + }, + { + "id": 2, + "code": "savingsInterestCalculationType.averagedailybalance", + "value": "Average Daily Balance" + } + ], + "interestCalculationDaysInYearTypeOptions": [ + { + "id": 360, + "code": "savingsInterestCalculationDaysInYearType.days360", + "value": "360 Days" + }, + { + "id": 365, + "code": "savingsInterestCalculationDaysInYearType.days365", + "value": "365 Days" + } + ], + "lockinPeriodFrequencyTypeOptions": [ + { + "id": 0, + "code": "savings.lockin.savingsPeriodFrequencyType.days", + "value": "Days" + }, + { + "id": 1, + "code": "savings.lockin.savingsPeriodFrequencyType.weeks", + "value": "Weeks" + }, + { + "id": 2, + "code": "savings.lockin.savingsPeriodFrequencyType.months", + "value": "Months" + }, + { + "id": 3, + "code": "savings.lockin.savingsPeriodFrequencyType.years", + "value": "Years" + } + ], + "withdrawalFeeTypeOptions": [ + { + "id": 1, + "code": "savingsWithdrawalFeesType.flat", + "value": "Flat" + }, + { + "id": 2, + "code": "savingsWithdrawalFeesType.percent.of.amount", + "value": "% of Amount" + } + ], + "chargeOptions": [ + { + "id": 3, + "name": "Saving Accounts Processing Fee", + "active": true, + "penalty": false, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 2, + "chargeTimeType": { + "id": 3, + "code": "chargeTimeType.savingsActivation", + "value": "Savings Activation" + }, + "chargeAppliesTo": { + "id": 2, + "code": "chargeAppliesTo.savings", + "value": "Savings" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "chargePaymentMode": { + "id": 0, + "code": "chargepaymentmode.regular", + "value": "Regular" + } + } + ] +} + +
+
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/api/SelfSavingsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/api/SelfSavingsApiResource.java index bfcd11d057d..9eff7e7f824 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/api/SelfSavingsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/api/SelfSavingsApiResource.java @@ -18,9 +18,13 @@ */ package org.apache.fineract.portfolio.self.savings.api; +import java.util.HashMap; + import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -30,10 +34,13 @@ import javax.ws.rs.core.UriInfo; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.exception.ClientNotFoundException; import org.apache.fineract.portfolio.savings.api.SavingsAccountChargesApiResource; import org.apache.fineract.portfolio.savings.api.SavingsAccountTransactionsApiResource; import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.apache.fineract.portfolio.self.client.service.AppuserClientMapperReadService; +import org.apache.fineract.portfolio.self.savings.data.SelfSavingsAccountConstants; import org.apache.fineract.portfolio.self.savings.data.SelfSavingsDataValidator; import org.apache.fineract.portfolio.self.savings.service.AppuserSavingsMapperReadService; import org.apache.fineract.useradministration.domain.AppUser; @@ -52,6 +59,7 @@ public class SelfSavingsApiResource { private final SavingsAccountTransactionsApiResource savingsAccountTransactionsApiResource; private final AppuserSavingsMapperReadService appuserSavingsMapperReadService; private final SelfSavingsDataValidator dataValidator; + private final AppuserClientMapperReadService appUserClientMapperReadService; @Autowired public SelfSavingsApiResource( @@ -60,13 +68,15 @@ public SelfSavingsApiResource( final SavingsAccountChargesApiResource savingsAccountChargesApiResource, final SavingsAccountTransactionsApiResource savingsAccountTransactionsApiResource, final AppuserSavingsMapperReadService appuserSavingsMapperReadService, - final SelfSavingsDataValidator dataValidator) { + final SelfSavingsDataValidator dataValidator, + final AppuserClientMapperReadService appUserClientMapperReadService) { this.context = context; this.savingsAccountsApiResource = savingsAccountsApiResource; this.savingsAccountChargesApiResource = savingsAccountChargesApiResource; this.savingsAccountTransactionsApiResource = savingsAccountTransactionsApiResource; this.appuserSavingsMapperReadService = appuserSavingsMapperReadService; this.dataValidator = dataValidator; + this.appUserClientMapperReadService = appUserClientMapperReadService; } @GET @@ -145,4 +155,51 @@ private void validateAppuserSavingsAccountMapping(final Long accountId) { } } + @GET + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String template(@QueryParam("clientId") final Long clientId, @QueryParam("productId") final Long productId, + final String apiRequestBodyAsJson, @Context final UriInfo uriInfo) { + + validateAppuserClientsMapping(clientId); + Long groupId = null; + boolean staffInSelectedOfficeOnly = false; + return this.savingsAccountsApiResource.template(clientId, groupId, productId, staffInSelectedOfficeOnly, + uriInfo); + + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String submitSavingsAccountApplication(@QueryParam("command") final String commandParam, + @Context final UriInfo uriInfo, final String apiRequestBodyAsJson) { + + HashMap parameterMap = this.dataValidator.validateSavingsApplication(apiRequestBodyAsJson); + final Long clientId = (Long) parameterMap.get(SelfSavingsAccountConstants.clientIdParameterName); + validateAppuserClientsMapping(clientId); + return this.savingsAccountsApiResource.submitApplication(apiRequestBodyAsJson); + } + + @PUT + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String modifySavingsAccountApplication(@PathParam("accountId") final Long accountId, + @QueryParam("command") final String commandParam, final String apiRequestBodyAsJson) { + + validateAppuserSavingsAccountMapping(accountId); + this.dataValidator.validateSavingsApplication(apiRequestBodyAsJson); + return this.savingsAccountsApiResource.update(accountId, apiRequestBodyAsJson, commandParam); + } + + private void validateAppuserClientsMapping(final Long clientId) { + AppUser user = this.context.authenticatedUser(); + final boolean mappedClientId = this.appUserClientMapperReadService.isClientMappedToUser(clientId, user.getId()); + if (!mappedClientId) { + throw new ClientNotFoundException(clientId); + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsAccountConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsAccountConstants.java new file mode 100644 index 00000000000..ec37bbbcf76 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsAccountConstants.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.self.savings.data; + +public class SelfSavingsAccountConstants { + + public static final String savingsAccountResource = "savings"; + public static final String clientIdParameterName = "clientId"; + public static final String productIdParameterName = "productId"; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsDataValidator.java index 3cbadf9e6b9..7ed14ab87ff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/savings/data/SelfSavingsDataValidator.java @@ -20,19 +20,36 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang.StringUtils; import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.google.gson.JsonElement; + @Component public class SelfSavingsDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public SelfSavingsDataValidator(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } private static final Set allowedAssociationParameters = new HashSet<>( Arrays.asList(SavingsApiConstants.transactions, @@ -82,5 +99,32 @@ private void validateTemplate(final UriInfo uriInfo, unsupportedParams.add("template"); } } + + public HashMap validateSavingsApplication(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SelfSavingsAccountConstants.savingsAccountResource); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Long clientId = this.fromApiJsonHelper.extractLongNamed(SelfSavingsAccountConstants.clientIdParameterName, + element); + baseDataValidator.reset().parameter(SelfSavingsAccountConstants.clientIdParameterName).value(clientId).notNull() + .longGreaterThanZero(); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + HashMap parameterMap = new HashMap<>(); + parameterMap.put(SelfSavingsAccountConstants.clientIdParameterName, clientId); + + return parameterMap; + + } }