diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/constants/ShareAccountApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/constants/ShareAccountApiConstants.java index 6abef1d29..346accadd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/constants/ShareAccountApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/constants/ShareAccountApiConstants.java @@ -60,6 +60,8 @@ public interface ShareAccountApiConstants { String externalid_paramname = "externalId"; + String resolutionnumber_paramname = "resolutionNumber"; + String currency_paramname = "currencyCode"; String digitsafterdecimal_paramname = "digitsAfterDecimal"; @@ -102,9 +104,10 @@ public interface ShareAccountApiConstants { Set supportedParameters = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(locale_paramname, dateformat_paramname, id_paramname, clientid_paramname, productid_paramname, submitteddate_paramname, approveddate_paramname, externalid_paramname, - currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname, requestedshares_paramname, - savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname, minimumactiveperiod_paramname, - minimumactiveperiodfrequencytype_paramname, allowdividendcalculationforinactiveclients_paramname, charges_paramname, - applicationdate_param, purchaseddate_paramname, numberofshares_paramname, purchasedprice_paramname))); + resolutionnumber_paramname, currency_paramname, digitsafterdecimal_paramname, inmultiplesof_paramname, + requestedshares_paramname, savingsaccountid_paramname, lockinperiod_paramname, lockperiodfrequencytype_paramname, + minimumactiveperiod_paramname, minimumactiveperiodfrequencytype_paramname, + allowdividendcalculationforinactiveclients_paramname, charges_paramname, applicationdate_param, purchaseddate_paramname, + numberofshares_paramname, purchasedprice_paramname))); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/data/request/AccountRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/data/request/AccountRequest.java index 7181f7dc8..64f07ddba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/data/request/AccountRequest.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accounts/data/request/AccountRequest.java @@ -42,6 +42,7 @@ public class AccountRequest implements Serializable { private Long numberOfShares; private String allowDividendCalculationForInactiveClients; private String externalId; + private String resolutionNumber; private String minimumActivePeriodFrequencyType; private Long savingsAccountId; private String locale; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java index bae584d90..9f8b28e2f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountReadPlatformServiceImpl.java @@ -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.SavingsAccountData; 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.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData; 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.tax.data.TaxGroupData; 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.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); 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 "); this.selectFieldsSql = selectFieldsSqlBuilder.toString(); @@ -1129,12 +1138,48 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod, 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"), JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"), - JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), isDormancyTrackingActive, + computedDaysToInactive == null ? null : computedDaysToInactive.longValue(), + computedDaysToDormancy == null ? null : computedDaysToDormancy.longValue(), + computedDaysToEscheat == null ? null : computedDaysToEscheat.longValue(), + JdbcSupport.getInteger(rs, "interestCalculateSelector"), rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), @@ -1144,6 +1189,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); + result.setDormancyStatus(subStatus, reasonForBlock, lastActiveTransactionDate); result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"), JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"), rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"), @@ -1293,12 +1339,45 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, 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"), JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"), JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"), - rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"), - JdbcSupport.getInteger(rs, "daysToInactive"), JdbcSupport.getInteger(rs, "daysToDormancy"), - JdbcSupport.getInteger(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"), + rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), isDormancyTrackingActive, + computedDaysToInactive, computedDaysToDormancy, + computedDaysToEscheat, JdbcSupport.getInteger(rs, "interestCalculateSelector"), rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"), JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"), rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"), @@ -1308,6 +1387,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"), rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"), rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate")); + result.setDormancyStatus(subStatus, reasonForBlock, lastActiveTransactionDate); result.setCbsAccountFields(rs.getString("accountStatus"), rs.getString("modeOfOperation"), JdbcSupport.getLong(rs, "introducerClientId"), rs.getString("introducerName"), rs.getString("introducerCifNumber"), rs.getString("specialCategory"), rs.getString("weakerSection"), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountEditApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountEditApiResource.java new file mode 100644 index 000000000..144643bae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountEditApiResource.java @@ -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 toApiJsonSerializer; + private final DefaultToApiJsonSerializer 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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountData.java index 0430001bb..4114f219b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountData.java @@ -41,6 +41,8 @@ public class ShareAccountData implements Serializable, AccountData { private String externalId; + private String resolutionNumber; + private String savingsAccountNumber; private Long clientId; @@ -154,9 +156,10 @@ public class ShareAccountData implements Serializable, AccountData { private Collection minimumActivePeriodFrequencyTypeOptions; private Collection clientSavingsAccounts; - public ShareAccountData(final Long id, final String accountNo, final String externalId, final Long savingsAccountId, - final String savingsAccountNumber, final Long clientId, final String clientName, final Long productId, final String productName, - final ShareAccountStatusEnumData status, final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, + public ShareAccountData(final Long id, final String accountNo, final String externalId, final String resolutionNumber, + final Long savingsAccountId, final String savingsAccountNumber, final Long clientId, final String clientName, + final Long productId, final String productName, final ShareAccountStatusEnumData status, + final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, final ShareAccountSummaryData summaryData, final Collection charges, final Collection purchasedSharesData, final Integer lockinPeriod, final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum, @@ -164,6 +167,7 @@ public class ShareAccountData implements Serializable, AccountData { this.id = id; this.accountNo = accountNo; this.externalId = externalId; + this.resolutionNumber = resolutionNumber; this.savingsAccountId = savingsAccountId; this.savingsAccountNumber = savingsAccountNumber; this.clientId = clientId; @@ -208,9 +212,10 @@ public class ShareAccountData implements Serializable, AccountData { this.defaultShares = defaultShares; } - private ShareAccountData(final Long id, final String accountNo, final String externalId, final Long savingsAccountId, - final String savingsAccountNumber, final Long clientId, final String clientName, final Long productId, final String productName, - final ShareAccountStatusEnumData status, final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, + private ShareAccountData(final Long id, final String accountNo, final String externalId, final String resolutionNumber, + final Long savingsAccountId, final String savingsAccountNumber, final Long clientId, final String clientName, + final Long productId, final String productName, final ShareAccountStatusEnumData status, + final ShareAccountApplicationTimelineData timeline, final CurrencyData currency, final ShareAccountSummaryData summaryData, final Collection charges, final Collection purchasedSharesData, final Integer lockinPeriod, final EnumOptionData lockPeriodTypeEnum, final Integer minimumActivePeriod, final EnumOptionData minimumActivePeriodTypeEnum, @@ -221,6 +226,7 @@ public class ShareAccountData implements Serializable, AccountData { this.id = id; this.accountNo = accountNo; this.externalId = externalId; + this.resolutionNumber = resolutionNumber; this.savingsAccountId = savingsAccountId; this.savingsAccountNumber = savingsAccountNumber; this.clientId = clientId; @@ -250,15 +256,17 @@ public class ShareAccountData implements Serializable, AccountData { Collection chargeOptions, Collection clientSavingsAccounts, Collection lockinPeriodFrequencyTypeOptions, Collection minimumActivePeriodFrequencyTypeOptions) { - return new ShareAccountData(data.id, data.accountNo, data.externalId, data.savingsAccountId, data.savingsAccountNumber, - data.clientId, data.clientName, data.productId, data.productName, data.status, data.timeline, data.currency, data.summary, - data.charges, data.purchasedShares, data.lockinPeriod, data.lockPeriodTypeEnum, data.minimumActivePeriod, - data.minimumActivePeriodTypeEnum, data.allowDividendCalculationForInactiveClients, productOptions, chargeOptions, - clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, minimumActivePeriodFrequencyTypeOptions, data.currentMarketPrice); + return new ShareAccountData(data.id, data.accountNo, data.externalId, data.resolutionNumber, data.savingsAccountId, + data.savingsAccountNumber, data.clientId, data.clientName, data.productId, data.productName, data.status, data.timeline, + data.currency, data.summary, data.charges, data.purchasedShares, data.lockinPeriod, data.lockPeriodTypeEnum, + data.minimumActivePeriod, data.minimumActivePeriodTypeEnum, data.allowDividendCalculationForInactiveClients, + productOptions, chargeOptions, clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, + minimumActivePeriodFrequencyTypeOptions, data.currentMarketPrice); } public static ShareAccountData lookup(final Long id, final String accountNo, final Long clientId, final String clientName) { String externalId = null; + String resolutionNumber = null; ShareAccountSummaryData summaryData = null; Collection purchasedSharesData = null; GLAccountData suspenseAccount = null; @@ -282,10 +290,11 @@ public class ShareAccountData implements Serializable, AccountData { final Long productId = null; final Collection productOptions = null; - return new ShareAccountData(id, accountNo, externalId, savingsAccountId, savingsAccountNumber, clientId, clientName, productId, - productName, status, timeline, currency, summaryData, charges, purchasedSharesData, lockinPeriod, lockPeriodTypeEnum, - minimumActivePeriod, minimumActivePeriodTypeEnum, allowDividendCalculationForInactiveClients, productOptions, chargeOptions, - clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, minimumActivePeriodFrequencyTypeOption, currenMarketPrice); + return new ShareAccountData(id, accountNo, externalId, resolutionNumber, savingsAccountId, savingsAccountNumber, clientId, + clientName, productId, productName, status, timeline, currency, summaryData, charges, purchasedSharesData, lockinPeriod, + lockPeriodTypeEnum, minimumActivePeriod, minimumActivePeriodTypeEnum, allowDividendCalculationForInactiveClients, + productOptions, chargeOptions, clientSavingsAccounts, lockinPeriodFrequencyTypeOptions, + minimumActivePeriodFrequencyTypeOption, currenMarketPrice); } public void setCurrentMarketPrice(final BigDecimal currentMarketPrice) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountPendingEditData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountPendingEditData.java new file mode 100644 index 000000000..19356392c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountPendingEditData.java @@ -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() {} +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java index ee231b701..cfeab7ae1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccount.java @@ -105,6 +105,9 @@ public class ShareAccount extends AbstractPersistableCustom { @Column(name = "external_id") private String externalId; + @Column(name = "resolution_number", nullable = false) + private String resolutionNumber; + @Column(name = "account_no", length = 20, unique = true, nullable = false) private String accountNumber; @@ -151,10 +154,11 @@ public class ShareAccount extends AbstractPersistableCustom { } - public ShareAccount(final Client client, final ShareProduct shareProduct, final String externalId, final MonetaryCurrency currency, - final SavingsAccount savingsAccount, final String accountNo, final Long totalSharesApproved, final Long totalSharesPending, - final Set purchasedShares, final Boolean allowDividendCalculationForInactiveClients, - final Integer lockinPeriodFrequency, final PeriodFrequencyType lockPeriodType, final Integer minimumActivePeriodFrequency, + public ShareAccount(final Client client, final ShareProduct shareProduct, final String externalId, final String resolutionNumber, + final MonetaryCurrency currency, final SavingsAccount savingsAccount, final String accountNo, + final Long totalSharesApproved, final Long totalSharesPending, final Set purchasedShares, + final Boolean allowDividendCalculationForInactiveClients, final Integer lockinPeriodFrequency, + final PeriodFrequencyType lockPeriodType, final Integer minimumActivePeriodFrequency, final PeriodFrequencyType minimumActivePeriodType, Set charges, AppUser submittedBy, final LocalDate submittedDate, AppUser approvedBy, LocalDate approvedDate, AppUser rejectedBy, LocalDate rejectedDate, AppUser activatedBy, LocalDate activatedDate, AppUser closedBy, LocalDate closedDate, AppUser modifiedBy, @@ -163,6 +167,7 @@ public class ShareAccount extends AbstractPersistableCustom { this.client = client; this.shareProduct = shareProduct; this.externalId = externalId; + this.resolutionNumber = resolutionNumber; this.currency = currency; this.savingsAccount = savingsAccount; if (StringUtils.isBlank(accountNo)) { @@ -235,6 +240,19 @@ public class ShareAccount extends AbstractPersistableCustom { 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) { boolean returnValue = false; if (!this.savingsAccount.getId().equals(savingsAccount.getId())) { @@ -561,6 +579,22 @@ public class ShareAccount extends AbstractPersistableCustom { 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() { return this.activatedDate; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEdit.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEdit.java new file mode 100644 index 000000000..7f18b507a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEdit.java @@ -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 { + + 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:" 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEditRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEditRepository.java new file mode 100644 index 000000000..2c4ae98ed --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/domain/ShareAccountPendingEditRepository.java @@ -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 { + + Optional findByShareAccountIdAndStatus(Long shareAccountId, String status); + + List findByStatus(String status); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditPendingApprovalException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditPendingApprovalException.java new file mode 100644 index 000000000..cec2ac50d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditPendingApprovalException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditSelfApprovalNotAllowedException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditSelfApprovalNotAllowedException.java new file mode 100644 index 000000000..24d0feba3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountEditSelfApprovalNotAllowedException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountNoPendingEditException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountNoPendingEditException.java new file mode 100644 index 000000000..d13d0e27f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/exception/ShareAccountNoPendingEditException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java index bb9477fd0..ba9fc54ce 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/serialization/ShareAccountDataSerializer.java @@ -145,6 +145,10 @@ public class ShareAccountDataSerializer { final String externalId = this.fromApiJsonHelper.extractStringNamed(ShareAccountApiConstants.externalid_paramname, element); // 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); baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull() .longGreaterThanZero(); @@ -216,10 +220,10 @@ public class ShareAccountDataSerializer { Set sharesPurchased = new HashSet<>(); sharesPurchased.add(transaction); - ShareAccount account = new ShareAccount(client, shareProduct, externalId, currency, savingsAccount, accountNo, approvedShares, - pendingShares, sharesPurchased, allowdividendsForInactiveClients, lockinPeriod, lockPeriodEnum, minimumActivePeriod, - minimumActivePeriodEnum, charges, submittedBy, submittedDate, approvedBy, approvedDate, rejectedBy, rejectedDate, - activatedBy, activatedDate, closedBy, closedDate, modifiedBy, modifiedDate); + ShareAccount account = new ShareAccount(client, shareProduct, externalId, resolutionNumber, currency, savingsAccount, accountNo, + approvedShares, pendingShares, sharesPurchased, allowdividendsForInactiveClients, lockinPeriod, lockPeriodEnum, + minimumActivePeriod, minimumActivePeriodEnum, charges, submittedBy, submittedDate, approvedBy, approvedDate, rejectedBy, + rejectedDate, activatedBy, activatedDate, closedBy, closedDate, modifiedBy, modifiedDate); for (ShareAccountTransaction pur : sharesPurchased) { 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)) { Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed(ShareAccountApiConstants.savingsaccountid_paramname, element); baseDataValidator.reset().parameter(ShareAccountApiConstants.savingsaccountid_paramname).value(savingsAccountId).notNull() diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformService.java new file mode 100644 index 000000000..d963bbe61 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformService.java @@ -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 retrieveAllPendingEdits(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..f039e2dc7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountEditWritePlatformServiceJpaRepositoryImpl.java @@ -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 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 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 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java index 7124ff789..9e8064092 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java @@ -241,7 +241,8 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat final Collection purchasedShares) { this.charges = charges; 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("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, ") @@ -278,6 +279,7 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat final Long id = rs.getLong("id"); final String accountNo = rs.getString("accountNo"); final String externalId = rs.getString("externalId"); + final String resolutionNumber = rs.getString("resolutionNumber"); final Long savingsAccountId = JdbcSupport.getLong(rs, "savings_account_id"); final String savingsAccountNumber = rs.getString("savingsAccNo"); final Long clientId = JdbcSupport.getLong(rs, "clientId"); @@ -349,9 +351,10 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat final String shortProductName = null; final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline); - return new ShareAccountData(id, accountNo, externalId, savingsAccountId, savingsAccountNumber, clientId, clientName, productId, - productName, status, timeline, currency, summary, charges, purchasedShares, lockinPeriodFrequency, - lockinPeriodFrequencyType, minimumActivePeriod, minimumActivePeriodType, allowdividendsforinactiveclients); + return new ShareAccountData(id, accountNo, externalId, resolutionNumber, savingsAccountId, savingsAccountNumber, clientId, + clientName, productId, productName, status, timeline, currency, summary, charges, purchasedShares, + lockinPeriodFrequency, lockinPeriodFrequencyType, minimumActivePeriod, minimumActivePeriodType, + allowdividendsforinactiveclients); } @@ -392,6 +395,7 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat final Long totalApprovedShares = null; final Long totalPendingShares = null; final String externalId = null; + final String resolutionNumber = null; final Long productId = null; final String productName = null; final String shortProductName = null; @@ -418,9 +422,9 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat final ShareAccountSummaryData summary = new ShareAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency, totalApprovedShares, totalPendingShares, timeline); - return new ShareAccountData(id, accountNo, externalId, clientId, clientName, productId, shortProductName, productId, - shortProductName, status, timeline, currency, summary, charges, purchasedSharesData, lockinPeriod, lockPeriodTypeEnum, - minimumActivePeriod, minimumActivePeriodTypeEnum, allowdividendsforinactiveclients); + return new ShareAccountData(id, accountNo, externalId, resolutionNumber, clientId, clientName, productId, shortProductName, + productId, shortProductName, status, timeline, currency, summary, charges, purchasedSharesData, lockinPeriod, + lockPeriodTypeEnum, minimumActivePeriod, minimumActivePeriodTypeEnum, allowdividendsforinactiveclients); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java index 13698df7a..c92890bda 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/start/ShareAccountsConfiguration.java @@ -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.service.SavingsAccountDomainService; 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.ShareAccountPendingEditRepository; import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountRepositoryWrapper; import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDataSerializer; 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.ShareAccountDividendReadPlatformService; 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.ShareAccountReadPlatformServiceImpl; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountSchedularService; @@ -121,4 +128,14 @@ public class ShareAccountsConfiguration { accountNumberGenerator, accountNumberFormatRepository, journalEntryWritePlatformService, noteRepository, 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); + } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index cd113a4f9..591e9c83e 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -460,4 +460,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0360_add_resolution_number_to_share_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0360_add_resolution_number_to_share_account.xml new file mode 100644 index 000000000..4bf83ca25 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0360_add_resolution_number_to_share_account.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0361_add_share_account_pending_edit_table.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0361_add_share_account_pending_edit_table.xml new file mode 100644 index 000000000..ca3597500 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0361_add_share_account_pending_edit_table.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0362_add_share_account_edit_approval_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0362_add_share_account_edit_approval_permissions.xml new file mode 100644 index 000000000..3973b4140 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0362_add_share_account_edit_approval_permissions.xml @@ -0,0 +1,83 @@ + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'SUBMITEDIT_SHAREACCOUNT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SHAREACCOUNT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECTEDIT_SHAREACCOUNT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SHAREACCOUNT_CHECKER' + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java index 040e79e11..fd3888acb 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositAccountData.java @@ -116,6 +116,21 @@ public final class FixedDepositAccountData extends DepositAccountData { private Boolean deceasedSettlement; 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 private java.util.List> jointHolders; @@ -314,6 +329,9 @@ public final class FixedDepositAccountData extends DepositAccountData { this.jointHolders = src.jointHolders; this.isSeniorCitizen = src.isSeniorCitizen; 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, diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java index 601397d41..690f4c275 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositAccountData.java @@ -147,6 +147,21 @@ public final class RecurringDepositAccountData extends DepositAccountData { private Boolean deceasedSettlement; 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 accountTypeOptions) { this.accountTypeOptions = accountTypeOptions; } @@ -238,6 +253,9 @@ public final class RecurringDepositAccountData extends DepositAccountData { this.jointHolders = src.jointHolders; this.isSeniorCitizen = src.isSeniorCitizen; 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, diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java index 030703976..f95ec0d68 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java @@ -748,10 +748,17 @@ public final class ProductPendingEditFieldMapper { f.setTotalShares(command.longValueOfParameterNamed("totalShares")); present.add("totalShares"); } - if (command.hasParameter("totalSharesIssued") || command.hasParameter("issuedShares")) { - final Long val = command.hasParameter("totalSharesIssued") ? command.longValueOfParameterNamed("totalSharesIssued") : command.longValueOfParameterNamed("issuedShares"); + if (command.hasParameter("sharesIssued") || command.hasParameter("totalSharesIssued") || command.hasParameter("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); - present.add("totalSharesIssued"); + present.add("sharesIssued"); } if (command.hasParameter("unitPrice")) { f.setUnitPrice(command.bigDecimalValueOfParameterNamed("unitPrice")); @@ -820,7 +827,7 @@ public final class ProductPendingEditFieldMapper { return; } 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, "minimumShares", f.getMinimumShares()); putNumber(root, submitted, "nominalShares", f.getNominalShares());