Files
cbsnexora-backend/IMPLEMENTATION_GUIDE_NEW_PAYLOAD.md
T

669 lines
23 KiB
Markdown

# 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<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`:
```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`:
```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`:
```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`:
```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<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"
));
```
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<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**
```java
@Repository
public interface SavingsProductFacilityConfigRepository extends
JpaRepository<SavingsProductFacilityConfig, Long> {
SavingsProductFacilityConfig findBySavingsProductId(Long savingsProductId);
}
```
### **SavingsProductFacilityLimitRepository.java**
```java
@Repository
public interface SavingsProductFacilityLimitRepository extends
JpaRepository<SavingsProductFacilityLimit, Long> {
List<SavingsProductFacilityLimit> findBySavingsProductId(Long savingsProductId);
}
```
### **SavingsProductDepositLimitKycRepository.java**
```java
@Repository
public interface SavingsProductDepositLimitKycRepository extends
JpaRepository<SavingsProductDepositLimitKyc, Long> {
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<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