# Savings Product Payload Implementation Guide ## Overview Your UI has been updated to send a comprehensive new payload with additional fields for the `/savingsproducts` POST endpoint. This document outlines **all changes required** in the backend to support these new fields. --- ## 🔍 New Fields in Your Payload ### **Simple Fields (Direct Columns in m_savings_product table)** | Field | Type | Example | Description | |-------|------|---------|-------------| | accountType | String | "Small Saving Account" | Type of savings account | | customerSegment | String | "Individual" | Customer classification | | productCode | String | "SB-IND-001" | Product identifier code | | effectiveDate | Date | "10 April 2026" | When the product becomes effective | | effectiveTo | Date | "10 April 2026" | Validity end date | | endDate | Date | "10 April 2026" | Product end date | | introducerRequired | Boolean | true | If introducer/referrer is mandatory | | shareLinkingEnabled | Boolean | true | If share account linking allowed | | nominationFacility | Boolean | true | If nomination facility available | | dormancyAlert | Boolean | true | If dormancy alerts enabled | | interestCalculateSelector | Integer | 2 | Interest calculation method selector | | averagePeriod | String | "Monthly" | Period for averaging balance | | autoPost | Boolean | true | Auto-post interest | | frequencyFixDay | Integer | 30 | Fixed day for frequency | | lastInterestDate | Date | "10 April 2026" | Last interest posting date | | enableLockinPeriod | Boolean | true | If lock-in period enforced | ### **Complex Objects (Require Separate Tables)** #### 1️⃣ **accountFacilityConfig** (Single object with facility booleans) ```json { "atmFacility": true, "atmWithdrawalFacility": true, "upiFacility": true, "posTransactionsFacility": true, "chequeBook": true, "impsFacility": true, "smsAlerts": true, "bbpsFacility": true, "insuranceCover": true, "bepgFacility": true, "neftFacility": true, "rtgsFacility": true, "interBankTotalCapFacility": true, "withdrawalFacility": true, "depositFacilityKyc": true, "virtualDebitCard": true } ``` → **Table:** `m_savings_product_facility_config` (One-to-One relationship) #### 2️⃣ **accountFacilityLimits** (Array of limit objects) ```json [ {"limitType": "ATM Withdrawal", "generalLimit": 40000, "minorAccountLimit": 5000, "smallSavingLimit": 10000}, {"limitType": "POS Transactions", "generalLimit": 100000, "minorAccountLimit": 2000, "smallSavingLimit": 10000}, ... ] ``` → **Table:** `m_savings_product_facility_limits` (One-to-Many relationship) #### 3️⃣ **depositLimitKyc** (Nested KYC limit rules) ```json { "general": {"kycType": "Full KYC", "limit": "No Limit"}, "minor": {"kycType": "Guardian KYC", "limit": 100000}, "smallSaving": {"kycType": "Min KYC", "limit": 50000} } ``` → **Table:** `m_savings_product_deposit_limit_kyc` (One-to-One relationship, or JSON column) --- ## 📋 Database Changes Required ### **1. Alter m_savings_product table** ```sql ALTER TABLE m_savings_product ADD COLUMN ( account_type VARCHAR(50), customer_segment VARCHAR(50), product_code VARCHAR(20) UNIQUE, effective_date DATE, effective_to DATE, end_date DATE, introducer_required BOOLEAN DEFAULT FALSE, share_linking_enabled BOOLEAN DEFAULT FALSE, nomination_facility BOOLEAN DEFAULT FALSE, dormancy_alert BOOLEAN DEFAULT FALSE, interest_calculate_selector INT, average_period VARCHAR(20), auto_post BOOLEAN DEFAULT FALSE, frequency_fix_day INT, last_interest_date DATE, enable_lockin_period BOOLEAN DEFAULT FALSE ); ``` ### **2. Create m_savings_product_facility_config table** (One-to-One) ```sql CREATE TABLE m_savings_product_facility_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, savings_product_id BIGINT NOT NULL UNIQUE, atm_facility BOOLEAN DEFAULT FALSE, atm_withdrawal_facility BOOLEAN DEFAULT FALSE, upi_facility BOOLEAN DEFAULT FALSE, pos_transactions_facility BOOLEAN DEFAULT FALSE, cheque_book BOOLEAN DEFAULT FALSE, imps_facility BOOLEAN DEFAULT FALSE, sms_alerts BOOLEAN DEFAULT FALSE, bbps_facility BOOLEAN DEFAULT FALSE, insurance_cover BOOLEAN DEFAULT FALSE, bepg_facility BOOLEAN DEFAULT FALSE, neft_facility BOOLEAN DEFAULT FALSE, rtgs_facility BOOLEAN DEFAULT FALSE, inter_bank_total_cap_facility BOOLEAN DEFAULT FALSE, withdrawal_facility BOOLEAN DEFAULT FALSE, deposit_facility_kyc BOOLEAN DEFAULT FALSE, virtual_debit_card BOOLEAN DEFAULT FALSE, created_date DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE ); ``` ### **3. Create m_savings_product_facility_limits table** (One-to-Many) ```sql CREATE TABLE m_savings_product_facility_limits ( id BIGINT PRIMARY KEY AUTO_INCREMENT, savings_product_id BIGINT NOT NULL, limit_type VARCHAR(100) NOT NULL, general_limit VARCHAR(50), -- Can be decimal or "N/A", "UNLIMITED" minor_account_limit VARCHAR(50), small_saving_limit VARCHAR(50), created_date DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE, UNIQUE KEY unique_product_limit_type (savings_product_id, limit_type) ); ``` ### **4. Create m_savings_product_deposit_limit_kyc table** (One-to-One) ```sql CREATE TABLE m_savings_product_deposit_limit_kyc ( id BIGINT PRIMARY KEY AUTO_INCREMENT, savings_product_id BIGINT NOT NULL UNIQUE, general_kyc_type VARCHAR(50), general_limit VARCHAR(50), -- "No Limit" or numeric minor_kyc_type VARCHAR(50), minor_limit BIGINT, small_saving_kyc_type VARCHAR(50), small_saving_limit BIGINT, created_date DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (savings_product_id) REFERENCES m_savings_product(id) ON DELETE CASCADE ); ``` --- ## 🏗️ Java Entity Classes to Create/Update ### **1. Update SavingsProduct Entity** Add these fields to `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java`: ```java @Column(name = "account_type", length = 50) private String accountType; @Column(name = "customer_segment", length = 50) private String customerSegment; @Column(name = "product_code", length = 20, unique = true) private String productCode; @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", length = 20) 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; // One-to-One relationship @OneToOne(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true) private SavingsProductFacilityConfig facilityConfig; // One-to-Many relationship @OneToMany(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true) private Set facilityLimits = new HashSet<>(); // One-to-One relationship @OneToOne(cascade = CascadeType.ALL, mappedBy = "savingsProduct", orphanRemoval = true) private SavingsProductDepositLimitKyc depositLimitKyc; ``` ### **2. Create SavingsProductFacilityConfig Entity** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductFacilityConfig.java`: ```java @Entity @Table(name = "m_savings_product_facility_config") public class SavingsProductFacilityConfig extends AbstractPersistableCustom { @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "savings_product_id", nullable = false) private SavingsProduct savingsProduct; @Column(name = "atm_facility") private Boolean atmFacility = false; @Column(name = "atm_withdrawal_facility") private Boolean atmWithdrawalFacility = false; @Column(name = "upi_facility") private Boolean upiFacility = false; @Column(name = "pos_transactions_facility") private Boolean posTransactionsFacility = false; @Column(name = "cheque_book") private Boolean chequeBook = false; @Column(name = "imps_facility") private Boolean impsFacility = false; @Column(name = "sms_alerts") private Boolean smsAlerts = false; @Column(name = "bbps_facility") private Boolean bbpsFacility = false; @Column(name = "insurance_cover") private Boolean insuranceCover = false; @Column(name = "bepg_facility") private Boolean bepgFacility = false; @Column(name = "neft_facility") private Boolean neftFacility = false; @Column(name = "rtgs_facility") private Boolean rtgsFacility = false; @Column(name = "inter_bank_total_cap_facility") private Boolean interBankTotalCapFacility = false; @Column(name = "withdrawal_facility") private Boolean withdrawalFacility = false; @Column(name = "deposit_facility_kyc") private Boolean depositFacilityKyc = false; @Column(name = "virtual_debit_card") private Boolean virtualDebitCard = false; // Getters and setters } ``` ### **3. Create SavingsProductFacilityLimit Entity** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductFacilityLimit.java`: ```java @Entity @Table(name = "m_savings_product_facility_limits") public class SavingsProductFacilityLimit extends AbstractPersistableCustom { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "savings_product_id", nullable = false) private SavingsProduct savingsProduct; @Column(name = "limit_type", length = 100, nullable = false) private String limitType; @Column(name = "general_limit", length = 50) private String generalLimit; // Can be "UNLIMITED", "N/A", or numeric value @Column(name = "minor_account_limit", length = 50) private String minorAccountLimit; @Column(name = "small_saving_limit", length = 50) private String smallSavingLimit; // Getters and setters public static SavingsProductFacilityLimit create( SavingsProduct savingsProduct, String limitType, String generalLimit, String minorAccountLimit, String smallSavingLimit) { SavingsProductFacilityLimit limit = new SavingsProductFacilityLimit(); limit.savingsProduct = savingsProduct; limit.limitType = limitType; limit.generalLimit = generalLimit; limit.minorAccountLimit = minorAccountLimit; limit.smallSavingLimit = smallSavingLimit; return limit; } } ``` ### **4. Create SavingsProductDepositLimitKyc Entity** `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductDepositLimitKyc.java`: ```java @Entity @Table(name = "m_savings_product_deposit_limit_kyc") public class SavingsProductDepositLimitKyc extends AbstractPersistableCustom { @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "savings_product_id", nullable = false) private SavingsProduct savingsProduct; @Column(name = "general_kyc_type", length = 50) private String generalKycType; @Column(name = "general_limit", length = 50) private String generalLimit; // "No Limit" or numeric @Column(name = "minor_kyc_type", length = 50) private String minorKycType; @Column(name = "minor_limit") private Long minorLimit; @Column(name = "small_saving_kyc_type", length = 50) private String smallSavingKycType; @Column(name = "small_saving_limit") private Long smallSavingLimit; // Getters and setters } ``` --- ## 🔑 API Constants to Add Update `fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java`: ```java // New Savings Product Fields public static final String accountTypeParamName = "accountType"; public static final String customerSegmentParamName = "customerSegment"; public static final String productCodeParamName = "productCode"; public static final String effectiveDateParamName = "effectiveDate"; public static final String effectiveToParamName = "effectiveTo"; public static final String endDateParamName = "endDate"; public static final String introducerRequiredParamName = "introducerRequired"; public static final String shareLinkingEnabledParamName = "shareLinkingEnabled"; public static final String nominationFacilityParamName = "nominationFacility"; public static final String dormancyAlertParamName = "dormancyAlert"; public static final String interestCalculateSelectorParamName = "interestCalculateSelector"; public static final String averagePeriodParamName = "averagePeriod"; public static final String autoPostParamName = "autoPost"; public static final String frequencyFixDayParamName = "frequencyFixDay"; public static final String lastInterestDateParamName = "lastInterestDate"; public static final String enableLockinPeriodParamName = "enableLockinPeriod"; // Complex objects public static final String accountFacilityConfigParamName = "accountFacilityConfig"; public static final String accountFacilityLimitsParamName = "accountFacilityLimits"; public static final String depositLimitKycParamName = "depositLimitKyc"; // Facility config fields public static final String atmFacilityParamName = "atmFacility"; public static final String atmWithdrawalFacilityParamName = "atmWithdrawalFacility"; public static final String upiFacilityParamName = "upiFacility"; // ... etc for all facility fields // Limit fields public static final String limitTypeParamName = "limitType"; public static final String generalLimitParamName = "generalLimit"; public static final String minorAccountLimitParamName = "minorAccountLimit"; public static final String smallSavingLimitParamName = "smallSavingLimit"; ``` --- ## ✅ Validator Updates Update `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java`: 1. **Add to parameter whitelist:** ```java private static final Set SAVINGS_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( // ... existing parameters ... "accountType", "customerSegment", "productCode", "effectiveDate", "effectiveTo", "endDate", "introducerRequired", "shareLinkingEnabled", "nominationFacility", "dormancyAlert", "interestCalculateSelector", "averagePeriod", "autoPost", "frequencyFixDay", "lastInterestDate", "enableLockinPeriod", "accountFacilityConfig", "accountFacilityLimits", "depositLimitKyc" )); ``` 2. **Add validation logic in validateForCreate() and validateForUpdate():** ```java if (command.stringValueOfParameterNamedAllowingNull("productCode") != null) { final String productCode = command.stringValueOfParameterNamed("productCode"); if (StringUtils.isBlank(productCode)) { baseDataValidator.reset().parameter("productCode").value(productCode).notBlank(); } } if (command.hasParameter("effectiveDate")) { final LocalDate effectiveDate = command.localDateValueOfParameterNamed("effectiveDate"); baseDataValidator.reset().parameter("effectiveDate").value(effectiveDate).notNull(); } // Similar for other date fields if (command.hasParameter("effectiveTo") && command.hasParameter("effectiveDate")) { final LocalDate effectiveDate = command.localDateValueOfParameterNamed("effectiveDate"); final LocalDate effectiveTo = command.localDateValueOfParameterNamed("effectiveTo"); if (effectiveTo.isBefore(effectiveDate)) { baseDataValidator.reset().parameter("effectiveTo").value(effectiveTo) .failWithCode("error.msg.effectiveto.cannot.be.before.effectivedate"); } } ``` --- ## 🔧 Assembler Updates Update `fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java`: Add helper methods to extract and assemble nested objects: ```java public SavingsProductFacilityConfig assembleAccountFacilityConfig(final JsonCommand command, final SavingsProduct product) { final JsonObject jsonObject = command.parsedJson(); if (!jsonObject.has("accountFacilityConfig")) { return null; } final JsonObject facilityConfigJson = jsonObject.getAsJsonObject("accountFacilityConfig"); SavingsProductFacilityConfig config = new SavingsProductFacilityConfig(); config.setSavingsProduct(product); config.setAtmFacility(facilityConfigJson.has("atmFacility") ? facilityConfigJson.getAsJsonPrimitive("atmFacility").getAsBoolean() : false); config.setAtmWithdrawalFacility(facilityConfigJson.has("atmWithdrawalFacility") ? facilityConfigJson.getAsJsonPrimitive("atmWithdrawalFacility").getAsBoolean() : false); // ... set other facilities ... return config; } public Set assembleAccountFacilityLimits(final JsonCommand command, final SavingsProduct product) { final JsonObject jsonObject = command.parsedJson(); final Set limits = new HashSet<>(); if (!jsonObject.has("accountFacilityLimits")) { return limits; } final JsonArray limitsArray = jsonObject.getAsJsonArray("accountFacilityLimits"); for (JsonElement element : limitsArray) { final JsonObject limitJson = element.getAsJsonObject(); SavingsProductFacilityLimit limit = SavingsProductFacilityLimit.create( product, limitJson.has("limitType") ? limitJson.getAsJsonPrimitive("limitType").getAsString() : null, limitJson.has("generalLimit") ? limitJson.getAsJsonPrimitive("generalLimit").getAsString() : null, limitJson.has("minorAccountLimit") ? limitJson.getAsJsonPrimitive("minorAccountLimit").getAsString() : null, limitJson.has("smallSavingLimit") ? limitJson.getAsJsonPrimitive("smallSavingLimit").getAsString() : null ); limits.add(limit); } return limits; } public SavingsProductDepositLimitKyc assembleDepositLimitKyc(final JsonCommand command, final SavingsProduct product) { final JsonObject jsonObject = command.parsedJson(); if (!jsonObject.has("depositLimitKyc")) { return null; } final JsonObject kycJson = jsonObject.getAsJsonObject("depositLimitKyc"); SavingsProductDepositLimitKyc kyc = new SavingsProductDepositLimitKyc(); kyc.setSavingsProduct(product); if (kycJson.has("general")) { final JsonObject generalJson = kycJson.getAsJsonObject("general"); kyc.setGeneralKycType(generalJson.has("kycType") ? generalJson.getAsJsonPrimitive("kycType").getAsString() : null); kyc.setGeneralLimit(generalJson.has("limit") ? generalJson.getAsJsonPrimitive("limit").getAsString() : null); } // ... handle minor and smallSaving similarly ... return kyc; } ``` --- ## 💾 Repository Classes to Create ### **SavingsProductFacilityConfigRepository.java** ```java @Repository public interface SavingsProductFacilityConfigRepository extends JpaRepository { SavingsProductFacilityConfig findBySavingsProductId(Long savingsProductId); } ``` ### **SavingsProductFacilityLimitRepository.java** ```java @Repository public interface SavingsProductFacilityLimitRepository extends JpaRepository { List findBySavingsProductId(Long savingsProductId); } ``` ### **SavingsProductDepositLimitKycRepository.java** ```java @Repository public interface SavingsProductDepositLimitKycRepository extends JpaRepository { SavingsProductDepositLimitKyc findBySavingsProductId(Long savingsProductId); } ``` --- ## 🚀 Service Updates Update `SavingsProductWritePlatformServiceImpl.create()` method: ```java public CommandProcessingResult create(final JsonCommand command) { try { this.savingsProductDataValidator.validateForCreate(command.json()); // ... existing code ... // Assemble new objects final SavingsProductFacilityConfig facilityConfig = this.savingsProductAssembler.assembleAccountFacilityConfig(command, product); final Set facilityLimits = this.savingsProductAssembler.assembleAccountFacilityLimits(command, product); final SavingsProductDepositLimitKyc depositLimitKyc = this.savingsProductAssembler.assembleDepositLimitKyc(command, product); if (facilityConfig != null) { product.setFacilityConfig(facilityConfig); facilityConfigRepository.saveAndFlush(facilityConfig); } if (!facilityLimits.isEmpty()) { product.setFacilityLimits(facilityLimits); facilityLimitRepository.saveAll(facilityLimits); } if (depositLimitKyc != null) { product.setDepositLimitKyc(depositLimitKyc); depositLimitKycRepository.saveAndFlush(depositLimitKyc); } // Save main product final SavingsProduct productSaved = this.savingProductRepository.saveAndFlush(product); return new CommandProcessingResultBuilder() .withCommandId(command.commandId()) .withEntityId(productSaved.getId()) .build(); } catch (final DataAccessException e) { // error handling } } ``` --- ## 📊 Read Service Updates Update `SavingsProductReadPlatformService` to include new fields in queries and DTO. --- ## 🎯 Summary of Changes | Component | Type | Files to Modify/Create | |-----------|------|----------------------| | Database | Migration | SQL scripts | | Entities | Create/Modify | 4 entity classes | | Constants | Modify | SavingsApiConstants.java | | Validator | Modify | SavingsProductDataValidator.java | | Assembler | Modify | SavingsProductAssembler.java | | Repositories | Create | 3 repository interfaces | | Services | Modify | SavingsProductWrite/ReadPlatformService | | DAO | Modify | SQL queries in read service | --- ## 📌 Implementation Priority 1. **Phase 1:** Create database tables 2. **Phase 2:** Create entity classes 3. **Phase 3:** Add API constants and validator rules 4. **Phase 4:** Create repositories and update service layer 5. **Phase 5:** Update read service and DTOs 6. **Phase 6:** Test with your POST endpoint --- ## ⚠️ Important Notes - ✅ Fields like `otherGlMappings`, `isDormancyTrackingActive`, etc. **already exist** in the system - ✅ Ensure backward compatibility - make all new fields optional in the validator - ✅ Use cascade = CascadeType.ALL and orphanRemoval = true for nested entities - ✅ Add unique constraints where needed (e.g., productCode, limit combinations) - ✅ Consider adding indexes on frequently queried fields - ✅ Update DTO classes to include these new fields in API responses