Merge remote-tracking branch 'nexora/uate' into merge-uate-into-dev
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
# Conflicts: # .gitignore # fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml # fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FacilityChargeAutoApplyService.java
This commit is contained in:
+3
@@ -932,6 +932,9 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
}
|
||||
|
||||
private BigDecimal getEffectiveOverdraftInterestRateAsFraction(MathContext mc) {
|
||||
if (this.nominalAnnualInterestRateOverdraft == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return this.nominalAnnualInterestRateOverdraft.divide(BigDecimal.valueOf(100L), mc);
|
||||
}
|
||||
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Holds a pending edit request for an ACTIVE savings account's Nominee + Digital & Account Features.
|
||||
* Presence of a row here with {@code status = PENDING} (enforced via the unique
|
||||
* {@link #pendingLockKey}) is itself the lock - nothing is applied to the real
|
||||
* {@code m_savings_account_facility_config} / {@code m_client_nominee} rows until a checker approves
|
||||
* (see SavingsAccountEditWritePlatformServiceImpl). Only one nominee change (add/update/delete) can be
|
||||
* proposed per request, so its fields are kept as flat typed columns on this same table.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_savings_account_pending_edit")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SavingsAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_APPROVED = "APPROVED";
|
||||
public static final String STATUS_REJECTED = "REJECTED";
|
||||
|
||||
public static final String NOMINEE_OP_ADD = "ADD";
|
||||
public static final String NOMINEE_OP_UPDATE = "UPDATE";
|
||||
public static final String NOMINEE_OP_DELETE = "DELETE";
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "savings_account_id", nullable = false)
|
||||
private SavingsAccount savingsAccount;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "createdby_id", nullable = false)
|
||||
private AppUser maker;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "lastmodifiedby_id")
|
||||
private AppUser checker;
|
||||
|
||||
@Column(name = "created_date", nullable = false)
|
||||
private LocalDateTime createdOn;
|
||||
|
||||
@Column(name = "lastmodified_date")
|
||||
private LocalDateTime checkedOn;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@Column(name = "reject_reason", length = 500)
|
||||
private String rejectReason;
|
||||
|
||||
/** "SAVINGS_ACCOUNT_EDIT:<savingsAccountId>" while PENDING, NULL otherwise - the real DB-level lock. */
|
||||
@Column(name = "pending_lock_key", length = 40)
|
||||
private String pendingLockKey;
|
||||
|
||||
/** Links to the audit-only m_portfolio_command_source row created for this edit's submit/approve/reject. */
|
||||
@Column(name = "command_source_id")
|
||||
private Long commandSourceId;
|
||||
|
||||
@Column(name = "hold_transaction_id")
|
||||
private Long holdTransactionId;
|
||||
|
||||
@Column(name = "hold_amount", precision = 19, scale = 6)
|
||||
private BigDecimal holdAmount;
|
||||
|
||||
@Column(name = "facility_config_submitted", nullable = false)
|
||||
private boolean facilityConfigSubmitted = false;
|
||||
|
||||
@Column(name = "atm_facility")
|
||||
private Boolean atmFacility;
|
||||
|
||||
@Column(name = "atm_withdrawal_facility")
|
||||
private Boolean atmWithdrawalFacility;
|
||||
|
||||
@Column(name = "upi_facility")
|
||||
private Boolean upiFacility;
|
||||
|
||||
@Column(name = "pos_transactions_facility")
|
||||
private Boolean posTransactionsFacility;
|
||||
|
||||
@Column(name = "cheque_book")
|
||||
private Boolean chequeBook;
|
||||
|
||||
@Column(name = "imps_facility")
|
||||
private Boolean impsFacility;
|
||||
|
||||
@Column(name = "sms_alerts")
|
||||
private Boolean smsAlerts;
|
||||
|
||||
@Column(name = "bbps_facility")
|
||||
private Boolean bbpsFacility;
|
||||
|
||||
@Column(name = "insurance_cover")
|
||||
private Boolean insuranceCover;
|
||||
|
||||
@Column(name = "bepg_facility")
|
||||
private Boolean bepgFacility;
|
||||
|
||||
@Column(name = "neft_facility")
|
||||
private Boolean neftFacility;
|
||||
|
||||
@Column(name = "rtgs_facility")
|
||||
private Boolean rtgsFacility;
|
||||
|
||||
@Column(name = "inter_bank_total_cap_facility")
|
||||
private Boolean interBankTotalCapFacility;
|
||||
|
||||
@Column(name = "withdrawal_facility")
|
||||
private Boolean withdrawalFacility;
|
||||
|
||||
@Column(name = "deposit_facility_kyc")
|
||||
private Boolean depositFacilityKyc;
|
||||
|
||||
@Column(name = "virtual_debit_card")
|
||||
private Boolean virtualDebitCard;
|
||||
|
||||
@Column(name = "nominee_submitted", nullable = false)
|
||||
private boolean nomineeSubmitted = false;
|
||||
|
||||
@Column(name = "nominee_operation", length = 10)
|
||||
private String nomineeOperation;
|
||||
|
||||
@Column(name = "nominee_id")
|
||||
private Long nomineeId;
|
||||
|
||||
@Column(name = "nominee_client_id")
|
||||
private Long nomineeClientId;
|
||||
|
||||
@Column(name = "relationship_cv_id")
|
||||
private Long relationshipCvId;
|
||||
|
||||
@Column(name = "percentage_share", precision = 5, scale = 2)
|
||||
private BigDecimal percentageShare;
|
||||
|
||||
@Column(name = "nominee_is_active")
|
||||
private Boolean nomineeIsActive;
|
||||
|
||||
@Column(name = "nominee_is_minor")
|
||||
private Boolean nomineeIsMinor;
|
||||
|
||||
@Column(name = "guardian_client_id")
|
||||
private Long guardianClientId;
|
||||
|
||||
@Column(name = "guardian_relationship_cv_id")
|
||||
private Long guardianRelationshipCvId;
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
@Column(name = "nominee_remarks", length = 500)
|
||||
private String nomineeRemarks;
|
||||
|
||||
protected SavingsAccountPendingEdit() {}
|
||||
|
||||
public SavingsAccountPendingEdit(final SavingsAccount savingsAccount, final AppUser maker) {
|
||||
this.savingsAccount = savingsAccount;
|
||||
this.maker = maker;
|
||||
this.createdOn = LocalDateTime.now();
|
||||
this.status = STATUS_PENDING;
|
||||
this.pendingLockKey = lockKeyFor(savingsAccount.getId());
|
||||
}
|
||||
|
||||
public static String lockKeyFor(final Long savingsAccountId) {
|
||||
return "SAVINGS_ACCOUNT_EDIT:" + savingsAccountId;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return STATUS_PENDING.equals(this.status);
|
||||
}
|
||||
|
||||
public void markApproved(final AppUser checker) {
|
||||
this.status = STATUS_APPROVED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
public void markRejected(final AppUser checker, final String reason) {
|
||||
this.status = STATUS_REJECTED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.rejectReason = reason;
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SavingsAccountPendingEditRepository extends JpaRepository<SavingsAccountPendingEdit, Long> {
|
||||
|
||||
Optional<SavingsAccountPendingEdit> findBySavingsAccountIdAndStatus(Long savingsAccountId, String status);
|
||||
|
||||
List<SavingsAccountPendingEdit> findByStatus(String status);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown at edit-submission time when the account's available balance cannot cover the charge for
|
||||
* the digital/account feature(s) the maker is requesting to enable.
|
||||
*/
|
||||
public class SavingsAccountEditInsufficientBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditInsufficientBalanceException(final Long savingsId, final BigDecimal availableBalance,
|
||||
final BigDecimal requiredAmount) {
|
||||
super("error.msg.savingsaccount.edit.insufficient.balance",
|
||||
"Account balance is insufficient for the requested feature(s). Available balance: " + availableBalance
|
||||
+ ", required: " + requiredAmount,
|
||||
savingsId, availableBalance, requiredAmount);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditPendingApprovalException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.edit.pending.approval",
|
||||
"Savings account with identifier " + savingsId + " already has a Nominee/Digital-Feature edit pending approval",
|
||||
savingsId);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountEditSelfApprovalNotAllowedException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.edit.self.approval.not.allowed",
|
||||
"The maker of the pending Nominee/Digital-Feature edit for savings account " + savingsId
|
||||
+ " cannot also approve or reject it",
|
||||
savingsId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsAccountNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsAccountNoPendingEditException(final Long savingsId) {
|
||||
super("error.msg.savingsaccount.no.pending.edit",
|
||||
"Savings account with identifier " + savingsId + " has no Nominee/Digital-Feature edit awaiting approval", savingsId);
|
||||
}
|
||||
}
|
||||
+318
-243
@@ -1,243 +1,318 @@
|
||||
/**
|
||||
* 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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.MonthDay;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMapping;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfig;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reads account's facility config (Yes/No per facility) and auto-applies
|
||||
* the corresponding charge from m_facility_charge_mapping.
|
||||
* No manual charge selection needed — fully automatic.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FacilityChargeAutoApplyService {
|
||||
|
||||
private final FacilityChargeMappingRepository mappingRepository;
|
||||
private final SavingsAccountChargeRepository savingsAccountChargeRepository;
|
||||
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
|
||||
|
||||
public void applyFacilityChargesOnActivation(final SavingsAccount account) {
|
||||
// Use account-level facility config — this reflects what was selected at account opening time
|
||||
final SavingsAccountFacilityConfig config = account.getFacilityConfig();
|
||||
|
||||
if (config == null) {
|
||||
// Fallback: use product-level config if account config not set
|
||||
applyFromProductConfig(account);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmFacility()), "ATM_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmWithdrawalFacility()), "ATM_WITHDRAWAL", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getUpiFacility()), "UPI_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getPosTransactionsFacility()), "POS_TRANSACTIONS", chargesToApply);
|
||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
||||
// the facility enabled.
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getImpsFacility()), "IMPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getSmsAlerts()), "SMS_ALERTS", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBbpsFacility()), "BBPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInsuranceCover()), "INSURANCE_COVER", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBepgFacility()), "BEPG_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getNeftFacility()), "NEFT_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getRtgsFacility()), "RTGS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInterBankTotalCapFacility()),"INTER_BANK_TOTAL_CAP",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getWithdrawalFacility()), "WITHDRAWAL_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getDepositFacilityKyc()), "DEPOSIT_FACILITY_KYC",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getVirtualDebitCard()), "VIRTUAL_DEBIT_CARD", chargesToApply);
|
||||
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
log.info("Auto-applied {} facility charge(s) to savings account id={}",
|
||||
chargesToApply.size(), account.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all unpaid SavingsActivation charges on the account.
|
||||
* Called after facility charges are saved and flushed, so amounts are properly initialized.
|
||||
*/
|
||||
public List<SavingsAccountTransaction> collectActivationCharges(final SavingsAccount account) {
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
final LocalDate activationDate = account.getActivationDate();
|
||||
|
||||
for (final SavingsAccountCharge charge : account.charges()) {
|
||||
if (!charge.isSavingsActivation()) continue;
|
||||
if (charge.isPaid() || charge.isWaived()) continue;
|
||||
|
||||
final BigDecimal amount = charge.getAmount(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
try {
|
||||
final org.apache.fineract.organisation.monetary.domain.Money amountMoney =
|
||||
org.apache.fineract.organisation.monetary.domain.Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, activationDate, false, null);
|
||||
if (txn != null) {
|
||||
transactions.add(txn);
|
||||
log.info("Collected activation charge '{}' ₹{} from savings account id={}",
|
||||
charge.getCharge().getName(), amount, account.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not collect activation charge '{}' from savings account id={}: {}",
|
||||
charge.getCharge() != null ? charge.getCharge().getName() : "?", account.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(activationDate);
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private void autoCollectSavingsActivationCharges(final SavingsAccount account,
|
||||
final List<SavingsAccountCharge> charges) {
|
||||
final LocalDate activationDate = account.getActivationDate();
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
|
||||
for (final SavingsAccountCharge charge : charges) {
|
||||
if (!charge.isSavingsActivation()) continue;
|
||||
final BigDecimal amount = charge.getAmountOutstanding(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
try {
|
||||
final Money amountMoney = Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, activationDate, false, null);
|
||||
if (txn != null) transactions.add(txn);
|
||||
log.info("Auto-collected facility charge '{}' ₹{} from account id={}",
|
||||
charge.getCharge().getName(), amount, account.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not auto-collect facility charge '{}' from account id={}: {}",
|
||||
charge.getCharge().getName(), account.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(activationDate);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyIfEnabled(final SavingsAccount account, final boolean facilityEnabled,
|
||||
final String facilityType, final List<SavingsAccountCharge> collector) {
|
||||
|
||||
if (!facilityEnabled) return;
|
||||
|
||||
// Skip if this charge is already applied to the account
|
||||
boolean alreadyApplied = account.charges().stream()
|
||||
.anyMatch(c -> c.getCharge() != null
|
||||
&& c.getCharge().getName() != null
|
||||
&& c.getCharge().getName().equals(getChargeName(facilityType)));
|
||||
if (alreadyApplied) return;
|
||||
|
||||
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, // chargeTime derived from Charge definition
|
||||
null, // chargeCalculation derived from Charge definition
|
||||
resolveDueDate(charge),
|
||||
true,
|
||||
feeOnMonthDay,
|
||||
charge.getFeeInterval());
|
||||
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
collector.add(accountCharge);
|
||||
});
|
||||
}
|
||||
|
||||
private MonthDay resolveFeeOnMonthDay(final Charge charge) {
|
||||
if (charge.getFeeOnMonthDay() != null) return charge.getFeeOnMonthDay();
|
||||
if (charge.isMonthlyFee() || charge.isAnnualFee()) {
|
||||
// Default: 1st of the month
|
||||
return MonthDay.of(LocalDate.now().plusMonths(1).getMonth(), 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalDate resolveDueDate(final Charge charge) {
|
||||
if (charge.isMonthlyFee()) {
|
||||
return LocalDate.now().plusMonths(1).withDayOfMonth(1);
|
||||
} else if (charge.isAnnualFee()) {
|
||||
return LocalDate.now().plusYears(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getChargeName(final String facilityType) {
|
||||
return mappingRepository.findByFacilityType(facilityType)
|
||||
.map(FacilityChargeMapping::getChargeName)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
private void applyFromProductConfig(final SavingsAccount account) {
|
||||
final SavingsProductFacilityConfig config = account.savingsProduct() != null
|
||||
? account.savingsProduct().getFacilityConfig() : null;
|
||||
if (config == null) return;
|
||||
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmFacility()), "ATM_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmWithdrawalFacility()), "ATM_WITHDRAWAL", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getUpiFacility()), "UPI_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getPosTransactionsFacility()), "POS_TRANSACTIONS", chargesToApply);
|
||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
||||
// the facility enabled.
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getImpsFacility()), "IMPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getSmsAlerts()), "SMS_ALERTS", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBbpsFacility()), "BBPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInsuranceCover()), "INSURANCE_COVER", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBepgFacility()), "BEPG_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getNeftFacility()), "NEFT_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getRtgsFacility()), "RTGS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInterBankTotalCapFacility()),"INTER_BANK_TOTAL_CAP",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getWithdrawalFacility()), "WITHDRAWAL_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getDepositFacilityKyc()), "DEPOSIT_FACILITY_KYC",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getVirtualDebitCard()), "VIRTUAL_DEBIT_CARD", chargesToApply);
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.MonthDay;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMapping;
|
||||
import org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfig;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reads account's facility config (Yes/No per facility) and auto-applies
|
||||
* the corresponding charge from m_facility_charge_mapping.
|
||||
* No manual charge selection needed — fully automatic.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FacilityChargeAutoApplyService {
|
||||
|
||||
private final FacilityChargeMappingRepository mappingRepository;
|
||||
private final SavingsAccountChargeRepository savingsAccountChargeRepository;
|
||||
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
|
||||
|
||||
public void applyFacilityChargesOnActivation(final SavingsAccount account) {
|
||||
// Use account-level facility config — this reflects what was selected at account opening time
|
||||
final SavingsAccountFacilityConfig config = account.getFacilityConfig();
|
||||
|
||||
if (config == null) {
|
||||
// Fallback: use product-level config if account config not set
|
||||
applyFromProductConfig(account);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmFacility()), "ATM_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmWithdrawalFacility()), "ATM_WITHDRAWAL", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getUpiFacility()), "UPI_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getPosTransactionsFacility()), "POS_TRANSACTIONS", chargesToApply);
|
||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
||||
// the facility enabled.
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getImpsFacility()), "IMPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getSmsAlerts()), "SMS_ALERTS", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBbpsFacility()), "BBPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInsuranceCover()), "INSURANCE_COVER", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBepgFacility()), "BEPG_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getNeftFacility()), "NEFT_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getRtgsFacility()), "RTGS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInterBankTotalCapFacility()),"INTER_BANK_TOTAL_CAP",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getWithdrawalFacility()), "WITHDRAWAL_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getDepositFacilityKyc()), "DEPOSIT_FACILITY_KYC",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getVirtualDebitCard()), "VIRTUAL_DEBIT_CARD", chargesToApply);
|
||||
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
log.info("Auto-applied {} facility charge(s) to savings account id={}",
|
||||
chargesToApply.size(), account.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of the mapped charge amounts for the given facility types - used to size the hold placed at
|
||||
* edit-submission time (see SavingsAccountEditWritePlatformServiceImpl) before any charge is actually applied.
|
||||
*/
|
||||
public BigDecimal totalChargeAmountFor(final java.util.Set<String> facilityTypes) {
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (final String facilityType : facilityTypes) {
|
||||
final java.util.Optional<FacilityChargeMapping> mapping = mappingRepository.findByFacilityType(facilityType);
|
||||
if (mapping.isPresent() && mapping.get().getCharge() != null) {
|
||||
total = total.add(mapping.get().getCharge().getAmount());
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fresh {@link SavingsAccountCharge} for each of the given (newly re-enabled) facility types, so approving
|
||||
* a Nominee/Digital-Feature edit always charges again for the transition - unlike
|
||||
* {@link #applyFacilityChargesOnActivation(SavingsAccount)}'s {@code applyIfEnabled}, this deliberately has NO
|
||||
* "already applied" dedup-by-name guard: a facility that was enabled at activation, disabled, then re-enabled via
|
||||
* an approved edit must be charged again, even though a (now-paid, now-irrelevant) charge object with the same
|
||||
* name already sits in {@code account.charges()} from the earlier cycle.
|
||||
*/
|
||||
public List<SavingsAccountCharge> applyChargesForFacilities(final SavingsAccount account, final java.util.Set<String> facilityTypes) {
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
for (final String facilityType : facilityTypes) {
|
||||
mappingRepository.findByFacilityType(facilityType).ifPresent(mapping -> {
|
||||
final Charge charge = mapping.getCharge();
|
||||
if (charge == null) {
|
||||
return;
|
||||
}
|
||||
final MonthDay feeOnMonthDay = resolveFeeOnMonthDay(charge);
|
||||
final SavingsAccountCharge accountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount(charge, charge.getAmount(),
|
||||
null, null, resolveDueDate(charge), true, feeOnMonthDay, charge.getFeeInterval());
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
chargesToApply.add(accountCharge);
|
||||
});
|
||||
}
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
}
|
||||
return chargesToApply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pays exactly the given charges (typically the just-added result of {@link #applyChargesForFacilities}) as of
|
||||
* {@code transactionDate}. Unlike {@link #collectActivationCharges(SavingsAccount)} - which posts at the
|
||||
* account's activation date and sweeps every unpaid activation-type charge - this is scoped to a specific set of
|
||||
* charges and a caller-supplied date, for posting a Nominee/Digital-Feature edit's charges on the day it's
|
||||
* approved rather than backdating them to activation.
|
||||
*/
|
||||
public List<SavingsAccountTransaction> collectCharges(final SavingsAccount account, final List<SavingsAccountCharge> charges,
|
||||
final LocalDate transactionDate) {
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
for (final SavingsAccountCharge charge : charges) {
|
||||
if (charge.isPaid() || charge.isWaived()) continue;
|
||||
final BigDecimal amount = charge.getAmountOutstanding(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
final Money amountMoney = Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, transactionDate, false, null);
|
||||
if (txn != null) {
|
||||
transactions.add(txn);
|
||||
log.info("Collected facility charge '{}' ₹{} from savings account id={}", charge.getCharge().getName(), amount,
|
||||
account.getId());
|
||||
}
|
||||
}
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(transactionDate);
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all unpaid SavingsActivation charges on the account.
|
||||
* Called after facility charges are saved and flushed, so amounts are properly initialized.
|
||||
*/
|
||||
public List<SavingsAccountTransaction> collectActivationCharges(final SavingsAccount account) {
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
final LocalDate activationDate = account.getActivationDate();
|
||||
|
||||
for (final SavingsAccountCharge charge : account.charges()) {
|
||||
if (!charge.isSavingsActivation()) continue;
|
||||
if (charge.isPaid() || charge.isWaived()) continue;
|
||||
|
||||
final BigDecimal amount = charge.getAmount(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
try {
|
||||
final org.apache.fineract.organisation.monetary.domain.Money amountMoney =
|
||||
org.apache.fineract.organisation.monetary.domain.Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, activationDate, false, null);
|
||||
if (txn != null) {
|
||||
transactions.add(txn);
|
||||
log.info("Collected activation charge '{}' ₹{} from savings account id={}",
|
||||
charge.getCharge().getName(), amount, account.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not collect activation charge '{}' from savings account id={}: {}",
|
||||
charge.getCharge() != null ? charge.getCharge().getName() : "?", account.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(activationDate);
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private void autoCollectSavingsActivationCharges(final SavingsAccount account,
|
||||
final List<SavingsAccountCharge> charges) {
|
||||
final LocalDate activationDate = account.getActivationDate();
|
||||
final List<SavingsAccountTransaction> transactions = new ArrayList<>();
|
||||
|
||||
for (final SavingsAccountCharge charge : charges) {
|
||||
if (!charge.isSavingsActivation()) continue;
|
||||
final BigDecimal amount = charge.getAmountOutstanding(account.getCurrency()).getAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) continue;
|
||||
|
||||
try {
|
||||
final Money amountMoney = Money.of(account.getCurrency(), amount);
|
||||
final SavingsAccountTransaction txn = account.payCharge(charge, amountMoney, activationDate, false, null);
|
||||
if (txn != null) transactions.add(txn);
|
||||
log.info("Auto-collected facility charge '{}' ₹{} from account id={}",
|
||||
charge.getCharge().getName(), amount, account.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not auto-collect facility charge '{}' from account id={}: {}",
|
||||
charge.getCharge().getName(), account.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(activationDate);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyIfEnabled(final SavingsAccount account, final boolean facilityEnabled,
|
||||
final String facilityType, final List<SavingsAccountCharge> collector) {
|
||||
|
||||
if (!facilityEnabled) return;
|
||||
|
||||
// Skip if this charge is already applied to the account
|
||||
boolean alreadyApplied = account.charges().stream()
|
||||
.anyMatch(c -> c.getCharge() != null
|
||||
&& c.getCharge().getName() != null
|
||||
&& c.getCharge().getName().equals(getChargeName(facilityType)));
|
||||
if (alreadyApplied) return;
|
||||
|
||||
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, // chargeTime derived from Charge definition
|
||||
null, // chargeCalculation derived from Charge definition
|
||||
resolveDueDate(charge),
|
||||
true,
|
||||
feeOnMonthDay,
|
||||
charge.getFeeInterval());
|
||||
|
||||
accountCharge.update(account);
|
||||
account.charges().add(accountCharge);
|
||||
collector.add(accountCharge);
|
||||
});
|
||||
}
|
||||
|
||||
private MonthDay resolveFeeOnMonthDay(final Charge charge) {
|
||||
if (charge.getFeeOnMonthDay() != null) return charge.getFeeOnMonthDay();
|
||||
if (charge.isMonthlyFee() || charge.isAnnualFee()) {
|
||||
// Default: 1st of the month
|
||||
return MonthDay.of(LocalDate.now().plusMonths(1).getMonth(), 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalDate resolveDueDate(final Charge charge) {
|
||||
if (charge.isMonthlyFee()) {
|
||||
return LocalDate.now().plusMonths(1).withDayOfMonth(1);
|
||||
} else if (charge.isAnnualFee()) {
|
||||
return LocalDate.now().plusYears(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getChargeName(final String facilityType) {
|
||||
return mappingRepository.findByFacilityType(facilityType)
|
||||
.map(FacilityChargeMapping::getChargeName)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
private void applyFromProductConfig(final SavingsAccount account) {
|
||||
final SavingsProductFacilityConfig config = account.savingsProduct() != null
|
||||
? account.savingsProduct().getFacilityConfig() : null;
|
||||
if (config == null) return;
|
||||
|
||||
final List<SavingsAccountCharge> chargesToApply = new ArrayList<>();
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmFacility()), "ATM_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getAtmWithdrawalFacility()), "ATM_WITHDRAWAL", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getUpiFacility()), "UPI_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getPosTransactionsFacility()), "POS_TRANSACTIONS", chargesToApply);
|
||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
||||
// the facility enabled.
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getImpsFacility()), "IMPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getSmsAlerts()), "SMS_ALERTS", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBbpsFacility()), "BBPS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInsuranceCover()), "INSURANCE_COVER", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getBepgFacility()), "BEPG_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getNeftFacility()), "NEFT_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getRtgsFacility()), "RTGS_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getInterBankTotalCapFacility()),"INTER_BANK_TOTAL_CAP",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getWithdrawalFacility()), "WITHDRAWAL_FACILITY", chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getDepositFacilityKyc()), "DEPOSIT_FACILITY_KYC",chargesToApply);
|
||||
applyIfEnabled(account, Boolean.TRUE.equals(config.getVirtualDebitCard()), "VIRTUAL_DEBIT_CARD", chargesToApply);
|
||||
if (!chargesToApply.isEmpty()) {
|
||||
savingsAccountChargeRepository.saveAll(chargesToApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountPendingEditData;
|
||||
|
||||
public interface SavingsAccountEditWritePlatformService {
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long savingsId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long savingsId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long savingsId, String rejectReason);
|
||||
|
||||
SavingsAccountPendingEditData retrievePendingEdit(Long savingsId);
|
||||
|
||||
java.util.Collection<SavingsAccountPendingEditData> retrieveAllPendingEdits();
|
||||
}
|
||||
+2
@@ -51,4 +51,6 @@
|
||||
<include file="parts/2027_add_pending_edit_satellite_tables.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2028_update_saving_product_temp_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2029_remove_proposed_payload_json.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2030_add_savings_account_pending_edit_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2031_add_savings_account_edit_approval_permissions.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Holds a pending edit request for an ACTIVE savings account's Nominee + Digital & Account
|
||||
Features, mirroring the m_savings_product_temp maker-checker pattern: presence of a row here
|
||||
with status=PENDING (enforced via the unique pending_lock_key) is itself the lock, and nothing
|
||||
is applied to the real m_savings_account_facility_config / m_client_nominee rows until a
|
||||
checker approves (see SavingsAccountEditWritePlatformServiceImpl). One row per request; only
|
||||
one nominee change (add/update/delete) can be proposed per request, so its fields are kept as
|
||||
flat typed columns on this same table rather than a satellite table.
|
||||
-->
|
||||
<changeSet author="fineract" id="2030-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_savings_account_pending_edit" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_savings_account_pending_edit">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="savings_account_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_saoe_savings_account_id" references="m_savings_account(id)" />
|
||||
</column>
|
||||
<column name="createdby_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_saoe_maker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="lastmodifiedby_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_checker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="created_date" type="DATETIME">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="lastmodified_date" type="DATETIME">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="reject_reason" type="VARCHAR(500)" />
|
||||
<column name="pending_lock_key" type="VARCHAR(40)">
|
||||
<constraints nullable="true" unique="true" uniqueConstraintName="saoe_unq_pending_lock_key" />
|
||||
</column>
|
||||
<column name="command_source_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_command_source_id" references="m_portfolio_command_source(id)" />
|
||||
</column>
|
||||
|
||||
<!-- Hold placed at submission time for the total charge of newly-enabled facilities; released
|
||||
(no charge) on reject, released-then-converted-to-a-posted-charge on approve. -->
|
||||
<column name="hold_transaction_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_hold_txn_id" references="m_savings_account_transaction(id)" />
|
||||
</column>
|
||||
<column name="hold_amount" type="DECIMAL(19,6)" />
|
||||
|
||||
<!-- Digital & Account Features - same 16 columns/names as m_savings_account_facility_config.
|
||||
NULL = maker did not touch this toggle; facility_config_submitted tells approve()/UI
|
||||
whether this block was submitted at all. -->
|
||||
<column name="facility_config_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="atm_facility" type="BOOLEAN" />
|
||||
<column name="atm_withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="upi_facility" type="BOOLEAN" />
|
||||
<column name="pos_transactions_facility" type="BOOLEAN" />
|
||||
<column name="cheque_book" type="BOOLEAN" />
|
||||
<column name="imps_facility" type="BOOLEAN" />
|
||||
<column name="sms_alerts" type="BOOLEAN" />
|
||||
<column name="bbps_facility" type="BOOLEAN" />
|
||||
<column name="insurance_cover" type="BOOLEAN" />
|
||||
<column name="bepg_facility" type="BOOLEAN" />
|
||||
<column name="neft_facility" type="BOOLEAN" />
|
||||
<column name="rtgs_facility" type="BOOLEAN" />
|
||||
<column name="inter_bank_total_cap_facility" type="BOOLEAN" />
|
||||
<column name="withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="deposit_facility_kyc" type="BOOLEAN" />
|
||||
<column name="virtual_debit_card" type="BOOLEAN" />
|
||||
|
||||
<!-- Nominee change - one per request (add / update / delete), same fields as m_client_nominee. -->
|
||||
<column name="nominee_submitted" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="nominee_operation" type="VARCHAR(10)" />
|
||||
<column name="nominee_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_nominee_id" references="m_client_nominee(id)" />
|
||||
</column>
|
||||
<column name="nominee_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_nominee_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="relationship_cv_id" type="BIGINT" />
|
||||
<column name="percentage_share" type="DECIMAL(5,2)" />
|
||||
<column name="nominee_is_active" type="BOOLEAN" />
|
||||
<column name="nominee_is_minor" type="BOOLEAN" />
|
||||
<column name="guardian_client_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_saoe_guardian_client_id" references="m_client(id)" />
|
||||
</column>
|
||||
<column name="guardian_relationship_cv_id" type="BIGINT" />
|
||||
<column name="effective_date" type="DATE" />
|
||||
<column name="nominee_remarks" type="VARCHAR(500)" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Permissions gating the Nominee + Digital & Account Features maker-checker edit flow for
|
||||
savings accounts. SUBMITEDIT lets a maker propose the edit (this is the "Edit" action shown
|
||||
on the account view); APPROVEEDIT/REJECTEDIT let a checker resolve it. The _CHECKER permission
|
||||
follows the "<ACTION>_<ENTITY>_CHECKER" convention (see 2022_add_product_checker_permissions.xml)
|
||||
so the pending request also surfaces in the generic Checker Inbox for limited checkers.
|
||||
-->
|
||||
<changeSet author="fineract" id="2031-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'SUBMITEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="SUBMITEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="SUBMITEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="APPROVEEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECTEDIT_SAVINGSACCOUNT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="REJECTEDIT_SAVINGSACCOUNT" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="REJECTEDIT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2031-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVEEDIT_SAVINGSACCOUNT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_savings" />
|
||||
<column name="code" value="APPROVEEDIT_SAVINGSACCOUNT_CHECKER" />
|
||||
<column name="entity_name" value="SAVINGSACCOUNT" />
|
||||
<column name="action_name" value="APPROVEEDIT_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user