existing account saving and current add or remove nominee and digital & account features and also FE & RD nominee add or remove
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.currentaccount.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Read-side view of a single pending current account Nominee/Digital-Feature edit
|
||||
* (m_current_account_pending_edit), for the Approvals -> Pending Current Account Edits screen.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class CurrentAccountPendingEditData implements Serializable {
|
||||
|
||||
private Long id;
|
||||
private Long currentAccountId;
|
||||
private String accountNo;
|
||||
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 BigDecimal holdAmount;
|
||||
private boolean facilityConfigSubmitted;
|
||||
private boolean nomineeSubmitted;
|
||||
private Map<String, Object> proposedFacilityConfig;
|
||||
private Map<String, Object> proposedNominee;
|
||||
|
||||
public CurrentAccountPendingEditData() {}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.savings.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Read-side view of a single pending savings account Nominee/Digital-Feature edit
|
||||
* (m_savings_account_pending_edit), for the Approvals -> Pending Savings Account Edits screen.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class SavingsAccountPendingEditData implements Serializable {
|
||||
|
||||
private Long id;
|
||||
private Long savingsAccountId;
|
||||
private String accountNo;
|
||||
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 BigDecimal holdAmount;
|
||||
private boolean facilityConfigSubmitted;
|
||||
private boolean nomineeSubmitted;
|
||||
private Map<String, Object> proposedFacilityConfig;
|
||||
private Map<String, Object> proposedNominee;
|
||||
private String depositAccountType;
|
||||
|
||||
public SavingsAccountPendingEditData() {}
|
||||
}
|
||||
+27
@@ -99,4 +99,31 @@ public class CurrentAccountFacilityConfig extends AbstractPersistableCustom<Long
|
||||
cfg.currentAccount = account;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial update: only overwrites fields present (non-null) in the given JSON, leaving all others untouched.
|
||||
* Mirrors {@code SavingsAccountFacilityConfig.updateFrom(JsonObject)}. Used by the Nominee/Digital-Feature
|
||||
* edit-approval flow, which only ever submits the toggles the maker actually touched.
|
||||
*/
|
||||
public void updateFrom(com.google.gson.JsonObject config) {
|
||||
if (config == null) return;
|
||||
if (config.has("atmFacility")) setAtmFacility(config.get("atmFacility").getAsBoolean());
|
||||
if (config.has("atmWithdrawalFacility")) setAtmWithdrawalFacility(config.get("atmWithdrawalFacility").getAsBoolean());
|
||||
if (config.has("upiFacility")) setUpiFacility(config.get("upiFacility").getAsBoolean());
|
||||
if (config.has("posTransactionsFacility")) setPosTransactionsFacility(config.get("posTransactionsFacility").getAsBoolean());
|
||||
if (config.has("chequeBook")) setChequeBook(config.get("chequeBook").getAsBoolean());
|
||||
if (config.has("impsFacility")) setImpsFacility(config.get("impsFacility").getAsBoolean());
|
||||
if (config.has("smsAlerts")) setSmsAlerts(config.get("smsAlerts").getAsBoolean());
|
||||
if (config.has("bbpsFacility")) setBbpsFacility(config.get("bbpsFacility").getAsBoolean());
|
||||
if (config.has("insuranceCover")) setInsuranceCover(config.get("insuranceCover").getAsBoolean());
|
||||
if (config.has("bepgFacility")) setBepgFacility(config.get("bepgFacility").getAsBoolean());
|
||||
if (config.has("neftFacility")) setNeftFacility(config.get("neftFacility").getAsBoolean());
|
||||
if (config.has("rtgsFacility")) setRtgsFacility(config.get("rtgsFacility").getAsBoolean());
|
||||
if (config.has("interBankTotalCapFacility")) setInterBankTotalCapFacility(config.get("interBankTotalCapFacility").getAsBoolean());
|
||||
if (config.has("withdrawalFacility")) setWithdrawalFacility(config.get("withdrawalFacility").getAsBoolean());
|
||||
if (config.has("depositFacilityKyc")) setDepositFacilityKyc(config.get("depositFacilityKyc").getAsBoolean());
|
||||
if (config.has("virtualDebitCard")) setVirtualDebitCard(config.get("virtualDebitCard").getAsBoolean());
|
||||
if (config.has("internetBanking")) setInternetBanking(config.get("internetBanking").getAsBoolean());
|
||||
if (config.has("mobileBanking")) setMobileBanking(config.get("mobileBanking").getAsBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.currentaccount.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* This repository did not previously exist - {@link CurrentAccountFacilityConfig} rows were only ever persisted via
|
||||
* cascade from {@link CurrentAccountRepository} (see {@code CurrentAccount#facilityConfig}, {@code cascade = ALL}).
|
||||
* Added so the Nominee/Digital-Feature edit-approval flow can save the config row directly, mirroring
|
||||
* {@code SavingsAccountFacilityConfigRepository}.
|
||||
*/
|
||||
@Repository
|
||||
public interface CurrentAccountFacilityConfigRepository extends JpaRepository<CurrentAccountFacilityConfig, Long> {
|
||||
|
||||
Optional<CurrentAccountFacilityConfig> findByCurrentAccountId(Long currentAccountId);
|
||||
}
|
||||
+596
@@ -0,0 +1,596 @@
|
||||
/**
|
||||
* 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.currentaccount.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Holds a pending edit request for an ACTIVE current account's Nominee + Digital & Account Features.
|
||||
* 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_current_account_facility_config} / {@code m_client_nominee}
|
||||
* rows until a checker approves (see CurrentAccountEditWritePlatformServiceJpaRepositoryImpl). Mirrors
|
||||
* {@code SavingsAccountPendingEdit} but additionally carries a full point-in-time "wide snapshot" of every business
|
||||
* column on {@link CurrentAccount} at submission time, for audit/UI display purposes only (not used to drive
|
||||
* approve/reject logic, which reads the live account).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_current_account_pending_edit")
|
||||
@Getter
|
||||
@Setter
|
||||
public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_APPROVED = "APPROVED";
|
||||
public static final String STATUS_REJECTED = "REJECTED";
|
||||
|
||||
public static final String NOMINEE_OP_ADD = "ADD";
|
||||
public static final String NOMINEE_OP_UPDATE = "UPDATE";
|
||||
public static final String NOMINEE_OP_DELETE = "DELETE";
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "current_account_id", nullable = false)
|
||||
private CurrentAccount currentAccount;
|
||||
|
||||
@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;
|
||||
|
||||
/** "CURRENT_ACCOUNT_EDIT:<currentAccountId>" while PENDING, NULL otherwise - the real DB-level lock. */
|
||||
@Column(name = "pending_lock_key", length = 40)
|
||||
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;
|
||||
|
||||
@Column(name = "hold_transaction_id")
|
||||
private Long holdTransactionId;
|
||||
|
||||
@Column(name = "hold_amount", precision = 19, scale = 6)
|
||||
private BigDecimal holdAmount;
|
||||
|
||||
// ── Digital & Account Features (same names as m_current_account_facility_config) ──
|
||||
@Column(name = "facility_config_submitted", nullable = false)
|
||||
private boolean facilityConfigSubmitted = false;
|
||||
|
||||
@Column(name = "atm_facility")
|
||||
private Boolean atmFacility;
|
||||
|
||||
@Column(name = "atm_withdrawal_facility")
|
||||
private Boolean atmWithdrawalFacility;
|
||||
|
||||
@Column(name = "upi_facility")
|
||||
private Boolean upiFacility;
|
||||
|
||||
@Column(name = "pos_transactions_facility")
|
||||
private Boolean posTransactionsFacility;
|
||||
|
||||
@Column(name = "cheque_book")
|
||||
private Boolean chequeBook;
|
||||
|
||||
@Column(name = "imps_facility")
|
||||
private Boolean impsFacility;
|
||||
|
||||
@Column(name = "sms_alerts")
|
||||
private Boolean smsAlerts;
|
||||
|
||||
@Column(name = "bbps_facility")
|
||||
private Boolean bbpsFacility;
|
||||
|
||||
@Column(name = "insurance_cover")
|
||||
private Boolean insuranceCover;
|
||||
|
||||
@Column(name = "bepg_facility")
|
||||
private Boolean bepgFacility;
|
||||
|
||||
@Column(name = "neft_facility")
|
||||
private Boolean neftFacility;
|
||||
|
||||
@Column(name = "rtgs_facility")
|
||||
private Boolean rtgsFacility;
|
||||
|
||||
@Column(name = "inter_bank_total_cap_facility")
|
||||
private Boolean interBankTotalCapFacility;
|
||||
|
||||
@Column(name = "withdrawal_facility")
|
||||
private Boolean withdrawalFacility;
|
||||
|
||||
@Column(name = "deposit_facility_kyc")
|
||||
private Boolean depositFacilityKyc;
|
||||
|
||||
@Column(name = "virtual_debit_card")
|
||||
private Boolean virtualDebitCard;
|
||||
|
||||
@Column(name = "internet_banking")
|
||||
private Boolean internetBanking;
|
||||
|
||||
@Column(name = "mobile_banking")
|
||||
private Boolean mobileBanking;
|
||||
|
||||
// ── Nominee change - one per request (add / update / delete) ──
|
||||
@Column(name = "nominee_submitted", nullable = false)
|
||||
private boolean nomineeSubmitted = false;
|
||||
|
||||
@Column(name = "nominee_operation", length = 10)
|
||||
private String nomineeOperation;
|
||||
|
||||
@Column(name = "nominee_id")
|
||||
private Long nomineeId;
|
||||
|
||||
@Column(name = "nominee_client_id")
|
||||
private Long nomineeClientId;
|
||||
|
||||
@Column(name = "relationship_cv_id")
|
||||
private Long relationshipCvId;
|
||||
|
||||
@Column(name = "percentage_share", precision = 5, scale = 2)
|
||||
private BigDecimal percentageShare;
|
||||
|
||||
@Column(name = "nominee_is_active")
|
||||
private Boolean nomineeIsActive;
|
||||
|
||||
@Column(name = "nominee_is_minor")
|
||||
private Boolean nomineeIsMinor;
|
||||
|
||||
@Column(name = "guardian_client_id")
|
||||
private Long guardianClientId;
|
||||
|
||||
@Column(name = "guardian_relationship_cv_id")
|
||||
private Long guardianRelationshipCvId;
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
@Column(name = "nominee_remarks", length = 500)
|
||||
private String nomineeRemarks;
|
||||
|
||||
// ── Wide point-in-time snapshot of every business column on CurrentAccount at submission time ──
|
||||
// (audit/UI display only - approve()/reject() logic never reads these, it reads the live account)
|
||||
@Column(name = "account_no", length = 20)
|
||||
private String snapshotAccountNo;
|
||||
|
||||
@Column(name = "external_id", length = 100)
|
||||
private String snapshotExternalId;
|
||||
|
||||
@Column(name = "client_id")
|
||||
private Long snapshotClientId;
|
||||
|
||||
@Column(name = "product_id")
|
||||
private Long snapshotProductId;
|
||||
|
||||
@Column(name = "office_id")
|
||||
private Long snapshotOfficeId;
|
||||
|
||||
@Column(name = "staff_id")
|
||||
private Long snapshotStaffId;
|
||||
|
||||
@Column(name = "status_enum")
|
||||
private Integer snapshotStatusEnum;
|
||||
|
||||
@Column(name = "sub_status_enum")
|
||||
private Integer snapshotSubStatusEnum;
|
||||
|
||||
@Column(name = "account_type_enum")
|
||||
private Integer snapshotAccountTypeEnum;
|
||||
|
||||
@Column(name = "submittedon_date")
|
||||
private LocalDate snapshotSubmittedOnDate;
|
||||
|
||||
@Column(name = "approvedon_date")
|
||||
private LocalDate snapshotApprovedOnDate;
|
||||
|
||||
@Column(name = "activatedon_date")
|
||||
private LocalDate snapshotActivatedOnDate;
|
||||
|
||||
@Column(name = "rejectedon_date")
|
||||
private LocalDate snapshotRejectedOnDate;
|
||||
|
||||
@Column(name = "withdrawnon_date")
|
||||
private LocalDate snapshotWithdrawnOnDate;
|
||||
|
||||
@Column(name = "closedon_date")
|
||||
private LocalDate snapshotClosedOnDate;
|
||||
|
||||
@Column(name = "submittedon_userid")
|
||||
private Long snapshotSubmittedOnUserId;
|
||||
|
||||
@Column(name = "approvedon_userid")
|
||||
private Long snapshotApprovedOnUserId;
|
||||
|
||||
@Column(name = "activatedon_userid")
|
||||
private Long snapshotActivatedOnUserId;
|
||||
|
||||
@Column(name = "rejectedon_userid")
|
||||
private Long snapshotRejectedOnUserId;
|
||||
|
||||
@Column(name = "withdrawnon_userid")
|
||||
private Long snapshotWithdrawnOnUserId;
|
||||
|
||||
@Column(name = "closedon_userid")
|
||||
private Long snapshotClosedOnUserId;
|
||||
|
||||
@Column(name = "currency_code", length = 3)
|
||||
private String snapshotCurrencyCode;
|
||||
|
||||
@Column(name = "currency_digits")
|
||||
private Integer snapshotCurrencyDigits;
|
||||
|
||||
@Column(name = "currency_multiplesof")
|
||||
private Integer snapshotCurrencyMultiplesOf;
|
||||
|
||||
@Column(name = "nominal_annual_interest_rate", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotNominalAnnualInterestRate;
|
||||
|
||||
@Column(name = "interest_compounding_period_enum")
|
||||
private Integer snapshotInterestCompoundingPeriodEnum;
|
||||
|
||||
@Column(name = "interest_posting_period_enum")
|
||||
private Integer snapshotInterestPostingPeriodEnum;
|
||||
|
||||
@Column(name = "interest_calculation_type_enum")
|
||||
private Integer snapshotInterestCalculationTypeEnum;
|
||||
|
||||
@Column(name = "interest_calculation_days_in_year_type_enum")
|
||||
private Integer snapshotInterestCalculationDaysInYearTypeEnum;
|
||||
|
||||
@Column(name = "min_balance_for_interest_calculation", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotMinBalanceForInterestCalculation;
|
||||
|
||||
@Column(name = "start_interest_calculation_date")
|
||||
private LocalDate snapshotStartInterestCalculationDate;
|
||||
|
||||
@Column(name = "accrued_till_date")
|
||||
private LocalDate snapshotAccruedTillDate;
|
||||
|
||||
@Column(name = "last_interest_calculation_date")
|
||||
private LocalDate snapshotLastInterestCalculationDate;
|
||||
|
||||
@Column(name = "last_closed_business_date")
|
||||
private LocalDate snapshotLastClosedBusinessDate;
|
||||
|
||||
@Column(name = "withhold_tax")
|
||||
private Boolean snapshotWithholdTax;
|
||||
|
||||
@Column(name = "min_required_opening_balance", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotMinRequiredOpeningBalance;
|
||||
|
||||
@Column(name = "lockin_period_frequency")
|
||||
private Integer snapshotLockinPeriodFrequency;
|
||||
|
||||
@Column(name = "lockin_period_frequency_enum")
|
||||
private Integer snapshotLockinPeriodFrequencyEnum;
|
||||
|
||||
@Column(name = "lockedin_until_date_derived")
|
||||
private LocalDate snapshotLockedInUntilDate;
|
||||
|
||||
@Column(name = "enforce_min_required_balance")
|
||||
private Boolean snapshotEnforceMinRequiredBalance;
|
||||
|
||||
@Column(name = "min_required_balance", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotMinRequiredBalance;
|
||||
|
||||
@Column(name = "allow_overdraft")
|
||||
private Boolean snapshotAllowOverdraft;
|
||||
|
||||
@Column(name = "overdraft_limit", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotOverdraftLimit;
|
||||
|
||||
@Column(name = "nominal_annual_interest_rate_overdraft", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotNominalAnnualInterestRateOverdraft;
|
||||
|
||||
@Column(name = "min_overdraft_for_interest_calculation", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotMinOverdraftForInterestCalculation;
|
||||
|
||||
@Column(name = "is_lien_allowed")
|
||||
private Boolean snapshotLienAllowed;
|
||||
|
||||
@Column(name = "max_allowed_lien_limit", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotMaxAllowedLienLimit;
|
||||
|
||||
@Column(name = "on_hold_funds_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotOnHoldFundsDerived;
|
||||
|
||||
@Column(name = "account_balance_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotAccountBalanceDerived;
|
||||
|
||||
@Column(name = "total_deposits_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalDepositsDerived;
|
||||
|
||||
@Column(name = "total_withdrawals_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalWithdrawalsDerived;
|
||||
|
||||
@Column(name = "total_interest_earned_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalInterestEarnedDerived;
|
||||
|
||||
@Column(name = "total_interest_posted_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalInterestPostedDerived;
|
||||
|
||||
@Column(name = "total_overdraft_interest_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalOverdraftInterestDerived;
|
||||
|
||||
@Column(name = "total_withhold_tax_derived", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalWithholdTaxDerived;
|
||||
|
||||
@Column(name = "total_savings_amount_on_hold", precision = 19, scale = 6)
|
||||
private BigDecimal snapshotTotalSavingsAmountOnHold;
|
||||
|
||||
@Column(name = "withdrawal_fee_for_transfer")
|
||||
private Boolean snapshotWithdrawalFeeForTransfer;
|
||||
|
||||
@Column(name = "reason_for_block", length = 500)
|
||||
private String snapshotReasonForBlock;
|
||||
|
||||
@Column(name = "msme")
|
||||
private Boolean snapshotMsme;
|
||||
|
||||
@Column(name = "auto_debit_facility")
|
||||
private Boolean snapshotAutoDebitFacility;
|
||||
|
||||
@Column(name = "balance_swap_option")
|
||||
private Boolean snapshotBalanceSwapOption;
|
||||
|
||||
@Column(name = "establishment_date")
|
||||
private LocalDate snapshotEstablishmentDate;
|
||||
|
||||
@Column(name = "start_operation_date")
|
||||
private LocalDate snapshotStartOperationDate;
|
||||
|
||||
@Column(name = "is_non_individual_client")
|
||||
private Boolean snapshotNonIndividualClient;
|
||||
|
||||
@Column(name = "signatory_rights", length = 100)
|
||||
private String snapshotSignatoryRights;
|
||||
|
||||
@Column(name = "cbs_account_type", length = 100)
|
||||
private String snapshotCbsAccountType;
|
||||
|
||||
@Column(name = "customer_segment", length = 100)
|
||||
private String snapshotCustomerSegment;
|
||||
|
||||
@Column(name = "account_status", length = 50)
|
||||
private String snapshotAccountStatus;
|
||||
|
||||
@Column(name = "mode_of_operation", length = 50)
|
||||
private String snapshotModeOfOperation;
|
||||
|
||||
@Column(name = "special_category", length = 100)
|
||||
private String snapshotSpecialCategory;
|
||||
|
||||
@Column(name = "introducer_client_id")
|
||||
private Long snapshotIntroducerClientId;
|
||||
|
||||
@Column(name = "introducer_name", length = 200)
|
||||
private String snapshotIntroducerName;
|
||||
|
||||
@Column(name = "introducer_cif_number", length = 50)
|
||||
private String snapshotIntroducerCifNumber;
|
||||
|
||||
@Column(name = "nominee_data", columnDefinition = "TEXT")
|
||||
private String snapshotNomineeData;
|
||||
|
||||
@Column(name = "interest_calculate_selector")
|
||||
private Integer snapshotInterestCalculateSelector;
|
||||
|
||||
@Column(name = "average_period", length = 20)
|
||||
private String snapshotAveragePeriod;
|
||||
|
||||
@Column(name = "auto_post")
|
||||
private Boolean snapshotAutoPost;
|
||||
|
||||
@Column(name = "frequency_fix_day")
|
||||
private Integer snapshotFrequencyFixDay;
|
||||
|
||||
@Column(name = "enable_lockin_period")
|
||||
private Boolean snapshotEnableLockinPeriod;
|
||||
|
||||
@Column(name = "balance_frequency", length = 50)
|
||||
private String snapshotBalanceFrequency;
|
||||
|
||||
@Column(name = "avg_balance_calc_period", length = 10)
|
||||
private String snapshotAvgBalanceCalcPeriod;
|
||||
|
||||
@Column(name = "account_closed_zero_balance_in_day_closing")
|
||||
private Boolean snapshotAccountClosedZeroBalanceInDayClosing;
|
||||
|
||||
@Column(name = "valid_for_period", length = 10)
|
||||
private String snapshotValidForPeriod;
|
||||
|
||||
@Column(name = "od_account_closed_zero_balance_in_day_closing")
|
||||
private Boolean snapshotOdAccountClosedZeroBalanceInDayClosing;
|
||||
|
||||
@Column(name = "is_linking_enable")
|
||||
private Boolean snapshotShareLinkingEnabled;
|
||||
|
||||
@Column(name = "terminate_true")
|
||||
private Boolean snapshotTerminateTrue;
|
||||
|
||||
protected CurrentAccountPendingEdit() {}
|
||||
|
||||
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
|
||||
this.currentAccount = currentAccount;
|
||||
this.maker = maker;
|
||||
this.createdOn = LocalDateTime.now();
|
||||
this.status = STATUS_PENDING;
|
||||
this.pendingLockKey = lockKeyFor(currentAccount.getId());
|
||||
}
|
||||
|
||||
public static String lockKeyFor(final Long currentAccountId) {
|
||||
return "CURRENT_ACCOUNT_EDIT:" + currentAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new pending-edit row and copies every business column off the live {@code account} onto this
|
||||
* request's wide snapshot fields, for audit/UI display purposes only.
|
||||
*/
|
||||
public static CurrentAccountPendingEdit snapshotOf(final CurrentAccount account, final AppUser maker) {
|
||||
final CurrentAccountPendingEdit p = new CurrentAccountPendingEdit(account, maker);
|
||||
|
||||
p.snapshotAccountNo = account.getAccountNo();
|
||||
p.snapshotExternalId = account.getExternalId();
|
||||
p.snapshotClientId = account.getClient() != null ? account.getClient().getId() : null;
|
||||
p.snapshotProductId = account.getProductId();
|
||||
p.snapshotOfficeId = account.getOffice() != null ? account.getOffice().getId() : null;
|
||||
p.snapshotStaffId = account.getStaff() != null ? account.getStaff().getId() : null;
|
||||
p.snapshotStatusEnum = account.getStatusEnum();
|
||||
p.snapshotSubStatusEnum = account.getSubStatusEnum();
|
||||
p.snapshotAccountTypeEnum = account.getAccountTypeEnum();
|
||||
|
||||
p.snapshotSubmittedOnDate = account.getSubmittedOnDate();
|
||||
p.snapshotApprovedOnDate = account.getApprovedOnDate();
|
||||
p.snapshotActivatedOnDate = account.getActivatedOnDate();
|
||||
p.snapshotRejectedOnDate = account.getRejectedOnDate();
|
||||
p.snapshotWithdrawnOnDate = account.getWithdrawnOnDate();
|
||||
p.snapshotClosedOnDate = account.getClosedOnDate();
|
||||
|
||||
p.snapshotSubmittedOnUserId = account.getSubmittedOnByUser() != null ? account.getSubmittedOnByUser().getId() : null;
|
||||
p.snapshotApprovedOnUserId = account.getApprovedOnByUser() != null ? account.getApprovedOnByUser().getId() : null;
|
||||
p.snapshotActivatedOnUserId = account.getActivatedOnByUser() != null ? account.getActivatedOnByUser().getId() : null;
|
||||
p.snapshotRejectedOnUserId = account.getRejectedOnByUser() != null ? account.getRejectedOnByUser().getId() : null;
|
||||
p.snapshotWithdrawnOnUserId = account.getWithdrawnOnByUser() != null ? account.getWithdrawnOnByUser().getId() : null;
|
||||
p.snapshotClosedOnUserId = account.getClosedOnByUser() != null ? account.getClosedOnByUser().getId() : null;
|
||||
|
||||
p.snapshotCurrencyCode = account.getCurrencyCode();
|
||||
p.snapshotCurrencyDigits = account.getCurrencyDigits();
|
||||
p.snapshotCurrencyMultiplesOf = account.getCurrencyMultiplesOf();
|
||||
|
||||
p.snapshotNominalAnnualInterestRate = account.getNominalAnnualInterestRate();
|
||||
p.snapshotInterestCompoundingPeriodEnum = account.getInterestCompoundingPeriodEnum();
|
||||
p.snapshotInterestPostingPeriodEnum = account.getInterestPostingPeriodEnum();
|
||||
p.snapshotInterestCalculationTypeEnum = account.getInterestCalculationTypeEnum();
|
||||
p.snapshotInterestCalculationDaysInYearTypeEnum = account.getInterestCalculationDaysInYearTypeEnum();
|
||||
p.snapshotMinBalanceForInterestCalculation = account.getMinBalanceForInterestCalculation();
|
||||
p.snapshotStartInterestCalculationDate = account.getStartInterestCalculationDate();
|
||||
p.snapshotAccruedTillDate = account.getAccruedTillDate();
|
||||
p.snapshotLastInterestCalculationDate = account.getLastInterestCalculationDate();
|
||||
p.snapshotLastClosedBusinessDate = account.getLastClosedBusinessDate();
|
||||
|
||||
p.snapshotWithholdTax = account.isWithholdTax();
|
||||
|
||||
p.snapshotMinRequiredOpeningBalance = account.getMinRequiredOpeningBalance();
|
||||
p.snapshotLockinPeriodFrequency = account.getLockinPeriodFrequency();
|
||||
p.snapshotLockinPeriodFrequencyEnum = account.getLockinPeriodFrequencyEnum();
|
||||
p.snapshotLockedInUntilDate = account.getLockedInUntilDate();
|
||||
p.snapshotEnforceMinRequiredBalance = account.isEnforceMinRequiredBalance();
|
||||
p.snapshotMinRequiredBalance = account.getMinRequiredBalance();
|
||||
|
||||
p.snapshotAllowOverdraft = account.isAllowOverdraft();
|
||||
p.snapshotOverdraftLimit = account.getOverdraftLimit();
|
||||
p.snapshotNominalAnnualInterestRateOverdraft = account.getNominalAnnualInterestRateOverdraft();
|
||||
p.snapshotMinOverdraftForInterestCalculation = account.getMinOverdraftForInterestCalculation();
|
||||
|
||||
p.snapshotLienAllowed = account.isLienAllowed();
|
||||
p.snapshotMaxAllowedLienLimit = account.getMaxAllowedLienLimit();
|
||||
p.snapshotOnHoldFundsDerived = account.getOnHoldFundsDerived();
|
||||
|
||||
p.snapshotAccountBalanceDerived = account.getAccountBalanceDerived();
|
||||
p.snapshotTotalDepositsDerived = account.getTotalDepositsDerived();
|
||||
p.snapshotTotalWithdrawalsDerived = account.getTotalWithdrawalsDerived();
|
||||
p.snapshotTotalInterestEarnedDerived = account.getTotalInterestEarnedDerived();
|
||||
p.snapshotTotalInterestPostedDerived = account.getTotalInterestPostedDerived();
|
||||
p.snapshotTotalOverdraftInterestDerived = account.getTotalOverdraftInterestDerived();
|
||||
p.snapshotTotalWithholdTaxDerived = account.getTotalWithholdTaxDerived();
|
||||
p.snapshotTotalSavingsAmountOnHold = account.getTotalSavingsAmountOnHold();
|
||||
|
||||
p.snapshotWithdrawalFeeForTransfer = account.isWithdrawalFeeForTransfer();
|
||||
p.snapshotReasonForBlock = account.getReasonForBlock();
|
||||
|
||||
p.snapshotMsme = account.isMsme();
|
||||
p.snapshotAutoDebitFacility = account.isAutoDebitFacility();
|
||||
p.snapshotBalanceSwapOption = account.isBalanceSwapOption();
|
||||
p.snapshotEstablishmentDate = account.getEstablishmentDate();
|
||||
p.snapshotStartOperationDate = account.getStartOperationDate();
|
||||
p.snapshotNonIndividualClient = account.isNonIndividualClient();
|
||||
p.snapshotSignatoryRights = account.getSignatoryRights();
|
||||
|
||||
p.snapshotCbsAccountType = account.getCbsAccountType();
|
||||
p.snapshotCustomerSegment = account.getCustomerSegment();
|
||||
p.snapshotAccountStatus = account.getAccountStatus();
|
||||
p.snapshotModeOfOperation = account.getModeOfOperation();
|
||||
p.snapshotSpecialCategory = account.getSpecialCategory();
|
||||
|
||||
p.snapshotIntroducerClientId = account.getIntroducerClientId();
|
||||
p.snapshotIntroducerName = account.getIntroducerName();
|
||||
p.snapshotIntroducerCifNumber = account.getIntroducerCifNumber();
|
||||
|
||||
p.snapshotNomineeData = account.getNomineeData();
|
||||
|
||||
p.snapshotInterestCalculateSelector = account.getInterestCalculateSelector();
|
||||
p.snapshotAveragePeriod = account.getAveragePeriod();
|
||||
p.snapshotAutoPost = account.isAutoPost();
|
||||
p.snapshotFrequencyFixDay = account.getFrequencyFixDay();
|
||||
p.snapshotEnableLockinPeriod = account.isEnableLockinPeriod();
|
||||
p.snapshotBalanceFrequency = account.getBalanceFrequency();
|
||||
p.snapshotAvgBalanceCalcPeriod = account.getAvgBalanceCalcPeriod();
|
||||
|
||||
p.snapshotAccountClosedZeroBalanceInDayClosing = account.getAccountClosedZeroBalanceInDayClosing();
|
||||
p.snapshotValidForPeriod = account.getValidForPeriod();
|
||||
p.snapshotOdAccountClosedZeroBalanceInDayClosing = account.isOdAccountClosedZeroBalanceInDayClosing();
|
||||
|
||||
p.snapshotShareLinkingEnabled = account.isShareLinkingEnabled();
|
||||
p.snapshotTerminateTrue = account.isTerminateTrue();
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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.currentaccount.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface CurrentAccountPendingEditRepository extends JpaRepository<CurrentAccountPendingEdit, Long> {
|
||||
|
||||
Optional<CurrentAccountPendingEdit> findByCurrentAccountIdAndStatus(Long currentAccountId, String status);
|
||||
|
||||
List<CurrentAccountPendingEdit> findByStatus(String status);
|
||||
}
|
||||
+37
@@ -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.currentaccount.exception;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown at edit-submission time when the account's available balance cannot cover the charge for
|
||||
* the digital/account feature(s) the maker is requesting to enable.
|
||||
*/
|
||||
public class CurrentAccountEditInsufficientBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public CurrentAccountEditInsufficientBalanceException(final Long currentAccountId, final BigDecimal availableBalance,
|
||||
final BigDecimal requiredAmount) {
|
||||
super("error.msg.currentaccount.edit.insufficient.balance",
|
||||
"Account balance is insufficient for the requested feature(s). Available balance: " + availableBalance
|
||||
+ ", required: " + requiredAmount,
|
||||
currentAccountId, availableBalance, requiredAmount);
|
||||
}
|
||||
}
|
||||
+30
@@ -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.currentaccount.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class CurrentAccountEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public CurrentAccountEditPendingApprovalException(final Long currentAccountId) {
|
||||
super("error.msg.currentaccount.edit.pending.approval.exists",
|
||||
"Current account with identifier " + currentAccountId + " already has a Nominee/Digital-Feature edit pending approval",
|
||||
currentAccountId);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.currentaccount.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class CurrentAccountEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public CurrentAccountEditSelfApprovalNotAllowedException(final Long currentAccountId) {
|
||||
super("error.msg.currentaccount.edit.self.approval.not.allowed",
|
||||
"The maker of the pending Nominee/Digital-Feature edit for current account " + currentAccountId
|
||||
+ " cannot also approve or reject it",
|
||||
currentAccountId);
|
||||
}
|
||||
}
|
||||
+30
@@ -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.currentaccount.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class CurrentAccountNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public CurrentAccountNoPendingEditException(final Long currentAccountId) {
|
||||
super("error.msg.currentaccount.no.pending.edit",
|
||||
"Current account with identifier " + currentAccountId + " has no Nominee/Digital-Feature edit awaiting approval",
|
||||
currentAccountId);
|
||||
}
|
||||
}
|
||||
+37
@@ -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.currentaccount.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.currentaccount.data.CurrentAccountPendingEditData;
|
||||
|
||||
public interface CurrentAccountEditWritePlatformService {
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long currentAccountId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long currentAccountId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long currentAccountId, String rejectReason);
|
||||
|
||||
CurrentAccountPendingEditData retrievePendingEdit(Long currentAccountId);
|
||||
|
||||
Collection<CurrentAccountPendingEditData> retrieveAllPendingEdits();
|
||||
}
|
||||
+10
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.currentaccount.service;
|
||||
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
@@ -42,4 +43,13 @@ public interface CurrentAccountWritePlatformService {
|
||||
CommandProcessingResult deleteApplication(Long accountId);
|
||||
|
||||
CommandProcessingResult terminateAccount(Long accountId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Resolves each of the given (newly-enabled) facility types to its mapped charge via
|
||||
* {@code m_facility_charge_mapping}, creates a {@code CurrentAccountCharge} for it (skipping any facility type
|
||||
* for which the account already has a paid, non-waived charge for the same mapped {@code Charge}), collects it
|
||||
* from the account's running balance as of today, and posts the corresponding GL journal entries. Reuses the
|
||||
* same private charge-creation-and-collection helper that {@code activate()} uses for activation-time charges.
|
||||
*/
|
||||
void applyAndCollectFacilityCharges(Long currentAccountId, Set<String> newlyEnabledFacilityTypes);
|
||||
}
|
||||
|
||||
+14
-1
@@ -44,6 +44,8 @@ import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -62,6 +64,7 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
private final ClientNomineeRepository nomineeRepository;
|
||||
private final CodeValueRepositoryWrapper codeValueRepository;
|
||||
private final SavingsAccountRepository savingsAccountRepository;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final LoanRepository loanRepository;
|
||||
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
|
||||
|
||||
@@ -299,8 +302,18 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.loan.account.not.found",
|
||||
"Loan account '" + accountNo + "' not found for client " + clientId + ".", "accountNo");
|
||||
}
|
||||
} else if ("CB".equals(accountType)) {
|
||||
final CurrentAccount ca = this.currentAccountRepository.findByAccountNo(accountNo).orElse(null);
|
||||
if (ca == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.current.account.not.found",
|
||||
"Account '" + accountNo + "' not found.", "accountNo");
|
||||
}
|
||||
if (ca.getClient() == null || !ca.getClient().getId().equals(clientId)) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.account.does.not.belong.to.client",
|
||||
"Account '" + accountNo + "' does not belong to client " + clientId + ".", "accountNo");
|
||||
}
|
||||
} else {
|
||||
// SB, CB, FD, RD are all savings accounts with different deposit_type_enum
|
||||
// SB, FD, RD are all savings accounts with different deposit_type_enum
|
||||
final SavingsAccount sa = savingsAccountRepository.findSavingsAccountByAccountNumber(accountNo);
|
||||
if (sa == null) {
|
||||
throw new PlatformDataIntegrityException("error.msg.nominee.savings.account.not.found",
|
||||
|
||||
+109
@@ -39,23 +39,29 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.exception.UnrecognizedQueryParamException;
|
||||
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.infrastructure.security.service.SqlValidator;
|
||||
import org.apache.fineract.portfolio.currentaccount.data.CurrentAccountPendingEditData;
|
||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -76,6 +82,9 @@ public class CurrentAccountsApiResource {
|
||||
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
|
||||
private final CurrentAccountReadPlatformService currentAccountReadPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final CurrentAccountEditWritePlatformService currentAccountEditWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<CurrentAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Path("template")
|
||||
@@ -229,4 +238,104 @@ public class CurrentAccountsApiResource {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{accountId}/edit")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Submit a Nominee/Digital-Feature edit for a Current Account",
|
||||
description = "Edits are held in m_current_account_pending_edit (a pending row there is itself the lock) and only "
|
||||
+ "applied to the real account once a checker approves - see /currentaccounts/{accountId}/edit/approve and /reject below.")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String submitEdit(@PathParam("accountId") final Long accountId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
this.context.authenticatedUser().validateHasPermissionTo("SUBMITEDIT_CURRENTACCOUNT");
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(accountId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.currentAccountEditWritePlatformService.submitEditForApproval(accountId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Current Account Nominee/Digital-Feature edits pending approval")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String retrieveAllPendingEdits(@Context final UriInfo uriInfo) {
|
||||
|
||||
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
|
||||
|
||||
final Collection<CurrentAccountPendingEditData> pendingEdits = this.currentAccountEditWritePlatformService
|
||||
.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 Nominee/Digital-Feature edit for a Current Account, if any")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String retrievePendingEdit(@PathParam("accountId") final Long accountId, @Context final UriInfo uriInfo) {
|
||||
|
||||
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
|
||||
|
||||
final CurrentAccountPendingEditData pendingEdit = this.currentAccountEditWritePlatformService.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 Current Account Nominee/Digital-Feature edit",
|
||||
description = "Releases the submission-time hold, posts any newly-enabled feature charges, and applies the "
|
||||
+ "Nominee/Digital-Feature changes to the account. The maker of the edit cannot also approve it.")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String approvePendingEdit(@PathParam("accountId") final Long accountId) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVEEDIT_CURRENTACCOUNT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve current account edits");
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.currentAccountEditWritePlatformService.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 Current Account Nominee/Digital-Feature edit",
|
||||
description = "Releases the submission-time hold without applying any change. The maker of the edit cannot also reject it.")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String rejectPendingEdit(@PathParam("accountId") final Long accountId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECTEDIT_CURRENTACCOUNT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject current 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.currentAccountEditWritePlatformService.rejectEdit(accountId, rejectReason);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+104
@@ -58,8 +58,11 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.UploadRequest;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
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.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
||||
@@ -69,9 +72,11 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountPendingEditData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountTemplateReadPlatformService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
@@ -95,6 +100,9 @@ public class SavingsAccountsApiResource {
|
||||
private final BulkImportWorkbookService bulkImportWorkbookService;
|
||||
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
||||
private final SqlValidator sqlValidator;
|
||||
private final SavingsAccountEditWritePlatformService savingsAccountEditWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Path("template")
|
||||
@@ -413,6 +421,102 @@ public class SavingsAccountsApiResource {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{accountId}/edit")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Submit a Nominee/Digital-Feature edit for a Savings Account",
|
||||
description = "Edits are held in m_savings_account_pending_edit (a pending row there is itself the lock) and only "
|
||||
+ "applied to the real account once a checker approves - see /savingsaccounts/{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_SAVINGSACCOUNT");
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(accountId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.savingsAccountEditWritePlatformService.submitEditForApproval(accountId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Savings Account Nominee/Digital-Feature edits pending approval")
|
||||
public String retrieveAllPendingEdits(@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||
|
||||
final Collection<SavingsAccountPendingEditData> pendingEdits = this.savingsAccountEditWritePlatformService
|
||||
.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 Nominee/Digital-Feature edit for a Savings Account, if any")
|
||||
public String retrievePendingEdit(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||
|
||||
final SavingsAccountPendingEditData pendingEdit = this.savingsAccountEditWritePlatformService.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 Savings Account Nominee/Digital-Feature edit",
|
||||
description = "Releases the submission-time hold, posts any newly-enabled feature charges, and applies the "
|
||||
+ "Nominee/Digital-Feature 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_SAVINGSACCOUNT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve savings account edits");
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.savingsAccountEditWritePlatformService.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 Savings Account Nominee/Digital-Feature edit",
|
||||
description = "Releases the submission-time hold without applying any change. 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_SAVINGSACCOUNT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject savings 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.savingsAccountEditWritePlatformService.rejectEdit(accountId, rejectReason);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{accountId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
/**
|
||||
* 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.savings.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
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.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.data.CurrentAccountPendingEditData;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountPendingEdit;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountEditInsufficientBalanceException;
|
||||
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountEditSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Maker-checker edit flow for an ACTIVE current account's Nominee + Digital & Account Features. Mirrors
|
||||
* {@code SavingsAccountEditWritePlatformServiceJpaRepositoryImpl}'s submitEditForApproval/approveEdit/rejectEdit
|
||||
* pattern: a row in {@code m_current_account_pending_edit} is itself the lock, and nothing is applied to the real
|
||||
* {@code m_current_account_facility_config} / {@code m_client_nominee} tables until a checker approves.
|
||||
*
|
||||
* <p>
|
||||
* Charge handling for a newly-enabled facility (one with a mapped charge in m_facility_charge_mapping): at
|
||||
* submission a HOLD is placed for the total charge amount (blocking that much of the available balance without
|
||||
* touching it); on approval the hold is released and the charge is posted for real (via
|
||||
* {@link CurrentAccountWritePlatformService#applyAndCollectFacilityCharges(Long, Set)}); on rejection the hold is
|
||||
* simply released.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class CurrentAccountEditWritePlatformServiceJpaRepositoryImpl implements CurrentAccountEditWritePlatformService {
|
||||
|
||||
/** field name (as sent in accountFacilityConfig JSON / stored on the pending edit) -> facility_type code. */
|
||||
private static final Map<String, String> FACILITY_TYPES = new LinkedHashMap<>();
|
||||
static {
|
||||
FACILITY_TYPES.put("atmFacility", "ATM_FACILITY");
|
||||
FACILITY_TYPES.put("atmWithdrawalFacility", "ATM_WITHDRAWAL");
|
||||
FACILITY_TYPES.put("upiFacility", "UPI_FACILITY");
|
||||
FACILITY_TYPES.put("posTransactionsFacility", "POS_TRANSACTIONS");
|
||||
FACILITY_TYPES.put("chequeBook", "CHEQUE_BOOK");
|
||||
FACILITY_TYPES.put("impsFacility", "IMPS_FACILITY");
|
||||
FACILITY_TYPES.put("smsAlerts", "SMS_ALERTS");
|
||||
FACILITY_TYPES.put("bbpsFacility", "BBPS_FACILITY");
|
||||
FACILITY_TYPES.put("insuranceCover", "INSURANCE_COVER");
|
||||
FACILITY_TYPES.put("bepgFacility", "BEPG_FACILITY");
|
||||
FACILITY_TYPES.put("neftFacility", "NEFT_FACILITY");
|
||||
FACILITY_TYPES.put("rtgsFacility", "RTGS_FACILITY");
|
||||
FACILITY_TYPES.put("interBankTotalCapFacility", "INTER_BANK_TOTAL_CAP");
|
||||
FACILITY_TYPES.put("withdrawalFacility", "WITHDRAWAL_FACILITY");
|
||||
FACILITY_TYPES.put("depositFacilityKyc", "DEPOSIT_FACILITY_KYC");
|
||||
FACILITY_TYPES.put("virtualDebitCard", "VIRTUAL_DEBIT_CARD");
|
||||
FACILITY_TYPES.put("internetBanking", "INTERNET_BANKING");
|
||||
FACILITY_TYPES.put("mobileBanking", "MOBILE_BANKING");
|
||||
}
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final CurrentAccountPendingEditRepository pendingEditRepository;
|
||||
private final CurrentAccountFacilityConfigRepository facilityConfigRepository;
|
||||
private final org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository facilityChargeMappingRepository;
|
||||
private final CurrentAccountTransactionWritePlatformService currentAccountTransactionWritePlatformService;
|
||||
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
|
||||
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long currentAccountId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final CurrentAccount account = this.currentAccountRepository.findById(currentAccountId)
|
||||
.orElseThrow(() -> new CurrentAccountNotFoundException(currentAccountId));
|
||||
|
||||
if (this.pendingEditRepository.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||
.isPresent()) {
|
||||
throw new CurrentAccountEditPendingApprovalException(currentAccountId);
|
||||
}
|
||||
|
||||
final JsonObject root = command.parsedJson().getAsJsonObject();
|
||||
final CurrentAccountFacilityConfig currentConfig = account.getFacilityConfig();
|
||||
|
||||
final CurrentAccountPendingEdit pendingEdit = CurrentAccountPendingEdit.snapshotOf(account, maker);
|
||||
|
||||
// ── Digital & Account Features ──
|
||||
final Set<String> newlyEnabledFacilityTypes = new HashSet<>();
|
||||
if (root.has("accountFacilityConfig") && root.get("accountFacilityConfig").isJsonObject()) {
|
||||
final JsonObject fc = root.getAsJsonObject("accountFacilityConfig");
|
||||
pendingEdit.setFacilityConfigSubmitted(true);
|
||||
populateFacilityField(fc, "atmFacility", currentConfig != null && currentConfig.isAtmFacility(), pendingEdit::setAtmFacility,
|
||||
newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "atmWithdrawalFacility", currentConfig != null && currentConfig.isAtmWithdrawalFacility(),
|
||||
pendingEdit::setAtmWithdrawalFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "upiFacility", currentConfig != null && currentConfig.isUpiFacility(), pendingEdit::setUpiFacility,
|
||||
newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "posTransactionsFacility", currentConfig != null && currentConfig.isPosTransactionsFacility(),
|
||||
pendingEdit::setPosTransactionsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "chequeBook", currentConfig != null && currentConfig.isChequeBook(), pendingEdit::setChequeBook,
|
||||
newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "impsFacility", currentConfig != null && currentConfig.isImpsFacility(),
|
||||
pendingEdit::setImpsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "smsAlerts", currentConfig != null && currentConfig.isSmsAlerts(), pendingEdit::setSmsAlerts,
|
||||
newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "bbpsFacility", currentConfig != null && currentConfig.isBbpsFacility(),
|
||||
pendingEdit::setBbpsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "insuranceCover", currentConfig != null && currentConfig.isInsuranceCover(),
|
||||
pendingEdit::setInsuranceCover, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "bepgFacility", currentConfig != null && currentConfig.isBepgFacility(),
|
||||
pendingEdit::setBepgFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "neftFacility", currentConfig != null && currentConfig.isNeftFacility(),
|
||||
pendingEdit::setNeftFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "rtgsFacility", currentConfig != null && currentConfig.isRtgsFacility(),
|
||||
pendingEdit::setRtgsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "interBankTotalCapFacility", currentConfig != null && currentConfig.isInterBankTotalCapFacility(),
|
||||
pendingEdit::setInterBankTotalCapFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "withdrawalFacility", currentConfig != null && currentConfig.isWithdrawalFacility(),
|
||||
pendingEdit::setWithdrawalFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "depositFacilityKyc", currentConfig != null && currentConfig.isDepositFacilityKyc(),
|
||||
pendingEdit::setDepositFacilityKyc, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "virtualDebitCard", currentConfig != null && currentConfig.isVirtualDebitCard(),
|
||||
pendingEdit::setVirtualDebitCard, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "internetBanking", currentConfig != null && currentConfig.isInternetBanking(),
|
||||
pendingEdit::setInternetBanking, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "mobileBanking", currentConfig != null && currentConfig.isMobileBanking(),
|
||||
pendingEdit::setMobileBanking, newlyEnabledFacilityTypes);
|
||||
}
|
||||
|
||||
// ── Nominee (single add / update / delete per request) ──
|
||||
if (root.has("nominee") && root.get("nominee").isJsonObject()) {
|
||||
final JsonObject nom = root.getAsJsonObject("nominee");
|
||||
pendingEdit.setNomineeSubmitted(true);
|
||||
pendingEdit.setNomineeOperation(nom.has("operation") ? nom.get("operation").getAsString() : null);
|
||||
pendingEdit.setNomineeId(nom.has("nomineeId") && !nom.get("nomineeId").isJsonNull() ? nom.get("nomineeId").getAsLong() : null);
|
||||
pendingEdit.setNomineeClientId(
|
||||
nom.has("nomineeClientId") && !nom.get("nomineeClientId").isJsonNull() ? nom.get("nomineeClientId").getAsLong()
|
||||
: null);
|
||||
pendingEdit.setRelationshipCvId(
|
||||
nom.has("relationshipId") && !nom.get("relationshipId").isJsonNull() ? nom.get("relationshipId").getAsLong() : null);
|
||||
pendingEdit.setPercentageShare(
|
||||
nom.has("percentageShare") && !nom.get("percentageShare").isJsonNull() ? nom.get("percentageShare").getAsBigDecimal()
|
||||
: null);
|
||||
pendingEdit.setNomineeIsActive(
|
||||
nom.has("isActive") && !nom.get("isActive").isJsonNull() ? nom.get("isActive").getAsBoolean() : null);
|
||||
pendingEdit.setNomineeIsMinor(
|
||||
nom.has("nomineeIsMinor") && !nom.get("nomineeIsMinor").isJsonNull() ? nom.get("nomineeIsMinor").getAsBoolean()
|
||||
: null);
|
||||
pendingEdit.setGuardianClientId(
|
||||
nom.has("guardianClientId") && !nom.get("guardianClientId").isJsonNull() ? nom.get("guardianClientId").getAsLong()
|
||||
: null);
|
||||
pendingEdit.setGuardianRelationshipCvId(
|
||||
nom.has("guardianRelationshipId") && !nom.get("guardianRelationshipId").isJsonNull()
|
||||
? nom.get("guardianRelationshipId").getAsLong()
|
||||
: null);
|
||||
pendingEdit.setEffectiveDate(
|
||||
nom.has("effectiveDate") && !nom.get("effectiveDate").isJsonNull() ? LocalDate.parse(nom.get("effectiveDate").getAsString())
|
||||
: null);
|
||||
pendingEdit.setNomineeRemarks(nom.has("remarks") && !nom.get("remarks").isJsonNull() ? nom.get("remarks").getAsString() : null);
|
||||
}
|
||||
|
||||
// ── Hold the charge amount for newly-enabled facilities before anything is persisted ──
|
||||
final BigDecimal holdNeeded = totalChargeAmountFor(newlyEnabledFacilityTypes);
|
||||
if (holdNeeded.compareTo(BigDecimal.ZERO) > 0) {
|
||||
final BigDecimal currentBalance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived()
|
||||
: BigDecimal.ZERO;
|
||||
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||
final BigDecimal availableBalance = currentBalance.subtract(onHold);
|
||||
if (availableBalance.compareTo(holdNeeded) < 0) {
|
||||
throw new CurrentAccountEditInsufficientBalanceException(currentAccountId, availableBalance, holdNeeded);
|
||||
}
|
||||
|
||||
final JsonObject holdJson = new JsonObject();
|
||||
holdJson.addProperty("locale", "en");
|
||||
holdJson.addProperty("dateFormat", "yyyy-MM-dd");
|
||||
holdJson.addProperty("transactionDate", DateUtils.getBusinessLocalDate().toString());
|
||||
holdJson.addProperty("transactionAmount", holdNeeded);
|
||||
holdJson.addProperty("reasonForBlock", "Pending approval of Nominee/Digital-Feature edit");
|
||||
final JsonCommand holdCommand = JsonCommand.fromJsonElement(currentAccountId, holdJson, this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult holdResult = this.currentAccountTransactionWritePlatformService.holdAmount(currentAccountId,
|
||||
holdCommand);
|
||||
pendingEdit.setHoldTransactionId(holdResult.getResourceId());
|
||||
pendingEdit.setHoldAmount(holdNeeded);
|
||||
}
|
||||
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("SUBMITEDIT") //
|
||||
.entityName("CURRENTACCOUNT") //
|
||||
.resourceGetUrl("/currentaccounts/" + currentAccountId) //
|
||||
.resourceId(currentAccountId) //
|
||||
.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(currentAccountId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long currentAccountId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final CurrentAccountPendingEdit pendingEdit = this.pendingEditRepository
|
||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||
}
|
||||
|
||||
final CurrentAccount account = this.currentAccountRepository.findById(currentAccountId)
|
||||
.orElseThrow(() -> new CurrentAccountNotFoundException(currentAccountId));
|
||||
final CurrentAccountFacilityConfig currentConfig = account.getFacilityConfig();
|
||||
final Set<String> newlyEnabledFacilityTypes = pendingEdit.isFacilityConfigSubmitted()
|
||||
? computeNewlyEnabled(pendingEdit, currentConfig)
|
||||
: Set.of();
|
||||
|
||||
// 1) Release the hold placed at submission time.
|
||||
if (pendingEdit.getHoldTransactionId() != null) {
|
||||
this.currentAccountTransactionWritePlatformService.releaseAmount(currentAccountId, pendingEdit.getHoldTransactionId());
|
||||
}
|
||||
|
||||
// 2) Post the real charge(s) for newly-enabled facilities, today.
|
||||
if (!newlyEnabledFacilityTypes.isEmpty()) {
|
||||
this.currentAccountWritePlatformService.applyAndCollectFacilityCharges(currentAccountId, newlyEnabledFacilityTypes);
|
||||
}
|
||||
|
||||
// 3) Apply the facility-config toggles to the real row.
|
||||
if (pendingEdit.isFacilityConfigSubmitted()) {
|
||||
CurrentAccountFacilityConfig config = account.getFacilityConfig();
|
||||
if (config == null) {
|
||||
config = CurrentAccountFacilityConfig.createFor(account);
|
||||
account.setFacilityConfig(config);
|
||||
}
|
||||
config.updateFrom(buildFacilityConfigJson(pendingEdit));
|
||||
this.facilityConfigRepository.saveAndFlush(config);
|
||||
}
|
||||
|
||||
// 4) Apply the nominee change to the real table.
|
||||
if (pendingEdit.isNomineeSubmitted()) {
|
||||
applyNomineeChange(account, pendingEdit);
|
||||
}
|
||||
|
||||
this.currentAccountRepository.saveAndFlush(account);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
updateAuditEntry(pendingEdit, checker, false);
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(currentAccountId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long currentAccountId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final CurrentAccountPendingEdit pendingEdit = this.pendingEditRepository
|
||||
.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new CurrentAccountNoPendingEditException(currentAccountId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new CurrentAccountEditSelfApprovalNotAllowedException(currentAccountId);
|
||||
}
|
||||
|
||||
if (pendingEdit.getHoldTransactionId() != null) {
|
||||
this.currentAccountTransactionWritePlatformService.releaseAmount(currentAccountId, pendingEdit.getHoldTransactionId());
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
updateAuditEntry(pendingEdit, checker, true);
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(currentAccountId).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurrentAccountPendingEditData retrievePendingEdit(final Long currentAccountId) {
|
||||
return this.pendingEditRepository.findByCurrentAccountIdAndStatus(currentAccountId, CurrentAccountPendingEdit.STATUS_PENDING)
|
||||
.map(this::toData).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Collection<CurrentAccountPendingEditData> retrieveAllPendingEdits() {
|
||||
return this.pendingEditRepository.findByStatus(CurrentAccountPendingEdit.STATUS_PENDING).stream().map(this::toData).toList();
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Sum of the mapped charge amounts for the given facility types - used to size the hold placed at
|
||||
* edit-submission time, before any charge is actually applied.
|
||||
*/
|
||||
private BigDecimal totalChargeAmountFor(final Set<String> facilityTypes) {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (final String facilityType : facilityTypes) {
|
||||
final var mapping = this.facilityChargeMappingRepository.findByFacilityType(facilityType);
|
||||
if (mapping.isPresent() && mapping.get().getCharge() != null) {
|
||||
total = total.add(mapping.get().getCharge().getAmount());
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private void applyNomineeChange(final CurrentAccount account, final CurrentAccountPendingEdit pendingEdit) {
|
||||
final Long clientId = account.getClient() != null ? account.getClient().getId() : null;
|
||||
final String operation = pendingEdit.getNomineeOperation();
|
||||
|
||||
if (CurrentAccountPendingEdit.NOMINEE_OP_DELETE.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.deleteNominee(clientId, pendingEdit.getNomineeId());
|
||||
return;
|
||||
}
|
||||
|
||||
final JsonObject nomineeJson = new JsonObject();
|
||||
nomineeJson.addProperty("locale", "en");
|
||||
nomineeJson.addProperty("dateFormat", "yyyy-MM-dd");
|
||||
nomineeJson.addProperty("accountNo", account.getAccountNo());
|
||||
nomineeJson.addProperty("accountType", "CB");
|
||||
if (pendingEdit.getNomineeClientId() != null) {
|
||||
nomineeJson.addProperty("nomineeClientId", pendingEdit.getNomineeClientId());
|
||||
}
|
||||
if (pendingEdit.getRelationshipCvId() != null) {
|
||||
nomineeJson.addProperty("relationshipId", pendingEdit.getRelationshipCvId());
|
||||
}
|
||||
if (pendingEdit.getPercentageShare() != null) {
|
||||
nomineeJson.addProperty("percentageShare", pendingEdit.getPercentageShare());
|
||||
}
|
||||
if (pendingEdit.getNomineeIsActive() != null) {
|
||||
nomineeJson.addProperty("isActive", pendingEdit.getNomineeIsActive());
|
||||
}
|
||||
if (pendingEdit.getNomineeIsMinor() != null) {
|
||||
nomineeJson.addProperty("nomineeIsMinor", pendingEdit.getNomineeIsMinor());
|
||||
}
|
||||
if (pendingEdit.getGuardianClientId() != null) {
|
||||
nomineeJson.addProperty("guardianClientId", pendingEdit.getGuardianClientId());
|
||||
}
|
||||
if (pendingEdit.getGuardianRelationshipCvId() != null) {
|
||||
nomineeJson.addProperty("guardianRelationshipId", pendingEdit.getGuardianRelationshipCvId());
|
||||
}
|
||||
if (pendingEdit.getEffectiveDate() != null) {
|
||||
nomineeJson.addProperty("effectiveDate", pendingEdit.getEffectiveDate().toString());
|
||||
}
|
||||
if (pendingEdit.getNomineeRemarks() != null) {
|
||||
nomineeJson.addProperty("remarks", pendingEdit.getNomineeRemarks());
|
||||
}
|
||||
final JsonCommand nomineeCommand = JsonCommand.fromJsonElement(clientId, nomineeJson, this.fromApiJsonHelper);
|
||||
|
||||
if (CurrentAccountPendingEdit.NOMINEE_OP_ADD.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.addNominee(clientId, nomineeCommand);
|
||||
} else if (CurrentAccountPendingEdit.NOMINEE_OP_UPDATE.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.updateNominee(clientId, pendingEdit.getNomineeId(), nomineeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonObject buildFacilityConfigJson(final CurrentAccountPendingEdit p) {
|
||||
final JsonObject fc = new JsonObject();
|
||||
putIfPresent(fc, "atmFacility", p.getAtmFacility());
|
||||
putIfPresent(fc, "atmWithdrawalFacility", p.getAtmWithdrawalFacility());
|
||||
putIfPresent(fc, "upiFacility", p.getUpiFacility());
|
||||
putIfPresent(fc, "posTransactionsFacility", p.getPosTransactionsFacility());
|
||||
putIfPresent(fc, "chequeBook", p.getChequeBook());
|
||||
putIfPresent(fc, "impsFacility", p.getImpsFacility());
|
||||
putIfPresent(fc, "smsAlerts", p.getSmsAlerts());
|
||||
putIfPresent(fc, "bbpsFacility", p.getBbpsFacility());
|
||||
putIfPresent(fc, "insuranceCover", p.getInsuranceCover());
|
||||
putIfPresent(fc, "bepgFacility", p.getBepgFacility());
|
||||
putIfPresent(fc, "neftFacility", p.getNeftFacility());
|
||||
putIfPresent(fc, "rtgsFacility", p.getRtgsFacility());
|
||||
putIfPresent(fc, "interBankTotalCapFacility", p.getInterBankTotalCapFacility());
|
||||
putIfPresent(fc, "withdrawalFacility", p.getWithdrawalFacility());
|
||||
putIfPresent(fc, "depositFacilityKyc", p.getDepositFacilityKyc());
|
||||
putIfPresent(fc, "virtualDebitCard", p.getVirtualDebitCard());
|
||||
putIfPresent(fc, "internetBanking", p.getInternetBanking());
|
||||
putIfPresent(fc, "mobileBanking", p.getMobileBanking());
|
||||
return fc;
|
||||
}
|
||||
|
||||
private void putIfPresent(final JsonObject json, final String field, final Boolean value) {
|
||||
if (value != null) {
|
||||
json.addProperty(field, value);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> computeNewlyEnabled(final CurrentAccountPendingEdit p, final CurrentAccountFacilityConfig current) {
|
||||
final Set<String> result = new HashSet<>();
|
||||
addIfNewlyEnabled(result, "atmFacility", p.getAtmFacility(), current != null && current.isAtmFacility());
|
||||
addIfNewlyEnabled(result, "atmWithdrawalFacility", p.getAtmWithdrawalFacility(), current != null && current.isAtmWithdrawalFacility());
|
||||
addIfNewlyEnabled(result, "upiFacility", p.getUpiFacility(), current != null && current.isUpiFacility());
|
||||
addIfNewlyEnabled(result, "posTransactionsFacility", p.getPosTransactionsFacility(),
|
||||
current != null && current.isPosTransactionsFacility());
|
||||
addIfNewlyEnabled(result, "chequeBook", p.getChequeBook(), current != null && current.isChequeBook());
|
||||
addIfNewlyEnabled(result, "impsFacility", p.getImpsFacility(), current != null && current.isImpsFacility());
|
||||
addIfNewlyEnabled(result, "smsAlerts", p.getSmsAlerts(), current != null && current.isSmsAlerts());
|
||||
addIfNewlyEnabled(result, "bbpsFacility", p.getBbpsFacility(), current != null && current.isBbpsFacility());
|
||||
addIfNewlyEnabled(result, "insuranceCover", p.getInsuranceCover(), current != null && current.isInsuranceCover());
|
||||
addIfNewlyEnabled(result, "bepgFacility", p.getBepgFacility(), current != null && current.isBepgFacility());
|
||||
addIfNewlyEnabled(result, "neftFacility", p.getNeftFacility(), current != null && current.isNeftFacility());
|
||||
addIfNewlyEnabled(result, "rtgsFacility", p.getRtgsFacility(), current != null && current.isRtgsFacility());
|
||||
addIfNewlyEnabled(result, "interBankTotalCapFacility", p.getInterBankTotalCapFacility(),
|
||||
current != null && current.isInterBankTotalCapFacility());
|
||||
addIfNewlyEnabled(result, "withdrawalFacility", p.getWithdrawalFacility(), current != null && current.isWithdrawalFacility());
|
||||
addIfNewlyEnabled(result, "depositFacilityKyc", p.getDepositFacilityKyc(), current != null && current.isDepositFacilityKyc());
|
||||
addIfNewlyEnabled(result, "virtualDebitCard", p.getVirtualDebitCard(), current != null && current.isVirtualDebitCard());
|
||||
addIfNewlyEnabled(result, "internetBanking", p.getInternetBanking(), current != null && current.isInternetBanking());
|
||||
addIfNewlyEnabled(result, "mobileBanking", p.getMobileBanking(), current != null && current.isMobileBanking());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addIfNewlyEnabled(final Set<String> result, final String field, final Boolean proposed, final boolean current) {
|
||||
if (Boolean.TRUE.equals(proposed) && !current) {
|
||||
result.add(FACILITY_TYPES.get(field));
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a single submitted field out of the incoming JSON, tracks whether it enables a previously-off facility. */
|
||||
private void populateFacilityField(final JsonObject fc, final String field, final boolean currentValue,
|
||||
final java.util.function.Consumer<Boolean> setter, final Set<String> newlyEnabledCollector) {
|
||||
if (!fc.has(field) || fc.get(field).isJsonNull()) {
|
||||
return;
|
||||
}
|
||||
final boolean proposed = fc.get(field).getAsBoolean();
|
||||
setter.accept(proposed);
|
||||
if (proposed && !currentValue) {
|
||||
newlyEnabledCollector.add(FACILITY_TYPES.get(field));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAuditEntry(final CurrentAccountPendingEdit 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("CURRENTACCOUNT") //
|
||||
.resourceGetUrl("/currentaccounts/" + pendingEdit.getCurrentAccount().getId()) //
|
||||
.resourceId(pendingEdit.getCurrentAccount().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 CurrentAccountPendingEditData toData(final CurrentAccountPendingEdit p) {
|
||||
final CurrentAccountPendingEditData data = new CurrentAccountPendingEditData();
|
||||
data.setId(p.getId());
|
||||
data.setCurrentAccountId(p.getCurrentAccount().getId());
|
||||
data.setAccountNo(p.getCurrentAccount().getAccountNo());
|
||||
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());
|
||||
data.setHoldAmount(p.getHoldAmount());
|
||||
data.setFacilityConfigSubmitted(p.isFacilityConfigSubmitted());
|
||||
data.setNomineeSubmitted(p.isNomineeSubmitted());
|
||||
|
||||
if (p.isFacilityConfigSubmitted()) {
|
||||
final Map<String, Object> fc = new LinkedHashMap<>();
|
||||
putIfNotNull(fc, "atmFacility", p.getAtmFacility());
|
||||
putIfNotNull(fc, "atmWithdrawalFacility", p.getAtmWithdrawalFacility());
|
||||
putIfNotNull(fc, "upiFacility", p.getUpiFacility());
|
||||
putIfNotNull(fc, "posTransactionsFacility", p.getPosTransactionsFacility());
|
||||
putIfNotNull(fc, "chequeBook", p.getChequeBook());
|
||||
putIfNotNull(fc, "impsFacility", p.getImpsFacility());
|
||||
putIfNotNull(fc, "smsAlerts", p.getSmsAlerts());
|
||||
putIfNotNull(fc, "bbpsFacility", p.getBbpsFacility());
|
||||
putIfNotNull(fc, "insuranceCover", p.getInsuranceCover());
|
||||
putIfNotNull(fc, "bepgFacility", p.getBepgFacility());
|
||||
putIfNotNull(fc, "neftFacility", p.getNeftFacility());
|
||||
putIfNotNull(fc, "rtgsFacility", p.getRtgsFacility());
|
||||
putIfNotNull(fc, "interBankTotalCapFacility", p.getInterBankTotalCapFacility());
|
||||
putIfNotNull(fc, "withdrawalFacility", p.getWithdrawalFacility());
|
||||
putIfNotNull(fc, "depositFacilityKyc", p.getDepositFacilityKyc());
|
||||
putIfNotNull(fc, "virtualDebitCard", p.getVirtualDebitCard());
|
||||
putIfNotNull(fc, "internetBanking", p.getInternetBanking());
|
||||
putIfNotNull(fc, "mobileBanking", p.getMobileBanking());
|
||||
data.setProposedFacilityConfig(fc);
|
||||
}
|
||||
|
||||
if (p.isNomineeSubmitted()) {
|
||||
final Map<String, Object> nom = new LinkedHashMap<>();
|
||||
putIfNotNull(nom, "operation", p.getNomineeOperation());
|
||||
putIfNotNull(nom, "nomineeId", p.getNomineeId());
|
||||
putIfNotNull(nom, "nomineeClientId", p.getNomineeClientId());
|
||||
putIfNotNull(nom, "relationshipId", p.getRelationshipCvId());
|
||||
putIfNotNull(nom, "percentageShare", p.getPercentageShare());
|
||||
putIfNotNull(nom, "isActive", p.getNomineeIsActive());
|
||||
putIfNotNull(nom, "nomineeIsMinor", p.getNomineeIsMinor());
|
||||
putIfNotNull(nom, "guardianClientId", p.getGuardianClientId());
|
||||
putIfNotNull(nom, "guardianRelationshipId", p.getGuardianRelationshipCvId());
|
||||
putIfNotNull(nom, "effectiveDate", p.getEffectiveDate());
|
||||
putIfNotNull(nom, "remarks", p.getNomineeRemarks());
|
||||
data.setProposedNominee(nom);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void putIfNotNull(final Map<String, Object> map, final String key, final Object value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
-3
@@ -70,9 +70,37 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
|
||||
response.put("partners", retrievePartners(accountId));
|
||||
response.put("charges", retrieveCharges(accountId));
|
||||
response.put("transactions", retrieveTransactions(accountId));
|
||||
response.put("accountFacilityConfig", retrieveFacilityConfig(accountId));
|
||||
return response;
|
||||
}
|
||||
|
||||
private Map<String, Object> retrieveFacilityConfig(Long accountId) {
|
||||
final String sql = "SELECT * FROM m_current_account_facility_config WHERE current_account_id = ?";
|
||||
List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
|
||||
final Map<String, Object> cfg = new LinkedHashMap<>();
|
||||
cfg.put("atmFacility", rs.getBoolean("atm_facility"));
|
||||
cfg.put("atmWithdrawalFacility", rs.getBoolean("atm_withdrawal_facility"));
|
||||
cfg.put("upiFacility", rs.getBoolean("upi_facility"));
|
||||
cfg.put("posTransactionsFacility", rs.getBoolean("pos_transactions_facility"));
|
||||
cfg.put("chequeBook", rs.getBoolean("cheque_book"));
|
||||
cfg.put("impsFacility", rs.getBoolean("imps_facility"));
|
||||
cfg.put("smsAlerts", rs.getBoolean("sms_alerts"));
|
||||
cfg.put("bbpsFacility", rs.getBoolean("bbps_facility"));
|
||||
cfg.put("insuranceCover", rs.getBoolean("insurance_cover"));
|
||||
cfg.put("bepgFacility", rs.getBoolean("bepg_facility"));
|
||||
cfg.put("neftFacility", rs.getBoolean("neft_facility"));
|
||||
cfg.put("rtgsFacility", rs.getBoolean("rtgs_facility"));
|
||||
cfg.put("interBankTotalCapFacility", rs.getBoolean("inter_bank_total_cap_facility"));
|
||||
cfg.put("withdrawalFacility", rs.getBoolean("withdrawal_facility"));
|
||||
cfg.put("depositFacilityKyc", rs.getBoolean("deposit_facility_kyc"));
|
||||
cfg.put("virtualDebitCard", rs.getBoolean("virtual_debit_card"));
|
||||
cfg.put("internetBanking", rs.getBoolean("internet_banking"));
|
||||
cfg.put("mobileBanking", rs.getBoolean("mobile_banking"));
|
||||
return cfg;
|
||||
}, accountId);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> retrieveJointHolders(Long accountId) {
|
||||
final String sql =
|
||||
"SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
||||
@@ -593,8 +621,18 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
|
||||
|
||||
private Map<String, Object> buildSummary(Map<String, Object> row) {
|
||||
Map<String, Object> s = new LinkedHashMap<>();
|
||||
s.put("accountBalance", row.get("account_balance_derived"));
|
||||
s.put("availableBalance", row.get("account_balance_derived"));
|
||||
BigDecimal balance = toBigDecimal(row.get("account_balance_derived"));
|
||||
if (balance == null) {
|
||||
balance = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal onHold = toBigDecimal(row.get("on_hold_funds_derived"));
|
||||
if (onHold == null) {
|
||||
onHold = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal available = balance.subtract(onHold);
|
||||
|
||||
s.put("accountBalance", balance);
|
||||
s.put("availableBalance", available);
|
||||
s.put("totalDeposits", row.get("total_deposits_derived"));
|
||||
s.put("totalWithdrawals", row.get("total_withdrawals_derived"));
|
||||
s.put("totalInterestEarned", row.get("total_interest_earned_derived"));
|
||||
@@ -605,7 +643,7 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
|
||||
s.put("totalWithdrawalFees", null);
|
||||
s.put("totalAnnualFees", null);
|
||||
s.put("lastInterestCalculationDate", row.get("last_interest_calculation_date"));
|
||||
s.put("onHoldFunds", row.get("on_hold_funds_derived"));
|
||||
s.put("onHoldFunds", onHold);
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+78
-19
@@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||
@@ -335,25 +336,11 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
}
|
||||
|
||||
// Auto-deduct activation charges (chargeTimeEnum=3) from opening balance
|
||||
for (final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge : account.getCharges()) {
|
||||
if (charge.getChargeTimeEnum() != null && charge.getChargeTimeEnum() == 3
|
||||
&& charge.getAmount() != null
|
||||
&& charge.getAmount().compareTo(java.math.BigDecimal.ZERO) > 0
|
||||
&& charge.getCharge() != null) {
|
||||
charge.setAmountPaidDerived(charge.getAmount());
|
||||
charge.setAmountOutstandingDerived(java.math.BigDecimal.ZERO);
|
||||
charge.setPaid(true);
|
||||
currentBalance = currentBalance.subtract(charge.getAmount());
|
||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction chargeTxn =
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
|
||||
.chargeDeduction(account, account.getOffice(), charge.getAmount(), activatedOnDate);
|
||||
chargeTxn.setRunningBalanceDerived(currentBalance);
|
||||
currentAccountTransactionRepository.saveAndFlush(chargeTxn);
|
||||
chargeEntries.add(new ChargeGlEntry(chargeTxn, charge));
|
||||
log.info("Auto-deducted activation charge '{}' {} from account id={}, running balance={}",
|
||||
charge.getCharge().getName(), charge.getAmount(), accountId, currentBalance);
|
||||
}
|
||||
}
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> activationCharges =
|
||||
account.getCharges().stream()
|
||||
.filter(c -> c.getChargeTimeEnum() != null && c.getChargeTimeEnum() == 3 && !c.isPaid())
|
||||
.toList();
|
||||
currentBalance = collectChargesAndPost(account, activationCharges, currentBalance, activatedOnDate, chargeEntries);
|
||||
|
||||
account.setAccountBalanceDerived(currentBalance);
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
@@ -716,6 +703,78 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn,
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge) {}
|
||||
|
||||
/**
|
||||
* Marks each of the given (unpaid) charges as paid, deducts its amount from {@code runningBalance}, creates a
|
||||
* {@code CurrentAccountTransaction.chargeDeduction(...)} row for it, and records a {@link ChargeGlEntry} for GL
|
||||
* posting. Shared by {@link #activate(Long, JsonCommand)} (activation-time charges) and
|
||||
* {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) so the
|
||||
* charge-collection logic isn't duplicated. Returns the resulting balance.
|
||||
*/
|
||||
private java.math.BigDecimal collectChargesAndPost(final CurrentAccount account,
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> charges,
|
||||
java.math.BigDecimal runningBalance, final LocalDate transactionDate,
|
||||
final java.util.List<ChargeGlEntry> chargeEntriesOut) {
|
||||
for (final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge : charges) {
|
||||
if (charge.getAmount() == null || charge.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0 || charge.getCharge() == null) {
|
||||
continue;
|
||||
}
|
||||
charge.setAmountPaidDerived(charge.getAmount());
|
||||
charge.setAmountOutstandingDerived(java.math.BigDecimal.ZERO);
|
||||
charge.setPaid(true);
|
||||
runningBalance = runningBalance.subtract(charge.getAmount());
|
||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction chargeTxn =
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
|
||||
.chargeDeduction(account, account.getOffice(), charge.getAmount(), transactionDate);
|
||||
chargeTxn.setRunningBalanceDerived(runningBalance);
|
||||
currentAccountTransactionRepository.saveAndFlush(chargeTxn);
|
||||
chargeEntriesOut.add(new ChargeGlEntry(chargeTxn, charge));
|
||||
log.info("[FacilityCharge] Collected charge '{}' {} from account id={}, running balance={}",
|
||||
charge.getCharge().getName(), charge.getAmount(), account.getId(), runningBalance);
|
||||
}
|
||||
return runningBalance;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void applyAndCollectFacilityCharges(final Long currentAccountId, final java.util.Set<String> newlyEnabledFacilityTypes) {
|
||||
if (newlyEnabledFacilityTypes == null || newlyEnabledFacilityTypes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final CurrentAccount account = findAccount(currentAccountId);
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> newCharges = new java.util.ArrayList<>();
|
||||
|
||||
for (final String facilityType : newlyEnabledFacilityTypes) {
|
||||
final var mappingOpt = facilityChargeMappingRepository.findByFacilityType(facilityType);
|
||||
if (mappingOpt.isEmpty()) {
|
||||
log.warn("[FacilityCharge] {} newly-enabled via edit-approval but NO mapping found in m_facility_charge_mapping",
|
||||
facilityType);
|
||||
continue;
|
||||
}
|
||||
final org.apache.fineract.portfolio.charge.domain.Charge charge = mappingOpt.get().getCharge();
|
||||
if (charge == null) {
|
||||
continue;
|
||||
}
|
||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge newCharge =
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge.createFromFacility(account, charge);
|
||||
account.getCharges().add(newCharge);
|
||||
newCharges.add(newCharge);
|
||||
}
|
||||
|
||||
if (newCharges.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
java.math.BigDecimal currentBalance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived()
|
||||
: java.math.BigDecimal.ZERO;
|
||||
final java.util.List<ChargeGlEntry> chargeEntries = new java.util.ArrayList<>();
|
||||
currentBalance = collectChargesAndPost(account, newCharges, currentBalance, DateUtils.getBusinessLocalDate(), chargeEntries);
|
||||
|
||||
account.setAccountBalanceDerived(currentBalance);
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
|
||||
postGlEntries(account, java.util.List.of(), chargeEntries);
|
||||
}
|
||||
|
||||
// entity type for savings/current accounts in acc_gl_journal_entry
|
||||
private static final int ENTITY_TYPE_SAVINGS = 2;
|
||||
|
||||
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* 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.savings.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
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.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountPendingEditData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfig;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountPendingEdit;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountEditInsufficientBalanceException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountEditSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNoPendingEditException;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Maker-checker edit flow for an ACTIVE savings account's Nominee + Digital & Account Features. Mirrors
|
||||
* {@code SavingsProductWritePlatformServiceJpaRepositoryImpl}'s submitEditForApproval/approveEdit/rejectEdit
|
||||
* pattern: a row in {@code m_savings_account_pending_edit} is itself the lock, and nothing is applied to the real
|
||||
* {@code m_savings_account_facility_config} / {@code m_client_nominee} tables until a checker approves.
|
||||
*
|
||||
* <p>
|
||||
* Charge handling for a newly-enabled facility (one with a mapped charge in m_facility_charge_mapping): at
|
||||
* submission a HOLD is placed for the total charge amount (blocking that much of the available balance without
|
||||
* touching it); on approval the hold is released and the charge is posted for real; on rejection the hold is simply
|
||||
* released.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements SavingsAccountEditWritePlatformService {
|
||||
|
||||
/** field name (as sent in accountFacilityConfig JSON / stored on the pending edit) -> facility_type code. */
|
||||
private static final Map<String, String> FACILITY_TYPES = new LinkedHashMap<>();
|
||||
static {
|
||||
FACILITY_TYPES.put("atmFacility", "ATM_FACILITY");
|
||||
FACILITY_TYPES.put("atmWithdrawalFacility", "ATM_WITHDRAWAL");
|
||||
FACILITY_TYPES.put("upiFacility", "UPI_FACILITY");
|
||||
FACILITY_TYPES.put("posTransactionsFacility", "POS_TRANSACTIONS");
|
||||
FACILITY_TYPES.put("chequeBook", "CHEQUE_BOOK");
|
||||
FACILITY_TYPES.put("impsFacility", "IMPS_FACILITY");
|
||||
FACILITY_TYPES.put("smsAlerts", "SMS_ALERTS");
|
||||
FACILITY_TYPES.put("bbpsFacility", "BBPS_FACILITY");
|
||||
FACILITY_TYPES.put("insuranceCover", "INSURANCE_COVER");
|
||||
FACILITY_TYPES.put("bepgFacility", "BEPG_FACILITY");
|
||||
FACILITY_TYPES.put("neftFacility", "NEFT_FACILITY");
|
||||
FACILITY_TYPES.put("rtgsFacility", "RTGS_FACILITY");
|
||||
FACILITY_TYPES.put("interBankTotalCapFacility", "INTER_BANK_TOTAL_CAP");
|
||||
FACILITY_TYPES.put("withdrawalFacility", "WITHDRAWAL_FACILITY");
|
||||
FACILITY_TYPES.put("depositFacilityKyc", "DEPOSIT_FACILITY_KYC");
|
||||
FACILITY_TYPES.put("virtualDebitCard", "VIRTUAL_DEBIT_CARD");
|
||||
}
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper;
|
||||
private final SavingsAccountPendingEditRepository pendingEditRepository;
|
||||
private final SavingsAccountFacilityConfigRepository facilityConfigRepository;
|
||||
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
||||
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
|
||||
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long savingsId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final SavingsAccount account = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsId);
|
||||
|
||||
if (this.pendingEditRepository.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING).isPresent()) {
|
||||
throw new SavingsAccountEditPendingApprovalException(savingsId);
|
||||
}
|
||||
|
||||
final JsonObject root = command.parsedJson().getAsJsonObject();
|
||||
final SavingsAccountFacilityConfig currentConfig = account.getFacilityConfig();
|
||||
|
||||
final SavingsAccountPendingEdit pendingEdit = new SavingsAccountPendingEdit(account, maker);
|
||||
|
||||
// ── Digital & Account Features ──
|
||||
final Set<String> newlyEnabledFacilityTypes = new HashSet<>();
|
||||
if (root.has("accountFacilityConfig") && root.get("accountFacilityConfig").isJsonObject()) {
|
||||
final JsonObject fc = root.getAsJsonObject("accountFacilityConfig");
|
||||
pendingEdit.setFacilityConfigSubmitted(true);
|
||||
populateFacilityField(fc, "atmFacility", currentConfig == null ? null : currentConfig.getAtmFacility(), pendingEdit::setAtmFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "atmWithdrawalFacility", currentConfig == null ? null : currentConfig.getAtmWithdrawalFacility(), pendingEdit::setAtmWithdrawalFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "upiFacility", currentConfig == null ? null : currentConfig.getUpiFacility(), pendingEdit::setUpiFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "posTransactionsFacility", currentConfig == null ? null : currentConfig.getPosTransactionsFacility(), pendingEdit::setPosTransactionsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "chequeBook", currentConfig == null ? null : currentConfig.getChequeBook(), pendingEdit::setChequeBook, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "impsFacility", currentConfig == null ? null : currentConfig.getImpsFacility(), pendingEdit::setImpsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "smsAlerts", currentConfig == null ? null : currentConfig.getSmsAlerts(), pendingEdit::setSmsAlerts, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "bbpsFacility", currentConfig == null ? null : currentConfig.getBbpsFacility(), pendingEdit::setBbpsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "insuranceCover", currentConfig == null ? null : currentConfig.getInsuranceCover(), pendingEdit::setInsuranceCover, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "bepgFacility", currentConfig == null ? null : currentConfig.getBepgFacility(), pendingEdit::setBepgFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "neftFacility", currentConfig == null ? null : currentConfig.getNeftFacility(), pendingEdit::setNeftFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "rtgsFacility", currentConfig == null ? null : currentConfig.getRtgsFacility(), pendingEdit::setRtgsFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "interBankTotalCapFacility", currentConfig == null ? null : currentConfig.getInterBankTotalCapFacility(), pendingEdit::setInterBankTotalCapFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "withdrawalFacility", currentConfig == null ? null : currentConfig.getWithdrawalFacility(), pendingEdit::setWithdrawalFacility, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "depositFacilityKyc", currentConfig == null ? null : currentConfig.getDepositFacilityKyc(), pendingEdit::setDepositFacilityKyc, newlyEnabledFacilityTypes);
|
||||
populateFacilityField(fc, "virtualDebitCard", currentConfig == null ? null : currentConfig.getVirtualDebitCard(), pendingEdit::setVirtualDebitCard, newlyEnabledFacilityTypes);
|
||||
}
|
||||
|
||||
// ── Nominee (single add / update / delete per request) ──
|
||||
if (root.has("nominee") && root.get("nominee").isJsonObject()) {
|
||||
final JsonObject nom = root.getAsJsonObject("nominee");
|
||||
pendingEdit.setNomineeSubmitted(true);
|
||||
pendingEdit.setNomineeOperation(nom.has("operation") ? nom.get("operation").getAsString() : null);
|
||||
pendingEdit.setNomineeId(nom.has("nomineeId") && !nom.get("nomineeId").isJsonNull() ? nom.get("nomineeId").getAsLong() : null);
|
||||
pendingEdit.setNomineeClientId(nom.has("nomineeClientId") && !nom.get("nomineeClientId").isJsonNull() ? nom.get("nomineeClientId").getAsLong() : null);
|
||||
pendingEdit.setRelationshipCvId(nom.has("relationshipId") && !nom.get("relationshipId").isJsonNull() ? nom.get("relationshipId").getAsLong() : null);
|
||||
pendingEdit.setPercentageShare(nom.has("percentageShare") && !nom.get("percentageShare").isJsonNull() ? nom.get("percentageShare").getAsBigDecimal() : null);
|
||||
pendingEdit.setNomineeIsActive(nom.has("isActive") && !nom.get("isActive").isJsonNull() ? nom.get("isActive").getAsBoolean() : null);
|
||||
pendingEdit.setNomineeIsMinor(nom.has("nomineeIsMinor") && !nom.get("nomineeIsMinor").isJsonNull() ? nom.get("nomineeIsMinor").getAsBoolean() : null);
|
||||
pendingEdit.setGuardianClientId(nom.has("guardianClientId") && !nom.get("guardianClientId").isJsonNull() ? nom.get("guardianClientId").getAsLong() : null);
|
||||
pendingEdit.setGuardianRelationshipCvId(nom.has("guardianRelationshipId") && !nom.get("guardianRelationshipId").isJsonNull() ? nom.get("guardianRelationshipId").getAsLong() : null);
|
||||
pendingEdit.setEffectiveDate(nom.has("effectiveDate") && !nom.get("effectiveDate").isJsonNull() ? LocalDate.parse(nom.get("effectiveDate").getAsString()) : null);
|
||||
pendingEdit.setNomineeRemarks(nom.has("remarks") && !nom.get("remarks").isJsonNull() ? nom.get("remarks").getAsString() : null);
|
||||
}
|
||||
|
||||
// ── Hold the charge amount for newly-enabled facilities before anything is persisted ──
|
||||
final BigDecimal holdNeeded = this.facilityChargeAutoApplyService.totalChargeAmountFor(newlyEnabledFacilityTypes);
|
||||
if (holdNeeded.compareTo(BigDecimal.ZERO) > 0) {
|
||||
final BigDecimal availableBalance = account.getWithdrawableBalance();
|
||||
if (availableBalance.compareTo(holdNeeded) < 0) {
|
||||
throw new SavingsAccountEditInsufficientBalanceException(savingsId, availableBalance, holdNeeded);
|
||||
}
|
||||
|
||||
final JsonObject holdJson = new JsonObject();
|
||||
holdJson.addProperty("locale", "en");
|
||||
holdJson.addProperty("dateFormat", "yyyy-MM-dd");
|
||||
holdJson.addProperty("transactionDate", DateUtils.getBusinessLocalDate().toString());
|
||||
holdJson.addProperty("transactionAmount", holdNeeded);
|
||||
holdJson.addProperty("lienAllowed", false);
|
||||
holdJson.addProperty("reasonForBlock", "Pending approval of Nominee/Digital-Feature edit");
|
||||
final JsonCommand holdCommand = JsonCommand.fromJsonElement(savingsId, holdJson, this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult holdResult = this.savingsAccountWritePlatformService.holdAmount(savingsId, holdCommand);
|
||||
pendingEdit.setHoldTransactionId(holdResult.getResourceId());
|
||||
pendingEdit.setHoldAmount(holdNeeded);
|
||||
}
|
||||
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("SUBMITEDIT") //
|
||||
.entityName("SAVINGSACCOUNT") //
|
||||
.resourceGetUrl("/savingsaccounts/" + savingsId) //
|
||||
.resourceId(savingsId) //
|
||||
.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(savingsId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long savingsId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final SavingsAccountPendingEdit pendingEdit = this.pendingEditRepository
|
||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||
}
|
||||
|
||||
final SavingsAccount account = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsId);
|
||||
final SavingsAccountFacilityConfig currentConfig = account.getFacilityConfig();
|
||||
final Set<String> newlyEnabledFacilityTypes = pendingEdit.isFacilityConfigSubmitted()
|
||||
? computeNewlyEnabled(pendingEdit, currentConfig)
|
||||
: Set.of();
|
||||
|
||||
// 1) Release the hold placed at submission time.
|
||||
if (pendingEdit.getHoldTransactionId() != null) {
|
||||
this.savingsAccountWritePlatformService.releaseAmount(savingsId, pendingEdit.getHoldTransactionId());
|
||||
}
|
||||
|
||||
// 2) Post the real charge(s) for newly-enabled facilities, today.
|
||||
if (!newlyEnabledFacilityTypes.isEmpty()) {
|
||||
final var chargesAdded = this.facilityChargeAutoApplyService.applyChargesForFacilities(account, newlyEnabledFacilityTypes);
|
||||
this.facilityChargeAutoApplyService.collectCharges(account, chargesAdded, DateUtils.getBusinessLocalDate());
|
||||
}
|
||||
|
||||
// 3) Apply the facility-config toggles to the real row.
|
||||
if (pendingEdit.isFacilityConfigSubmitted()) {
|
||||
SavingsAccountFacilityConfig config = account.getFacilityConfig();
|
||||
if (config == null) {
|
||||
config = new SavingsAccountFacilityConfig(account);
|
||||
account.setFacilityConfig(config);
|
||||
}
|
||||
config.updateFrom(buildFacilityConfigJson(pendingEdit));
|
||||
this.facilityConfigRepository.saveAndFlush(config);
|
||||
}
|
||||
|
||||
// 4) Apply the nominee change to the real table.
|
||||
if (pendingEdit.isNomineeSubmitted()) {
|
||||
applyNomineeChange(account, pendingEdit);
|
||||
}
|
||||
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
updateAuditEntry(pendingEdit, checker, false);
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(savingsId).build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long savingsId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final SavingsAccountPendingEdit pendingEdit = this.pendingEditRepository
|
||||
.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsAccountNoPendingEditException(savingsId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsAccountEditSelfApprovalNotAllowedException(savingsId);
|
||||
}
|
||||
|
||||
if (pendingEdit.getHoldTransactionId() != null) {
|
||||
this.savingsAccountWritePlatformService.releaseAmount(savingsId, pendingEdit.getHoldTransactionId());
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
updateAuditEntry(pendingEdit, checker, true);
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(savingsId).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountPendingEditData retrievePendingEdit(final Long savingsId) {
|
||||
return this.pendingEditRepository.findBySavingsAccountIdAndStatus(savingsId, SavingsAccountPendingEdit.STATUS_PENDING)
|
||||
.map(this::toData).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Collection<SavingsAccountPendingEditData> retrieveAllPendingEdits() {
|
||||
return this.pendingEditRepository.findByStatus(SavingsAccountPendingEdit.STATUS_PENDING).stream().map(this::toData).toList();
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void applyNomineeChange(final SavingsAccount account, final SavingsAccountPendingEdit pendingEdit) {
|
||||
final Long clientId = account.clientId();
|
||||
final String operation = pendingEdit.getNomineeOperation();
|
||||
|
||||
if (SavingsAccountPendingEdit.NOMINEE_OP_DELETE.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.deleteNominee(clientId, pendingEdit.getNomineeId());
|
||||
return;
|
||||
}
|
||||
|
||||
final String accountType;
|
||||
if (account.depositAccountType().isFixedDeposit()) {
|
||||
accountType = "FD";
|
||||
} else if (account.depositAccountType().isRecurringDeposit()) {
|
||||
accountType = "RD";
|
||||
} else {
|
||||
accountType = "SB";
|
||||
}
|
||||
|
||||
final JsonObject nomineeJson = new JsonObject();
|
||||
nomineeJson.addProperty("locale", "en");
|
||||
nomineeJson.addProperty("dateFormat", "yyyy-MM-dd");
|
||||
nomineeJson.addProperty("accountNo", account.getAccountNumber());
|
||||
nomineeJson.addProperty("accountType", accountType);
|
||||
if (pendingEdit.getNomineeClientId() != null) {
|
||||
nomineeJson.addProperty("nomineeClientId", pendingEdit.getNomineeClientId());
|
||||
}
|
||||
if (pendingEdit.getRelationshipCvId() != null) {
|
||||
nomineeJson.addProperty("relationshipId", pendingEdit.getRelationshipCvId());
|
||||
}
|
||||
if (pendingEdit.getPercentageShare() != null) {
|
||||
nomineeJson.addProperty("percentageShare", pendingEdit.getPercentageShare());
|
||||
}
|
||||
if (pendingEdit.getNomineeIsActive() != null) {
|
||||
nomineeJson.addProperty("isActive", pendingEdit.getNomineeIsActive());
|
||||
}
|
||||
if (pendingEdit.getNomineeIsMinor() != null) {
|
||||
nomineeJson.addProperty("nomineeIsMinor", pendingEdit.getNomineeIsMinor());
|
||||
}
|
||||
if (pendingEdit.getGuardianClientId() != null) {
|
||||
nomineeJson.addProperty("guardianClientId", pendingEdit.getGuardianClientId());
|
||||
}
|
||||
if (pendingEdit.getGuardianRelationshipCvId() != null) {
|
||||
nomineeJson.addProperty("guardianRelationshipId", pendingEdit.getGuardianRelationshipCvId());
|
||||
}
|
||||
if (pendingEdit.getEffectiveDate() != null) {
|
||||
nomineeJson.addProperty("effectiveDate", pendingEdit.getEffectiveDate().toString());
|
||||
}
|
||||
if (pendingEdit.getNomineeRemarks() != null) {
|
||||
nomineeJson.addProperty("remarks", pendingEdit.getNomineeRemarks());
|
||||
}
|
||||
final JsonCommand nomineeCommand = JsonCommand.fromJsonElement(clientId, nomineeJson, this.fromApiJsonHelper);
|
||||
|
||||
if (SavingsAccountPendingEdit.NOMINEE_OP_ADD.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.addNominee(clientId, nomineeCommand);
|
||||
} else if (SavingsAccountPendingEdit.NOMINEE_OP_UPDATE.equals(operation)) {
|
||||
this.clientNomineeWritePlatformService.updateNominee(clientId, pendingEdit.getNomineeId(), nomineeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonObject buildFacilityConfigJson(final SavingsAccountPendingEdit p) {
|
||||
final JsonObject fc = new JsonObject();
|
||||
putIfPresent(fc, "atmFacility", p.getAtmFacility());
|
||||
putIfPresent(fc, "atmWithdrawalFacility", p.getAtmWithdrawalFacility());
|
||||
putIfPresent(fc, "upiFacility", p.getUpiFacility());
|
||||
putIfPresent(fc, "posTransactionsFacility", p.getPosTransactionsFacility());
|
||||
putIfPresent(fc, "chequeBook", p.getChequeBook());
|
||||
putIfPresent(fc, "impsFacility", p.getImpsFacility());
|
||||
putIfPresent(fc, "smsAlerts", p.getSmsAlerts());
|
||||
putIfPresent(fc, "bbpsFacility", p.getBbpsFacility());
|
||||
putIfPresent(fc, "insuranceCover", p.getInsuranceCover());
|
||||
putIfPresent(fc, "bepgFacility", p.getBepgFacility());
|
||||
putIfPresent(fc, "neftFacility", p.getNeftFacility());
|
||||
putIfPresent(fc, "rtgsFacility", p.getRtgsFacility());
|
||||
putIfPresent(fc, "interBankTotalCapFacility", p.getInterBankTotalCapFacility());
|
||||
putIfPresent(fc, "withdrawalFacility", p.getWithdrawalFacility());
|
||||
putIfPresent(fc, "depositFacilityKyc", p.getDepositFacilityKyc());
|
||||
putIfPresent(fc, "virtualDebitCard", p.getVirtualDebitCard());
|
||||
return fc;
|
||||
}
|
||||
|
||||
private void putIfPresent(final JsonObject json, final String field, final Boolean value) {
|
||||
if (value != null) {
|
||||
json.addProperty(field, value);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> computeNewlyEnabled(final SavingsAccountPendingEdit p, final SavingsAccountFacilityConfig current) {
|
||||
final Set<String> result = new HashSet<>();
|
||||
addIfNewlyEnabled(result, "atmFacility", p.getAtmFacility(), current == null ? null : current.getAtmFacility());
|
||||
addIfNewlyEnabled(result, "atmWithdrawalFacility", p.getAtmWithdrawalFacility(), current == null ? null : current.getAtmWithdrawalFacility());
|
||||
addIfNewlyEnabled(result, "upiFacility", p.getUpiFacility(), current == null ? null : current.getUpiFacility());
|
||||
addIfNewlyEnabled(result, "posTransactionsFacility", p.getPosTransactionsFacility(), current == null ? null : current.getPosTransactionsFacility());
|
||||
addIfNewlyEnabled(result, "chequeBook", p.getChequeBook(), current == null ? null : current.getChequeBook());
|
||||
addIfNewlyEnabled(result, "impsFacility", p.getImpsFacility(), current == null ? null : current.getImpsFacility());
|
||||
addIfNewlyEnabled(result, "smsAlerts", p.getSmsAlerts(), current == null ? null : current.getSmsAlerts());
|
||||
addIfNewlyEnabled(result, "bbpsFacility", p.getBbpsFacility(), current == null ? null : current.getBbpsFacility());
|
||||
addIfNewlyEnabled(result, "insuranceCover", p.getInsuranceCover(), current == null ? null : current.getInsuranceCover());
|
||||
addIfNewlyEnabled(result, "bepgFacility", p.getBepgFacility(), current == null ? null : current.getBepgFacility());
|
||||
addIfNewlyEnabled(result, "neftFacility", p.getNeftFacility(), current == null ? null : current.getNeftFacility());
|
||||
addIfNewlyEnabled(result, "rtgsFacility", p.getRtgsFacility(), current == null ? null : current.getRtgsFacility());
|
||||
addIfNewlyEnabled(result, "interBankTotalCapFacility", p.getInterBankTotalCapFacility(), current == null ? null : current.getInterBankTotalCapFacility());
|
||||
addIfNewlyEnabled(result, "withdrawalFacility", p.getWithdrawalFacility(), current == null ? null : current.getWithdrawalFacility());
|
||||
addIfNewlyEnabled(result, "depositFacilityKyc", p.getDepositFacilityKyc(), current == null ? null : current.getDepositFacilityKyc());
|
||||
addIfNewlyEnabled(result, "virtualDebitCard", p.getVirtualDebitCard(), current == null ? null : current.getVirtualDebitCard());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addIfNewlyEnabled(final Set<String> result, final String field, final Boolean proposed, final Boolean current) {
|
||||
if (Boolean.TRUE.equals(proposed) && !Boolean.TRUE.equals(current)) {
|
||||
result.add(FACILITY_TYPES.get(field));
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a single submitted field out of the incoming JSON, tracks whether it enables a previously-off facility. */
|
||||
private void populateFacilityField(final JsonObject fc, final String field, final Boolean currentValue,
|
||||
final java.util.function.Consumer<Boolean> setter, final Set<String> newlyEnabledCollector) {
|
||||
if (!fc.has(field) || fc.get(field).isJsonNull()) {
|
||||
return;
|
||||
}
|
||||
final boolean proposed = fc.get(field).getAsBoolean();
|
||||
setter.accept(proposed);
|
||||
if (proposed && !Boolean.TRUE.equals(currentValue)) {
|
||||
newlyEnabledCollector.add(FACILITY_TYPES.get(field));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAuditEntry(final SavingsAccountPendingEdit 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("SAVINGSACCOUNT") //
|
||||
.resourceGetUrl("/savingsaccounts/" + pendingEdit.getSavingsAccount().getId()) //
|
||||
.resourceId(pendingEdit.getSavingsAccount().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 SavingsAccountPendingEditData toData(final SavingsAccountPendingEdit p) {
|
||||
final SavingsAccountPendingEditData data = new SavingsAccountPendingEditData();
|
||||
data.setId(p.getId());
|
||||
data.setSavingsAccountId(p.getSavingsAccount().getId());
|
||||
data.setAccountNo(p.getSavingsAccount().getAccountNumber());
|
||||
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());
|
||||
data.setHoldAmount(p.getHoldAmount());
|
||||
data.setFacilityConfigSubmitted(p.isFacilityConfigSubmitted());
|
||||
data.setNomineeSubmitted(p.isNomineeSubmitted());
|
||||
if (p.getSavingsAccount() != null) {
|
||||
data.setDepositAccountType(p.getSavingsAccount().depositAccountType().name());
|
||||
}
|
||||
|
||||
if (p.isFacilityConfigSubmitted()) {
|
||||
final Map<String, Object> fc = new LinkedHashMap<>();
|
||||
putIfNotNull(fc, "atmFacility", p.getAtmFacility());
|
||||
putIfNotNull(fc, "atmWithdrawalFacility", p.getAtmWithdrawalFacility());
|
||||
putIfNotNull(fc, "upiFacility", p.getUpiFacility());
|
||||
putIfNotNull(fc, "posTransactionsFacility", p.getPosTransactionsFacility());
|
||||
putIfNotNull(fc, "chequeBook", p.getChequeBook());
|
||||
putIfNotNull(fc, "impsFacility", p.getImpsFacility());
|
||||
putIfNotNull(fc, "smsAlerts", p.getSmsAlerts());
|
||||
putIfNotNull(fc, "bbpsFacility", p.getBbpsFacility());
|
||||
putIfNotNull(fc, "insuranceCover", p.getInsuranceCover());
|
||||
putIfNotNull(fc, "bepgFacility", p.getBepgFacility());
|
||||
putIfNotNull(fc, "neftFacility", p.getNeftFacility());
|
||||
putIfNotNull(fc, "rtgsFacility", p.getRtgsFacility());
|
||||
putIfNotNull(fc, "interBankTotalCapFacility", p.getInterBankTotalCapFacility());
|
||||
putIfNotNull(fc, "withdrawalFacility", p.getWithdrawalFacility());
|
||||
putIfNotNull(fc, "depositFacilityKyc", p.getDepositFacilityKyc());
|
||||
putIfNotNull(fc, "virtualDebitCard", p.getVirtualDebitCard());
|
||||
data.setProposedFacilityConfig(fc);
|
||||
}
|
||||
|
||||
if (p.isNomineeSubmitted()) {
|
||||
final Map<String, Object> nom = new LinkedHashMap<>();
|
||||
putIfNotNull(nom, "operation", p.getNomineeOperation());
|
||||
putIfNotNull(nom, "nomineeId", p.getNomineeId());
|
||||
putIfNotNull(nom, "nomineeClientId", p.getNomineeClientId());
|
||||
putIfNotNull(nom, "relationshipId", p.getRelationshipCvId());
|
||||
putIfNotNull(nom, "percentageShare", p.getPercentageShare());
|
||||
putIfNotNull(nom, "isActive", p.getNomineeIsActive());
|
||||
putIfNotNull(nom, "nomineeIsMinor", p.getNomineeIsMinor());
|
||||
putIfNotNull(nom, "guardianClientId", p.getGuardianClientId());
|
||||
putIfNotNull(nom, "guardianRelationshipId", p.getGuardianRelationshipCvId());
|
||||
putIfNotNull(nom, "effectiveDate", p.getEffectiveDate());
|
||||
putIfNotNull(nom, "remarks", p.getNomineeRemarks());
|
||||
data.setProposedNominee(nom);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void putIfNotNull(final Map<String, Object> map, final String key, final Object value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -152,6 +152,16 @@ import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatform
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountEditWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.CurrentAccountEditWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
|
||||
@@ -468,6 +478,35 @@ public class SavingsConfiguration {
|
||||
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SavingsAccountEditWritePlatformService.class)
|
||||
public SavingsAccountEditWritePlatformService savingsAccountEditWritePlatformService(PlatformSecurityContext context,
|
||||
SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper,
|
||||
org.apache.fineract.portfolio.savings.domain.SavingsAccountPendingEditRepository pendingEditRepository,
|
||||
SavingsAccountFacilityConfigRepository facilityConfigRepository, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
||||
SavingsAccountWritePlatformService savingsAccountWritePlatformService,
|
||||
org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService clientNomineeWritePlatformService,
|
||||
org.apache.fineract.commands.domain.CommandSourceRepository commandSourceRepository, FromJsonHelper fromApiJsonHelper) {
|
||||
return new SavingsAccountEditWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
||||
pendingEditRepository, facilityConfigRepository, facilityChargeAutoApplyService, savingsAccountWritePlatformService,
|
||||
clientNomineeWritePlatformService, commandSourceRepository, fromApiJsonHelper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CurrentAccountEditWritePlatformService.class)
|
||||
public CurrentAccountEditWritePlatformService currentAccountEditWritePlatformService(PlatformSecurityContext context,
|
||||
CurrentAccountRepository currentAccountRepository, CurrentAccountPendingEditRepository pendingEditRepository,
|
||||
CurrentAccountFacilityConfigRepository facilityConfigRepository,
|
||||
org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository facilityChargeMappingRepository,
|
||||
CurrentAccountTransactionWritePlatformService currentAccountTransactionWritePlatformService,
|
||||
CurrentAccountWritePlatformService currentAccountWritePlatformService,
|
||||
ClientNomineeWritePlatformService clientNomineeWritePlatformService, CommandSourceRepository commandSourceRepository,
|
||||
FromJsonHelper fromApiJsonHelper) {
|
||||
return new CurrentAccountEditWritePlatformServiceJpaRepositoryImpl(context, currentAccountRepository, pendingEditRepository,
|
||||
facilityConfigRepository, facilityChargeMappingRepository, currentAccountTransactionWritePlatformService,
|
||||
currentAccountWritePlatformService, clientNomineeWritePlatformService, commandSourceRepository, fromApiJsonHelper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SavingsApplicationProcessWritePlatformService.class)
|
||||
public SavingsApplicationProcessWritePlatformService savingsApplicationProcessWritePlatformService(PlatformSecurityContext context,
|
||||
|
||||
@@ -368,4 +368,6 @@
|
||||
<include file="parts/0338_add_soft_delete_to_tellers.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0339_add_share_linking_enabled_to_current_account.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0340_add_terminate_true_to_savings_and_current_account.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0341_add_current_account_pending_edit_table.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0342_add_current_account_edit_approval_permissions.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
<?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 an ACTIVE current account's Nominee + Digital & Account
|
||||
Features, mirroring m_savings_account_pending_edit's maker-checker pattern: presence of a row
|
||||
here with status=PENDING (enforced via the unique pending_lock_key) is itself the lock, and
|
||||
nothing is applied to the real m_current_account_facility_config / m_client_nominee rows until
|
||||
a checker approves (see CurrentAccountEditWritePlatformServiceJpaRepositoryImpl). One row per
|
||||
request; only one nominee change (add/update/delete) can be proposed per request, so its fields
|
||||
are kept as flat typed columns on this same table rather than a satellite table. Also carries a
|
||||
full point-in-time "wide snapshot" of every business column on m_current_account at submission
|
||||
time, for audit/UI display purposes only.
|
||||
-->
|
||||
<changeSet author="fineract" id="0341-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_current_account_pending_edit" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_current_account_pending_edit">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="current_account_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_caoe_current_account_id" references="m_current_account(id)" />
|
||||
</column>
|
||||
<column name="createdby_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_caoe_maker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="lastmodifiedby_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_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(40)">
|
||||
<constraints nullable="true" unique="true" uniqueConstraintName="caoe_unq_pending_lock_key" />
|
||||
</column>
|
||||
<column name="command_source_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_command_source_id" references="m_portfolio_command_source(id)" />
|
||||
</column>
|
||||
|
||||
<!-- Hold placed at submission time for the total charge of newly-enabled facilities; released
|
||||
(no charge) on reject, released-then-converted-to-a-posted-charge on approve. -->
|
||||
<column name="hold_transaction_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_hold_txn_id" references="m_current_account_transaction(id)" />
|
||||
</column>
|
||||
<column name="hold_amount" type="DECIMAL(19,6)" />
|
||||
|
||||
<!-- Digital & Account Features - same 18 columns/names as m_current_account_facility_config.
|
||||
NULL = maker did not touch this toggle; facility_config_submitted tells approve()/UI
|
||||
whether this block was submitted at all. -->
|
||||
<column name="facility_config_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="atm_facility" type="BOOLEAN" />
|
||||
<column name="atm_withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="upi_facility" type="BOOLEAN" />
|
||||
<column name="pos_transactions_facility" type="BOOLEAN" />
|
||||
<column name="cheque_book" type="BOOLEAN" />
|
||||
<column name="imps_facility" type="BOOLEAN" />
|
||||
<column name="sms_alerts" type="BOOLEAN" />
|
||||
<column name="bbps_facility" type="BOOLEAN" />
|
||||
<column name="insurance_cover" type="BOOLEAN" />
|
||||
<column name="bepg_facility" type="BOOLEAN" />
|
||||
<column name="neft_facility" type="BOOLEAN" />
|
||||
<column name="rtgs_facility" type="BOOLEAN" />
|
||||
<column name="inter_bank_total_cap_facility" type="BOOLEAN" />
|
||||
<column name="withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="deposit_facility_kyc" type="BOOLEAN" />
|
||||
<column name="virtual_debit_card" type="BOOLEAN" />
|
||||
<column name="internet_banking" type="BOOLEAN" />
|
||||
<column name="mobile_banking" type="BOOLEAN" />
|
||||
|
||||
<!-- Nominee change - one per request (add / update / delete), same fields as m_client_nominee. -->
|
||||
<column name="nominee_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="nominee_operation" type="VARCHAR(10)" />
|
||||
<column name="nominee_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_nominee_id" references="m_client_nominee(id)" />
|
||||
</column>
|
||||
<column name="nominee_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_nominee_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="relationship_cv_id" type="BIGINT" />
|
||||
<column name="percentage_share" type="DECIMAL(5,2)" />
|
||||
<column name="nominee_is_active" type="BOOLEAN" />
|
||||
<column name="nominee_is_minor" type="BOOLEAN" />
|
||||
<column name="guardian_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_caoe_guardian_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="guardian_relationship_cv_id" type="BIGINT" />
|
||||
<column name="effective_date" type="DATE" />
|
||||
<column name="nominee_remarks" type="VARCHAR(500)" />
|
||||
|
||||
<!-- Wide point-in-time snapshot of every business column on m_current_account at submission
|
||||
time (audit/UI display only - approve()/reject() logic reads the live account, not this).
|
||||
All nullable, no unique constraints, no defaults - this is a frozen copy. -->
|
||||
<column name="account_no" type="VARCHAR(20)" />
|
||||
<column name="external_id" type="VARCHAR(100)" />
|
||||
<column name="client_id" type="BIGINT" />
|
||||
<column name="product_id" type="BIGINT" />
|
||||
<column name="office_id" type="BIGINT" />
|
||||
<column name="staff_id" type="BIGINT" />
|
||||
<column name="status_enum" type="INTEGER" />
|
||||
<column name="sub_status_enum" type="INTEGER" />
|
||||
<column name="account_type_enum" type="INTEGER" />
|
||||
<column name="submittedon_date" type="DATE" />
|
||||
<column name="approvedon_date" type="DATE" />
|
||||
<column name="activatedon_date" type="DATE" />
|
||||
<column name="rejectedon_date" type="DATE" />
|
||||
<column name="withdrawnon_date" type="DATE" />
|
||||
<column name="closedon_date" type="DATE" />
|
||||
<column name="submittedon_userid" type="BIGINT" />
|
||||
<column name="approvedon_userid" type="BIGINT" />
|
||||
<column name="activatedon_userid" type="BIGINT" />
|
||||
<column name="rejectedon_userid" type="BIGINT" />
|
||||
<column name="withdrawnon_userid" type="BIGINT" />
|
||||
<column name="closedon_userid" type="BIGINT" />
|
||||
<column name="currency_code" type="VARCHAR(3)" />
|
||||
<column name="currency_digits" type="INTEGER" />
|
||||
<column name="currency_multiplesof" type="INTEGER" />
|
||||
<column name="nominal_annual_interest_rate" type="DECIMAL(19,6)" />
|
||||
<column name="interest_compounding_period_enum" type="INTEGER" />
|
||||
<column name="interest_posting_period_enum" type="INTEGER" />
|
||||
<column name="interest_calculation_type_enum" type="INTEGER" />
|
||||
<column name="interest_calculation_days_in_year_type_enum" type="INTEGER" />
|
||||
<column name="min_balance_for_interest_calculation" type="DECIMAL(19,6)" />
|
||||
<column name="start_interest_calculation_date" type="DATE" />
|
||||
<column name="accrued_till_date" type="DATE" />
|
||||
<column name="last_interest_calculation_date" type="DATE" />
|
||||
<column name="last_closed_business_date" type="DATE" />
|
||||
<column name="withhold_tax" type="BOOLEAN" />
|
||||
<column name="min_required_opening_balance" type="DECIMAL(19,6)" />
|
||||
<column name="lockin_period_frequency" type="INTEGER" />
|
||||
<column name="lockin_period_frequency_enum" type="INTEGER" />
|
||||
<column name="lockedin_until_date_derived" type="DATE" />
|
||||
<column name="enforce_min_required_balance" type="BOOLEAN" />
|
||||
<column name="min_required_balance" type="DECIMAL(19,6)" />
|
||||
<column name="allow_overdraft" type="BOOLEAN" />
|
||||
<column name="overdraft_limit" type="DECIMAL(19,6)" />
|
||||
<column name="nominal_annual_interest_rate_overdraft" type="DECIMAL(19,6)" />
|
||||
<column name="min_overdraft_for_interest_calculation" type="DECIMAL(19,6)" />
|
||||
<column name="is_lien_allowed" type="BOOLEAN" />
|
||||
<column name="max_allowed_lien_limit" type="DECIMAL(19,6)" />
|
||||
<column name="on_hold_funds_derived" type="DECIMAL(19,6)" />
|
||||
<column name="account_balance_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_deposits_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_withdrawals_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_interest_earned_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_interest_posted_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_overdraft_interest_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_withhold_tax_derived" type="DECIMAL(19,6)" />
|
||||
<column name="total_savings_amount_on_hold" type="DECIMAL(19,6)" />
|
||||
<column name="withdrawal_fee_for_transfer" type="BOOLEAN" />
|
||||
<column name="reason_for_block" type="VARCHAR(500)" />
|
||||
<column name="msme" type="BOOLEAN" />
|
||||
<column name="auto_debit_facility" type="BOOLEAN" />
|
||||
<column name="balance_swap_option" type="BOOLEAN" />
|
||||
<column name="establishment_date" type="DATE" />
|
||||
<column name="start_operation_date" type="DATE" />
|
||||
<column name="is_non_individual_client" type="BOOLEAN" />
|
||||
<column name="signatory_rights" type="VARCHAR(100)" />
|
||||
<column name="cbs_account_type" type="VARCHAR(100)" />
|
||||
<column name="customer_segment" type="VARCHAR(100)" />
|
||||
<column name="account_status" type="VARCHAR(50)" />
|
||||
<column name="mode_of_operation" type="VARCHAR(50)" />
|
||||
<column name="special_category" type="VARCHAR(100)" />
|
||||
<column name="introducer_client_id" type="BIGINT" />
|
||||
<column name="introducer_name" type="VARCHAR(200)" />
|
||||
<column name="introducer_cif_number" type="VARCHAR(50)" />
|
||||
<column name="nominee_data" type="TEXT" />
|
||||
<column name="interest_calculate_selector" type="INTEGER" />
|
||||
<column name="average_period" type="VARCHAR(20)" />
|
||||
<column name="auto_post" type="BOOLEAN" />
|
||||
<column name="frequency_fix_day" type="INTEGER" />
|
||||
<column name="enable_lockin_period" type="BOOLEAN" />
|
||||
<column name="balance_frequency" type="VARCHAR(50)" />
|
||||
<column name="avg_balance_calc_period" type="VARCHAR(10)" />
|
||||
<column name="account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
|
||||
<column name="valid_for_period" type="VARCHAR(10)" />
|
||||
<column name="od_account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
|
||||
<column name="is_linking_enable" type="BOOLEAN" />
|
||||
<column name="terminate_true" type="BOOLEAN" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?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 Nominee + Digital & Account Features maker-checker edit flow for
|
||||
current accounts. SUBMITEDIT lets a maker propose the edit (this is the "Edit" action shown
|
||||
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. Grouping matches the rest of CURRENTACCOUNT's
|
||||
permissions in 0225_add_current_account_support.xml ("portfolio").
|
||||
-->
|
||||
<changeSet author="fineract" id="0342-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'SUBMITEDIT_CURRENTACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio" />
|
||||
<column name="code" value="SUBMITEDIT_CURRENTACCOUNT" />
|
||||
<column name="entity_name" value="CURRENTACCOUNT" />
|
||||
<column name="action_name" value="SUBMITEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0342-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_CURRENTACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio" />
|
||||
<column name="code" value="APPROVEEDIT_CURRENTACCOUNT" />
|
||||
<column name="entity_name" value="CURRENTACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0342-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECTEDIT_CURRENTACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio" />
|
||||
<column name="code" value="REJECTEDIT_CURRENTACCOUNT" />
|
||||
<column name="entity_name" value="CURRENTACCOUNT" />
|
||||
<column name="action_name" value="REJECTEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0342-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_CURRENTACCOUNT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio" />
|
||||
<column name="code" value="APPROVEEDIT_CURRENTACCOUNT_CHECKER" />
|
||||
<column name="entity_name" value="CURRENTACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+3
@@ -932,6 +932,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
}
|
||||
|
||||
private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) {
|
||||
if (this.nominalAnnualInterestRateOverdraft == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc);
|
||||
}
|
||||
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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.savings.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Holds a pending edit request for an ACTIVE savings account's Nominee + Digital & Account Features.
|
||||
* 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_savings_account_facility_config} / {@code m_client_nominee} rows until a checker approves
|
||||
* (see SavingsAccountEditWritePlatformServiceImpl). Only one nominee change (add/update/delete) can be
|
||||
* proposed per request, so its fields are kept as flat typed columns on this same table.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_savings_account_pending_edit")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SavingsAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_APPROVED = "APPROVED";
|
||||
public static final String STATUS_REJECTED = "REJECTED";
|
||||
|
||||
public static final String NOMINEE_OP_ADD = "ADD";
|
||||
public static final String NOMINEE_OP_UPDATE = "UPDATE";
|
||||
public static final String NOMINEE_OP_DELETE = "DELETE";
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "savings_account_id", nullable = false)
|
||||
private SavingsAccount savingsAccount;
|
||||
|
||||
@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;
|
||||
|
||||
/** "SAVINGS_ACCOUNT_EDIT:<savingsAccountId>" while PENDING, NULL otherwise - the real DB-level lock. */
|
||||
@Column(name = "pending_lock_key", length = 40)
|
||||
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;
|
||||
|
||||
@Column(name = "hold_transaction_id")
|
||||
private Long holdTransactionId;
|
||||
|
||||
@Column(name = "hold_amount", precision = 19, scale = 6)
|
||||
private BigDecimal holdAmount;
|
||||
|
||||
@Column(name = "facility_config_submitted", nullable = false)
|
||||
private boolean facilityConfigSubmitted = false;
|
||||
|
||||
@Column(name = "atm_facility")
|
||||
private Boolean atmFacility;
|
||||
|
||||
@Column(name = "atm_withdrawal_facility")
|
||||
private Boolean atmWithdrawalFacility;
|
||||
|
||||
@Column(name = "upi_facility")
|
||||
private Boolean upiFacility;
|
||||
|
||||
@Column(name = "pos_transactions_facility")
|
||||
private Boolean posTransactionsFacility;
|
||||
|
||||
@Column(name = "cheque_book")
|
||||
private Boolean chequeBook;
|
||||
|
||||
@Column(name = "imps_facility")
|
||||
private Boolean impsFacility;
|
||||
|
||||
@Column(name = "sms_alerts")
|
||||
private Boolean smsAlerts;
|
||||
|
||||
@Column(name = "bbps_facility")
|
||||
private Boolean bbpsFacility;
|
||||
|
||||
@Column(name = "insurance_cover")
|
||||
private Boolean insuranceCover;
|
||||
|
||||
@Column(name = "bepg_facility")
|
||||
private Boolean bepgFacility;
|
||||
|
||||
@Column(name = "neft_facility")
|
||||
private Boolean neftFacility;
|
||||
|
||||
@Column(name = "rtgs_facility")
|
||||
private Boolean rtgsFacility;
|
||||
|
||||
@Column(name = "inter_bank_total_cap_facility")
|
||||
private Boolean interBankTotalCapFacility;
|
||||
|
||||
@Column(name = "withdrawal_facility")
|
||||
private Boolean withdrawalFacility;
|
||||
|
||||
@Column(name = "deposit_facility_kyc")
|
||||
private Boolean depositFacilityKyc;
|
||||
|
||||
@Column(name = "virtual_debit_card")
|
||||
private Boolean virtualDebitCard;
|
||||
|
||||
@Column(name = "nominee_submitted", nullable = false)
|
||||
private boolean nomineeSubmitted = false;
|
||||
|
||||
@Column(name = "nominee_operation", length = 10)
|
||||
private String nomineeOperation;
|
||||
|
||||
@Column(name = "nominee_id")
|
||||
private Long nomineeId;
|
||||
|
||||
@Column(name = "nominee_client_id")
|
||||
private Long nomineeClientId;
|
||||
|
||||
@Column(name = "relationship_cv_id")
|
||||
private Long relationshipCvId;
|
||||
|
||||
@Column(name = "percentage_share", precision = 5, scale = 2)
|
||||
private BigDecimal percentageShare;
|
||||
|
||||
@Column(name = "nominee_is_active")
|
||||
private Boolean nomineeIsActive;
|
||||
|
||||
@Column(name = "nominee_is_minor")
|
||||
private Boolean nomineeIsMinor;
|
||||
|
||||
@Column(name = "guardian_client_id")
|
||||
private Long guardianClientId;
|
||||
|
||||
@Column(name = "guardian_relationship_cv_id")
|
||||
private Long guardianRelationshipCvId;
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
@Column(name = "nominee_remarks", length = 500)
|
||||
private String nomineeRemarks;
|
||||
|
||||
protected SavingsAccountPendingEdit() {}
|
||||
|
||||
public SavingsAccountPendingEdit(final SavingsAccount savingsAccount, final AppUser maker) {
|
||||
this.savingsAccount = savingsAccount;
|
||||
this.maker = maker;
|
||||
this.createdOn = LocalDateTime.now();
|
||||
this.status = STATUS_PENDING;
|
||||
this.pendingLockKey = lockKeyFor(savingsAccount.getId());
|
||||
}
|
||||
|
||||
public static String lockKeyFor(final Long savingsAccountId) {
|
||||
return "SAVINGS_ACCOUNT_EDIT:" + savingsAccountId;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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.savings.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SavingsAccountPendingEditRepository extends JpaRepository<SavingsAccountPendingEdit, Long> {
|
||||
|
||||
Optional<SavingsAccountPendingEdit> findBySavingsAccountIdAndStatus(Long savingsAccountId, String status);
|
||||
|
||||
List<SavingsAccountPendingEdit> findByStatus(String status);
|
||||
}
|
||||
+37
@@ -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.savings.exception;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown at edit-submission time when the account's available balance cannot cover the charge for
|
||||
* the digital/account feature(s) the maker is requesting to enable.
|
||||
*/
|
||||
public class SavingsAccountEditInsufficientBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditInsufficientBalanceException(final Long savingsId, final BigDecimal availableBalance,
|
||||
final BigDecimal requiredAmount) {
|
||||
super("error.msg.savingsaccount.edit.insufficient.balance",
|
||||
"Account balance is insufficient for the requested feature(s). Available balance: " + availableBalance
|
||||
+ ", required: " + requiredAmount,
|
||||
savingsId, availableBalance, requiredAmount);
|
||||
}
|
||||
}
|
||||
+30
@@ -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.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditPendingApprovalException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.edit.pending.approval",
|
||||
"Savings account with identifier " + savingsId + " already has a Nominee/Digital-Feature edit pending approval",
|
||||
savingsId);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditSelfApprovalNotAllowedException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.edit.self.approval.not.allowed",
|
||||
"The maker of the pending Nominee/Digital-Feature edit for savings account " + savingsId
|
||||
+ " cannot also approve or reject it",
|
||||
savingsId);
|
||||
}
|
||||
}
|
||||
+29
@@ -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.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountNoPendingEditException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.no.pending.edit",
|
||||
"Savings account with identifier " + savingsId + " has no Nominee/Digital-Feature edit awaiting approval", savingsId);
|
||||
}
|
||||
}
|
||||
+75
@@ -88,6 +88,81 @@ public class FacilityChargeAutoApplyService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of the mapped charge amounts for the given facility types - used to size the hold placed at
|
||||
* edit-submission time (see SavingsAccountEditWritePlatformServiceImpl) before any charge is actually applied.
|
||||
*/
|
||||
public BigDecimal totalChargeAmountFor(final java.util.Set<String> facilityTypes) {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (final String facilityType : facilityTypes) {
|
||||
final java.util.Optional<FacilityChargeMapping> mapping = mappingRepository.findByFacilityType(facilityType);
|
||||
if (mapping.isPresent() && mapping.get().getCharge() != null) {
|
||||
total = total.add(mapping.get().getCharge().getAmount());
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fresh {@link SavingsAccountCharge} for each of the given (newly re-enabled) facility types, so approving
|
||||
* a Nominee/Digital-Feature edit always charges again for the transition - unlike
|
||||
* {@link #applyFacilityChargesOnActivation(SavingsAccount)}'s {@code applyIfEnabled}, this deliberately has NO
|
||||
* "already applied" dedup-by-name guard: a facility that was enabled at activation, disabled, then re-enabled via
|
||||
* an approved edit must be charged again, even though a (now-paid, now-irrelevant) charge object with the same
|
||||
* name already sits in {@code account.charges()} from the earlier cycle.
|
||||
*/
|
||||
public List<SavingsAccountCharge> applyChargesForFacilities(final SavingsAccount account, final java.util.Set<String> facilityTypes) {
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
for (final String facilityType : facilityTypes) {
|
||||
mappingRepository.findByFacilityType(facilityType).ifPresent(mapping -> {
|
||||
final Charge charge = mapping.getCharge();
|
||||
if (charge == null) {
|
||||
return;
|
||||
}
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(charge, charge.getAmount(),
|
||||
null, null, resolveDueDate(charge), true, feeOnMonthDay, charge.getFeeInterval());
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
chargesToApply.add(accountCharge);
|
||||
});
|
||||
}
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
}
|
||||
return chargesToApply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pays exactly the given charges (typically the just-added result of {@link #applyChargesForFacilities}) as of
|
||||
* {@code transactionDate}. Unlike {@link #collectActivationCharges(SavingsAccount)} - which posts at the
|
||||
* account's activation date and sweeps every unpaid activation-type charge - this is scoped to a specific set of
|
||||
* charges and a caller-supplied date, for posting a Nominee/Digital-Feature edit's charges on the day it's
|
||||
* approved rather than backdating them to activation.
|
||||
*/
|
||||
public List<SavingsAccountTransaction> collectCharges(final SavingsAccount account, final List<SavingsAccountCharge> charges,
|
||||
final LocalDate transactionDate) {
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
for (final SavingsAccountCharge charge : charges) {
|
||||
if (charge.isPaid() || charge.isWaived()) continue;
|
||||
final BigDecimal amount = charge.getAmountOutstanding(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
final Money amountMoney = Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, transactionDate, false, null);
|
||||
if (txn != null) {
|
||||
transactions.add(txn);
|
||||
log.info("Collected facility charge '{}' ₹{} from savings account id={}", charge.getCharge().getName(), amount,
|
||||
account.getId());
|
||||
}
|
||||
}
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(transactionDate);
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all unpaid SavingsActivation charges on the account.
|
||||
* Called after facility charges are saved and flushed, so amounts are properly initialized.
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.savings.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountPendingEditData;
|
||||
|
||||
public interface SavingsAccountEditWritePlatformService {
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long savingsId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long savingsId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long savingsId, String rejectReason);
|
||||
|
||||
SavingsAccountPendingEditData retrievePendingEdit(Long savingsId);
|
||||
|
||||
java.util.Collection<SavingsAccountPendingEditData> retrieveAllPendingEdits();
|
||||
}
|
||||
+2
@@ -51,4 +51,6 @@
|
||||
<include file="parts/2027_add_pending_edit_satellite_tables.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2028_update_saving_product_temp_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2029_remove_proposed_payload_json.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2030_add_savings_account_pending_edit_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2031_add_savings_account_edit_approval_permissions.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?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 an ACTIVE savings account's Nominee + Digital & Account
|
||||
Features, mirroring the m_savings_product_temp maker-checker pattern: presence of a row here
|
||||
with status=PENDING (enforced via the unique pending_lock_key) is itself the lock, and nothing
|
||||
is applied to the real m_savings_account_facility_config / m_client_nominee rows until a
|
||||
checker approves (see SavingsAccountEditWritePlatformServiceImpl). One row per request; only
|
||||
one nominee change (add/update/delete) can be proposed per request, so its fields are kept as
|
||||
flat typed columns on this same table rather than a satellite table.
|
||||
-->
|
||||
<changeSet author="fineract" id="2030-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_savings_account_pending_edit" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_savings_account_pending_edit">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="savings_account_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_saoe_savings_account_id" references="m_savings_account(id)" />
|
||||
</column>
|
||||
<column name="createdby_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_saoe_maker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="lastmodifiedby_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_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(40)">
|
||||
<constraints nullable="true" unique="true" uniqueConstraintName="saoe_unq_pending_lock_key" />
|
||||
</column>
|
||||
<column name="command_source_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_command_source_id" references="m_portfolio_command_source(id)" />
|
||||
</column>
|
||||
|
||||
<!-- Hold placed at submission time for the total charge of newly-enabled facilities; released
|
||||
(no charge) on reject, released-then-converted-to-a-posted-charge on approve. -->
|
||||
<column name="hold_transaction_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_hold_txn_id" references="m_savings_account_transaction(id)" />
|
||||
</column>
|
||||
<column name="hold_amount" type="DECIMAL(19,6)" />
|
||||
|
||||
<!-- Digital & Account Features - same 16 columns/names as m_savings_account_facility_config.
|
||||
NULL = maker did not touch this toggle; facility_config_submitted tells approve()/UI
|
||||
whether this block was submitted at all. -->
|
||||
<column name="facility_config_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="atm_facility" type="BOOLEAN" />
|
||||
<column name="atm_withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="upi_facility" type="BOOLEAN" />
|
||||
<column name="pos_transactions_facility" type="BOOLEAN" />
|
||||
<column name="cheque_book" type="BOOLEAN" />
|
||||
<column name="imps_facility" type="BOOLEAN" />
|
||||
<column name="sms_alerts" type="BOOLEAN" />
|
||||
<column name="bbps_facility" type="BOOLEAN" />
|
||||
<column name="insurance_cover" type="BOOLEAN" />
|
||||
<column name="bepg_facility" type="BOOLEAN" />
|
||||
<column name="neft_facility" type="BOOLEAN" />
|
||||
<column name="rtgs_facility" type="BOOLEAN" />
|
||||
<column name="inter_bank_total_cap_facility" type="BOOLEAN" />
|
||||
<column name="withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="deposit_facility_kyc" type="BOOLEAN" />
|
||||
<column name="virtual_debit_card" type="BOOLEAN" />
|
||||
|
||||
<!-- Nominee change - one per request (add / update / delete), same fields as m_client_nominee. -->
|
||||
<column name="nominee_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="nominee_operation" type="VARCHAR(10)" />
|
||||
<column name="nominee_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_nominee_id" references="m_client_nominee(id)" />
|
||||
</column>
|
||||
<column name="nominee_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_nominee_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="relationship_cv_id" type="BIGINT" />
|
||||
<column name="percentage_share" type="DECIMAL(5,2)" />
|
||||
<column name="nominee_is_active" type="BOOLEAN" />
|
||||
<column name="nominee_is_minor" type="BOOLEAN" />
|
||||
<column name="guardian_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_guardian_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="guardian_relationship_cv_id" type="BIGINT" />
|
||||
<column name="effective_date" type="DATE" />
|
||||
<column name="nominee_remarks" type="VARCHAR(500)" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?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 Nominee + Digital & Account Features maker-checker edit flow for
|
||||
savings accounts. SUBMITEDIT lets a maker propose the edit (this is the "Edit" action shown
|
||||
on the account view); APPROVEEDIT/REJECTEDIT let a checker resolve it. The _CHECKER permission
|
||||
follows the "<ACTION>_<ENTITY>_CHECKER" convention (see 2022_add_product_checker_permissions.xml)
|
||||
so the pending request also surfaces in the generic Checker Inbox for limited checkers.
|
||||
-->
|
||||
<changeSet author="fineract" id="2031-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'SUBMITEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="SUBMITEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="SUBMITEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="APPROVEEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECTEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="REJECTEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="REJECTEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SAVINGSACCOUNT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="APPROVEEDIT_SAVINGSACCOUNT_CHECKER" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user