Merge remote-tracking branch 'origin/uate' into dev
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s

This commit is contained in:
2026-07-27 14:24:16 +05:30
24 changed files with 1255 additions and 44 deletions
@@ -60,6 +60,8 @@ public interface ShareAccountApiConstants {
String externalid_paramname = "externalId"; String externalid_paramname = "externalId";
String resolutionnumber_paramname = "resolutionNumber";
String currency_paramname = "currencyCode"; String currency_paramname = "currencyCode";
String digitsafterdecimal_paramname = "digitsAfterDecimal"; String digitsafterdecimal_paramname = "digitsAfterDecimal";
@@ -102,9 +104,10 @@ public interface ShareAccountApiConstants {
Set<String> supportedParameters = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(locale_paramname, dateformat_paramname, Set<String> supportedParameters = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(locale_paramname, dateformat_paramname,
id_paramname, clientid_paramname, productid_paramname, submitteddate_paramname, approveddate_paramname, externalid_paramname, id_paramname, clientid_paramname, productid_paramname, submitteddate_paramname, approveddate_paramname, externalid_paramname,
currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname, requestedshares_paramname, resolutionnumber_paramname, currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname,
savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname, minimumactiveperiod_paramname, requestedshares_paramname, savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname,
minimumactiveperiodfrequencytype_paramname, allowdividendcalculationforinactiveclients_paramname, charges_paramname, minimumactiveperiod_paramname, minimumactiveperiodfrequencytype_paramname,
applicationdate_param, purchaseddate_paramname, numberofshares_paramname, purchasedprice_paramname))); allowdividendcalculationforinactiveclients_paramname, charges_paramname, applicationdate_param, purchaseddate_paramname,
numberofshares_paramname, purchasedprice_paramname)));
} }
@@ -42,6 +42,7 @@ public class AccountRequest implements Serializable {
private Long numberOfShares; private Long numberOfShares;
private String allowDividendCalculationForInactiveClients; private String allowDividendCalculationForInactiveClients;
private String externalId; private String externalId;
private String resolutionNumber;
private String minimumActivePeriodFrequencyType; private String minimumActivePeriodFrequencyType;
private Long savingsAccountId; private Long savingsAccountId;
private String locale; private String locale;
@@ -92,10 +92,12 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountApplicationTimel
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData; import org.apache.fineract.portfolio.savings.data.SavingsAccountStatusEnumData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountSubStatusEnumData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData; import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum;
import org.apache.fineract.portfolio.savings.exception.DepositAccountNotFoundException; import org.apache.fineract.portfolio.savings.exception.DepositAccountNotFoundException;
import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
@@ -847,6 +849,13 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
selectFieldsSqlBuilder.append("sa.senior_citizen_interest_rate as seniorCitizenInterestRate, "); selectFieldsSqlBuilder.append("sa.senior_citizen_interest_rate as seniorCitizenInterestRate, ");
selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,"); selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,");
selectFieldsSqlBuilder.append("sa.sub_status_enum as subStatusEnum, ");
selectFieldsSqlBuilder.append("sa.reason_for_block as reasonForBlock, ");
selectFieldsSqlBuilder.append("(select COALESCE(max(sat.transaction_date),sa.activatedon_date) ");
selectFieldsSqlBuilder.append("from m_savings_account_transaction as sat ");
selectFieldsSqlBuilder.append("where sat.is_reversed = false and sat.is_reversal = false ");
selectFieldsSqlBuilder.append("and sat.transaction_type_enum in (1,2) ");
selectFieldsSqlBuilder.append("and sat.savings_account_id = sa.id) as lastActiveTransactionDate, ");
selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
this.selectFieldsSql = selectFieldsSqlBuilder.toString(); this.selectFieldsSql = selectFieldsSqlBuilder.toString();
@@ -1129,12 +1138,48 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod,
depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, transferToSavingsId); depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, transferToSavingsId);
// Live per-account dormancy state: subStatus + the actual countdown, mirroring
// SavingsAccountReadPlatformServiceImpl.SavingAccountMapper. The isDormancyTrackingActive/
// daysToInactive/daysToDormancy/daysToEscheat columns read below are the product's
// configured thresholds; they're used here only as inputs to compute the live countdown.
final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum");
final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum);
final String reasonForBlock = rs.getString("reasonForBlock");
final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate");
final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive");
final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive");
final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy");
final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat");
Integer computedDaysToInactive = null;
Integer computedDaysToDormancy = null;
Integer computedDaysToEscheat = null;
final Integer accountStatusEnum = JdbcSupport.getInteger(rs, "statusEnum");
if (isDormancyTrackingActive && lastActiveTransactionDate != null && accountStatusEnum != null
&& accountStatusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) {
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) {
computedDaysToEscheat = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToEscheat));
}
if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) {
computedDaysToDormancy = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToDormancy));
}
if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) {
computedDaysToInactive = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToInactive));
}
}
result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"), result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"),
JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"),
JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"),
rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), isDormancyTrackingActive,
JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), computedDaysToInactive == null ? null : computedDaysToInactive.longValue(),
JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), computedDaysToDormancy == null ? null : computedDaysToDormancy.longValue(),
computedDaysToEscheat == null ? null : computedDaysToEscheat.longValue(),
JdbcSupport.getInteger(rs, "interestCalculateSelector"),
rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"),
JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"),
rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"),
@@ -1144,6 +1189,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
result.setDormancyStatus(subStatus, reasonForBlock, lastActiveTransactionDate);
result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"), result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"),
JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"), JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"),
rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"), rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"),
@@ -1293,12 +1339,45 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit,
allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited); allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited);
// Live per-account dormancy state: subStatus + the actual countdown, mirroring
// SavingsAccountReadPlatformServiceImpl.SavingAccountMapper. The isDormancyTrackingActive/
// daysToInactive/daysToDormancy/daysToEscheat columns read below are the product's
// configured thresholds; they're used here only as inputs to compute the live countdown.
final Integer subStatusEnum = JdbcSupport.getInteger(rs, "subStatusEnum");
final SavingsAccountSubStatusEnumData subStatus = SavingsEnumerations.subStatus(subStatusEnum);
final String reasonForBlock = rs.getString("reasonForBlock");
final LocalDate lastActiveTransactionDate = JdbcSupport.getLocalDate(rs, "lastActiveTransactionDate");
final boolean isDormancyTrackingActive = rs.getBoolean("isDormancyTrackingActive");
final Integer numDaysToInactive = JdbcSupport.getInteger(rs, "daysToInactive");
final Integer numDaysToDormancy = JdbcSupport.getInteger(rs, "daysToDormancy");
final Integer numDaysToEscheat = JdbcSupport.getInteger(rs, "daysToEscheat");
Integer computedDaysToInactive = null;
Integer computedDaysToDormancy = null;
Integer computedDaysToEscheat = null;
final Integer accountStatusEnum = JdbcSupport.getInteger(rs, "statusEnum");
if (isDormancyTrackingActive && lastActiveTransactionDate != null && accountStatusEnum != null
&& accountStatusEnum.equals(SavingsAccountStatusType.ACTIVE.getValue())) {
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
if (subStatusEnum < SavingsAccountSubStatusEnum.ESCHEAT.getValue()) {
computedDaysToEscheat = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToEscheat));
}
if (subStatusEnum < SavingsAccountSubStatusEnum.DORMANT.getValue()) {
computedDaysToDormancy = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToDormancy));
}
if (subStatusEnum < SavingsAccountSubStatusEnum.INACTIVE.getValue()) {
computedDaysToInactive = DateUtils.getExactDifferenceInDays(currentDate,
lastActiveTransactionDate.plusDays(numDaysToInactive));
}
}
result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("productCode"), result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("productCode"),
JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"),
JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"),
rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), isDormancyTrackingActive,
JdbcSupport.getInteger(rs, "daysToInactive"), JdbcSupport.getInteger(rs, "daysToDormancy"), computedDaysToInactive, computedDaysToDormancy,
JdbcSupport.getInteger(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), computedDaysToEscheat, JdbcSupport.getInteger(rs, "interestCalculateSelector"),
rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"),
JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"),
rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"),
@@ -1308,6 +1387,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
result.setDormancyStatus(subStatus, reasonForBlock, lastActiveTransactionDate);
result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"), result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"),
JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"), JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"),
rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"), rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"),
@@ -0,0 +1,154 @@
/**
* 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.shareaccounts.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountPendingEditData;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountEditWritePlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/shareaccounts")
@Component
@Tag(name = "Share Account Edit Approval", description = "Maker-checker workflow for editing a Share Account's details (linked savings account, dividend calculation for inactive clients, lockin period, minimum active period). Edits are held in m_share_account_pending_edit (a pending row there is itself the lock) and only applied to the real account once a checker approves.")
@RequiredArgsConstructor
public class ShareAccountEditApiResource {
private final PlatformSecurityContext context;
private final ShareAccountEditWritePlatformService shareAccountEditWritePlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<ShareAccountPendingEditData> pendingEditApiJsonSerializer;
@POST
@Path("{accountId}/edit")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit a details edit for a Share Account",
description = "Edits are held in m_share_account_pending_edit (a pending row there is itself the lock) and only "
+ "applied to the real account once a checker approves - see /shareaccounts/{accountId}/edit/approve and /reject below.")
public String submitEdit(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasPermissionTo("SUBMITEDIT_SHAREACCOUNT");
final JsonCommand command = JsonCommand.fromJsonElement(accountId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
this.fromApiJsonHelper);
final CommandProcessingResult result = this.shareAccountEditWritePlatformService.submitEditForApproval(accountId, command);
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("edit/pending")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Share Account details edits pending approval")
public String retrieveAllPendingEdits(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission("SHAREACCOUNT");
final Collection<ShareAccountPendingEditData> pendingEdits = this.shareAccountEditWritePlatformService
.retrieveAllPendingEdits();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.pendingEditApiJsonSerializer.serialize(settings, pendingEdits);
}
@GET
@Path("{accountId}/edit/pending")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve the pending details edit for a Share Account, if any")
public String retrievePendingEdit(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission("SHAREACCOUNT");
final ShareAccountPendingEditData pendingEdit = this.shareAccountEditWritePlatformService.retrievePendingEdit(accountId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.pendingEditApiJsonSerializer.serialize(settings, pendingEdit);
}
@POST
@Path("{accountId}/edit/approve")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve a pending Share Account details edit",
description = "Applies the proposed details changes to the account. The maker of the edit cannot also approve it.")
public String approvePendingEdit(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) {
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVEEDIT_SHAREACCOUNT")) {
throw new NoAuthorizationException("User has no authority to approve share account edits");
}
final CommandProcessingResult result = this.shareAccountEditWritePlatformService.approveEdit(accountId);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}/edit/reject")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Reject a pending Share Account details edit",
description = "Discards the proposed changes without applying them. The maker of the edit cannot also reject it.")
public String rejectPendingEdit(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECTEDIT_SHAREACCOUNT")) {
throw new NoAuthorizationException("User has no authority to reject share account edits");
}
String rejectReason = null;
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
}
}
final CommandProcessingResult result = this.shareAccountEditWritePlatformService.rejectEdit(accountId, rejectReason);
return this.toApiJsonSerializer.serialize(result);
}
}
@@ -41,6 +41,8 @@ public class ShareAccountData implements Serializable, AccountData {
private String externalId; private String externalId;
private String resolutionNumber;
private String savingsAccountNumber; private String savingsAccountNumber;
private Long clientId; private Long clientId;
@@ -154,9 +156,10 @@ public class ShareAccountData implements Serializable, AccountData {
private Collection<EnumOptionData> minimumActivePeriodFrequencyTypeOptions; private Collection<EnumOptionData> minimumActivePeriodFrequencyTypeOptions;
private Collection<SavingsAccountData> clientSavingsAccounts; private Collection<SavingsAccountData> clientSavingsAccounts;
public ShareAccountData(final Long id, final String accountNo, final String externalId, final Long savingsAccountId, public ShareAccountData(final Long id, final String accountNo, final String externalId, final String resolutionNumber,
final String savingsAccountNumber, final Long clientId, final String clientName, final Long productId, final String productName, final Long savingsAccountId, final String savingsAccountNumber, final Long clientId, final String clientName,
final ShareAccountStatusEnumData status, final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, final Long productId, final String productName, final ShareAccountStatusEnumData status,
final ShareAccountApplicationTimelineData timeline, final CurrencyData currency,
final ShareAccountSummaryData summaryData, final Collection<ShareAccountChargeData> charges, final ShareAccountSummaryData summaryData, final Collection<ShareAccountChargeData> charges,
final Collection<ShareAccountTransactionData> purchasedSharesData, final Integer lockinPeriod, final Collection<ShareAccountTransactionData> purchasedSharesData, final Integer lockinPeriod,
final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum, final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum,
@@ -164,6 +167,7 @@ public class ShareAccountData implements Serializable, AccountData {
this.id = id; this.id = id;
this.accountNo = accountNo; this.accountNo = accountNo;
this.externalId = externalId; this.externalId = externalId;
this.resolutionNumber = resolutionNumber;
this.savingsAccountId = savingsAccountId; this.savingsAccountId = savingsAccountId;
this.savingsAccountNumber = savingsAccountNumber; this.savingsAccountNumber = savingsAccountNumber;
this.clientId = clientId; this.clientId = clientId;
@@ -208,9 +212,10 @@ public class ShareAccountData implements Serializable, AccountData {
this.defaultShares = defaultShares; this.defaultShares = defaultShares;
} }
private ShareAccountData(final Long id, final String accountNo, final String externalId, final Long savingsAccountId, private ShareAccountData(final Long id, final String accountNo, final String externalId, final String resolutionNumber,
final String savingsAccountNumber, final Long clientId, final String clientName, final Long productId, final String productName, final Long savingsAccountId, final String savingsAccountNumber, final Long clientId, final String clientName,
final ShareAccountStatusEnumData status, final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, final Long productId, final String productName, final ShareAccountStatusEnumData status,
final ShareAccountApplicationTimelineData timeline, final CurrencyData currency,
final ShareAccountSummaryData summaryData, final Collection<ShareAccountChargeData> charges, final ShareAccountSummaryData summaryData, final Collection<ShareAccountChargeData> charges,
final Collection<ShareAccountTransactionData> purchasedSharesData, final Integer lockinPeriod, final Collection<ShareAccountTransactionData> purchasedSharesData, final Integer lockinPeriod,
final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum, final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum,
@@ -221,6 +226,7 @@ public class ShareAccountData implements Serializable, AccountData {
this.id = id; this.id = id;
this.accountNo = accountNo; this.accountNo = accountNo;
this.externalId = externalId; this.externalId = externalId;
this.resolutionNumber = resolutionNumber;
this.savingsAccountId = savingsAccountId; this.savingsAccountId = savingsAccountId;
this.savingsAccountNumber = savingsAccountNumber; this.savingsAccountNumber = savingsAccountNumber;
this.clientId = clientId; this.clientId = clientId;
@@ -250,15 +256,17 @@ public class ShareAccountData implements Serializable, AccountData {
Collection<ChargeData> chargeOptions, Collection<SavingsAccountData> clientSavingsAccounts, Collection<ChargeData> chargeOptions, Collection<SavingsAccountData> clientSavingsAccounts,
Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions,
Collection<EnumOptionData> minimumActivePeriodFrequencyTypeOptions) { Collection<EnumOptionData> minimumActivePeriodFrequencyTypeOptions) {
return new ShareAccountData(data.id, data.accountNo, data.externalId, data.savingsAccountId, data.savingsAccountNumber, return new ShareAccountData(data.id, data.accountNo, data.externalId, data.resolutionNumber, data.savingsAccountId,
data.clientId, data.clientName, data.productId, data.productName, data.status, data.timeline, data.currency, data.summary, data.savingsAccountNumber, data.clientId, data.clientName, data.productId, data.productName, data.status, data.timeline,
data.charges, data.purchasedShares, data.lockinPeriod, data.lockPeriodTypeEnum, data.minimumActivePeriod, data.currency, data.summary, data.charges, data.purchasedShares, data.lockinPeriod, data.lockPeriodTypeEnum,
data.minimumActivePeriodTypeEnum, data.allowDividendCalculationForInactiveClients, productOptions, chargeOptions, data.minimumActivePeriod, data.minimumActivePeriodTypeEnum, data.allowDividendCalculationForInactiveClients,
clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, minimumActivePeriodFrequencyTypeOptions, data.currentMarketPrice); productOptions, chargeOptions, clientSavingsAccounts, lockinPeriodFrequencyTypeOptions,
minimumActivePeriodFrequencyTypeOptions, data.currentMarketPrice);
} }
public static ShareAccountData lookup(final Long id, final String accountNo, final Long clientId, final String clientName) { public static ShareAccountData lookup(final Long id, final String accountNo, final Long clientId, final String clientName) {
String externalId = null; String externalId = null;
String resolutionNumber = null;
ShareAccountSummaryData summaryData = null; ShareAccountSummaryData summaryData = null;
Collection<ShareAccountTransactionData> purchasedSharesData = null; Collection<ShareAccountTransactionData> purchasedSharesData = null;
GLAccountData suspenseAccount = null; GLAccountData suspenseAccount = null;
@@ -282,10 +290,11 @@ public class ShareAccountData implements Serializable, AccountData {
final Long productId = null; final Long productId = null;
final Collection<ProductData> productOptions = null; final Collection<ProductData> productOptions = null;
return new ShareAccountData(id, accountNo, externalId, savingsAccountId, savingsAccountNumber, clientId, clientName, productId, return new ShareAccountData(id, accountNo, externalId, resolutionNumber, savingsAccountId, savingsAccountNumber, clientId,
productName, status, timeline, currency, summaryData, charges, purchasedSharesData, lockinPeriod, lockPeriodTypeEnum, clientName, productId, productName, status, timeline, currency, summaryData, charges, purchasedSharesData, lockinPeriod,
minimumActivePeriod, minimumActivePeriodTypeEnum, allowDividendCalculationForInactiveClients, productOptions, chargeOptions, lockPeriodTypeEnum, minimumActivePeriod, minimumActivePeriodTypeEnum, allowDividendCalculationForInactiveClients,
clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, minimumActivePeriodFrequencyTypeOption, currenMarketPrice); productOptions, chargeOptions, clientSavingsAccounts, lockinPeriodFrequencyTypeOptions,
minimumActivePeriodFrequencyTypeOption, currenMarketPrice);
} }
public void setCurrentMarketPrice(final BigDecimal currentMarketPrice) { public void setCurrentMarketPrice(final BigDecimal currentMarketPrice) {
@@ -0,0 +1,57 @@
/**
* 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.shareaccounts.data;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* Read-side view of a single pending Share Account "details" edit (m_share_account_pending_edit), for the Approvals
* -> Pending Share Account Edits screen.
*/
@Getter
@Setter
public class ShareAccountPendingEditData implements Serializable {
private Long id;
private Long shareAccountId;
private String accountNo;
private Long clientId;
private String clientName;
private Long makerId;
private String makerUsername;
private Long checkerId;
private String checkerUsername;
private LocalDateTime createdOn;
private LocalDateTime checkedOn;
private String status;
private String rejectReason;
private Long savingsAccountId;
private String savingsAccountNo;
private Boolean allowDividendCalculationForInactiveClients;
private Integer lockinPeriodFrequency;
private Integer lockinPeriodFrequencyType;
private Integer minimumActivePeriodFrequency;
private Integer minimumActivePeriodFrequencyType;
public ShareAccountPendingEditData() {}
}
@@ -105,6 +105,9 @@ public class ShareAccount extends AbstractPersistableCustom<Long> {
@Column(name = "external_id") @Column(name = "external_id")
private String externalId; private String externalId;
@Column(name = "resolution_number", nullable = false)
private String resolutionNumber;
@Column(name = "account_no", length = 20, unique = true, nullable = false) @Column(name = "account_no", length = 20, unique = true, nullable = false)
private String accountNumber; private String accountNumber;
@@ -151,10 +154,11 @@ public class ShareAccount extends AbstractPersistableCustom<Long> {
} }
public ShareAccount(final Client client, final ShareProduct shareProduct, final String externalId, final MonetaryCurrency currency, public ShareAccount(final Client client, final ShareProduct shareProduct, final String externalId, final String resolutionNumber,
final SavingsAccount savingsAccount, final String accountNo, final Long totalSharesApproved, final Long totalSharesPending, final MonetaryCurrency currency, final SavingsAccount savingsAccount, final String accountNo,
final Set<ShareAccountTransaction> purchasedShares, final Boolean allowDividendCalculationForInactiveClients, final Long totalSharesApproved, final Long totalSharesPending, final Set<ShareAccountTransaction> purchasedShares,
final Integer lockinPeriodFrequency, final PeriodFrequencyType lockPeriodType, final Integer minimumActivePeriodFrequency, final Boolean allowDividendCalculationForInactiveClients, final Integer lockinPeriodFrequency,
final PeriodFrequencyType lockPeriodType, final Integer minimumActivePeriodFrequency,
final PeriodFrequencyType minimumActivePeriodType, Set<ShareAccountCharge> charges, AppUser submittedBy, final PeriodFrequencyType minimumActivePeriodType, Set<ShareAccountCharge> charges, AppUser submittedBy,
final LocalDate submittedDate, AppUser approvedBy, LocalDate approvedDate, AppUser rejectedBy, LocalDate rejectedDate, final LocalDate submittedDate, AppUser approvedBy, LocalDate approvedDate, AppUser rejectedBy, LocalDate rejectedDate,
AppUser activatedBy, LocalDate activatedDate, AppUser closedBy, LocalDate closedDate, AppUser modifiedBy, AppUser activatedBy, LocalDate activatedDate, AppUser closedBy, LocalDate closedDate, AppUser modifiedBy,
@@ -163,6 +167,7 @@ public class ShareAccount extends AbstractPersistableCustom<Long> {
this.client = client; this.client = client;
this.shareProduct = shareProduct; this.shareProduct = shareProduct;
this.externalId = externalId; this.externalId = externalId;
this.resolutionNumber = resolutionNumber;
this.currency = currency; this.currency = currency;
this.savingsAccount = savingsAccount; this.savingsAccount = savingsAccount;
if (StringUtils.isBlank(accountNo)) { if (StringUtils.isBlank(accountNo)) {
@@ -235,6 +240,19 @@ public class ShareAccount extends AbstractPersistableCustom<Long> {
return toReturn; return toReturn;
} }
public String getResolutionNumber() {
return this.resolutionNumber;
}
public boolean setResolutionNumber(final String resolutionNumber) {
boolean toReturn = false;
if (this.resolutionNumber == null || !this.resolutionNumber.equals(resolutionNumber)) {
this.resolutionNumber = resolutionNumber;
toReturn = true;
}
return toReturn;
}
public boolean setSavingsAccount(final SavingsAccount savingsAccount) { public boolean setSavingsAccount(final SavingsAccount savingsAccount) {
boolean returnValue = false; boolean returnValue = false;
if (!this.savingsAccount.getId().equals(savingsAccount.getId())) { if (!this.savingsAccount.getId().equals(savingsAccount.getId())) {
@@ -561,6 +579,22 @@ public class ShareAccount extends AbstractPersistableCustom<Long> {
return this.lockinPeriodFrequencyType; return this.lockinPeriodFrequencyType;
} }
public Integer getMinimumActivePeriodFrequency() {
return this.minimumActivePeriodFrequency;
}
public PeriodFrequencyType getMinimumActivePeriodFrequencyType() {
return this.minimumActivePeriodFrequencyType;
}
public Boolean getAllowDividendCalculationForInactiveClients() {
return this.allowDividendCalculationForInactiveClients;
}
public SavingsAccount getSavingsAccount() {
return this.savingsAccount;
}
public LocalDate getActivatedDate() { public LocalDate getActivatedDate() {
return this.activatedDate; return this.activatedDate;
} }
@@ -0,0 +1,138 @@
/**
* 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.shareaccounts.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Holds a pending edit request for a Share Account's "details" fields (linked savings account,
* dividend-calculation-for-inactive-clients, lockin period, minimum active period). Presence of a row here with
* {@code status = PENDING} (enforced via the unique {@link #pendingLockKey}) is itself the lock - nothing is applied
* to the real {@code m_share_account} row until a checker approves (see
* ShareAccountEditWritePlatformServiceJpaRepositoryImpl).
*/
@Entity
@Table(name = "m_share_account_pending_edit")
@Getter
@Setter
public class ShareAccountPendingEdit extends AbstractPersistableCustom<Long> {
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_APPROVED = "APPROVED";
public static final String STATUS_REJECTED = "REJECTED";
@ManyToOne
@JoinColumn(name = "share_account_id", nullable = false)
private ShareAccount shareAccount;
@ManyToOne
@JoinColumn(name = "createdby_id", nullable = false)
private AppUser maker;
@ManyToOne
@JoinColumn(name = "lastmodifiedby_id")
private AppUser checker;
@Column(name = "created_date", nullable = false)
private LocalDateTime createdOn;
@Column(name = "lastmodified_date")
private LocalDateTime checkedOn;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "reject_reason", length = 500)
private String rejectReason;
/** "SHARE_ACCOUNT_EDIT:<shareAccountId>" while PENDING, NULL otherwise - the real DB-level lock. */
@Column(name = "pending_lock_key", length = 60)
private String pendingLockKey;
/** Links to the audit-only m_portfolio_command_source row created for this edit's submit/approve/reject. */
@Column(name = "command_source_id")
private Long commandSourceId;
@ManyToOne
@JoinColumn(name = "savings_account_id")
private SavingsAccount savingsAccount;
@Column(name = "allow_dividends_inactive_clients")
private Boolean allowDividendCalculationForInactiveClients;
@Column(name = "lockin_period_frequency")
private Integer lockinPeriodFrequency;
@Enumerated(EnumType.ORDINAL)
@Column(name = "lockin_period_frequency_enum")
private PeriodFrequencyType lockinPeriodFrequencyType;
@Column(name = "minimum_active_period_frequency")
private Integer minimumActivePeriodFrequency;
@Enumerated(EnumType.ORDINAL)
@Column(name = "minimum_active_period_frequency_enum")
private PeriodFrequencyType minimumActivePeriodFrequencyType;
protected ShareAccountPendingEdit() {}
public ShareAccountPendingEdit(final ShareAccount shareAccount, final AppUser maker) {
this.shareAccount = shareAccount;
this.maker = maker;
this.createdOn = LocalDateTime.now();
this.status = STATUS_PENDING;
this.pendingLockKey = lockKeyFor(shareAccount.getId());
}
public static String lockKeyFor(final Long shareAccountId) {
return "SHARE_ACCOUNT_EDIT:" + shareAccountId;
}
public boolean isPending() {
return STATUS_PENDING.equals(this.status);
}
public void markApproved(final AppUser checker) {
this.status = STATUS_APPROVED;
this.checker = checker;
this.checkedOn = LocalDateTime.now();
this.pendingLockKey = null;
}
public void markRejected(final AppUser checker, final String reason) {
this.status = STATUS_REJECTED;
this.checker = checker;
this.checkedOn = LocalDateTime.now();
this.rejectReason = reason;
this.pendingLockKey = null;
}
}
@@ -0,0 +1,32 @@
/**
* 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.shareaccounts.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ShareAccountPendingEditRepository extends JpaRepository<ShareAccountPendingEdit, Long> {
Optional<ShareAccountPendingEdit> findByShareAccountIdAndStatus(Long shareAccountId, String status);
List<ShareAccountPendingEdit> findByStatus(String status);
}
@@ -0,0 +1,29 @@
/**
* 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.shareaccounts.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ShareAccountEditPendingApprovalException extends AbstractPlatformDomainRuleException {
public ShareAccountEditPendingApprovalException(final Long shareAccountId) {
super("error.msg.shareaccount.edit.pending.approval",
"Share account with identifier " + shareAccountId + " already has a details edit pending approval", shareAccountId);
}
}
@@ -0,0 +1,30 @@
/**
* 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.shareaccounts.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ShareAccountEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
public ShareAccountEditSelfApprovalNotAllowedException(final Long shareAccountId) {
super("error.msg.shareaccount.edit.self.approval.not.allowed",
"The maker of the pending details edit for share account " + shareAccountId + " cannot also approve or reject it",
shareAccountId);
}
}
@@ -0,0 +1,29 @@
/**
* 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.shareaccounts.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ShareAccountNoPendingEditException extends AbstractPlatformDomainRuleException {
public ShareAccountNoPendingEditException(final Long shareAccountId) {
super("error.msg.shareaccount.no.pending.edit",
"Share account with identifier " + shareAccountId + " has no details edit awaiting approval", shareAccountId);
}
}
@@ -145,6 +145,10 @@ public class ShareAccountDataSerializer {
final String externalId = this.fromApiJsonHelper.extractStringNamed(ShareAccountApiConstants.externalid_paramname, element); final String externalId = this.fromApiJsonHelper.extractStringNamed(ShareAccountApiConstants.externalid_paramname, element);
// baseDataValidator.reset().parameter(ShareAccountApiConstants.externalid_paramname).value(externalId).notNull(); // baseDataValidator.reset().parameter(ShareAccountApiConstants.externalid_paramname).value(externalId).notNull();
final String resolutionNumber = this.fromApiJsonHelper.extractStringNamed(ShareAccountApiConstants.resolutionnumber_paramname,
element);
baseDataValidator.reset().parameter(ShareAccountApiConstants.resolutionnumber_paramname).value(resolutionNumber).notBlank();
Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element); Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element);
baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull() baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull()
.longGreaterThanZero(); .longGreaterThanZero();
@@ -216,10 +220,10 @@ public class ShareAccountDataSerializer {
Set<ShareAccountTransaction> sharesPurchased = new HashSet<>(); Set<ShareAccountTransaction> sharesPurchased = new HashSet<>();
sharesPurchased.add(transaction); sharesPurchased.add(transaction);
ShareAccount account = new ShareAccount(client, shareProduct, externalId, currency, savingsAccount, accountNo, approvedShares, ShareAccount account = new ShareAccount(client, shareProduct, externalId, resolutionNumber, currency, savingsAccount, accountNo,
pendingShares, sharesPurchased, allowdividendsForInactiveClients, lockinPeriod, lockPeriodEnum, minimumActivePeriod, approvedShares, pendingShares, sharesPurchased, allowdividendsForInactiveClients, lockinPeriod, lockPeriodEnum,
minimumActivePeriodEnum, charges, submittedBy, submittedDate, approvedBy, approvedDate, rejectedBy, rejectedDate, minimumActivePeriod, minimumActivePeriodEnum, charges, submittedBy, submittedDate, approvedBy, approvedDate, rejectedBy,
activatedBy, activatedDate, closedBy, closedDate, modifiedBy, modifiedDate); rejectedDate, activatedBy, activatedDate, closedBy, closedDate, modifiedBy, modifiedDate);
for (ShareAccountTransaction pur : sharesPurchased) { for (ShareAccountTransaction pur : sharesPurchased) {
pur.setShareAccount(account); pur.setShareAccount(account);
@@ -300,6 +304,15 @@ public class ShareAccountDataSerializer {
} }
} }
if (this.fromApiJsonHelper.parameterExists(ShareAccountApiConstants.resolutionnumber_paramname, element)) {
final String resolutionNumber = this.fromApiJsonHelper
.extractStringNamed(ShareAccountApiConstants.resolutionnumber_paramname, element);
baseDataValidator.reset().parameter(ShareAccountApiConstants.resolutionnumber_paramname).value(resolutionNumber).notBlank();
if (account.setResolutionNumber(resolutionNumber)) {
actualChanges.put(ShareAccountApiConstants.resolutionnumber_paramname, resolutionNumber);
}
}
if (this.fromApiJsonHelper.parameterExists(ShareAccountApiConstants.savingsaccountid_paramname, element)) { if (this.fromApiJsonHelper.parameterExists(ShareAccountApiConstants.savingsaccountid_paramname, element)) {
Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element); Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element);
baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull() baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull()
@@ -0,0 +1,37 @@
/**
* 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.shareaccounts.service;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountPendingEditData;
public interface ShareAccountEditWritePlatformService {
CommandProcessingResult submitEditForApproval(Long shareAccountId, JsonCommand command);
CommandProcessingResult approveEdit(Long shareAccountId);
CommandProcessingResult rejectEdit(Long shareAccountId, String rejectReason);
ShareAccountPendingEditData retrievePendingEdit(Long shareAccountId);
Collection<ShareAccountPendingEditData> retrieveAllPendingEdits();
}
@@ -0,0 +1,302 @@
/**
* 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.shareaccounts.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.accounts.constants.ShareAccountApiConstants;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountPendingEditData;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountPendingEdit;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountPendingEditRepository;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper;
import org.apache.fineract.portfolio.shareaccounts.exception.ShareAccountEditPendingApprovalException;
import org.apache.fineract.portfolio.shareaccounts.exception.ShareAccountEditSelfApprovalNotAllowedException;
import org.apache.fineract.portfolio.shareaccounts.exception.ShareAccountNoPendingEditException;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.transaction.annotation.Transactional;
/**
* Maker-checker edit flow for a Share Account's "details" fields (linked savings account,
* allow-dividend-calculation-for-inactive-clients, lockin period, minimum active period). Mirrors
* {@code SavingsAccountEditWritePlatformServiceJpaRepositoryImpl}'s submitEditForApproval/approveEdit/rejectEdit
* pattern: a row in {@code m_share_account_pending_edit} is itself the lock, and nothing is applied to the real
* {@code m_share_account} row until a checker approves. Unlike the Savings Account version, this edit carries no
* balance/hold implications, so the maker's full intended new state of the 6 fields is captured (falling back to the
* account's current value for any field the maker didn't touch) rather than a partial toggle diff.
*/
@RequiredArgsConstructor
public class ShareAccountEditWritePlatformServiceJpaRepositoryImpl implements ShareAccountEditWritePlatformService {
private final PlatformSecurityContext context;
private final ShareAccountRepositoryWrapper shareAccountRepositoryWrapper;
private final ShareAccountPendingEditRepository pendingEditRepository;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final CommandSourceRepository commandSourceRepository;
@Transactional
@Override
public CommandProcessingResult submitEditForApproval(final Long shareAccountId, final JsonCommand command) {
final AppUser maker = this.context.authenticatedUser();
final ShareAccount account = this.shareAccountRepositoryWrapper.findOneWithNotFoundDetection(shareAccountId);
if (this.pendingEditRepository.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
.isPresent()) {
throw new ShareAccountEditPendingApprovalException(shareAccountId);
}
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("sharesaccount");
final ShareAccountPendingEdit pendingEdit = new ShareAccountPendingEdit(account, maker);
if (command.hasParameter(ShareAccountApiConstants.savingsaccountid_paramname)) {
final Long savingsAccountId = command.longValueOfParameterNamed(ShareAccountApiConstants.savingsaccountid_paramname);
baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull()
.longGreaterThanZero();
if (!this.savingsAccountReadPlatformService.isAccountBelongsToClient(account.getClientId(), savingsAccountId,
DepositAccountType.SAVINGS_DEPOSIT, account.getShareProduct().getCurrency().getCode())) {
throw new SavingsAccountNotFoundException(savingsAccountId);
}
pendingEdit.setSavingsAccount(this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId));
} else {
pendingEdit.setSavingsAccount(account.getSavingsAccount());
}
if (command.hasParameter(ShareAccountApiConstants.allowdividendcalculationforinactiveclients_paramname)) {
pendingEdit.setAllowDividendCalculationForInactiveClients(
command.booleanObjectValueOfParameterNamed(ShareAccountApiConstants.allowdividendcalculationforinactiveclients_paramname));
} else {
pendingEdit.setAllowDividendCalculationForInactiveClients(account.getAllowDividendCalculationForInactiveClients());
}
if (command.hasParameter(ShareAccountApiConstants.lockinperiod_paramname)) {
final Integer lockinPeriod = command.integerValueOfParameterNamed(ShareAccountApiConstants.lockinperiod_paramname);
baseDataValidator.reset().parameter(ShareAccountApiConstants.lockinperiod_paramname).value(lockinPeriod).notNull();
pendingEdit.setLockinPeriodFrequency(lockinPeriod);
} else {
pendingEdit.setLockinPeriodFrequency(account.getLockinPeriodFrequency());
}
if (command.hasParameter(ShareAccountApiConstants.lockperiodfrequencytype_paramname)) {
pendingEdit.setLockinPeriodFrequencyType(
extractPeriodType(command.integerValueOfParameterNamed(ShareAccountApiConstants.lockperiodfrequencytype_paramname)));
} else {
pendingEdit.setLockinPeriodFrequencyType(account.getLockinPeriodFrequencyType());
}
if (command.hasParameter(ShareAccountApiConstants.minimumactiveperiod_paramname)) {
final Integer minimumActivePeriod = command.integerValueOfParameterNamed(ShareAccountApiConstants.minimumactiveperiod_paramname);
baseDataValidator.reset().parameter(ShareAccountApiConstants.minimumactiveperiod_paramname).value(minimumActivePeriod)
.notNull();
pendingEdit.setMinimumActivePeriodFrequency(minimumActivePeriod);
} else {
pendingEdit.setMinimumActivePeriodFrequency(account.getMinimumActivePeriodFrequency());
}
if (command.hasParameter(ShareAccountApiConstants.minimumactiveperiodfrequencytype_paramname)) {
pendingEdit.setMinimumActivePeriodFrequencyType(extractPeriodType(
command.integerValueOfParameterNamed(ShareAccountApiConstants.minimumactiveperiodfrequencytype_paramname)));
} else {
pendingEdit.setMinimumActivePeriodFrequencyType(account.getMinimumActivePeriodFrequencyType());
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("SUBMITEDIT") //
.entityName("SHAREACCOUNT") //
.resourceGetUrl("/shareaccounts/" + shareAccountId) //
.resourceId(shareAccountId) //
.commandAsJson(command.json()) //
.maker(maker) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(UUID.randomUUID().toString()) //
.sanitized(false) //
.build();
this.commandSourceRepository.saveAndFlush(auditEntry);
pendingEdit.setCommandSourceId(auditEntry.getId());
this.pendingEditRepository.saveAndFlush(pendingEdit);
return new CommandProcessingResultBuilder().withEntityId(shareAccountId).build();
}
@Transactional
@Override
public CommandProcessingResult approveEdit(final Long shareAccountId) {
final AppUser checker = this.context.authenticatedUser();
final ShareAccountPendingEdit pendingEdit = this.pendingEditRepository
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
}
final ShareAccount account = this.shareAccountRepositoryWrapper.findOneWithNotFoundDetection(shareAccountId);
if (pendingEdit.getSavingsAccount() != null) {
account.setSavingsAccount(pendingEdit.getSavingsAccount());
}
account.setAllowDividendCalculationForInactiveClients(pendingEdit.getAllowDividendCalculationForInactiveClients());
if (pendingEdit.getLockinPeriodFrequency() != null) {
account.setLockPeriod(pendingEdit.getLockinPeriodFrequency());
}
if (pendingEdit.getLockinPeriodFrequencyType() != null) {
account.setLockPeriodFrequencyEnum(pendingEdit.getLockinPeriodFrequencyType());
}
if (pendingEdit.getMinimumActivePeriodFrequency() != null) {
account.setminimumActivePeriod(pendingEdit.getMinimumActivePeriodFrequency());
}
if (pendingEdit.getMinimumActivePeriodFrequencyType() != null) {
account.setminimumActivePeriodTypeEnum(pendingEdit.getMinimumActivePeriodFrequencyType());
}
this.shareAccountRepositoryWrapper.saveAndFlush(account);
pendingEdit.markApproved(checker);
this.pendingEditRepository.saveAndFlush(pendingEdit);
updateAuditEntry(pendingEdit, checker, false);
return new CommandProcessingResultBuilder().withEntityId(shareAccountId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectEdit(final Long shareAccountId, final String rejectReason) {
final AppUser checker = this.context.authenticatedUser();
final ShareAccountPendingEdit pendingEdit = this.pendingEditRepository
.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
.orElseThrow(() -> new ShareAccountNoPendingEditException(shareAccountId));
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
throw new ShareAccountEditSelfApprovalNotAllowedException(shareAccountId);
}
pendingEdit.markRejected(checker, rejectReason);
this.pendingEditRepository.saveAndFlush(pendingEdit);
updateAuditEntry(pendingEdit, checker, true);
return new CommandProcessingResultBuilder().withEntityId(shareAccountId).build();
}
@Override
public ShareAccountPendingEditData retrievePendingEdit(final Long shareAccountId) {
return this.pendingEditRepository.findByShareAccountIdAndStatus(shareAccountId, ShareAccountPendingEdit.STATUS_PENDING)
.map(this::toData).orElse(null);
}
@Override
public Collection<ShareAccountPendingEditData> retrieveAllPendingEdits() {
return this.pendingEditRepository.findByStatus(ShareAccountPendingEdit.STATUS_PENDING).stream().map(this::toData).toList();
}
// ── helpers ──────────────────────────────────────────────────────────────
private PeriodFrequencyType extractPeriodType(final Integer value) {
return value == null ? null : PeriodFrequencyType.fromInt(value);
}
private void updateAuditEntry(final ShareAccountPendingEdit pendingEdit, final AppUser checker, final boolean rejected) {
Optional<CommandSource> existing = Optional.empty();
if (pendingEdit.getCommandSourceId() != null) {
existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
}
final CommandSource auditEntry = existing.orElseGet(() -> CommandSource.builder() //
.actionName(rejected ? "REJECTEDIT" : "APPROVEEDIT") //
.entityName("SHAREACCOUNT") //
.resourceGetUrl("/shareaccounts/" + pendingEdit.getShareAccount().getId()) //
.resourceId(pendingEdit.getShareAccount().getId()) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
.idempotencyKey(UUID.randomUUID().toString()) //
.sanitized(false) //
.build());
if (rejected) {
auditEntry.markAsRejected(checker);
} else {
auditEntry.markAsChecked(checker);
}
this.commandSourceRepository.saveAndFlush(auditEntry);
}
private ShareAccountPendingEditData toData(final ShareAccountPendingEdit p) {
final ShareAccountPendingEditData data = new ShareAccountPendingEditData();
data.setId(p.getId());
data.setShareAccountId(p.getShareAccount().getId());
data.setAccountNo(p.getShareAccount().getAccountNumber());
data.setClientId(p.getShareAccount().getClientId());
data.setMakerId(p.getMaker().getId());
data.setMakerUsername(p.getMaker().getUsername());
if (p.getChecker() != null) {
data.setCheckerId(p.getChecker().getId());
data.setCheckerUsername(p.getChecker().getUsername());
}
data.setCreatedOn(p.getCreatedOn());
data.setCheckedOn(p.getCheckedOn());
data.setStatus(p.getStatus());
data.setRejectReason(p.getRejectReason());
if (p.getSavingsAccount() != null) {
data.setSavingsAccountId(p.getSavingsAccount().getId());
data.setSavingsAccountNo(p.getSavingsAccount().getAccountNumber());
}
data.setAllowDividendCalculationForInactiveClients(p.getAllowDividendCalculationForInactiveClients());
data.setLockinPeriodFrequency(p.getLockinPeriodFrequency());
if (p.getLockinPeriodFrequencyType() != null) {
data.setLockinPeriodFrequencyType(p.getLockinPeriodFrequencyType().getValue());
}
data.setMinimumActivePeriodFrequency(p.getMinimumActivePeriodFrequency());
if (p.getMinimumActivePeriodFrequencyType() != null) {
data.setMinimumActivePeriodFrequencyType(p.getMinimumActivePeriodFrequencyType().getValue());
}
return data;
}
}
@@ -241,7 +241,8 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
final Collection<ShareAccountTransactionData> purchasedShares) { final Collection<ShareAccountTransactionData> purchasedShares) {
this.charges = charges; this.charges = charges;
this.purchasedShares = purchasedShares; this.purchasedShares = purchasedShares;
StringBuilder buff = new StringBuilder().append("sa.id as id, sa.external_id as externalId, sa.status_enum as statusEnum, ") StringBuilder buff = new StringBuilder().append(
"sa.id as id, sa.external_id as externalId, sa.resolution_number as resolutionNumber, sa.status_enum as statusEnum, ")
.append("sa.savings_account_id, msa.account_no as savingsAccNo, ") .append("sa.savings_account_id, msa.account_no as savingsAccNo, ")
.append("c.id as clientId, c.display_name as clientName, ") .append("c.id as clientId, c.display_name as clientName, ")
.append("sa.account_no as accountNo, sa.total_approved_shares as approvedShares, sa.total_pending_shares as pendingShares, ") .append("sa.account_no as accountNo, sa.total_approved_shares as approvedShares, sa.total_pending_shares as pendingShares, ")
@@ -278,6 +279,7 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
final Long id = rs.getLong("id"); final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo"); final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId"); final String externalId = rs.getString("externalId");
final String resolutionNumber = rs.getString("resolutionNumber");
final Long savingsAccountId = JdbcSupport.getLong(rs, "savings_account_id"); final Long savingsAccountId = JdbcSupport.getLong(rs, "savings_account_id");
final String savingsAccountNumber = rs.getString("savingsAccNo"); final String savingsAccountNumber = rs.getString("savingsAccNo");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); final Long clientId = JdbcSupport.getLong(rs, "clientId");
@@ -349,9 +351,10 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
final String shortProductName = null; final String shortProductName = null;
final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName, final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName,
shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline); shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline);
return new ShareAccountData(id, accountNo, externalId, savingsAccountId, savingsAccountNumber, clientId, clientName, productId, return new ShareAccountData(id, accountNo, externalId, resolutionNumber, savingsAccountId, savingsAccountNumber, clientId,
productName, status, timeline, currency, summary, charges, purchasedShares, lockinPeriodFrequency, clientName, productId, productName, status, timeline, currency, summary, charges, purchasedShares,
lockinPeriodFrequencyType, minimumActivePeriod, minimumActivePeriodType, allowdividendsforinactiveclients); lockinPeriodFrequency, lockinPeriodFrequencyType, minimumActivePeriod, minimumActivePeriodType,
allowdividendsforinactiveclients);
} }
@@ -392,6 +395,7 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
final Long totalApprovedShares = null; final Long totalApprovedShares = null;
final Long totalPendingShares = null; final Long totalPendingShares = null;
final String externalId = null; final String externalId = null;
final String resolutionNumber = null;
final Long productId = null; final Long productId = null;
final String productName = null; final String productName = null;
final String shortProductName = null; final String shortProductName = null;
@@ -418,9 +422,9 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName, final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName,
shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline); shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline);
return new ShareAccountData(id, accountNo, externalId, clientId, clientName, productId, shortProductName, productId, return new ShareAccountData(id, accountNo, externalId, resolutionNumber, clientId, clientName, productId, shortProductName,
shortProductName, status, timeline, currency, summary, charges, purchasedSharesData, lockinPeriod, lockPeriodTypeEnum, productId, shortProductName, status, timeline, currency, summary, charges, purchasedSharesData, lockinPeriod,
minimumActivePeriod, minimumActivePeriodTypeEnum, allowdividendsforinactiveclients); lockPeriodTypeEnum, minimumActivePeriod, minimumActivePeriodTypeEnum, allowdividendsforinactiveclients);
} }
} }
@@ -34,7 +34,12 @@ import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository; import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountDividendRepository;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountPendingEditRepository;
import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper;
import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer;
import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService; import org.apache.fineract.portfolio.shareaccounts.service.PurchasedSharesReadPlatformService;
@@ -44,6 +49,8 @@ import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountChargeRea
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountCommandsServiceImpl;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformService;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountDividendReadPlatformServiceImpl;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountEditWritePlatformService;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountEditWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService;
@@ -121,4 +128,14 @@ public class ShareAccountsConfiguration {
accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository, accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository,
jdbcTemplate, businessEventNotifierService); jdbcTemplate, businessEventNotifierService);
} }
@Bean
@ConditionalOnMissingBean(ShareAccountEditWritePlatformService.class)
public ShareAccountEditWritePlatformService shareAccountEditWritePlatformService(PlatformSecurityContext context,
ShareAccountRepositoryWrapper shareAccountRepositoryWrapper, ShareAccountPendingEditRepository pendingEditRepository,
SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper,
SavingsAccountReadPlatformService savingsAccountReadPlatformService, CommandSourceRepository commandSourceRepository) {
return new ShareAccountEditWritePlatformServiceJpaRepositoryImpl(context, shareAccountRepositoryWrapper, pendingEditRepository,
savingsAccountRepositoryWrapper, savingsAccountReadPlatformService, commandSourceRepository);
}
} }
@@ -466,4 +466,7 @@
<include file="parts/0357_add_dd_limit_to_sponsor_dd_gl_mapping.xml" relativeToChangelogFile="true"/> <include file="parts/0357_add_dd_limit_to_sponsor_dd_gl_mapping.xml" relativeToChangelogFile="true"/>
<include file="parts/0358_add_batch_id_to_sponsor_dd_issue_request.xml" relativeToChangelogFile="true"/> <include file="parts/0358_add_batch_id_to_sponsor_dd_issue_request.xml" relativeToChangelogFile="true"/>
<include file="parts/0359_reclassify_dd_gst_payable_as_income.xml" relativeToChangelogFile="true"/> <include file="parts/0359_reclassify_dd_gst_payable_as_income.xml" relativeToChangelogFile="true"/>
<include file="parts/0360_add_resolution_number_to_share_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0361_add_share_account_pending_edit_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0362_add_share_account_edit_approval_permissions.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_share_account">
<column name="resolution_number" type="VARCHAR(100)" defaultValue="">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Holds a pending edit request for a Share Account's "details" fields (linked savings account,
dividend-calculation-for-inactive-clients, lockin period, minimum active period). Presence of a
row here with status=PENDING (enforced via the unique pending_lock_key) is itself the lock -
nothing is applied to the real m_share_account row until a checker approves
(see ShareAccountEditWritePlatformServiceJpaRepositoryImpl).
-->
<changeSet author="fineract" id="1">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_share_account_pending_edit" />
</not>
</preConditions>
<createTable tableName="m_share_account_pending_edit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="share_account_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_shaoe_share_account_id" references="m_share_account(id)" />
</column>
<column name="createdby_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_shaoe_maker_id" references="m_appuser(id)" />
</column>
<column name="lastmodifiedby_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_shaoe_checker_id" references="m_appuser(id)" />
</column>
<column name="created_date" type="DATETIME">
<constraints nullable="false" />
</column>
<column name="lastmodified_date" type="DATETIME">
<constraints nullable="true" />
</column>
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
<constraints nullable="false" />
</column>
<column name="reject_reason" type="VARCHAR(500)" />
<column name="pending_lock_key" type="VARCHAR(60)">
<constraints nullable="true" unique="true" uniqueConstraintName="shaoe_unq_pending_lock_key" />
</column>
<column name="command_source_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_shaoe_command_source_id" references="m_portfolio_command_source(id)" />
</column>
<!-- Proposed new values for the 6 editable "details" fields. NULL means the maker left that
field unchanged from the account's current value at submission time. -->
<column name="savings_account_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_shaoe_savings_account_id" references="m_savings_account(id)" />
</column>
<column name="allow_dividends_inactive_clients" type="BOOLEAN" />
<column name="lockin_period_frequency" type="INTEGER" />
<column name="lockin_period_frequency_enum" type="SMALLINT" />
<column name="minimum_active_period_frequency" type="INTEGER" />
<column name="minimum_active_period_frequency_enum" type="SMALLINT" />
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Permissions gating the Share Account "Edit Details" maker-checker flow. SUBMITEDIT lets a maker
propose the edit (the "Edit Details" action on the account view); APPROVEEDIT/REJECTEDIT let a
checker resolve it. The _CHECKER permission follows the "<ACTION>_<ENTITY>_CHECKER" convention so
the pending request also surfaces in the generic Checker Inbox for limited checkers.
-->
<changeSet author="fineract" id="1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'SUBMITEDIT_SHAREACCOUNT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="SHAREACCOUNT" />
<column name="code" value="SUBMITEDIT_SHAREACCOUNT" />
<column name="entity_name" value="SHAREACCOUNT" />
<column name="action_name" value="SUBMITEDIT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SHAREACCOUNT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="SHAREACCOUNT" />
<column name="code" value="APPROVEEDIT_SHAREACCOUNT" />
<column name="entity_name" value="SHAREACCOUNT" />
<column name="action_name" value="APPROVEEDIT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="3">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECTEDIT_SHAREACCOUNT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="SHAREACCOUNT" />
<column name="code" value="REJECTEDIT_SHAREACCOUNT" />
<column name="entity_name" value="SHAREACCOUNT" />
<column name="action_name" value="REJECTEDIT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="4">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SHAREACCOUNT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="SHAREACCOUNT" />
<column name="code" value="APPROVEEDIT_SHAREACCOUNT_CHECKER" />
<column name="entity_name" value="SHAREACCOUNT" />
<column name="action_name" value="APPROVEEDIT_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -116,6 +116,21 @@ public final class FixedDepositAccountData extends DepositAccountData {
private Boolean deceasedSettlement; private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate; private Boolean allowDebitTxnAfterDueDate;
// Live per-account dormancy state (distinct from the product-level
// isDormancyTrackingActive/daysToInactive/daysToDormancy/daysToEscheat
// thresholds above): the account's actual current sub-status and the
// computed countdown, mirroring SavingsAccountData.
private SavingsAccountSubStatusEnumData subStatus;
private String reasonForBlock;
private LocalDate lastActiveTransactionDate;
public void setDormancyStatus(final SavingsAccountSubStatusEnumData subStatus, final String reasonForBlock,
final LocalDate lastActiveTransactionDate) {
this.subStatus = subStatus;
this.reasonForBlock = reasonForBlock;
this.lastActiveTransactionDate = lastActiveTransactionDate;
}
// Joint holders for JOINT account status // Joint holders for JOINT account status
private java.util.List<java.util.Map<String, Object>> jointHolders; private java.util.List<java.util.Map<String, Object>> jointHolders;
@@ -314,6 +329,9 @@ public final class FixedDepositAccountData extends DepositAccountData {
this.jointHolders = src.jointHolders; this.jointHolders = src.jointHolders;
this.isSeniorCitizen = src.isSeniorCitizen; this.isSeniorCitizen = src.isSeniorCitizen;
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate; this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
this.subStatus = src.subStatus;
this.reasonForBlock = src.reasonForBlock;
this.lastActiveTransactionDate = src.lastActiveTransactionDate;
} }
private FixedDepositAccountData(Long clientId, Long productId, Long fieldofficerId, LocalDate submittedOnDate, private FixedDepositAccountData(Long clientId, Long productId, Long fieldofficerId, LocalDate submittedOnDate,
@@ -147,6 +147,21 @@ public final class RecurringDepositAccountData extends DepositAccountData {
private Boolean deceasedSettlement; private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate; private Boolean allowDebitTxnAfterDueDate;
// Live per-account dormancy state (distinct from the product-level
// isDormancyTrackingActive/daysToInactive/daysToDormancy/daysToEscheat
// thresholds above): the account's actual current sub-status and the
// computed countdown, mirroring SavingsAccountData.
private SavingsAccountSubStatusEnumData subStatus;
private String reasonForBlock;
private LocalDate lastActiveTransactionDate;
public void setDormancyStatus(final SavingsAccountSubStatusEnumData subStatus, final String reasonForBlock,
final LocalDate lastActiveTransactionDate) {
this.subStatus = subStatus;
this.reasonForBlock = reasonForBlock;
this.lastActiveTransactionDate = lastActiveTransactionDate;
}
public void setAccountTypeOptions(Collection<CodeValueData> accountTypeOptions) { public void setAccountTypeOptions(Collection<CodeValueData> accountTypeOptions) {
this.accountTypeOptions = accountTypeOptions; this.accountTypeOptions = accountTypeOptions;
} }
@@ -238,6 +253,9 @@ public final class RecurringDepositAccountData extends DepositAccountData {
this.jointHolders = src.jointHolders; this.jointHolders = src.jointHolders;
this.isSeniorCitizen = src.isSeniorCitizen; this.isSeniorCitizen = src.isSeniorCitizen;
this.seniorCitizenInterestRate = src.seniorCitizenInterestRate; this.seniorCitizenInterestRate = src.seniorCitizenInterestRate;
this.subStatus = src.subStatus;
this.reasonForBlock = src.reasonForBlock;
this.lastActiveTransactionDate = src.lastActiveTransactionDate;
} }
public void setCbsProductFields(String accountType, String customerSegment, String productCode, public void setCbsProductFields(String accountType, String customerSegment, String productCode,
@@ -748,10 +748,17 @@ public final class ProductPendingEditFieldMapper {
f.setTotalShares(command.longValueOfParameterNamed("totalShares")); f.setTotalShares(command.longValueOfParameterNamed("totalShares"));
present.add("totalShares"); present.add("totalShares");
} }
if (command.hasParameter("totalSharesIssued") || command.hasParameter("issuedShares")) { if (command.hasParameter("sharesIssued") || command.hasParameter("totalSharesIssued") || command.hasParameter("issuedShares")) {
final Long val = command.hasParameter("totalSharesIssued") ? command.longValueOfParameterNamed("totalSharesIssued") : command.longValueOfParameterNamed("issuedShares"); final Long val;
if (command.hasParameter("sharesIssued")) {
val = command.longValueOfParameterNamed("sharesIssued");
} else if (command.hasParameter("totalSharesIssued")) {
val = command.longValueOfParameterNamed("totalSharesIssued");
} else {
val = command.longValueOfParameterNamed("issuedShares");
}
f.setTotalSharesIssued(val); f.setTotalSharesIssued(val);
present.add("totalSharesIssued"); present.add("sharesIssued");
} }
if (command.hasParameter("unitPrice")) { if (command.hasParameter("unitPrice")) {
f.setUnitPrice(command.bigDecimalValueOfParameterNamed("unitPrice")); f.setUnitPrice(command.bigDecimalValueOfParameterNamed("unitPrice"));
@@ -820,7 +827,7 @@ public final class ProductPendingEditFieldMapper {
return; return;
} }
putNumber(root, submitted, "totalShares", f.getTotalShares()); putNumber(root, submitted, "totalShares", f.getTotalShares());
putNumber(root, submitted, "totalSharesIssued", f.getTotalSharesIssued()); putNumber(root, submitted, "sharesIssued", f.getTotalSharesIssued());
putNumber(root, submitted, "unitPrice", f.getUnitPrice()); putNumber(root, submitted, "unitPrice", f.getUnitPrice());
putNumber(root, submitted, "minimumShares", f.getMinimumShares()); putNumber(root, submitted, "minimumShares", f.getMinimumShares());
putNumber(root, submitted, "nominalShares", f.getNominalShares()); putNumber(root, submitted, "nominalShares", f.getNominalShares());