Files
cbsnexora-backend/IMPLEMENTATION_GUIDE_NEW_PAYLOAD.md

23 KiB

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)

{
  "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)

[
  {"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)

{
  "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

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)

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)

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)

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:

@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<SavingsProductFacilityLimit> 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:

@Entity
@Table(name = "m_savings_product_facility_config")
public class SavingsProductFacilityConfig extends AbstractPersistableCustom<Long> {
    
    @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:

@Entity
@Table(name = "m_savings_product_facility_limits")
public class SavingsProductFacilityLimit extends AbstractPersistableCustom<Long> {
    
    @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:

@Entity
@Table(name = "m_savings_product_deposit_limit_kyc")
public class SavingsProductDepositLimitKyc extends AbstractPersistableCustom<Long> {
    
    @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:

// 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:
private static final Set<String> 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"
    ));
  1. Add validation logic in validateForCreate() and validateForUpdate():
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:

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<SavingsProductFacilityLimit> assembleAccountFacilityLimits(final JsonCommand command, final SavingsProduct product) {
    final JsonObject jsonObject = command.parsedJson();
    final Set<SavingsProductFacilityLimit> 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

@Repository
public interface SavingsProductFacilityConfigRepository extends 
        JpaRepository<SavingsProductFacilityConfig, Long> {
    SavingsProductFacilityConfig findBySavingsProductId(Long savingsProductId);
}

SavingsProductFacilityLimitRepository.java

@Repository
public interface SavingsProductFacilityLimitRepository extends 
        JpaRepository<SavingsProductFacilityLimit, Long> {
    List<SavingsProductFacilityLimit> findBySavingsProductId(Long savingsProductId);
}

SavingsProductDepositLimitKycRepository.java

@Repository
public interface SavingsProductDepositLimitKycRepository extends 
        JpaRepository<SavingsProductDepositLimitKyc, Long> {
    SavingsProductDepositLimitKyc findBySavingsProductId(Long savingsProductId);
}

🚀 Service Updates

Update SavingsProductWritePlatformServiceImpl.create() method:

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<SavingsProductFacilityLimit> 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