implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits

This commit is contained in:
keshav898966
2026-07-03 15:15:09 +05:30
parent 99c153830c
commit b0bfa386ce
71 changed files with 5031 additions and 26 deletions
@@ -495,6 +495,7 @@ public final class FixedDepositProductData extends DepositProductData {
private Boolean allowDebitTxnAfterDueDate;
private Boolean accountClosedZeroBalanceInDayClosing;
private String balanceFrequency;
private Boolean isPendingApproval;
// Template option setters
public void setInterestCalculationTypeOption(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.interestCalculationTypeOption = v; }
@@ -541,6 +542,7 @@ public final class FixedDepositProductData extends DepositProductData {
public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; }
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; }
// CBS field getters
public String getProductCode() { return productCode; }
@@ -98,6 +98,7 @@ public final class RecurringDepositProductData extends DepositProductData {
private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate;
private Boolean withdrawalFeeForTransfers;
private Boolean isPendingApproval;
public void setInterestCalculationTypeOption(Collection<EnumOptionData> v) { this.interestCalculationTypeOption = v; }
public void setPeriodFrequencyOptions(Collection<EnumOptionData> v) { this.periodFrequencyOptions = v; }
@@ -140,6 +141,7 @@ public final class RecurringDepositProductData extends DepositProductData {
public void setDeceasedSettlement(Boolean v) { this.deceasedSettlement = v; }
public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; }
public void setWithdrawalFeeForTransfers(Boolean v) { this.withdrawalFeeForTransfers = v; }
public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; }
/** Copy all CBS custom fields from source into this instance. */
public void copyCbsFieldsFrom(final RecurringDepositProductData src) {
@@ -0,0 +1,39 @@
/**
* 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.Embeddable;
import lombok.Getter;
import lombok.Setter;
@Embeddable
@Getter
@Setter
public class CurrentOnlyEditFields {
@Column(name = "avg_balance_calc_period")
private String avgBalanceCalcPeriod;
@Column(name = "valid_for_period")
private String validForPeriod;
@Column(name = "od_account_closed_zero_balance_in_day_closing")
private Boolean odAccountClosedZeroBalanceInDayClosing;
}
@@ -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.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.Getter;
import lombok.Setter;
/**
* Mirrors the "depositLimitKyc" JSON object - a 1:1 relationship today, so kept flat here. Note:
* the real JSON shape is nested ({@code {general: {kycType, limit}, minor: {...}, smallSaving:
* {...}}}), not flat siblings - the replay step is responsible for re-nesting these columns back
* into that shape before handing off to the unchanged assembler.
*/
@Embeddable
@Getter
@Setter
public class DepositLimitKycEditFields {
@Column(name = "kyc_general_type")
private String generalType;
@Column(name = "kyc_general_limit")
private String generalLimit;
@Column(name = "kyc_minor_type")
private String minorType;
@Column(name = "kyc_minor_limit")
private Long minorLimit;
@Column(name = "kyc_small_saving_type")
private String smallSavingType;
@Column(name = "kyc_small_saving_limit")
private Long smallSavingLimit;
}
@@ -0,0 +1,79 @@
/**
* 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.Embeddable;
import lombok.Getter;
import lombok.Setter;
/** Mirrors the "accountFacilityConfig" JSON object - a 1:1 relationship today, so kept flat here. */
@Embeddable
@Getter
@Setter
public class FacilityConfigEditFields {
@Column(name = "fc_atm_facility")
private Boolean atmFacility;
@Column(name = "fc_atm_withdrawal_facility")
private Boolean atmWithdrawalFacility;
@Column(name = "fc_upi_facility")
private Boolean upiFacility;
@Column(name = "fc_pos_transactions_facility")
private Boolean posTransactionsFacility;
@Column(name = "fc_cheque_book")
private Boolean chequeBook;
@Column(name = "fc_imps_facility")
private Boolean impsFacility;
@Column(name = "fc_sms_alerts")
private Boolean smsAlerts;
@Column(name = "fc_bbps_facility")
private Boolean bbpsFacility;
@Column(name = "fc_insurance_cover")
private Boolean insuranceCover;
@Column(name = "fc_bepg_facility")
private Boolean bepgFacility;
@Column(name = "fc_neft_facility")
private Boolean neftFacility;
@Column(name = "fc_rtgs_facility")
private Boolean rtgsFacility;
@Column(name = "fc_inter_bank_total_cap_facility")
private Boolean interBankTotalCapFacility;
@Column(name = "fc_withdrawal_facility")
private Boolean withdrawalFacility;
@Column(name = "fc_deposit_facility_kyc")
private Boolean depositFacilityKyc;
@Column(name = "fc_virtual_debit_card")
private Boolean virtualDebitCard;
}
@@ -0,0 +1,71 @@
/**
* 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.Embeddable;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
/** Also used by Recurring Deposit, which extends Fixed Deposit. */
@Embeddable
@Getter
@Setter
public class FixedDepositEditFields {
@Column(name = "fd_type")
private String fdType;
@Column(name = "pre_closure_penal_applicable")
private Boolean preClosurePenalApplicable;
@Column(name = "pre_closure_penal_interest")
private BigDecimal preClosurePenalInterest;
@Column(name = "pre_closure_penal_interest_on_type_id")
private Integer preClosurePenalInterestOnTypeId;
@Column(name = "min_deposit_term")
private Integer minDepositTerm;
@Column(name = "max_deposit_term")
private Integer maxDepositTerm;
@Column(name = "min_deposit_term_type_id")
private Integer minDepositTermTypeId;
@Column(name = "max_deposit_term_type_id")
private Integer maxDepositTermTypeId;
@Column(name = "in_multiples_of_deposit_term")
private Integer inMultiplesOfDepositTerm;
@Column(name = "in_multiples_of_deposit_term_type_id")
private Integer inMultiplesOfDepositTermTypeId;
@Column(name = "min_deposit_amount")
private BigDecimal minDepositAmount;
@Column(name = "max_deposit_amount")
private BigDecimal maxDepositAmount;
@Column(name = "deposit_amount")
private BigDecimal depositAmount;
}
@@ -0,0 +1,78 @@
/**
* 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.Embeddable;
import lombok.Getter;
import lombok.Setter;
/**
* Scalar (1:1) GL account mapping ids. No FK constraints to the accounting GL table - the
* existing validator already checks GL account validity at submit time. The 3 array-shaped GL
* mappings (payment-channel/fee/penalty to income account) live in m_product_temp_gl_mapping
* instead, since those are genuinely one-to-many.
*/
@Embeddable
@Getter
@Setter
public class GlAccountMappingEditFields {
@Column(name = "gl_savings_reference_account_id")
private Long savingsReferenceAccountId;
@Column(name = "gl_savings_control_account_id")
private Long savingsControlAccountId;
@Column(name = "gl_income_from_fee_account_id")
private Long incomeFromFeeAccountId;
@Column(name = "gl_income_from_penalty_account_id")
private Long incomeFromPenaltyAccountId;
@Column(name = "gl_interest_on_savings_account_id")
private Long interestOnSavingsAccountId;
@Column(name = "gl_transfers_in_suspense_account_id")
private Long transfersInSuspenseAccountId;
@Column(name = "gl_overdraft_portfolio_control_id")
private Long overdraftPortfolioControlId;
@Column(name = "gl_income_from_interest_id")
private Long incomeFromInterestId;
@Column(name = "gl_write_off_account_id")
private Long writeOffAccountId;
@Column(name = "gl_escheat_liability_id")
private Long escheatLiabilityId;
@Column(name = "gl_penalties_receivable_account_id")
private Long penaltiesReceivableAccountId;
@Column(name = "gl_interest_receivable_account_id")
private Long interestReceivableAccountId;
@Column(name = "gl_fees_receivable_account_id")
private Long feesReceivableAccountId;
@Column(name = "gl_interest_payable_account_id")
private Long interestPayableAccountId;
}
@@ -0,0 +1,209 @@
/**
* 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.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Holds a single pending product edit, shared across product types (Savings, Current Account, Share) in one table
* (m_savings_product_temp). Presence of a row here for a given (productType, productId) with
* {@code status = PENDING} is itself the lock - no separate "is locked" column anywhere. The full edit request body
* is kept in {@link #proposedPayloadJson} so approval can replay it through each product's normal update path
* (covering charges, GL/accounting mappings and other related tables, not just scalar columns).
*/
@Entity
@Table(name = "m_savings_product_temp")
@Getter
@Setter
public class ProductPendingEdit 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 TYPE_SAVINGS = "SAVINGS";
public static final String TYPE_CURRENT = "CURRENT";
public static final String TYPE_SHARE = "SHARE";
public static final String TYPE_FD = "FD";
public static final String TYPE_RD = "RD";
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(name = "product_type", nullable = false, length = 20)
private String productType;
@ManyToOne
@JoinColumn(name = "maker_id", nullable = false)
private AppUser maker;
@ManyToOne
@JoinColumn(name = "checker_id")
private AppUser checker;
@Column(name = "created_on", nullable = false)
private LocalDateTime createdOn;
@Column(name = "checked_on")
private LocalDateTime checkedOn;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "reject_reason", length = 500)
private String rejectReason;
@Lob
@Column(name = "proposed_payload_json", nullable = false)
private String proposedPayloadJson;
/**
* "<productType>:<productId>" while PENDING, NULL otherwise - the real DB-level lock. Unique constraints treat
* NULL as distinct in both PostgreSQL and MySQL/MariaDB, so only one PENDING row per (productType, productId)
* can ever exist, while resolved rows are kept indefinitely for audit history without blocking future edits.
*/
@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;
/**
* Typed replacement for proposedPayloadJson - one embeddable group per concern, all mapped
* onto this same physical table. proposedPayloadJson is still populated (kept for a safe
* deprecation window) but a product type's write-service uses these typed columns, plus the
* satellite tables (ProductPendingEditCharge/FacilityLimit/GlMapping/MarketPrice) and the
* ProductPendingEditSubmittedField manifest, to build and replay an edit once its cutover
* (see SavingsProductWritePlatformServiceJpaRepositoryImpl and siblings) is complete.
*/
@Embedded
private SavingsCoreEditFields coreFields = new SavingsCoreEditFields();
@Embedded
private CurrentOnlyEditFields currentOnlyFields = new CurrentOnlyEditFields();
@Embedded
private ShareOnlyEditFields shareOnlyFields = new ShareOnlyEditFields();
@Embedded
private FixedDepositEditFields fixedDepositFields = new FixedDepositEditFields();
@Embedded
private RecurringDepositEditFields recurringDepositFields = new RecurringDepositEditFields();
@Embedded
private FacilityConfigEditFields facilityConfigFields = new FacilityConfigEditFields();
@Embedded
private DepositLimitKycEditFields depositLimitKycFields = new DepositLimitKycEditFields();
@Embedded
private GlAccountMappingEditFields glAccountMappingFields = new GlAccountMappingEditFields();
protected ProductPendingEdit() {}
public ProductPendingEdit(final String productType, final Long productId, final AppUser maker, final String proposedPayloadJson) {
this.productType = productType;
this.productId = productId;
this.maker = maker;
this.proposedPayloadJson = proposedPayloadJson;
this.createdOn = LocalDateTime.now();
this.status = STATUS_PENDING;
this.pendingLockKey = lockKeyFor(productType, productId);
}
public static String lockKeyFor(final String productType, final Long productId) {
return productType + ":" + productId;
}
public Long getProductId() {
return this.productId;
}
public String getProductType() {
return this.productType;
}
public AppUser getMaker() {
return this.maker;
}
public AppUser getChecker() {
return this.checker;
}
public LocalDateTime getCreatedOn() {
return this.createdOn;
}
public LocalDateTime getCheckedOn() {
return this.checkedOn;
}
public String getStatus() {
return this.status;
}
public String getRejectReason() {
return this.rejectReason;
}
public String getProposedPayloadJson() {
return this.proposedPayloadJson;
}
public Long getCommandSourceId() {
return this.commandSourceId;
}
public void setCommandSourceId(final Long commandSourceId) {
this.commandSourceId = commandSourceId;
}
public boolean isPending() {
return STATUS_PENDING.equals(this.status);
}
public void markApproved(final AppUser checker) {
this.status = STATUS_APPROVED;
this.checker = checker;
this.checkedOn = LocalDateTime.now();
this.pendingLockKey = null;
}
public void markRejected(final AppUser checker, final String reason) {
this.status = STATUS_REJECTED;
this.checker = checker;
this.checkedOn = LocalDateTime.now();
this.rejectReason = reason;
this.pendingLockKey = null;
}
}
@@ -0,0 +1,50 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/** One row per charge id proposed to be attached to the product, mirroring the "charges" array. */
@Entity
@Table(name = "m_product_temp_charge")
@Getter
@Setter
@NoArgsConstructor
public class ProductPendingEditCharge extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "pending_edit_id", nullable = false)
private ProductPendingEdit pendingEdit;
@Column(name = "charge_id", nullable = false)
private Long chargeId;
public ProductPendingEditCharge(final ProductPendingEdit pendingEdit, final Long chargeId) {
this.pendingEdit = pendingEdit;
this.chargeId = chargeId;
}
}
@@ -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.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductPendingEditChargeRepository extends JpaRepository<ProductPendingEditCharge, Long> {
List<ProductPendingEditCharge> findByPendingEditId(Long pendingEditId);
}
@@ -0,0 +1,63 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/** One row per entry of the "accountFacilityLimits" array. Values can be "UNLIMITED"/"N/A"/numeric-as-string. */
@Entity
@Table(name = "m_product_temp_facility_limit")
@Getter
@Setter
@NoArgsConstructor
public class ProductPendingEditFacilityLimit extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "pending_edit_id", nullable = false)
private ProductPendingEdit pendingEdit;
@Column(name = "limit_type", nullable = false)
private String limitType;
@Column(name = "general_limit")
private String generalLimit;
@Column(name = "minor_account_limit")
private String minorAccountLimit;
@Column(name = "small_saving_limit")
private String smallSavingLimit;
public ProductPendingEditFacilityLimit(final ProductPendingEdit pendingEdit, final String limitType, final String generalLimit,
final String minorAccountLimit, final String smallSavingLimit) {
this.pendingEdit = pendingEdit;
this.limitType = limitType;
this.generalLimit = generalLimit;
this.minorAccountLimit = minorAccountLimit;
this.smallSavingLimit = smallSavingLimit;
}
}
@@ -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.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductPendingEditFacilityLimitRepository extends JpaRepository<ProductPendingEditFacilityLimit, Long> {
List<ProductPendingEditFacilityLimit> findByPendingEditId(Long pendingEditId);
}
@@ -0,0 +1,696 @@
/**
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
/**
* Shared submit-time extraction / approve-time reconstruction logic for the scalar and 1:1
* "container" fields that Savings, Current, Fixed Deposit and Recurring Deposit products all have
* in common (FD/RD extend the Savings field set). Each populateXxx method reads present JSON
* parameters into the corresponding typed embeddable, returning the set of parameter names it
* found (which the caller must persist as {@link ProductPendingEditSubmittedField} rows - this is
* how the manifest preserves "field omitted" vs "field explicitly null" vs "field has a value").
* Each writeXxxToJson method is the exact inverse, used at approval time to rebuild a JsonObject
* equivalent to what the maker originally submitted, before handing it to the unchanged
* update()/updateProduct() methods via JsonCommand.fromJsonElement(...).
*
* Share Product's field set is largely disjoint from this shared core and is mapped separately.
*/
public final class ProductPendingEditFieldMapper {
private ProductPendingEditFieldMapper() {}
// ---- Savings/Current/FD/RD shared core fields ----------------------------------------------
public static Set<String> populateCoreFields(final JsonCommand command, final SavingsCoreEditFields f) {
final Set<String> present = new HashSet<>();
if (command.hasParameter("name")) {
f.setName(command.stringValueOfParameterNamed("name"));
present.add("name");
}
if (command.hasParameter("shortName")) {
f.setShortName(command.stringValueOfParameterNamed("shortName"));
present.add("shortName");
}
if (command.hasParameter("description")) {
f.setDescription(command.stringValueOfParameterNamed("description"));
present.add("description");
}
if (command.hasParameter("currencyCode")) {
f.setCurrencyCode(command.stringValueOfParameterNamed("currencyCode"));
present.add("currencyCode");
}
if (command.hasParameter("digitsAfterDecimal")) {
f.setDigitsAfterDecimal(command.integerValueOfParameterNamed("digitsAfterDecimal"));
present.add("digitsAfterDecimal");
}
if (command.hasParameter("inMultiplesOf")) {
f.setInMultiplesOf(command.integerValueOfParameterNamed("inMultiplesOf"));
present.add("inMultiplesOf");
}
if (command.hasParameter("accountingRule")) {
f.setAccountingRule(command.integerValueOfParameterNamed("accountingRule"));
present.add("accountingRule");
}
if (command.hasParameter("nominalAnnualInterestRate")) {
f.setNominalAnnualInterestRate(command.bigDecimalValueOfParameterNamed("nominalAnnualInterestRate"));
present.add("nominalAnnualInterestRate");
}
if (command.hasParameter("interestCompoundingPeriodType")) {
f.setInterestCompoundingPeriodType(command.integerValueOfParameterNamed("interestCompoundingPeriodType"));
present.add("interestCompoundingPeriodType");
}
if (command.hasParameter("interestPostingPeriodType")) {
f.setInterestPostingPeriodType(command.integerValueOfParameterNamed("interestPostingPeriodType"));
present.add("interestPostingPeriodType");
}
if (command.hasParameter("interestCalculationType")) {
f.setInterestCalculationType(command.integerValueOfParameterNamed("interestCalculationType"));
present.add("interestCalculationType");
}
if (command.hasParameter("interestCalculationDaysInYearType")) {
f.setInterestCalculationDaysInYearType(command.integerValueOfParameterNamed("interestCalculationDaysInYearType"));
present.add("interestCalculationDaysInYearType");
}
if (command.hasParameter("minRequiredOpeningBalance")) {
f.setMinRequiredOpeningBalance(command.bigDecimalValueOfParameterNamed("minRequiredOpeningBalance"));
present.add("minRequiredOpeningBalance");
}
if (command.hasParameter("lockinPeriodFrequency")) {
f.setLockinPeriodFrequency(command.integerValueOfParameterNamed("lockinPeriodFrequency"));
present.add("lockinPeriodFrequency");
}
if (command.hasParameter("lockinPeriodFrequencyType")) {
f.setLockinPeriodFrequencyType(command.integerValueOfParameterNamed("lockinPeriodFrequencyType"));
present.add("lockinPeriodFrequencyType");
}
if (command.hasParameter("withdrawalFeeForTransfers")) {
f.setWithdrawalFeeForTransfers(command.booleanObjectValueOfParameterNamed("withdrawalFeeForTransfers"));
present.add("withdrawalFeeForTransfers");
}
if (command.hasParameter("allowOverdraft")) {
f.setAllowOverdraft(command.booleanObjectValueOfParameterNamed("allowOverdraft"));
present.add("allowOverdraft");
}
if (command.hasParameter("overdraftLimit")) {
f.setOverdraftLimit(command.bigDecimalValueOfParameterNamed("overdraftLimit"));
present.add("overdraftLimit");
}
if (command.hasParameter("nominalAnnualInterestRateOverdraft")) {
f.setNominalAnnualInterestRateOverdraft(command.bigDecimalValueOfParameterNamed("nominalAnnualInterestRateOverdraft"));
present.add("nominalAnnualInterestRateOverdraft");
}
if (command.hasParameter("minOverdraftForInterestCalculation")) {
f.setMinOverdraftForInterestCalculation(command.bigDecimalValueOfParameterNamed("minOverdraftForInterestCalculation"));
present.add("minOverdraftForInterestCalculation");
}
if (command.hasParameter("enforceMinRequiredBalance")) {
f.setEnforceMinRequiredBalance(command.booleanObjectValueOfParameterNamed("enforceMinRequiredBalance"));
present.add("enforceMinRequiredBalance");
}
if (command.hasParameter("minRequiredBalance")) {
f.setMinRequiredBalance(command.bigDecimalValueOfParameterNamed("minRequiredBalance"));
present.add("minRequiredBalance");
}
if (command.hasParameter("lienAllowed")) {
f.setLienAllowed(command.booleanObjectValueOfParameterNamed("lienAllowed"));
present.add("lienAllowed");
}
if (command.hasParameter("maxAllowedLienLimit")) {
f.setMaxAllowedLienLimit(command.bigDecimalValueOfParameterNamed("maxAllowedLienLimit"));
present.add("maxAllowedLienLimit");
}
if (command.hasParameter("minBalanceForInterestCalculation")) {
f.setMinBalanceForInterestCalculation(command.bigDecimalValueOfParameterNamed("minBalanceForInterestCalculation"));
present.add("minBalanceForInterestCalculation");
}
if (command.hasParameter("withHoldTax")) {
f.setWithHoldTax(command.booleanObjectValueOfParameterNamed("withHoldTax"));
present.add("withHoldTax");
}
if (command.hasParameter("taxGroupId")) {
f.setTaxGroupId(command.longValueOfParameterNamed("taxGroupId"));
present.add("taxGroupId");
}
if (command.hasParameter("otherGlMappings")) {
f.setOtherGlMappings(command.stringValueOfParameterNamed("otherGlMappings"));
present.add("otherGlMappings");
}
if (command.hasParameter("isDormancyTrackingActive")) {
f.setIsDormancyTrackingActive(command.booleanObjectValueOfParameterNamed("isDormancyTrackingActive"));
present.add("isDormancyTrackingActive");
}
if (command.hasParameter("daysToInactive")) {
f.setDaysToInactive(command.integerValueOfParameterNamed("daysToInactive"));
present.add("daysToInactive");
}
if (command.hasParameter("daysToDormancy")) {
f.setDaysToDormancy(command.integerValueOfParameterNamed("daysToDormancy"));
present.add("daysToDormancy");
}
if (command.hasParameter("daysToEscheat")) {
f.setDaysToEscheat(command.integerValueOfParameterNamed("daysToEscheat"));
present.add("daysToEscheat");
}
if (command.hasParameter("accountType")) {
f.setAccountType(command.stringValueOfParameterNamed("accountType"));
present.add("accountType");
}
if (command.hasParameter("customerSegment")) {
f.setCustomerSegment(command.stringValueOfParameterNamed("customerSegment"));
present.add("customerSegment");
}
if (command.hasParameter("productCode")) {
f.setProductCode(command.stringValueOfParameterNamed("productCode"));
present.add("productCode");
}
if (command.hasParameter("glAccountId")) {
f.setGlAccountId(command.longValueOfParameterNamed("glAccountId"));
present.add("glAccountId");
}
if (command.hasParameter("effectiveDate")) {
f.setEffectiveDate(command.localDateValueOfParameterNamed("effectiveDate"));
present.add("effectiveDate");
}
if (command.hasParameter("effectiveTo")) {
f.setEffectiveTo(command.localDateValueOfParameterNamed("effectiveTo"));
present.add("effectiveTo");
}
if (command.hasParameter("endDate")) {
f.setEndDate(command.localDateValueOfParameterNamed("endDate"));
present.add("endDate");
}
if (command.hasParameter("introducerRequired")) {
f.setIntroducerRequired(command.booleanObjectValueOfParameterNamed("introducerRequired"));
present.add("introducerRequired");
}
if (command.hasParameter("shareLinkingEnabled")) {
f.setShareLinkingEnabled(command.booleanObjectValueOfParameterNamed("shareLinkingEnabled"));
present.add("shareLinkingEnabled");
}
if (command.hasParameter("nominationFacility")) {
f.setNominationFacility(command.booleanObjectValueOfParameterNamed("nominationFacility"));
present.add("nominationFacility");
}
if (command.hasParameter("dormancyAlert")) {
f.setDormancyAlert(command.booleanObjectValueOfParameterNamed("dormancyAlert"));
present.add("dormancyAlert");
}
if (command.hasParameter("interestCalculateSelector")) {
f.setInterestCalculateSelector(command.integerValueOfParameterNamed("interestCalculateSelector"));
present.add("interestCalculateSelector");
}
if (command.hasParameter("averagePeriod")) {
f.setAveragePeriod(command.stringValueOfParameterNamed("averagePeriod"));
present.add("averagePeriod");
}
if (command.hasParameter("autoPost")) {
f.setAutoPost(command.booleanObjectValueOfParameterNamed("autoPost"));
present.add("autoPost");
}
if (command.hasParameter("frequencyFixDay")) {
f.setFrequencyFixDay(command.integerValueOfParameterNamed("frequencyFixDay"));
present.add("frequencyFixDay");
}
if (command.hasParameter("lastInterestDate")) {
f.setLastInterestDate(command.localDateValueOfParameterNamed("lastInterestDate"));
present.add("lastInterestDate");
}
if (command.hasParameter("enableLockinPeriod")) {
f.setEnableLockinPeriod(command.booleanObjectValueOfParameterNamed("enableLockinPeriod"));
present.add("enableLockinPeriod");
}
if (command.hasParameter("balanceFrequency")) {
f.setBalanceFrequency(command.stringValueOfParameterNamed("balanceFrequency"));
present.add("balanceFrequency");
}
if (command.hasParameter("accountClosedZeroBalanceInDayClosing")) {
f.setAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("accountClosedZeroBalanceInDayClosing"));
present.add("accountClosedZeroBalanceInDayClosing");
}
if (command.hasParameter("balanceSwapOption")) {
f.setBalanceSwapOption(command.booleanObjectValueOfParameterNamed("balanceSwapOption"));
present.add("balanceSwapOption");
}
if (command.hasParameter("msme")) {
f.setMsme(command.booleanObjectValueOfParameterNamed("msme"));
present.add("msme");
}
if (command.hasParameter("autoDebitFacility")) {
f.setAutoDebitFacility(command.booleanObjectValueOfParameterNamed("autoDebitFacility"));
present.add("autoDebitFacility");
}
if (command.hasParameter("loanFacility")) {
f.setLoanFacility(command.booleanObjectValueOfParameterNamed("loanFacility"));
present.add("loanFacility");
}
if (command.hasParameter("prematureWithdrawalOption")) {
f.setPrematureWithdrawalOption(command.booleanObjectValueOfParameterNamed("prematureWithdrawalOption"));
present.add("prematureWithdrawalOption");
}
if (command.hasParameter("tenureFlexibility")) {
f.setTenureFlexibility(command.booleanObjectValueOfParameterNamed("tenureFlexibility"));
present.add("tenureFlexibility");
}
if (command.hasParameter("lienMarked")) {
f.setLienMarked(command.booleanObjectValueOfParameterNamed("lienMarked"));
present.add("lienMarked");
}
if (command.hasParameter("maturityInstructions")) {
f.setMaturityInstructions(command.booleanObjectValueOfParameterNamed("maturityInstructions"));
present.add("maturityInstructions");
}
if (command.hasParameter("creditMaturityProceeds")) {
f.setCreditMaturityProceeds(command.booleanObjectValueOfParameterNamed("creditMaturityProceeds"));
present.add("creditMaturityProceeds");
}
if (command.hasParameter("penaltyGracePeriodMissedInstallments")) {
f.setPenaltyGracePeriodMissedInstallments(command.booleanObjectValueOfParameterNamed("penaltyGracePeriodMissedInstallments"));
present.add("penaltyGracePeriodMissedInstallments");
}
if (command.hasParameter("panelRequestApplicable")) {
f.setPanelRequestApplicable(command.booleanObjectValueOfParameterNamed("panelRequestApplicable"));
present.add("panelRequestApplicable");
}
if (command.hasParameter("noPenaltyNomineeHeirClosure")) {
f.setNoPenaltyNomineeHeirClosure(command.booleanObjectValueOfParameterNamed("noPenaltyNomineeHeirClosure"));
present.add("noPenaltyNomineeHeirClosure");
}
if (command.hasParameter("deceasedSettlement")) {
f.setDeceasedSettlement(command.booleanObjectValueOfParameterNamed("deceasedSettlement"));
present.add("deceasedSettlement");
}
if (command.hasParameter("allowDebitTxnAfterDueDate")) {
f.setAllowDebitTxnAfterDueDate(command.booleanObjectValueOfParameterNamed("allowDebitTxnAfterDueDate"));
present.add("allowDebitTxnAfterDueDate");
}
return present;
}
public static void writeCoreFieldsToJson(final JsonObject root, final SavingsCoreEditFields f, final Set<String> submitted) {
putString(root, submitted, "name", f.getName());
putString(root, submitted, "shortName", f.getShortName());
putString(root, submitted, "description", f.getDescription());
putString(root, submitted, "currencyCode", f.getCurrencyCode());
putNumber(root, submitted, "digitsAfterDecimal", f.getDigitsAfterDecimal());
putNumber(root, submitted, "inMultiplesOf", f.getInMultiplesOf());
putNumber(root, submitted, "accountingRule", f.getAccountingRule());
putNumber(root, submitted, "nominalAnnualInterestRate", f.getNominalAnnualInterestRate());
putNumber(root, submitted, "interestCompoundingPeriodType", f.getInterestCompoundingPeriodType());
putNumber(root, submitted, "interestPostingPeriodType", f.getInterestPostingPeriodType());
putNumber(root, submitted, "interestCalculationType", f.getInterestCalculationType());
putNumber(root, submitted, "interestCalculationDaysInYearType", f.getInterestCalculationDaysInYearType());
putNumber(root, submitted, "minRequiredOpeningBalance", f.getMinRequiredOpeningBalance());
putNumber(root, submitted, "lockinPeriodFrequency", f.getLockinPeriodFrequency());
putNumber(root, submitted, "lockinPeriodFrequencyType", f.getLockinPeriodFrequencyType());
putBoolean(root, submitted, "withdrawalFeeForTransfers", f.getWithdrawalFeeForTransfers());
putBoolean(root, submitted, "allowOverdraft", f.getAllowOverdraft());
putNumber(root, submitted, "overdraftLimit", f.getOverdraftLimit());
putNumber(root, submitted, "nominalAnnualInterestRateOverdraft", f.getNominalAnnualInterestRateOverdraft());
putNumber(root, submitted, "minOverdraftForInterestCalculation", f.getMinOverdraftForInterestCalculation());
putBoolean(root, submitted, "enforceMinRequiredBalance", f.getEnforceMinRequiredBalance());
putNumber(root, submitted, "minRequiredBalance", f.getMinRequiredBalance());
putBoolean(root, submitted, "lienAllowed", f.getLienAllowed());
putNumber(root, submitted, "maxAllowedLienLimit", f.getMaxAllowedLienLimit());
putNumber(root, submitted, "minBalanceForInterestCalculation", f.getMinBalanceForInterestCalculation());
putBoolean(root, submitted, "withHoldTax", f.getWithHoldTax());
putNumber(root, submitted, "taxGroupId", f.getTaxGroupId());
putString(root, submitted, "otherGlMappings", f.getOtherGlMappings());
putBoolean(root, submitted, "isDormancyTrackingActive", f.getIsDormancyTrackingActive());
putNumber(root, submitted, "daysToInactive", f.getDaysToInactive());
putNumber(root, submitted, "daysToDormancy", f.getDaysToDormancy());
putNumber(root, submitted, "daysToEscheat", f.getDaysToEscheat());
putString(root, submitted, "accountType", f.getAccountType());
putString(root, submitted, "customerSegment", f.getCustomerSegment());
putString(root, submitted, "productCode", f.getProductCode());
putNumber(root, submitted, "glAccountId", f.getGlAccountId());
putDate(root, submitted, "effectiveDate", f.getEffectiveDate());
putDate(root, submitted, "effectiveTo", f.getEffectiveTo());
putDate(root, submitted, "endDate", f.getEndDate());
putBoolean(root, submitted, "introducerRequired", f.getIntroducerRequired());
putBoolean(root, submitted, "shareLinkingEnabled", f.getShareLinkingEnabled());
putBoolean(root, submitted, "nominationFacility", f.getNominationFacility());
putBoolean(root, submitted, "dormancyAlert", f.getDormancyAlert());
putNumber(root, submitted, "interestCalculateSelector", f.getInterestCalculateSelector());
putString(root, submitted, "averagePeriod", f.getAveragePeriod());
putBoolean(root, submitted, "autoPost", f.getAutoPost());
putNumber(root, submitted, "frequencyFixDay", f.getFrequencyFixDay());
putDate(root, submitted, "lastInterestDate", f.getLastInterestDate());
putBoolean(root, submitted, "enableLockinPeriod", f.getEnableLockinPeriod());
putString(root, submitted, "balanceFrequency", f.getBalanceFrequency());
putBoolean(root, submitted, "accountClosedZeroBalanceInDayClosing", f.getAccountClosedZeroBalanceInDayClosing());
putBoolean(root, submitted, "balanceSwapOption", f.getBalanceSwapOption());
putBoolean(root, submitted, "msme", f.getMsme());
putBoolean(root, submitted, "autoDebitFacility", f.getAutoDebitFacility());
putBoolean(root, submitted, "loanFacility", f.getLoanFacility());
putBoolean(root, submitted, "prematureWithdrawalOption", f.getPrematureWithdrawalOption());
putBoolean(root, submitted, "tenureFlexibility", f.getTenureFlexibility());
putBoolean(root, submitted, "lienMarked", f.getLienMarked());
putBoolean(root, submitted, "maturityInstructions", f.getMaturityInstructions());
putBoolean(root, submitted, "creditMaturityProceeds", f.getCreditMaturityProceeds());
putBoolean(root, submitted, "penaltyGracePeriodMissedInstallments", f.getPenaltyGracePeriodMissedInstallments());
putBoolean(root, submitted, "panelRequestApplicable", f.getPanelRequestApplicable());
putBoolean(root, submitted, "noPenaltyNomineeHeirClosure", f.getNoPenaltyNomineeHeirClosure());
putBoolean(root, submitted, "deceasedSettlement", f.getDeceasedSettlement());
putBoolean(root, submitted, "allowDebitTxnAfterDueDate", f.getAllowDebitTxnAfterDueDate());
}
// ---- accountFacilityConfig (1:1 container) --------------------------------------------------
public static boolean populateFacilityConfig(final JsonCommand command, final FacilityConfigEditFields f) {
if (!command.hasParameter("accountFacilityConfig")) {
return false;
}
f.setAtmFacility(booleanOf(command, "accountFacilityConfig", "atmFacility"));
f.setAtmWithdrawalFacility(booleanOf(command, "accountFacilityConfig", "atmWithdrawalFacility"));
f.setUpiFacility(booleanOf(command, "accountFacilityConfig", "upiFacility"));
f.setPosTransactionsFacility(booleanOf(command, "accountFacilityConfig", "posTransactionsFacility"));
f.setChequeBook(booleanOf(command, "accountFacilityConfig", "chequeBook"));
f.setImpsFacility(booleanOf(command, "accountFacilityConfig", "impsFacility"));
f.setSmsAlerts(booleanOf(command, "accountFacilityConfig", "smsAlerts"));
f.setBbpsFacility(booleanOf(command, "accountFacilityConfig", "bbpsFacility"));
f.setInsuranceCover(booleanOf(command, "accountFacilityConfig", "insuranceCover"));
f.setBepgFacility(booleanOf(command, "accountFacilityConfig", "bepgFacility"));
f.setNeftFacility(booleanOf(command, "accountFacilityConfig", "neftFacility"));
f.setRtgsFacility(booleanOf(command, "accountFacilityConfig", "rtgsFacility"));
f.setInterBankTotalCapFacility(booleanOf(command, "accountFacilityConfig", "interBankTotalCapFacility"));
f.setWithdrawalFacility(booleanOf(command, "accountFacilityConfig", "withdrawalFacility"));
f.setDepositFacilityKyc(booleanOf(command, "accountFacilityConfig", "depositFacilityKyc"));
f.setVirtualDebitCard(booleanOf(command, "accountFacilityConfig", "virtualDebitCard"));
return true;
}
public static void writeFacilityConfigToJson(final JsonObject root, final FacilityConfigEditFields f, final boolean submitted) {
if (!submitted) {
return;
}
final JsonObject config = new JsonObject();
config.addProperty("atmFacility", f.getAtmFacility());
config.addProperty("atmWithdrawalFacility", f.getAtmWithdrawalFacility());
config.addProperty("upiFacility", f.getUpiFacility());
config.addProperty("posTransactionsFacility", f.getPosTransactionsFacility());
config.addProperty("chequeBook", f.getChequeBook());
config.addProperty("impsFacility", f.getImpsFacility());
config.addProperty("smsAlerts", f.getSmsAlerts());
config.addProperty("bbpsFacility", f.getBbpsFacility());
config.addProperty("insuranceCover", f.getInsuranceCover());
config.addProperty("bepgFacility", f.getBepgFacility());
config.addProperty("neftFacility", f.getNeftFacility());
config.addProperty("rtgsFacility", f.getRtgsFacility());
config.addProperty("interBankTotalCapFacility", f.getInterBankTotalCapFacility());
config.addProperty("withdrawalFacility", f.getWithdrawalFacility());
config.addProperty("depositFacilityKyc", f.getDepositFacilityKyc());
config.addProperty("virtualDebitCard", f.getVirtualDebitCard());
root.add("accountFacilityConfig", config);
}
// ---- depositLimitKyc (1:1 container, nested general/minor/smallSaving shape) ----------------
public static boolean populateDepositLimitKyc(final JsonCommand command, final DepositLimitKycEditFields f) {
if (!command.hasParameter("depositLimitKyc")) {
return false;
}
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("depositLimitKyc");
if (raw == null || !raw.isJsonObject()) {
return true;
}
final JsonObject kyc = raw.getAsJsonObject();
if (kyc.has("general") && kyc.get("general").isJsonObject()) {
final JsonObject general = kyc.getAsJsonObject("general");
f.setGeneralType(stringOrNull(general, "kycType"));
f.setGeneralLimit(stringOrNull(general, "limit"));
}
if (kyc.has("minor") && kyc.get("minor").isJsonObject()) {
final JsonObject minor = kyc.getAsJsonObject("minor");
f.setMinorType(stringOrNull(minor, "kycType"));
final String minorLimit = stringOrNull(minor, "limit");
f.setMinorLimit(minorLimit == null ? null : Long.valueOf(minorLimit));
}
if (kyc.has("smallSaving") && kyc.get("smallSaving").isJsonObject()) {
final JsonObject smallSaving = kyc.getAsJsonObject("smallSaving");
f.setSmallSavingType(stringOrNull(smallSaving, "kycType"));
final String smallSavingLimit = stringOrNull(smallSaving, "limit");
f.setSmallSavingLimit(smallSavingLimit == null ? null : Long.valueOf(smallSavingLimit));
}
return true;
}
public static void writeDepositLimitKycToJson(final JsonObject root, final DepositLimitKycEditFields f, final boolean submitted) {
if (!submitted) {
return;
}
final JsonObject kyc = new JsonObject();
final JsonObject general = new JsonObject();
general.addProperty("kycType", f.getGeneralType());
general.addProperty("limit", f.getGeneralLimit());
kyc.add("general", general);
final JsonObject minor = new JsonObject();
minor.addProperty("kycType", f.getMinorType());
if (f.getMinorLimit() != null) {
minor.addProperty("limit", f.getMinorLimit());
}
kyc.add("minor", minor);
final JsonObject smallSaving = new JsonObject();
smallSaving.addProperty("kycType", f.getSmallSavingType());
if (f.getSmallSavingLimit() != null) {
smallSaving.addProperty("limit", f.getSmallSavingLimit());
}
kyc.add("smallSaving", smallSaving);
root.add("depositLimitKyc", kyc);
}
// ---- GL scalar account mapping (1:1) ----------------------------------------------------------
public static boolean populateGlAccountMapping(final JsonCommand command, final GlAccountMappingEditFields f) {
boolean any = false;
any |= setIfPresent(command, "savingsReferenceAccountId", f::setSavingsReferenceAccountId);
any |= setIfPresent(command, "savingsControlAccountId", f::setSavingsControlAccountId);
any |= setIfPresent(command, "incomeFromFeeAccountId", f::setIncomeFromFeeAccountId);
any |= setIfPresent(command, "incomeFromPenaltyAccountId", f::setIncomeFromPenaltyAccountId);
any |= setIfPresent(command, "interestOnSavingsAccountId", f::setInterestOnSavingsAccountId);
any |= setIfPresent(command, "transfersInSuspenseAccountId", f::setTransfersInSuspenseAccountId);
any |= setIfPresent(command, "overdraftPortfolioControlId", f::setOverdraftPortfolioControlId);
any |= setIfPresent(command, "incomeFromInterestId", f::setIncomeFromInterestId);
any |= setIfPresent(command, "writeOffAccountId", f::setWriteOffAccountId);
any |= setIfPresent(command, "escheatLiabilityId", f::setEscheatLiabilityId);
any |= setIfPresent(command, "penaltiesReceivableAccountId", f::setPenaltiesReceivableAccountId);
any |= setIfPresent(command, "interestReceivableAccountId", f::setInterestReceivableAccountId);
any |= setIfPresent(command, "feesReceivableAccountId", f::setFeesReceivableAccountId);
any |= setIfPresent(command, "interestPayableAccountId", f::setInterestPayableAccountId);
return any;
}
public static void writeGlAccountMappingToJson(final JsonObject root, final GlAccountMappingEditFields f) {
putNumber(root, "savingsReferenceAccountId", f.getSavingsReferenceAccountId());
putNumber(root, "savingsControlAccountId", f.getSavingsControlAccountId());
putNumber(root, "incomeFromFeeAccountId", f.getIncomeFromFeeAccountId());
putNumber(root, "incomeFromPenaltyAccountId", f.getIncomeFromPenaltyAccountId());
putNumber(root, "interestOnSavingsAccountId", f.getInterestOnSavingsAccountId());
putNumber(root, "transfersInSuspenseAccountId", f.getTransfersInSuspenseAccountId());
putNumber(root, "overdraftPortfolioControlId", f.getOverdraftPortfolioControlId());
putNumber(root, "incomeFromInterestId", f.getIncomeFromInterestId());
putNumber(root, "writeOffAccountId", f.getWriteOffAccountId());
putNumber(root, "escheatLiabilityId", f.getEscheatLiabilityId());
putNumber(root, "penaltiesReceivableAccountId", f.getPenaltiesReceivableAccountId());
putNumber(root, "interestReceivableAccountId", f.getInterestReceivableAccountId());
putNumber(root, "feesReceivableAccountId", f.getFeesReceivableAccountId());
putNumber(root, "interestPayableAccountId", f.getInterestPayableAccountId());
}
// ---- charges (1:many, satellite table) --------------------------------------------------------
/** Returns null if "charges" wasn't present at all, else the (possibly empty) list of charge ids. */
public static List<Long> extractChargeIds(final JsonCommand command) {
if (!command.hasParameter("charges")) {
return null;
}
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("charges");
final List<Long> ids = new java.util.ArrayList<>();
if (raw != null && raw.isJsonArray()) {
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
if (el.isJsonObject() && el.getAsJsonObject().has("id")) {
ids.add(el.getAsJsonObject().get("id").getAsLong());
}
}
}
return ids;
}
public static void writeChargesToJson(final JsonObject root, final List<ProductPendingEditCharge> charges) {
final JsonArray array = new JsonArray();
for (final ProductPendingEditCharge charge : charges) {
final JsonObject entry = new JsonObject();
entry.addProperty("id", charge.getChargeId());
array.add(entry);
}
root.add("charges", array);
}
// ---- accountFacilityLimits (1:many, satellite table) -------------------------------------------
public static List<ProductPendingEditFacilityLimit> extractFacilityLimits(final JsonCommand command, final ProductPendingEdit edit) {
if (!command.hasParameter("accountFacilityLimits")) {
return null;
}
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("accountFacilityLimits");
final List<ProductPendingEditFacilityLimit> limits = new java.util.ArrayList<>();
if (raw != null && raw.isJsonArray()) {
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
final JsonObject o = el.getAsJsonObject();
limits.add(new ProductPendingEditFacilityLimit(edit, stringOrNull(o, "limitType"), stringOrNull(o, "generalLimit"),
stringOrNull(o, "minorAccountLimit"), stringOrNull(o, "smallSavingLimit")));
}
}
return limits;
}
public static void writeFacilityLimitsToJson(final JsonObject root, final List<ProductPendingEditFacilityLimit> limits) {
final JsonArray array = new JsonArray();
for (final ProductPendingEditFacilityLimit limit : limits) {
final JsonObject entry = new JsonObject();
entry.addProperty("limitType", limit.getLimitType());
entry.addProperty("generalLimit", limit.getGeneralLimit());
entry.addProperty("minorAccountLimit", limit.getMinorAccountLimit());
entry.addProperty("smallSavingLimit", limit.getSmallSavingLimit());
array.add(entry);
}
root.add("accountFacilityLimits", array);
}
// ---- GL array mappings (1:many, satellite table) -----------------------------------------------
public static List<ProductPendingEditGlMapping> extractGlArrayMappings(final JsonCommand command, final ProductPendingEdit edit) {
final List<ProductPendingEditGlMapping> mappings = new java.util.ArrayList<>();
boolean any = false;
any |= extractGlArray(command, edit, "paymentChannelToFundSourceMappings", ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL,
"paymentTypeId", mappings);
any |= extractGlArray(command, edit, "feeToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_FEE, "chargeId", mappings);
any |= extractGlArray(command, edit, "penaltyToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_PENALTY, "chargeId",
mappings);
return any ? mappings : null;
}
private static boolean extractGlArray(final JsonCommand command, final ProductPendingEdit edit, final String paramName,
final String mappingType, final String idFieldName, final List<ProductPendingEditGlMapping> out) {
if (!command.hasParameter(paramName)) {
return false;
}
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get(paramName);
if (raw != null && raw.isJsonArray()) {
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
final JsonObject o = el.getAsJsonObject();
final Long idFieldValue = o.has(idFieldName) ? o.get(idFieldName).getAsLong() : null;
final Long glAccountId = o.has("glAccountId") ? o.get("glAccountId").getAsLong() : null;
final Long paymentTypeId = "paymentTypeId".equals(idFieldName) ? idFieldValue : null;
final Long chargeId = "chargeId".equals(idFieldName) ? idFieldValue : null;
out.add(new ProductPendingEditGlMapping(edit, mappingType, paymentTypeId, chargeId, glAccountId));
}
}
return true;
}
public static void writeGlArrayMappingsToJson(final JsonObject root, final List<ProductPendingEditGlMapping> mappings) {
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL, "paymentChannelToFundSourceMappings",
"paymentTypeId");
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_FEE, "feeToIncomeAccountMappings", "chargeId");
writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PENALTY, "penaltyToIncomeAccountMappings", "chargeId");
}
private static void writeGlArrayGroup(final JsonObject root, final List<ProductPendingEditGlMapping> mappings, final String type,
final String paramName, final String idFieldName) {
final JsonArray array = new JsonArray();
for (final ProductPendingEditGlMapping mapping : mappings) {
if (!type.equals(mapping.getMappingType())) {
continue;
}
final JsonObject entry = new JsonObject();
final Long idFieldValue = "paymentTypeId".equals(idFieldName) ? mapping.getPaymentTypeId() : mapping.getChargeId();
entry.addProperty(idFieldName, idFieldValue);
entry.addProperty("glAccountId", mapping.getGlAccountId());
array.add(entry);
}
root.add(paramName, array);
}
// ---- small helpers -----------------------------------------------------------------------------
private static boolean booleanOf(final JsonCommand command, final String containerName, final String fieldName) {
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get(containerName);
if (raw == null || !raw.isJsonObject()) {
return false;
}
final JsonObject container = raw.getAsJsonObject();
return container.has(fieldName) && !container.get(fieldName).isJsonNull() && container.get(fieldName).getAsBoolean();
}
private static String stringOrNull(final JsonObject o, final String field) {
return (o.has(field) && !o.get(field).isJsonNull()) ? o.get(field).getAsString() : null;
}
private interface LongSetter {
void set(Long value);
}
private static boolean setIfPresent(final JsonCommand command, final String paramName, final LongSetter setter) {
if (!command.hasParameter(paramName)) {
return false;
}
setter.set(command.longValueOfParameterNamed(paramName));
return true;
}
private static void putString(final JsonObject root, final Set<String> submitted, final String name, final String value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putNumber(final JsonObject root, final Set<String> submitted, final String name, final Number value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putBoolean(final JsonObject root, final Set<String> submitted, final String name, final Boolean value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putDate(final JsonObject root, final Set<String> submitted, final String name, final LocalDate value) {
if (submitted.contains(name)) {
root.addProperty(name, value == null ? null : value.toString());
}
}
private static void putNumber(final JsonObject root, final String name, final Number value) {
if (value != null) {
root.addProperty(name, value);
}
}
}
@@ -0,0 +1,70 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/**
* One row per entry of the 3 array-shaped GL mappings (paymentChannelToFundSourceMappings,
* feeToIncomeAccountMappings, penaltyToIncomeAccountMappings), distinguished by mappingType.
*/
@Entity
@Table(name = "m_product_temp_gl_mapping")
@Getter
@Setter
@NoArgsConstructor
public class ProductPendingEditGlMapping extends AbstractPersistableCustom<Long> {
public static final String TYPE_PAYMENT_CHANNEL = "PAYMENT_CHANNEL";
public static final String TYPE_FEE = "FEE";
public static final String TYPE_PENALTY = "PENALTY";
@ManyToOne
@JoinColumn(name = "pending_edit_id", nullable = false)
private ProductPendingEdit pendingEdit;
@Column(name = "mapping_type", nullable = false)
private String mappingType;
@Column(name = "payment_type_id")
private Long paymentTypeId;
@Column(name = "charge_id")
private Long chargeId;
@Column(name = "gl_account_id", nullable = false)
private Long glAccountId;
public ProductPendingEditGlMapping(final ProductPendingEdit pendingEdit, final String mappingType, final Long paymentTypeId,
final Long chargeId, final Long glAccountId) {
this.pendingEdit = pendingEdit;
this.mappingType = mappingType;
this.paymentTypeId = paymentTypeId;
this.chargeId = chargeId;
this.glAccountId = glAccountId;
}
}
@@ -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.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductPendingEditGlMappingRepository extends JpaRepository<ProductPendingEditGlMapping, Long> {
List<ProductPendingEditGlMapping> findByPendingEditId(Long pendingEditId);
}
@@ -0,0 +1,66 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/** One row per entry of Share Product's "marketPrice" array (create/update/delete-by-id semantics). */
@Entity
@Table(name = "m_product_temp_share_market_price")
@Getter
@Setter
@NoArgsConstructor
public class ProductPendingEditMarketPrice extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "pending_edit_id", nullable = false)
private ProductPendingEdit pendingEdit;
/** id of the existing m_share_product_market_price row this entry updates/deletes, null if new. */
@Column(name = "source_id")
private Long sourceId;
@Column(name = "delete_flag", nullable = false)
private boolean deleteFlag;
@Column(name = "from_date")
private LocalDate fromDate;
@Column(name = "share_value")
private BigDecimal shareValue;
public ProductPendingEditMarketPrice(final ProductPendingEdit pendingEdit, final Long sourceId, final boolean deleteFlag,
final LocalDate fromDate, final BigDecimal shareValue) {
this.pendingEdit = pendingEdit;
this.sourceId = sourceId;
this.deleteFlag = deleteFlag;
this.fromDate = fromDate;
this.shareValue = shareValue;
}
}
@@ -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.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductPendingEditMarketPriceRepository extends JpaRepository<ProductPendingEditMarketPrice, Long> {
List<ProductPendingEditMarketPrice> findByPendingEditId(Long pendingEditId);
}
@@ -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 ProductPendingEditRepository extends JpaRepository<ProductPendingEdit, Long> {
Optional<ProductPendingEdit> findByProductTypeAndProductIdAndStatus(String productType, Long productId, String status);
List<ProductPendingEdit> findByProductTypeAndStatus(String productType, String status);
}
@@ -0,0 +1,56 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/**
* Records which top-level JSON parameter names were actually present in the maker's original
* request. Typed nullable columns can't by themselves distinguish "field omitted" from "field
* explicitly sent as null" from "field has a value" - and every existing command.hasParameter(...)
* branch in the write-services and assemblers depends on that distinction - so this table is the
* relational replacement for the information a raw JSON blob used to carry implicitly.
*/
@Entity
@Table(name = "m_product_temp_submitted_field")
@Getter
@Setter
@NoArgsConstructor
public class ProductPendingEditSubmittedField extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "pending_edit_id", nullable = false)
private ProductPendingEdit pendingEdit;
@Column(name = "field_name", nullable = false)
private String fieldName;
public ProductPendingEditSubmittedField(final ProductPendingEdit pendingEdit, final String fieldName) {
this.pendingEdit = pendingEdit;
this.fieldName = fieldName;
}
}
@@ -0,0 +1,34 @@
/**
* 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 org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductPendingEditSubmittedFieldRepository extends JpaRepository<ProductPendingEditSubmittedField, Long> {
List<ProductPendingEditSubmittedField> findByPendingEditId(Long pendingEditId);
@Query("SELECT f.fieldName FROM ProductPendingEditSubmittedField f WHERE f.pendingEdit.id = :pendingEditId")
List<String> findFieldNamesByPendingEditId(@Param("pendingEditId") Long pendingEditId);
}
@@ -0,0 +1,39 @@
/**
* 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.Embeddable;
import lombok.Getter;
import lombok.Setter;
@Embeddable
@Getter
@Setter
public class RecurringDepositEditFields {
@Column(name = "is_mandatory_deposit")
private Boolean isMandatoryDeposit;
@Column(name = "allow_withdrawal")
private Boolean allowWithdrawal;
@Column(name = "adjust_advance_towards_future_payments")
private Boolean adjustAdvanceTowardsFuturePayments;
}
@@ -0,0 +1,234 @@
/**
* 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.Embeddable;
import jakarta.persistence.Lob;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.Setter;
/**
* Scalar edit fields shared by Savings, Current, Fixed Deposit and Recurring Deposit products
* (FD/RD extend the Savings field set). Column names are the snake_case of the JSON API parameter
* name each field stores, not the underlying product table's own column name.
*/
@Embeddable
@Getter
@Setter
public class SavingsCoreEditFields {
@Column(name = "name")
private String name;
@Column(name = "short_name")
private String shortName;
@Column(name = "description")
private String description;
@Column(name = "currency_code")
private String currencyCode;
@Column(name = "digits_after_decimal")
private Integer digitsAfterDecimal;
@Column(name = "in_multiples_of")
private Integer inMultiplesOf;
@Column(name = "accounting_rule")
private Integer accountingRule;
@Column(name = "nominal_annual_interest_rate")
private BigDecimal nominalAnnualInterestRate;
@Column(name = "interest_compounding_period_type")
private Integer interestCompoundingPeriodType;
@Column(name = "interest_posting_period_type")
private Integer interestPostingPeriodType;
@Column(name = "interest_calculation_type")
private Integer interestCalculationType;
@Column(name = "interest_calculation_days_in_year_type")
private Integer interestCalculationDaysInYearType;
@Column(name = "min_required_opening_balance")
private BigDecimal minRequiredOpeningBalance;
@Column(name = "lockin_period_frequency")
private Integer lockinPeriodFrequency;
@Column(name = "lockin_period_frequency_type")
private Integer lockinPeriodFrequencyType;
@Column(name = "withdrawal_fee_for_transfers")
private Boolean withdrawalFeeForTransfers;
@Column(name = "allow_overdraft")
private Boolean allowOverdraft;
@Column(name = "overdraft_limit")
private BigDecimal overdraftLimit;
@Column(name = "nominal_annual_interest_rate_overdraft")
private BigDecimal nominalAnnualInterestRateOverdraft;
@Column(name = "min_overdraft_for_interest_calculation")
private BigDecimal minOverdraftForInterestCalculation;
@Column(name = "enforce_min_required_balance")
private Boolean enforceMinRequiredBalance;
@Column(name = "min_required_balance")
private BigDecimal minRequiredBalance;
@Column(name = "lien_allowed")
private Boolean lienAllowed;
@Column(name = "max_allowed_lien_limit")
private BigDecimal maxAllowedLienLimit;
@Column(name = "min_balance_for_interest_calculation")
private BigDecimal minBalanceForInterestCalculation;
@Column(name = "with_hold_tax")
private Boolean withHoldTax;
@Column(name = "tax_group_id")
private Long taxGroupId;
@Lob
@Column(name = "other_gl_mappings")
private String otherGlMappings;
@Column(name = "is_dormancy_tracking_active")
private Boolean isDormancyTrackingActive;
@Column(name = "days_to_inactive")
private Integer daysToInactive;
@Column(name = "days_to_dormancy")
private Integer daysToDormancy;
@Column(name = "days_to_escheat")
private Integer daysToEscheat;
@Column(name = "account_type")
private String accountType;
@Column(name = "customer_segment")
private String customerSegment;
@Column(name = "product_code")
private String productCode;
@Column(name = "gl_account_id")
private Long glAccountId;
@Column(name = "effective_date")
private LocalDate effectiveDate;
@Column(name = "effective_to")
private LocalDate effectiveTo;
@Column(name = "end_date")
private LocalDate endDate;
@Column(name = "introducer_required")
private Boolean introducerRequired;
@Column(name = "share_linking_enabled")
private Boolean shareLinkingEnabled;
@Column(name = "nomination_facility")
private Boolean nominationFacility;
@Column(name = "dormancy_alert")
private Boolean dormancyAlert;
@Column(name = "interest_calculate_selector")
private Integer interestCalculateSelector;
@Column(name = "average_period")
private String averagePeriod;
@Column(name = "auto_post")
private Boolean autoPost;
@Column(name = "frequency_fix_day")
private Integer frequencyFixDay;
@Column(name = "last_interest_date")
private LocalDate lastInterestDate;
@Column(name = "enable_lockin_period")
private Boolean enableLockinPeriod;
@Column(name = "balance_frequency")
private String balanceFrequency;
@Column(name = "account_closed_zero_balance_in_day_closing")
private Boolean accountClosedZeroBalanceInDayClosing;
@Column(name = "balance_swap_option")
private Boolean balanceSwapOption;
@Column(name = "msme")
private Boolean msme;
@Column(name = "auto_debit_facility")
private Boolean autoDebitFacility;
@Column(name = "loan_facility")
private Boolean loanFacility;
@Column(name = "premature_withdrawal_option")
private Boolean prematureWithdrawalOption;
@Column(name = "tenure_flexibility")
private Boolean tenureFlexibility;
@Column(name = "lien_marked")
private Boolean lienMarked;
@Column(name = "maturity_instructions")
private Boolean maturityInstructions;
@Column(name = "credit_maturity_proceeds")
private Boolean creditMaturityProceeds;
@Column(name = "penalty_grace_period_missed_installments")
private Boolean penaltyGracePeriodMissedInstallments;
@Column(name = "panel_request_applicable")
private Boolean panelRequestApplicable;
@Column(name = "no_penalty_nominee_heir_closure")
private Boolean noPenaltyNomineeHeirClosure;
@Column(name = "deceased_settlement")
private Boolean deceasedSettlement;
@Column(name = "allow_debit_txn_after_due_date")
private Boolean allowDebitTxnAfterDueDate;
}
@@ -0,0 +1,64 @@
/**
* 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.Embeddable;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
@Embeddable
@Getter
@Setter
public class ShareOnlyEditFields {
@Column(name = "total_shares")
private Long totalShares;
@Column(name = "total_shares_issued")
private Long totalSharesIssued;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "minimum_shares")
private Long minimumShares;
@Column(name = "nominal_shares")
private Long nominalShares;
@Column(name = "maximum_shares")
private Long maximumShares;
@Column(name = "allow_dividend_calc_for_inactive_clients")
private Boolean allowDividendCalcForInactiveClients;
@Column(name = "minimum_active_period_for_dividends")
private Integer minimumActivePeriodForDividends;
@Column(name = "minimum_active_period_frequency_type")
private Integer minimumActivePeriodFrequencyType;
@Column(name = "share_lockin_period")
private Integer shareLockinPeriod;
@Column(name = "share_lockin_period_frequency_type")
private Integer shareLockinPeriodFrequencyType;
}
@@ -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 SavingsProductEditPendingApprovalException extends AbstractPlatformDomainRuleException {
public SavingsProductEditPendingApprovalException(final Long productId) {
super("error.msg.savingsproduct.edit.pending.approval",
"Savings product with identifier " + productId + " already has an edit pending approval", productId);
}
}
@@ -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 SavingsProductNoPendingEditException extends AbstractPlatformDomainRuleException {
public SavingsProductNoPendingEditException(final Long productId) {
super("error.msg.savingsproduct.no.pending.edit",
"Savings product with identifier " + productId + " has no edit pending approval", productId);
}
}
@@ -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 SavingsProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
public SavingsProductSelfApprovalNotAllowedException(final Long productId) {
super("error.msg.savingsproduct.self.approval.not.allowed",
"The maker of the pending edit for savings product " + productId + " cannot also approve or reject it", productId);
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.service;
import java.util.Collection;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.DepositProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
public interface DepositProductReadPlatformService {
@@ -32,4 +33,8 @@ public interface DepositProductReadPlatformService {
DepositProductData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
Collection<SavingsProductTempData> retrieveAllPendingEdits(DepositAccountType depositAccountType);
SavingsProductTempData retrievePendingEdit(DepositAccountType depositAccountType, Long productId);
}
@@ -28,4 +28,10 @@ public interface FixedDepositProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command);
CommandProcessingResult delete(Long productId);
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
CommandProcessingResult approveEdit(Long productId);
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
}
@@ -28,4 +28,10 @@ public interface RecurringDepositProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command);
CommandProcessingResult delete(Long productId);
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
CommandProcessingResult approveEdit(Long productId);
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.service;
import java.util.Collection;
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
public interface SavingsProductReadPlatformService {
@@ -33,4 +34,8 @@ public interface SavingsProductReadPlatformService {
SavingsProductData retrieveOne(Long productId);
Collection<SavingsProductTempData> retrieveAllPendingEdits();
SavingsProductTempData retrievePendingEdit(Long productId);
}
@@ -28,4 +28,21 @@ public interface SavingsProductWritePlatformService {
CommandProcessingResult update(Long productId, JsonCommand command);
CommandProcessingResult delete(Long productId);
/**
* Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this
* product (that pending row is itself the lock).
*/
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
/**
* Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving
* user is the same user who submitted the edit.
*/
CommandProcessingResult approveEdit(Long productId);
/**
* Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit.
*/
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
}
@@ -39,4 +39,14 @@
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
<include file="parts/2015_add_senior_citizen_to_savings_account.xml" relativeToChangelogFile="true" />
<include file="parts/2016_add_share_linking_enabled_to_savings_account.xml" relativeToChangelogFile="true" />
<include file="parts/2017_add_saving_product_temp_table.xml" relativeToChangelogFile="true" />
<include file="parts/2018_fix_saving_product_temp_lock_constraint.xml" relativeToChangelogFile="true" />
<include file="parts/2019_add_command_source_link_to_saving_product_temp.xml" relativeToChangelogFile="true" />
<include file="parts/2020_generalize_product_temp_table.xml" relativeToChangelogFile="true" />
<include file="parts/2021_add_fd_rd_approval_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2022_add_product_checker_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2023_fix_product_approval_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2025_recreate_savings_product_temp_table.xml" relativeToChangelogFile="true" />
<include file="parts/2026_add_pending_edit_typed_columns.xml" relativeToChangelogFile="true" />
<include file="parts/2027_add_pending_edit_satellite_tables.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -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">
<changeSet author="fineract" id="2017-1">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_savings_product_temp" />
</not>
</preConditions>
<createTable tableName="m_savings_product_temp">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="savings_product_id" type="BIGINT">
<constraints nullable="false" unique="true" uniqueConstraintName="sp_temp_unq_savings_product_id"
foreignKeyName="fk_sp_temp_savings_product_id" references="m_savings_product(id)" />
</column>
<column name="maker_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_sp_temp_maker_id" references="m_appuser(id)" />
</column>
<column name="checker_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_sp_temp_checker_id" references="m_appuser(id)" />
</column>
<column name="created_on" type="DATETIME">
<constraints nullable="false" />
</column>
<column name="checked_on" 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)">
<constraints nullable="true" />
</column>
<column name="proposed_payload_json" type="CLOB">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
<changeSet author="fineract" id="2017-2">
<preConditions onFail="MARK_RAN">
<not>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT'</sqlCheck>
</not>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_SAVINGSPRODUCT" />
<column name="entity_name" value="SAVINGSPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_SAVINGSPRODUCT" />
<column name="entity_name" value="SAVINGSPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,54 @@
<?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">
<!--
The original unique constraint on savings_product_id blocked ANY future edit on a product forever, once a
single pending edit had been resolved - because resolved rows are kept (not deleted) for audit history.
The lock must only apply while status = PENDING. Unique constraints treat NULL as distinct in both
PostgreSQL and MySQL/MariaDB, so a nullable "lock key" column - set to the product id while pending, and
cleared back to NULL on approve/reject - gives a correct, cross-database concurrency-safe lock.
-->
<changeSet author="fineract" id="2018-1">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_savings_product_temp" columnName="savings_product_id" />
<not>
<columnExists tableName="m_savings_product_temp" columnName="pending_lock_key" />
</not>
</preConditions>
<dropUniqueConstraint tableName="m_savings_product_temp" constraintName="sp_temp_unq_savings_product_id" />
<addColumn tableName="m_savings_product_temp">
<column name="pending_lock_key" type="BIGINT">
<constraints nullable="true" />
</column>
</addColumn>
<sql>UPDATE m_savings_product_temp SET pending_lock_key = savings_product_id WHERE status = 'PENDING'</sql>
<addUniqueConstraint tableName="m_savings_product_temp" columnNames="pending_lock_key"
constraintName="sp_temp_unq_pending_lock_key" />
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,44 @@
<?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">
<!--
Links each pending edit to the audit-only m_portfolio_command_source row created for it, so that
submit/approve/reject also appear in System -> Audit Trails even though this feature bypasses the native
maker-checker execution pipeline.
-->
<changeSet author="fineract" id="2019-1">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="command_source_id" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="command_source_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_sp_temp_command_source_id"
references="m_portfolio_command_source(id)" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,107 @@
<?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">
<!--
Generalizes m_savings_product_temp (kept as one shared table, per requirement) so it can hold pending edits
for Current Account Products (m_current_product) and Share Products (m_share_product) as well as Savings
Products (m_savings_product) - not just savings. A single BIGINT "product_id" can't carry a normal FK
anymore since it now points at three different tables depending on product_type, so the old FK is dropped
(application code, not the DB, is now responsible for referential validity here).
pending_lock_key changes from BIGINT to VARCHAR because a raw numeric id is no longer unique across product
types (e.g. a current account product id=3 and a savings product id=3 must not collide). The new format is
"<PRODUCT_TYPE>:<product_id>", e.g. "SAVINGS:3" / "CURRENT:3" / "SHARE:3".
-->
<changeSet author="fineract" id="2020-1">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="product_type" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="product_type" type="VARCHAR(20)" defaultValue="SAVINGS">
<constraints nullable="false" />
</column>
</addColumn>
<dropForeignKeyConstraint baseTableName="m_savings_product_temp" constraintName="fk_sp_temp_savings_product_id" />
<renameColumn tableName="m_savings_product_temp" oldColumnName="savings_product_id" newColumnName="product_id"
columnDataType="BIGINT" />
<dropUniqueConstraint tableName="m_savings_product_temp" constraintName="sp_temp_unq_pending_lock_key" />
<addColumn tableName="m_savings_product_temp">
<column name="pending_lock_key_new" type="VARCHAR(40)">
<constraints nullable="true" />
</column>
</addColumn>
<sql dbms="postgresql">UPDATE m_savings_product_temp SET pending_lock_key_new = 'SAVINGS:' || product_id WHERE status = 'PENDING'</sql>
<sql dbms="mysql,mariadb">UPDATE m_savings_product_temp SET pending_lock_key_new = CONCAT('SAVINGS:', product_id) WHERE status = 'PENDING'</sql>
<dropColumn tableName="m_savings_product_temp" columnName="pending_lock_key" />
<renameColumn tableName="m_savings_product_temp" oldColumnName="pending_lock_key_new" newColumnName="pending_lock_key"
columnDataType="VARCHAR(40)" />
<addUniqueConstraint tableName="m_savings_product_temp" columnNames="pending_lock_key"
constraintName="sp_temp_unq_pending_lock_key" />
</changeSet>
<changeSet author="fineract" id="2020-2">
<preConditions onFail="MARK_RAN">
<not>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT'</sqlCheck>
</not>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_CURRENTPRODUCT" />
<column name="entity_name" value="CURRENTPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_CURRENTPRODUCT" />
<column name="entity_name" value="CURRENTPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_SHAREPRODUCT" />
<column name="entity_name" value="SHAREPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_SHAREPRODUCT" />
<column name="entity_name" value="SHAREPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="2021-1">
<preConditions onFail="MARK_RAN">
<not>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT'</sqlCheck>
</not>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_FIXEDDEPOSITPRODUCT" />
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_FIXEDDEPOSITPRODUCT" />
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_RECURRINGDEPOSITPRODUCT" />
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_RECURRINGDEPOSITPRODUCT" />
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,103 @@
<?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">
<!--
AuditReadPlatformServiceImpl.retrieveAllEntriesToBeChecked() (backing GET /v1/makercheckers, the "Checker
Inbox" tab) shows a pending m_portfolio_command_source row to a "limited" checker only if their role holds
a permission whose action_name is "<ACTION>_CHECKER" and entity_name matches the row's entity_name (code
convention: "<ACTION>_<ENTITY>_CHECKER", matching how core Fineract permissions like UPDATE_ROLE_CHECKER
are seeded). Our custom pending-approval CommandSource rows for product edits (SAVINGSPRODUCT,
CURRENTPRODUCT, SHAREPRODUCT, FIXEDDEPOSITPRODUCT, RECURRINGDEPOSITPRODUCT) already satisfy the base
status=AWAITING_APPROVAL filter, but without these permission rows a limited checker's role has nothing to
match against, so the rows never show up in Checker Inbox - only in our own Pending Product Edits screen.
UPDATE_SAVINGSPRODUCT_CHECKER already exists as a native Fineract permission, so each row below is its own
changeset with its own precondition - one already-existing code must not block inserting the others.
-->
<changeSet author="fineract" id="2022-1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SAVINGSPRODUCT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="UPDATE_SAVINGSPRODUCT_CHECKER" />
<column name="entity_name" value="SAVINGSPRODUCT" />
<column name="action_name" value="UPDATE_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2022-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_CURRENTPRODUCT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="UPDATE_CURRENTPRODUCT_CHECKER" />
<column name="entity_name" value="CURRENTPRODUCT" />
<column name="action_name" value="UPDATE_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2022-3">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SHAREPRODUCT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="UPDATE_SHAREPRODUCT_CHECKER" />
<column name="entity_name" value="SHAREPRODUCT" />
<column name="action_name" value="UPDATE_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2022-4">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_FIXEDDEPOSITPRODUCT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="UPDATE_FIXEDDEPOSITPRODUCT_CHECKER" />
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
<column name="action_name" value="UPDATE_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2022-5">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_RECURRINGDEPOSITPRODUCT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="UPDATE_RECURRINGDEPOSITPRODUCT_CHECKER" />
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
<column name="action_name" value="UPDATE_CHECKER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,165 @@
<?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">
<!--
Changesets 2017-2, 2020-2 and 2021-1 used an inverted precondition - `<not><sqlCheck expectedResult="0">...`
actually means "run only if the row ALREADY exists", the opposite of the intended idempotent-insert guard.
On a fresh database (count=0) that precondition evaluates to false, so onFail="MARK_RAN" silently skipped
the insert entirely - the APPROVE_*/REJECT_* permissions for all five product types were marked as applied
in databasechangelog but never actually inserted. Those changesets are already recorded and must not be
edited (Liquibase would fail on checksum mismatch), so this migration re-inserts the missing rows under
fresh changeset ids, each with a correct (non-inverted) precondition and split per row so one already-
existing code can never block the others.
-->
<changeSet author="fineract" id="2023-1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_SAVINGSPRODUCT" />
<column name="entity_name" value="SAVINGSPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SAVINGSPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_SAVINGSPRODUCT" />
<column name="entity_name" value="SAVINGSPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-3">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_CURRENTPRODUCT" />
<column name="entity_name" value="CURRENTPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-4">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_CURRENTPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_CURRENTPRODUCT" />
<column name="entity_name" value="CURRENTPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-5">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SHAREPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_SHAREPRODUCT" />
<column name="entity_name" value="SHAREPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-6">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SHAREPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_SHAREPRODUCT" />
<column name="entity_name" value="SHAREPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-7">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_FIXEDDEPOSITPRODUCT" />
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-8">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_FIXEDDEPOSITPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_FIXEDDEPOSITPRODUCT" />
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-9">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_RECURRINGDEPOSITPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="APPROVE_RECURRINGDEPOSITPRODUCT" />
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
<column name="action_name" value="APPROVE" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
<changeSet author="fineract" id="2023-10">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_RECURRINGDEPOSITPRODUCT'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation" />
<column name="code" value="REJECT_RECURRINGDEPOSITPRODUCT" />
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
<column name="action_name" value="REJECT" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,78 @@
<?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">
<!--
m_savings_product_temp was dropped by an experimental changeset that was later removed from source without
restoring the table. Recreates it with the final shape from the 2017/2018/2019/2020 migrations (guarded so
it's a no-op wherever the table already exists) so ProductPendingEditRepository and the isPendingApproval
subqueries in the product read services stop failing with "relation does not exist".
-->
<changeSet author="fineract" id="2025-1">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_savings_product_temp" />
</not>
</preConditions>
<createTable tableName="m_savings_product_temp">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="product_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="product_type" type="VARCHAR(20)" defaultValue="SAVINGS">
<constraints nullable="false" />
</column>
<column name="maker_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_sp_temp_maker_id" references="m_appuser(id)" />
</column>
<column name="checker_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_sp_temp_checker_id" references="m_appuser(id)" />
</column>
<column name="created_on" type="DATETIME">
<constraints nullable="false" />
</column>
<column name="checked_on" 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)">
<constraints nullable="true" />
</column>
<column name="proposed_payload_json" type="CLOB">
<constraints nullable="false" />
</column>
<column name="pending_lock_key" type="VARCHAR(40)">
<constraints nullable="true" unique="true" uniqueConstraintName="sp_temp_unq_pending_lock_key" />
</column>
<column name="command_source_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_sp_temp_command_source_id"
references="m_portfolio_command_source(id)" />
</column>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,261 @@
<?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">
<!--
Adds typed columns to m_savings_product_temp so a pending product edit is stored as real,
individually-inspectable columns instead of only the opaque proposed_payload_json blob.
proposed_payload_json is kept (still written) for a safe deprecation window; nothing here
removes or alters any existing column. Every changeset is purely additive (addColumn only)
and guarded with a correctly-shaped precondition (<not><columnExists .../></not> - a
boolean precondition, unlike the historical bug in 2017-2/2020-2 which wrapped a numeric
<sqlCheck expectedResult="0"> in <not>, inverting its meaning).
Columns are grouped by which product type(s) apply them; unrelated columns are simply NULL
for a given row's product_type. Names are the snake_case of the JSON API parameter name the
column stores, not the source product table's own column name, since this table stores
proposed *edits* (parameter values), not a mirror of product-table storage.
-->
<!-- Group 1: fields shared across Savings, Current, FD and RD (FD/RD extend Savings) -->
<changeSet author="fineract" id="2026-1">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="name" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="name" type="VARCHAR(100)" />
<column name="short_name" type="VARCHAR(20)" />
<column name="description" type="VARCHAR(500)" />
<column name="currency_code" type="VARCHAR(3)" />
<column name="digits_after_decimal" type="INTEGER" />
<column name="in_multiples_of" type="INTEGER" />
<column name="accounting_rule" type="INTEGER" />
<column name="nominal_annual_interest_rate" type="DECIMAL(19,6)" />
<column name="interest_compounding_period_type" type="INTEGER" />
<column name="interest_posting_period_type" type="INTEGER" />
<column name="interest_calculation_type" type="INTEGER" />
<column name="interest_calculation_days_in_year_type" type="INTEGER" />
<column name="min_required_opening_balance" type="DECIMAL(19,6)" />
<column name="lockin_period_frequency" type="INTEGER" />
<column name="lockin_period_frequency_type" type="INTEGER" />
<column name="withdrawal_fee_for_transfers" type="BOOLEAN" />
<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="enforce_min_required_balance" type="BOOLEAN" />
<column name="min_required_balance" type="DECIMAL(19,6)" />
<column name="lien_allowed" type="BOOLEAN" />
<column name="max_allowed_lien_limit" type="DECIMAL(19,6)" />
<column name="min_balance_for_interest_calculation" type="DECIMAL(19,6)" />
<column name="with_hold_tax" type="BOOLEAN" />
<column name="tax_group_id" type="BIGINT" />
<column name="other_gl_mappings" type="CLOB" />
<column name="is_dormancy_tracking_active" type="BOOLEAN" />
<column name="days_to_inactive" type="INTEGER" />
<column name="days_to_dormancy" type="INTEGER" />
<column name="days_to_escheat" type="INTEGER" />
<column name="account_type" type="VARCHAR(50)" />
<column name="customer_segment" type="VARCHAR(50)" />
<column name="product_code" type="VARCHAR(20)" />
<column name="gl_account_id" type="BIGINT" />
<column name="effective_date" type="DATE" />
<column name="effective_to" type="DATE" />
<column name="end_date" type="DATE" />
<column name="introducer_required" type="BOOLEAN" />
<column name="share_linking_enabled" type="BOOLEAN" />
<column name="nomination_facility" type="BOOLEAN" />
<column name="dormancy_alert" type="BOOLEAN" />
<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="last_interest_date" type="DATE" />
<column name="enable_lockin_period" type="BOOLEAN" />
<column name="balance_frequency" type="VARCHAR(50)" />
<column name="account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
<column name="balance_swap_option" type="BOOLEAN" />
<column name="msme" type="BOOLEAN" />
<column name="auto_debit_facility" type="BOOLEAN" />
<column name="loan_facility" type="BOOLEAN" />
<column name="premature_withdrawal_option" type="BOOLEAN" />
<column name="tenure_flexibility" type="BOOLEAN" />
<column name="lien_marked" type="BOOLEAN" />
<column name="maturity_instructions" type="BOOLEAN" />
<column name="credit_maturity_proceeds" type="BOOLEAN" />
<column name="penalty_grace_period_missed_installments" type="BOOLEAN" />
<column name="panel_request_applicable" type="BOOLEAN" />
<column name="no_penalty_nominee_heir_closure" type="BOOLEAN" />
<column name="deceased_settlement" type="BOOLEAN" />
<column name="allow_debit_txn_after_due_date" type="BOOLEAN" />
</addColumn>
</changeSet>
<!-- Group 2: Current-product-only fields -->
<changeSet author="fineract" id="2026-2">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="avg_balance_calc_period" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="avg_balance_calc_period" type="VARCHAR(10)" />
<column name="valid_for_period" type="VARCHAR(10)" />
<column name="od_account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
</addColumn>
</changeSet>
<!-- Group 3: Share-product-only fields -->
<changeSet author="fineract" id="2026-3">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="total_shares" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="total_shares" type="BIGINT" />
<column name="total_shares_issued" type="BIGINT" />
<column name="unit_price" type="DECIMAL(19,6)" />
<column name="minimum_shares" type="BIGINT" />
<column name="nominal_shares" type="BIGINT" />
<column name="maximum_shares" type="BIGINT" />
<column name="allow_dividend_calc_for_inactive_clients" type="BOOLEAN" />
<column name="minimum_active_period_for_dividends" type="INTEGER" />
<column name="minimum_active_period_frequency_type" type="INTEGER" />
<column name="share_lockin_period" type="INTEGER" />
<column name="share_lockin_period_frequency_type" type="INTEGER" />
</addColumn>
</changeSet>
<!-- Group 4: Fixed-Deposit-only fields (also used by Recurring Deposit, which extends FD) -->
<changeSet author="fineract" id="2026-4">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="fd_type" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="fd_type" type="VARCHAR(20)" />
<column name="pre_closure_penal_applicable" type="BOOLEAN" />
<column name="pre_closure_penal_interest" type="DECIMAL(19,6)" />
<column name="pre_closure_penal_interest_on_type_id" type="INTEGER" />
<column name="min_deposit_term" type="INTEGER" />
<column name="max_deposit_term" type="INTEGER" />
<column name="min_deposit_term_type_id" type="INTEGER" />
<column name="max_deposit_term_type_id" type="INTEGER" />
<column name="in_multiples_of_deposit_term" type="INTEGER" />
<column name="in_multiples_of_deposit_term_type_id" type="INTEGER" />
<column name="min_deposit_amount" type="DECIMAL(19,6)" />
<column name="max_deposit_amount" type="DECIMAL(19,6)" />
<column name="deposit_amount" type="DECIMAL(19,6)" />
</addColumn>
</changeSet>
<!-- Group 5: Recurring-Deposit-only fields -->
<changeSet author="fineract" id="2026-5">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="is_mandatory_deposit" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="is_mandatory_deposit" type="BOOLEAN" />
<column name="allow_withdrawal" type="BOOLEAN" />
<column name="adjust_advance_towards_future_payments" type="BOOLEAN" />
</addColumn>
</changeSet>
<!-- Group 6: accountFacilityConfig (1:1 today - kept as flat columns, fc_ prefix) -->
<changeSet author="fineract" id="2026-6">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="fc_atm_facility" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="fc_atm_facility" type="BOOLEAN" />
<column name="fc_atm_withdrawal_facility" type="BOOLEAN" />
<column name="fc_upi_facility" type="BOOLEAN" />
<column name="fc_pos_transactions_facility" type="BOOLEAN" />
<column name="fc_cheque_book" type="BOOLEAN" />
<column name="fc_imps_facility" type="BOOLEAN" />
<column name="fc_sms_alerts" type="BOOLEAN" />
<column name="fc_bbps_facility" type="BOOLEAN" />
<column name="fc_insurance_cover" type="BOOLEAN" />
<column name="fc_bepg_facility" type="BOOLEAN" />
<column name="fc_neft_facility" type="BOOLEAN" />
<column name="fc_rtgs_facility" type="BOOLEAN" />
<column name="fc_inter_bank_total_cap_facility" type="BOOLEAN" />
<column name="fc_withdrawal_facility" type="BOOLEAN" />
<column name="fc_deposit_facility_kyc" type="BOOLEAN" />
<column name="fc_virtual_debit_card" type="BOOLEAN" />
</addColumn>
</changeSet>
<!-- Group 7: depositLimitKyc (1:1 today - kept as flat columns, kyc_ prefix) -->
<changeSet author="fineract" id="2026-7">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="kyc_general_type" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="kyc_general_type" type="VARCHAR(50)" />
<column name="kyc_general_limit" type="VARCHAR(50)" />
<column name="kyc_minor_type" type="VARCHAR(50)" />
<column name="kyc_minor_limit" type="BIGINT" />
<column name="kyc_small_saving_type" type="VARCHAR(50)" />
<column name="kyc_small_saving_limit" type="BIGINT" />
</addColumn>
</changeSet>
<!-- Group 8: GL account mapping scalar ids (1:1 today - kept as flat columns, gl_ prefix). No FK
constraints - the existing validator already checks GL account validity at submit time. -->
<changeSet author="fineract" id="2026-8">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product_temp" columnName="gl_savings_reference_account_id" />
</not>
</preConditions>
<addColumn tableName="m_savings_product_temp">
<column name="gl_savings_reference_account_id" type="BIGINT" />
<column name="gl_savings_control_account_id" type="BIGINT" />
<column name="gl_income_from_fee_account_id" type="BIGINT" />
<column name="gl_income_from_penalty_account_id" type="BIGINT" />
<column name="gl_interest_on_savings_account_id" type="BIGINT" />
<column name="gl_transfers_in_suspense_account_id" type="BIGINT" />
<column name="gl_overdraft_portfolio_control_id" type="BIGINT" />
<column name="gl_income_from_interest_id" type="BIGINT" />
<column name="gl_write_off_account_id" type="BIGINT" />
<column name="gl_escheat_liability_id" type="BIGINT" />
<column name="gl_penalties_receivable_account_id" type="BIGINT" />
<column name="gl_interest_receivable_account_id" type="BIGINT" />
<column name="gl_fees_receivable_account_id" type="BIGINT" />
<column name="gl_interest_payable_account_id" type="BIGINT" />
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,150 @@
<?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">
<!--
Satellite tables for the genuine one-to-many pieces of a pending product edit (charges,
facility limits, GL array mappings, share market prices) plus a "submitted field" manifest
that records which top-level JSON parameters were actually present in the maker's request.
The manifest table is not optional bookkeeping: raw JSON storage implicitly distinguishes
"field omitted" / "field explicitly null" / "field has a value", and every existing
command.hasParameter(...) branch in the write-services and assemblers depends on that
distinction. Typed nullable columns alone can't represent it, so this table is the
relational replacement for that information - one row per parameter name that was present
at submit time, per pending edit.
All tables reference m_savings_product_temp(id) - no cascade delete, since pending-edit
rows are never hard-deleted (only status-flipped) once resolved.
-->
<changeSet author="fineract" id="2027-1">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_product_temp_charge" />
</not>
</preConditions>
<createTable tableName="m_product_temp_charge">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="pending_edit_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_ptc_pending_edit_id" references="m_savings_product_temp(id)" />
</column>
<column name="charge_id" type="BIGINT">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
<changeSet author="fineract" id="2027-2">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_product_temp_facility_limit" />
</not>
</preConditions>
<createTable tableName="m_product_temp_facility_limit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="pending_edit_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_ptfl_pending_edit_id" references="m_savings_product_temp(id)" />
</column>
<column name="limit_type" type="VARCHAR(100)">
<constraints nullable="false" />
</column>
<column name="general_limit" type="VARCHAR(50)" />
<column name="minor_account_limit" type="VARCHAR(50)" />
<column name="small_saving_limit" type="VARCHAR(50)" />
</createTable>
</changeSet>
<changeSet author="fineract" id="2027-3">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_product_temp_gl_mapping" />
</not>
</preConditions>
<createTable tableName="m_product_temp_gl_mapping">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="pending_edit_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_ptgm_pending_edit_id" references="m_savings_product_temp(id)" />
</column>
<!-- PAYMENT_CHANNEL / FEE / PENALTY -->
<column name="mapping_type" type="VARCHAR(30)">
<constraints nullable="false" />
</column>
<column name="payment_type_id" type="BIGINT" />
<column name="charge_id" type="BIGINT" />
<column name="gl_account_id" type="BIGINT">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
<changeSet author="fineract" id="2027-4">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_product_temp_share_market_price" />
</not>
</preConditions>
<createTable tableName="m_product_temp_share_market_price">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="pending_edit_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_ptsmp_pending_edit_id" references="m_savings_product_temp(id)" />
</column>
<column name="source_id" type="BIGINT" />
<column name="delete_flag" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false" />
</column>
<column name="from_date" type="DATE" />
<column name="share_value" type="DECIMAL(19,6)" />
</createTable>
</changeSet>
<changeSet author="fineract" id="2027-5">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="m_product_temp_submitted_field" />
</not>
</preConditions>
<createTable tableName="m_product_temp_submitted_field">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="pending_edit_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_ptsf_pending_edit_id" references="m_savings_product_temp(id)" />
</column>
<column name="field_name" type="VARCHAR(100)">
<constraints nullable="false" />
</column>
</createTable>
<addUniqueConstraint tableName="m_product_temp_submitted_field" columnNames="pending_edit_id, field_name"
constraintName="ptsf_unq_pending_edit_field" />
</changeSet>
</databaseChangeLog>