diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java index 1a013ee43..4e44db25d 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.useradministration.domain.AppUser; @Entity @Table(name = "m_current_account", uniqueConstraints = { @@ -102,6 +103,30 @@ public class CurrentAccount extends AbstractAuditableCustom { @Column(name = "closedon_date") private LocalDate closedOnDate; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "submittedon_userid") + private AppUser submittedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "approvedon_userid") + private AppUser approvedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "activatedon_userid") + private AppUser activatedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "rejectedon_userid") + private AppUser rejectedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "withdrawnon_userid") + private AppUser withdrawnOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "closedon_userid") + private AppUser closedOnByUser; + // Currency (copied from product at account creation) @Column(name = "currency_code", nullable = false, length = 3) private String currencyCode; @@ -376,29 +401,34 @@ public class CurrentAccount extends AbstractAuditableCustom { public void updateAccountNo(String accountNo) { this.accountNo = accountNo; } - public void approve(LocalDate approvedOnDate) { + public void approve(LocalDate approvedOnDate, AppUser approvedOnByUser) { this.statusEnum = 200; this.approvedOnDate = approvedOnDate; + this.approvedOnByUser = approvedOnByUser; } - public void activate(LocalDate activatedOnDate) { + public void activate(LocalDate activatedOnDate, AppUser activatedOnByUser) { this.statusEnum = 300; this.activatedOnDate = activatedOnDate; + this.activatedOnByUser = activatedOnByUser; } - public void reject(LocalDate rejectedOnDate) { + public void reject(LocalDate rejectedOnDate, AppUser rejectedOnByUser) { this.statusEnum = 500; this.rejectedOnDate = rejectedOnDate; + this.rejectedOnByUser = rejectedOnByUser; } - public void withdraw(LocalDate withdrawnOnDate) { + public void withdraw(LocalDate withdrawnOnDate, AppUser withdrawnOnByUser) { this.statusEnum = 400; this.withdrawnOnDate = withdrawnOnDate; + this.withdrawnOnByUser = withdrawnOnByUser; } - public void close(LocalDate closedOnDate) { + public void close(LocalDate closedOnDate, AppUser closedOnByUser) { this.statusEnum = 600; this.closedOnDate = closedOnDate; + this.closedOnByUser = closedOnByUser; } public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java index 5b34709bf..ca585310b 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java @@ -37,6 +37,31 @@ public class CurrentAccountNumberGenerator { private final JdbcTemplate jdbcTemplate; + private String getNextSequence() { + final java.util.List accountNos = this.jdbcTemplate.query( + "SELECT account_no FROM m_current_account WHERE account_no IS NOT NULL", + (rs, rowNum) -> rs.getString(1)); + + long maxVal = 0; + for (String accNo : accountNos) { + if (accNo != null && accNo.length() >= 5) { + String suffix = accNo.substring(accNo.length() - 5); + try { + long val = Long.parseLong(suffix); + if (val > maxVal) { + maxVal = val; + } + } catch (NumberFormatException ignored) {} + } + } + + String nextSeq = StringUtils.leftPad(String.valueOf(maxVal + 1), 5, '0'); + if (nextSeq.length() > 5) { + nextSeq = nextSeq.substring(nextSeq.length() - 5); + } + return nextSeq; + } + public String generate(CurrentAccount account) { Long productId = account.getProductId(); @@ -59,11 +84,10 @@ public class CurrentAccountNumberGenerator { String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0'); if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2); - // Account ID — 5 digits - String accountId = StringUtils.leftPad(account.getId().toString(), 5, '0'); - if (accountId.length() > 5) accountId = accountId.substring(accountId.length() - 5); + // Next sequence from existing account numbers + String nextSeq = getNextSequence(); - return CBS_PREFIX + glCode + officeId + accountId; + return CBS_PREFIX + glCode + officeId + nextSeq; } public String preview(Long productId, Long officeId) { @@ -80,13 +104,10 @@ public class CurrentAccountNumberGenerator { String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0'); if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2); - // Next account ID estimate from m_current_account - Long maxId = jdbcTemplate.query( - "SELECT MAX(id) FROM m_current_account", - rs -> rs.next() ? rs.getLong(1) : 0L); - String nextSeq = StringUtils.leftPad(String.valueOf((maxId != null ? maxId : 0L) + 1), 5, '0'); - if (nextSeq.length() > 5) nextSeq = nextSeq.substring(nextSeq.length() - 5); + // Next sequence from existing account numbers + String nextSeq = getNextSequence(); return CBS_PREFIX + glCode + officeStr + nextSeq; } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java index 6f659759d..3c209996b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java @@ -25,6 +25,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import jakarta.persistence.PostLoad; import jakarta.persistence.Table; import java.math.BigDecimal; import java.math.MathContext; @@ -135,7 +136,17 @@ public class DepositAccountRecurringDetail extends AbstractPersistableCustom actualChanges.put(shareLinkingEnabledParamName, newValue); this.shareLinkingEnabled = newValue; } + + // CBS extended fields mapping during modify application + final String accountTypeParam = "accountType"; + if (command.isChangeInStringParameterNamed(accountTypeParam, this.cbsAccountType)) { + final String newValue = command.stringValueOfParameterNamed(accountTypeParam); + actualChanges.put(accountTypeParam, newValue); + this.cbsAccountType = newValue; + } + + final String customerSegmentParam = "customerSegment"; + if (command.isChangeInStringParameterNamed(customerSegmentParam, this.customerSegment)) { + final String newValue = command.stringValueOfParameterNamed(customerSegmentParam); + actualChanges.put(customerSegmentParam, newValue); + this.customerSegment = newValue; + } + + final String accountStatusParam = "accountStatus"; + if (command.isChangeInStringParameterNamed(accountStatusParam, this.accountStatus)) { + final String newValue = command.stringValueOfParameterNamed(accountStatusParam); + actualChanges.put(accountStatusParam, newValue); + this.accountStatus = newValue; + } + + final String modeOfOperationParam = "modeOfOperation"; + if (command.isChangeInStringParameterNamed(modeOfOperationParam, this.modeOfOperation)) { + final String newValue = command.stringValueOfParameterNamed(modeOfOperationParam); + actualChanges.put(modeOfOperationParam, newValue); + this.modeOfOperation = newValue; + } + + final String specialCategoryParam = "specialCategory"; + if (command.isChangeInStringParameterNamed(specialCategoryParam, this.specialCategory)) { + final String newValue = command.stringValueOfParameterNamed(specialCategoryParam); + actualChanges.put(specialCategoryParam, newValue); + this.specialCategory = newValue; + } + + final String weakerSectionParam = "weakerSection"; + if (command.isChangeInStringParameterNamed(weakerSectionParam, this.weakerSection)) { + final String newValue = command.stringValueOfParameterNamed(weakerSectionParam); + actualChanges.put(weakerSectionParam, newValue); + this.weakerSection = newValue; + } + + final String nominationFacilityParam = "nominationFacility"; + if (command.isChangeInBooleanParameterNamed(nominationFacilityParam, this.nominationFacility)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(nominationFacilityParam); + actualChanges.put(nominationFacilityParam, newValue); + this.nominationFacility = newValue; + } + + final String autoDebitFacilityParam = "autoDebitFacility"; + if (command.isChangeInBooleanParameterNamed(autoDebitFacilityParam, this.autoDebitFacility)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(autoDebitFacilityParam); + actualChanges.put(autoDebitFacilityParam, newValue); + this.autoDebitFacility = newValue; + } + + final String isSeniorCitizenParam = "isSeniorCitizen"; + if (command.isChangeInBooleanParameterNamed(isSeniorCitizenParam, this.isSeniorCitizen)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(isSeniorCitizenParam); + actualChanges.put(isSeniorCitizenParam, newValue); + this.isSeniorCitizen = newValue; + } + + final String seniorCitizenInterestRateParam = "seniorCitizenInterestRate"; + if (command.isChangeInBigDecimalParameterNamed(seniorCitizenInterestRateParam, this.seniorCitizenInterestRate)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(seniorCitizenInterestRateParam); + actualChanges.put(seniorCitizenInterestRateParam, newValue); + this.seniorCitizenInterestRate = newValue; + if (newValue != null && newValue.compareTo(BigDecimal.ZERO) > 0) { + updateNominalAnnualInterestRate(newValue); + } + } + + final String guardianNameParam = "guardianName"; + if (command.isChangeInStringParameterNamed(guardianNameParam, this.guardianName)) { + final String newValue = command.stringValueOfParameterNamed(guardianNameParam); + actualChanges.put(guardianNameParam, newValue); + this.guardianName = newValue; + } + + final String guardianCifNumberParam = "guardianCifNumber"; + if (command.isChangeInStringParameterNamed(guardianCifNumberParam, this.guardianCifNumber)) { + final String newValue = command.stringValueOfParameterNamed(guardianCifNumberParam); + actualChanges.put(guardianCifNumberParam, newValue); + this.guardianCifNumber = newValue; + } + + final String guardianRelationshipParam = "guardianRelationship"; + if (command.isChangeInStringParameterNamed(guardianRelationshipParam, this.guardianRelationship)) { + final String newValue = command.stringValueOfParameterNamed(guardianRelationshipParam); + actualChanges.put(guardianRelationshipParam, newValue); + this.guardianRelationship = newValue; + } + + final String minorDateOfBirthParam = "minorDateOfBirth"; + if (command.isChangeInLocalDateParameterNamed(minorDateOfBirthParam, this.minorDateOfBirth)) { + final LocalDate newValue = command.localDateValueOfParameterNamed(minorDateOfBirthParam); + actualChanges.put(minorDateOfBirthParam, newValue); + this.minorDateOfBirth = newValue; + } + + final String guardianClientIdParam = "guardianClientId"; + if (command.isChangeInLongParameterNamed(guardianClientIdParam, this.guardianClientId)) { + final Long newValue = command.longValueOfParameterNamed(guardianClientIdParam); + actualChanges.put(guardianClientIdParam, newValue); + this.guardianClientId = newValue; + } + + final String introducerIdParam = "introducerId"; + if (command.isChangeInLongParameterNamed(introducerIdParam, this.introducerClientId)) { + final Long newValue = command.longValueOfParameterNamed(introducerIdParam); + actualChanges.put(introducerIdParam, newValue); + this.introducerClientId = newValue; + } + + final String introducerNameParam = "introducerName"; + if (command.isChangeInStringParameterNamed(introducerNameParam, this.introducerName)) { + final String newValue = command.stringValueOfParameterNamed(introducerNameParam); + actualChanges.put(introducerNameParam, newValue); + this.introducerName = newValue; + } + + final String introducerCifNumberParam = "introducerCifNumber"; + if (command.isChangeInStringParameterNamed(introducerCifNumberParam, this.introducerCifNumber)) { + final String newValue = command.stringValueOfParameterNamed(introducerCifNumberParam); + actualChanges.put(introducerCifNumberParam, newValue); + this.introducerCifNumber = newValue; + } + + final String linkedAccountIdParam = "linkedAccountId"; + if (command.isChangeInLongParameterNamed(linkedAccountIdParam, this.linkedAccountId)) { + final Long newValue = command.longValueOfParameterNamed(linkedAccountIdParam); + actualChanges.put(linkedAccountIdParam, newValue); + this.linkedAccountId = newValue; + } + + final String linkedAccountTypeParam = "linkedAccountType"; + if (command.isChangeInStringParameterNamed(linkedAccountTypeParam, this.linkedAccountType)) { + final String newValue = command.stringValueOfParameterNamed(linkedAccountTypeParam); + actualChanges.put(linkedAccountTypeParam, newValue); + this.linkedAccountType = newValue; + } + + final String linkedAccountNoParam = "linkedAccountNo"; + if (command.isChangeInStringParameterNamed(linkedAccountNoParam, this.linkedAccountNo)) { + final String newValue = command.stringValueOfParameterNamed(linkedAccountNoParam); + actualChanges.put(linkedAccountNoParam, newValue); + this.linkedAccountNo = newValue; + } + + final String accountFacilityConfigParam = "accountFacilityConfig"; + if (command.hasParameter(accountFacilityConfigParam)) { + final com.google.gson.JsonElement parsedJson = command.parsedJson(); + if (parsedJson != null && parsedJson.isJsonObject()) { + final com.google.gson.JsonObject jsonObj = parsedJson.getAsJsonObject(); + if (jsonObj.has(accountFacilityConfigParam) && !jsonObj.get(accountFacilityConfigParam).isJsonNull() + && jsonObj.get(accountFacilityConfigParam).isJsonObject()) { + final com.google.gson.JsonObject configObj = jsonObj.get(accountFacilityConfigParam).getAsJsonObject(); + if (this.facilityConfig == null) { + this.facilityConfig = new SavingsAccountFacilityConfig(this); + } + this.facilityConfig.updateFrom(configObj); + actualChanges.put(accountFacilityConfigParam, configObj.toString()); + } + } + } + + final String jointHoldersParam = "jointHolders"; + if (command.hasParameter(jointHoldersParam)) { + final com.google.gson.JsonElement parsedJson = command.parsedJson(); + if (parsedJson != null && parsedJson.isJsonObject()) { + final com.google.gson.JsonObject jsonObj = parsedJson.getAsJsonObject(); + if (jsonObj.has(jointHoldersParam) && !jsonObj.get(jointHoldersParam).isJsonNull() + && jsonObj.get(jointHoldersParam).isJsonArray()) { + final com.google.gson.JsonArray holdersArray = jsonObj.get(jointHoldersParam).getAsJsonArray(); + final List holders = new ArrayList<>(); + for (int i = 0; i < holdersArray.size(); i++) { + final com.google.gson.JsonObject h = holdersArray.get(i).getAsJsonObject(); + final Long clientId = h.has("clientId") && !h.get("clientId").isJsonNull() + ? h.get("clientId").getAsLong() : null; + final String cifNumber = h.has("cifNumber") && !h.get("cifNumber").isJsonNull() + ? h.get("cifNumber").getAsString() : null; + final String displayName = h.has("displayName") && !h.get("displayName").isJsonNull() + ? h.get("displayName").getAsString() : null; + final BigDecimal sharePercentage = h.has("sharePercentage") && !h.get("sharePercentage").isJsonNull() + ? h.get("sharePercentage").getAsBigDecimal() : null; + final boolean isPrimary = h.has("isPrimary") && h.get("isPrimary").getAsBoolean(); + String signingAuthority = h.has("signingAuthority") && !h.get("signingAuthority").isJsonNull() + ? h.get("signingAuthority").getAsString() : null; + String role = h.has("role") && !h.get("role").isJsonNull() + ? h.get("role").getAsString() : null; + + if (signingAuthority == null || signingAuthority.trim().isEmpty()) { + signingAuthority = isPrimary ? "Mandatory" : "Optional"; + } + if (role == null || role.trim().isEmpty()) { + role = isPrimary ? "Primary Account Holder" : "Co-Holder"; + } + + final boolean isMandatory = h.has("isMandatory") && h.get("isMandatory").getAsBoolean(); + holders.add(new SavingsAccountJointHolder(this, clientId, cifNumber, displayName, + sharePercentage, signingAuthority, role, isPrimary, isMandatory)); + } + this.jointHolders.clear(); + this.jointHolders.addAll(holders); + actualChanges.put(jointHoldersParam, holdersArray.toString()); + } + } + } + validateLockinDetails(baseDataValidator); esnureOverdraftLimitsSetForOverdraftAccounts(); } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java index 93127dfed..3e5b80748 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java @@ -815,16 +815,20 @@ public class SavingsAccountCharge extends AbstractAuditableWithUTCDateTimeCustom return nextDueLocalDate; } + public Integer getFeeInterval() { + return this.feeInterval == null ? 1 : this.feeInterval; + } + private LocalDate calculateNextDueDate(final LocalDate date) { LocalDate nextDueLocalDate = null; if (isAnnualFee()) { nextDueLocalDate = date.withMonth(this.feeOnMonth).plusYears(1); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isMonthlyFee()) { - nextDueLocalDate = date.plusMonths(this.feeInterval); + nextDueLocalDate = date.plusMonths(getFeeInterval()); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isWeeklyFee()) { - nextDueLocalDate = date.plusWeeks(this.feeInterval); + nextDueLocalDate = date.plusWeeks(getFeeInterval()); nextDueLocalDate = setDayOfWeek(nextDueLocalDate); } return nextDueLocalDate; @@ -857,10 +861,10 @@ public class SavingsAccountCharge extends AbstractAuditableWithUTCDateTimeCustom nextDueLocalDate = nextDueLocalDate.withMonth(this.feeOnMonth).minusYears(1); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isMonthlyFee()) { - nextDueLocalDate = nextDueLocalDate.minusMonths(this.feeInterval); + nextDueLocalDate = nextDueLocalDate.minusMonths(getFeeInterval()); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isWeeklyFee()) { - nextDueLocalDate = nextDueLocalDate.minusDays(7 * this.feeInterval); + nextDueLocalDate = nextDueLocalDate.minusDays(7 * getFeeInterval()); nextDueLocalDate = setDayOfWeek(nextDueLocalDate); }