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;
|
cfg.currentAccount = account;
|
||||||
return cfg;
|
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;
|
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.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
@@ -42,4 +43,13 @@ public interface CurrentAccountWritePlatformService {
|
|||||||
CommandProcessingResult deleteApplication(Long accountId);
|
CommandProcessingResult deleteApplication(Long accountId);
|
||||||
|
|
||||||
CommandProcessingResult terminateAccount(Long accountId, JsonCommand command);
|
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.client.exception.ClientNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
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.SavingsAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -62,6 +64,7 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
|||||||
private final ClientNomineeRepository nomineeRepository;
|
private final ClientNomineeRepository nomineeRepository;
|
||||||
private final CodeValueRepositoryWrapper codeValueRepository;
|
private final CodeValueRepositoryWrapper codeValueRepository;
|
||||||
private final SavingsAccountRepository savingsAccountRepository;
|
private final SavingsAccountRepository savingsAccountRepository;
|
||||||
|
private final CurrentAccountRepository currentAccountRepository;
|
||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
|
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
|
||||||
|
|
||||||
@@ -299,8 +302,18 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
|
|||||||
throw new PlatformDataIntegrityException("error.msg.nominee.loan.account.not.found",
|
throw new PlatformDataIntegrityException("error.msg.nominee.loan.account.not.found",
|
||||||
"Loan account '" + accountNo + "' not found for client " + clientId + ".", "accountNo");
|
"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 {
|
} 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);
|
final SavingsAccount sa = savingsAccountRepository.findSavingsAccountByAccountNumber(accountNo);
|
||||||
if (sa == null) {
|
if (sa == null) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.nominee.savings.account.not.found",
|
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.GsonBuilder;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
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.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
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.PlatformSecurityContext;
|
||||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
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.DepositsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
|
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
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.currentaccount.service.CurrentAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -76,6 +82,9 @@ public class CurrentAccountsApiResource {
|
|||||||
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
|
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
|
||||||
private final CurrentAccountReadPlatformService currentAccountReadPlatformService;
|
private final CurrentAccountReadPlatformService currentAccountReadPlatformService;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final CurrentAccountEditWritePlatformService currentAccountEditWritePlatformService;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final DefaultToApiJsonSerializer<CurrentAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("template")
|
@Path("template")
|
||||||
@@ -229,4 +238,104 @@ public class CurrentAccountsApiResource {
|
|||||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
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.data.UploadRequest;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
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.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
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.ExternalIdFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.Page;
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
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.SavingsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||||
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountPendingEditData;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
|
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
|
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.SavingsAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountTemplateReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountTemplateReadPlatformService;
|
||||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||||
@@ -95,6 +100,9 @@ public class SavingsAccountsApiResource {
|
|||||||
private final BulkImportWorkbookService bulkImportWorkbookService;
|
private final BulkImportWorkbookService bulkImportWorkbookService;
|
||||||
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
||||||
private final SqlValidator sqlValidator;
|
private final SqlValidator sqlValidator;
|
||||||
|
private final SavingsAccountEditWritePlatformService savingsAccountEditWritePlatformService;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final DefaultToApiJsonSerializer<SavingsAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("template")
|
@Path("template")
|
||||||
@@ -413,6 +421,102 @@ public class SavingsAccountsApiResource {
|
|||||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
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
|
@DELETE
|
||||||
@Path("{accountId}")
|
@Path("{accountId}")
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@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("partners", retrievePartners(accountId));
|
||||||
response.put("charges", retrieveCharges(accountId));
|
response.put("charges", retrieveCharges(accountId));
|
||||||
response.put("transactions", retrieveTransactions(accountId));
|
response.put("transactions", retrieveTransactions(accountId));
|
||||||
|
response.put("accountFacilityConfig", retrieveFacilityConfig(accountId));
|
||||||
return response;
|
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) {
|
private List<Map<String, Object>> retrieveJointHolders(Long accountId) {
|
||||||
final String sql =
|
final String sql =
|
||||||
"SELECT jh.id, jh.client_id, jh.cif_number, jh.display_name, "
|
"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) {
|
private Map<String, Object> buildSummary(Map<String, Object> row) {
|
||||||
Map<String, Object> s = new LinkedHashMap<>();
|
Map<String, Object> s = new LinkedHashMap<>();
|
||||||
s.put("accountBalance", row.get("account_balance_derived"));
|
BigDecimal balance = toBigDecimal(row.get("account_balance_derived"));
|
||||||
s.put("availableBalance", 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("totalDeposits", row.get("total_deposits_derived"));
|
||||||
s.put("totalWithdrawals", row.get("total_withdrawals_derived"));
|
s.put("totalWithdrawals", row.get("total_withdrawals_derived"));
|
||||||
s.put("totalInterestEarned", row.get("total_interest_earned_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("totalWithdrawalFees", null);
|
||||||
s.put("totalAnnualFees", null);
|
s.put("totalAnnualFees", null);
|
||||||
s.put("lastInterestCalculationDate", row.get("last_interest_calculation_date"));
|
s.put("lastInterestCalculationDate", row.get("last_interest_calculation_date"));
|
||||||
s.put("onHoldFunds", row.get("on_hold_funds_derived"));
|
s.put("onHoldFunds", onHold);
|
||||||
return s;
|
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.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
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
|
// Auto-deduct activation charges (chargeTimeEnum=3) from opening balance
|
||||||
for (final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge : account.getCharges()) {
|
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> activationCharges =
|
||||||
if (charge.getChargeTimeEnum() != null && charge.getChargeTimeEnum() == 3
|
account.getCharges().stream()
|
||||||
&& charge.getAmount() != null
|
.filter(c -> c.getChargeTimeEnum() != null && c.getChargeTimeEnum() == 3 && !c.isPaid())
|
||||||
&& charge.getAmount().compareTo(java.math.BigDecimal.ZERO) > 0
|
.toList();
|
||||||
&& charge.getCharge() != null) {
|
currentBalance = collectChargesAndPost(account, activationCharges, currentBalance, activatedOnDate, chargeEntries);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
account.setAccountBalanceDerived(currentBalance);
|
account.setAccountBalanceDerived(currentBalance);
|
||||||
currentAccountRepository.saveAndFlush(account);
|
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.CurrentAccountTransaction txn,
|
||||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge) {}
|
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
|
// entity type for savings/current accounts in acc_gl_journal_entry
|
||||||
private static final int ENTITY_TYPE_SAVINGS = 2;
|
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.SavingsDropdownReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformServiceImpl;
|
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.SavingsProductWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformServiceJpaRepositoryImpl;
|
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformServiceJpaRepositoryImpl;
|
||||||
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
|
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
|
||||||
@@ -468,6 +478,35 @@ public class SavingsConfiguration {
|
|||||||
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
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
|
@Bean
|
||||||
@ConditionalOnMissingBean(SavingsApplicationProcessWritePlatformService.class)
|
@ConditionalOnMissingBean(SavingsApplicationProcessWritePlatformService.class)
|
||||||
public SavingsApplicationProcessWritePlatformService savingsApplicationProcessWritePlatformService(PlatformSecurityContext context,
|
public SavingsApplicationProcessWritePlatformService savingsApplicationProcessWritePlatformService(PlatformSecurityContext context,
|
||||||
|
|||||||
@@ -368,4 +368,6 @@
|
|||||||
<include file="parts/0338_add_soft_delete_to_tellers.xml" relativeToChangelogFile="true"/>
|
<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/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/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>
|
</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) {
|
private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) {
|
||||||
|
if (this.nominalAnnualInterestRateOverdraft == null) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc);
|
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.
|
* Collects all unpaid SavingsActivation charges on the account.
|
||||||
* Called after facility charges are saved and flushed, so amounts are properly initialized.
|
* 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/2027_add_pending_edit_satellite_tables.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/2028_update_saving_product_temp_table.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/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>
|
</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