Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f5a1babd | ||
|
|
20fe11e60d | ||
|
|
eb1aa767f7 | ||
|
|
8724c54d9b | ||
|
|
5c0a482f49 | ||
|
|
0e07e60328 | ||
|
|
e227e8ae0f | ||
|
|
231919b62b |
+9
-72
@@ -664,78 +664,6 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createAgent() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "AGENT";
|
||||
this.entityId = null;
|
||||
this.href = "/dds/agents/template";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateAgent(final Long agentId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "AGENT";
|
||||
this.entityId = agentId;
|
||||
this.href = "/dds/agents/" + agentId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createParty() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "PARTY";
|
||||
this.entityId = null;
|
||||
this.href = "/dds/parties/template";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateParty(final Long partyId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "PARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/dds/parties/" + partyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder closeParty(final Long partyId) {
|
||||
this.actionName = "CLOSE";
|
||||
this.entityName = "PARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/dds/parties/" + partyId + "?command=close";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createPartyTransaction() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "PARTYTRANSACTION";
|
||||
this.entityId = null;
|
||||
this.href = "/dds/partytransactions/template";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createCommissionSlabs() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "COMMISSIONSLAB";
|
||||
this.entityId = null;
|
||||
this.href = "/dds/commissionslabs/template";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder generateDdsBatchRun() {
|
||||
this.actionName = "GENERATE";
|
||||
this.entityName = "DDSBATCH";
|
||||
this.entityId = null;
|
||||
this.href = "/dds/batchruns/template";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder postDdsBatchRun(final Long batchRunId) {
|
||||
this.actionName = "POST";
|
||||
this.entityName = "DDSBATCH";
|
||||
this.entityId = batchRunId;
|
||||
this.href = "/dds/batchruns/" + batchRunId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createOfficeTransaction() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "OFFICETRANSACTION";
|
||||
@@ -2969,6 +2897,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder paymentFixedDepositAccount(final Long accountId) {
|
||||
this.actionName = "PAYMENT";
|
||||
this.entityName = "FIXEDDEPOSITACCOUNT";
|
||||
this.entityId = accountId;
|
||||
this.savingsId = accountId;
|
||||
this.href = "/fixeddepositaccounts/" + accountId + "?command=payment";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder fixedDepositAccountInterestCalculation(final Long accountId) {
|
||||
this.actionName = "CALCULATEINTEREST";
|
||||
this.entityName = "FIXEDDEPOSITACCOUNT";
|
||||
|
||||
+13
@@ -130,6 +130,19 @@ public class DataValidatorBuilder {
|
||||
this.dataValidationErrors.add(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #failWithCode(String, Object...)}, but the caller's own message is what's actually shown to the
|
||||
* user (that method always renders the generic "Failed data validation due to: {errorCode}." regardless of the
|
||||
* args passed to it).
|
||||
*/
|
||||
public void failWithCodeAndMessage(final String errorCode, final String defaultUserMessage,
|
||||
final Object... defaultUserMessageArgs) {
|
||||
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + "." + errorCode;
|
||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultUserMessage, this.parameter,
|
||||
this.value, defaultUserMessageArgs);
|
||||
this.dataValidationErrors.add(error);
|
||||
}
|
||||
|
||||
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
|
||||
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
|
||||
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
|
||||
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Immutable data object representing a DDS agent, including its collection-limit history and
|
||||
* guarantors. Carries the collection/commission/security-account fields merged in from SubAgent
|
||||
* (2026-08-24) - Agent now does the daily collection work directly.
|
||||
*/
|
||||
@Getter
|
||||
public final class AgentData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final String agentCode;
|
||||
private final String agentNameId;
|
||||
private final String agentAddress1;
|
||||
private final String agentAddress2;
|
||||
private final String agentCity;
|
||||
private final String agentPinCode;
|
||||
private final String agentTelephone;
|
||||
private final String agentSavingAccountType;
|
||||
private final String agentSavingAccountNo;
|
||||
private final LocalDate agentOpeningDate;
|
||||
private final LocalDate agentBirthDate;
|
||||
private final BigDecimal agentCurrentBalance;
|
||||
private final BigDecimal agentOpeningBalance;
|
||||
private final String homeBranch;
|
||||
private final String savingBranch;
|
||||
private final Boolean tdsApplicable;
|
||||
private final BigDecimal tdsPercent;
|
||||
private final BigDecimal currentTds;
|
||||
private final BigDecimal tdsAmount;
|
||||
private final String transferBranch;
|
||||
private final String transferAmountAccountType;
|
||||
private final String transferAccountNo;
|
||||
private final Boolean commissionCalculation;
|
||||
private final BigDecimal limit;
|
||||
private final BigDecimal machineRent;
|
||||
private final String agentSecurityBranch;
|
||||
private final String agentSecurityAccountType;
|
||||
private final String agentSecurityAccountNo;
|
||||
private final BigDecimal commission;
|
||||
private final BigDecimal otherCommission;
|
||||
private final BigDecimal agentDeposit;
|
||||
private final BigDecimal festivalAdvance;
|
||||
private final BigDecimal otherDeduction;
|
||||
private final Collection<AgentLimitData> limits;
|
||||
private final Collection<AgentGuarantorData> guarantors;
|
||||
|
||||
public static AgentData instance(final Long id, final Long clientId, final String clientName, final String agentCode,
|
||||
final String agentNameId, final String agentAddress1, final String agentAddress2, final String agentCity,
|
||||
final String agentPinCode, final String agentTelephone, final String agentSavingAccountType,
|
||||
final String agentSavingAccountNo, final LocalDate agentOpeningDate, final LocalDate agentBirthDate,
|
||||
final BigDecimal agentCurrentBalance, final BigDecimal agentOpeningBalance, final String homeBranch,
|
||||
final String savingBranch, final Boolean tdsApplicable, final BigDecimal tdsPercent, final BigDecimal currentTds,
|
||||
final BigDecimal tdsAmount, final String transferBranch, final String transferAmountAccountType,
|
||||
final String transferAccountNo, final Boolean commissionCalculation, final BigDecimal limit, final BigDecimal machineRent,
|
||||
final String agentSecurityBranch, final String agentSecurityAccountType, final String agentSecurityAccountNo,
|
||||
final BigDecimal commission, final BigDecimal otherCommission, final BigDecimal agentDeposit,
|
||||
final BigDecimal festivalAdvance, final BigDecimal otherDeduction, final Collection<AgentLimitData> limits,
|
||||
final Collection<AgentGuarantorData> guarantors) {
|
||||
return new AgentData(id, clientId, clientName, agentCode, agentNameId, agentAddress1, agentAddress2, agentCity, agentPinCode,
|
||||
agentTelephone, agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentCurrentBalance,
|
||||
agentOpeningBalance, homeBranch, savingBranch, tdsApplicable, tdsPercent, currentTds, tdsAmount, transferBranch,
|
||||
transferAmountAccountType, transferAccountNo, commissionCalculation, limit, machineRent, agentSecurityBranch,
|
||||
agentSecurityAccountType, agentSecurityAccountNo, commission, otherCommission, agentDeposit, festivalAdvance,
|
||||
otherDeduction, limits, guarantors);
|
||||
}
|
||||
|
||||
private AgentData(final Long id, final Long clientId, final String clientName, final String agentCode, final String agentNameId,
|
||||
final String agentAddress1, final String agentAddress2, final String agentCity, final String agentPinCode,
|
||||
final String agentTelephone, final String agentSavingAccountType, final String agentSavingAccountNo,
|
||||
final LocalDate agentOpeningDate, final LocalDate agentBirthDate, final BigDecimal agentCurrentBalance,
|
||||
final BigDecimal agentOpeningBalance, final String homeBranch, final String savingBranch, final Boolean tdsApplicable,
|
||||
final BigDecimal tdsPercent, final BigDecimal currentTds, final BigDecimal tdsAmount, final String transferBranch,
|
||||
final String transferAmountAccountType, final String transferAccountNo, final Boolean commissionCalculation,
|
||||
final BigDecimal limit, final BigDecimal machineRent, final String agentSecurityBranch,
|
||||
final String agentSecurityAccountType, final String agentSecurityAccountNo, final BigDecimal commission,
|
||||
final BigDecimal otherCommission, final BigDecimal agentDeposit, final BigDecimal festivalAdvance,
|
||||
final BigDecimal otherDeduction, final Collection<AgentLimitData> limits, final Collection<AgentGuarantorData> guarantors) {
|
||||
this.id = id;
|
||||
this.clientId = clientId;
|
||||
this.clientName = clientName;
|
||||
this.agentCode = agentCode;
|
||||
this.agentNameId = agentNameId;
|
||||
this.agentAddress1 = agentAddress1;
|
||||
this.agentAddress2 = agentAddress2;
|
||||
this.agentCity = agentCity;
|
||||
this.agentPinCode = agentPinCode;
|
||||
this.agentTelephone = agentTelephone;
|
||||
this.agentSavingAccountType = agentSavingAccountType;
|
||||
this.agentSavingAccountNo = agentSavingAccountNo;
|
||||
this.agentOpeningDate = agentOpeningDate;
|
||||
this.agentBirthDate = agentBirthDate;
|
||||
this.agentCurrentBalance = agentCurrentBalance;
|
||||
this.agentOpeningBalance = agentOpeningBalance;
|
||||
this.homeBranch = homeBranch;
|
||||
this.savingBranch = savingBranch;
|
||||
this.tdsApplicable = tdsApplicable;
|
||||
this.tdsPercent = tdsPercent;
|
||||
this.currentTds = currentTds;
|
||||
this.tdsAmount = tdsAmount;
|
||||
this.transferBranch = transferBranch;
|
||||
this.transferAmountAccountType = transferAmountAccountType;
|
||||
this.transferAccountNo = transferAccountNo;
|
||||
this.commissionCalculation = commissionCalculation;
|
||||
this.limit = limit;
|
||||
this.machineRent = machineRent;
|
||||
this.agentSecurityBranch = agentSecurityBranch;
|
||||
this.agentSecurityAccountType = agentSecurityAccountType;
|
||||
this.agentSecurityAccountNo = agentSecurityAccountNo;
|
||||
this.commission = commission;
|
||||
this.otherCommission = otherCommission;
|
||||
this.agentDeposit = agentDeposit;
|
||||
this.festivalAdvance = festivalAdvance;
|
||||
this.otherDeduction = otherDeduction;
|
||||
this.limits = limits;
|
||||
this.guarantors = guarantors;
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Immutable data object representing a single agent guarantor.
|
||||
*/
|
||||
@Getter
|
||||
public final class AgentGuarantorData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String guarantorName;
|
||||
private final String address1;
|
||||
private final String address2;
|
||||
private final String city;
|
||||
private final String pinCode;
|
||||
private final String telephone;
|
||||
|
||||
public static AgentGuarantorData instance(final Long id, final Long clientId, final String guarantorName, final String address1,
|
||||
final String address2, final String city, final String pinCode, final String telephone) {
|
||||
return new AgentGuarantorData(id, clientId, guarantorName, address1, address2, city, pinCode, telephone);
|
||||
}
|
||||
|
||||
private AgentGuarantorData(final Long id, final Long clientId, final String guarantorName, final String address1,
|
||||
final String address2, final String city, final String pinCode, final String telephone) {
|
||||
this.id = id;
|
||||
this.clientId = clientId;
|
||||
this.guarantorName = guarantorName;
|
||||
this.address1 = address1;
|
||||
this.address2 = address2;
|
||||
this.city = city;
|
||||
this.pinCode = pinCode;
|
||||
this.telephone = telephone;
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class AgentLimitData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final LocalDate fromDate;
|
||||
private final LocalDate toDate;
|
||||
private final BigDecimal limit;
|
||||
|
||||
public static AgentLimitData instance(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) {
|
||||
return new AgentLimitData(id, fromDate, toDate, limit);
|
||||
}
|
||||
|
||||
private AgentLimitData(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) {
|
||||
this.id = id;
|
||||
this.fromDate = fromDate;
|
||||
this.toDate = toDate;
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.domain;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
|
||||
/**
|
||||
* A DDS (Daily Deposit Scheme) collection agent. Can represent either a person or, at the top of the
|
||||
* hierarchy, the bank itself acting as its own agent. Holds its own settlement savings account and one
|
||||
* or more guarantors.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
|
||||
public class Agent extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "agent_code", length = 20, nullable = false)
|
||||
private String agentCode;
|
||||
|
||||
@Column(name = "agent_name", length = 100, nullable = false)
|
||||
private String agentName;
|
||||
|
||||
@Column(name = "agent_address1", length = 200, nullable = false)
|
||||
private String agentAddress1;
|
||||
|
||||
@Column(name = "agent_address2", length = 200)
|
||||
private String agentAddress2;
|
||||
|
||||
@Column(name = "agent_city", length = 100, nullable = false)
|
||||
private String agentCity;
|
||||
|
||||
@Column(name = "agent_pincode", length = 20, nullable = false)
|
||||
private String agentPinCode;
|
||||
|
||||
@Column(name = "agent_telephone", length = 20)
|
||||
private String agentTelephone;
|
||||
|
||||
@Column(name = "agent_saving_account_type", length = 100, nullable = false)
|
||||
private String agentSavingAccountType;
|
||||
|
||||
@Column(name = "agent_saving_account_no", length = 50, nullable = false)
|
||||
private String agentSavingAccountNo;
|
||||
|
||||
@Column(name = "agent_opening_date", nullable = false)
|
||||
private LocalDate agentOpeningDate;
|
||||
|
||||
@Column(name = "agent_birth_date", nullable = false)
|
||||
private LocalDate agentBirthDate;
|
||||
|
||||
@Column(name = "agent_current_balance", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal agentCurrentBalance;
|
||||
|
||||
@Column(name = "agent_opening_balance", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal agentOpeningBalance;
|
||||
|
||||
// Fields below were merged in from SubAgent (2026-08-24) - the Agent itself now does the daily
|
||||
// collection work directly; SubAgent no longer exists as a separate tier. Ag.Level / Upper Ag.Level
|
||||
// / Collector (MLM) / CRPB Points / Level Update Date were dropped - they only ever meant anything
|
||||
// for a multi-level hierarchy, which no longer exists here.
|
||||
@Column(name = "home_branch", length = 100, nullable = false)
|
||||
private String homeBranch;
|
||||
|
||||
@Column(name = "saving_branch", length = 100, nullable = false)
|
||||
private String savingBranch;
|
||||
|
||||
@Column(name = "tds_applicable", nullable = false)
|
||||
private boolean tdsApplicable;
|
||||
|
||||
@Column(name = "tds_percent", scale = 6, precision = 19)
|
||||
private BigDecimal tdsPercent;
|
||||
|
||||
@Column(name = "current_tds", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal currentTds;
|
||||
|
||||
@Column(name = "tds_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal tdsAmount;
|
||||
|
||||
@Column(name = "transfer_branch", length = 100)
|
||||
private String transferBranch;
|
||||
|
||||
@Column(name = "transfer_amount_account_type", length = 100)
|
||||
private String transferAmountAccountType;
|
||||
|
||||
@Column(name = "transfer_account_no", length = 50)
|
||||
private String transferAccountNo;
|
||||
|
||||
@Column(name = "collector_commission_calculation", nullable = false)
|
||||
private boolean commissionCalculation;
|
||||
|
||||
@Column(name = "collector_limit_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal limitAmount;
|
||||
|
||||
@Column(name = "machine_rent", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal machineRent;
|
||||
|
||||
@Column(name = "agent_security_branch", length = 100)
|
||||
private String agentSecurityBranch;
|
||||
|
||||
@Column(name = "agent_security_account_type", length = 100)
|
||||
private String agentSecurityAccountType;
|
||||
|
||||
@Column(name = "agent_security_account_no", length = 50)
|
||||
private String agentSecurityAccountNo;
|
||||
|
||||
@Column(name = "commission", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal commission;
|
||||
|
||||
@Column(name = "other_commission", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal otherCommission;
|
||||
|
||||
@Column(name = "agent_deposit", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal agentDeposit;
|
||||
|
||||
@Column(name = "festival_advance", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal festivalAdvance;
|
||||
|
||||
@Column(name = "other_deduction", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal otherDeduction;
|
||||
|
||||
@Column(name = "last_commission_date")
|
||||
private LocalDate lastCommissionDate;
|
||||
|
||||
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@OrderBy("id")
|
||||
private List<AgentGuarantor> guarantors = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@OrderBy("id")
|
||||
private List<AgentLimit> limits = new ArrayList<>();
|
||||
|
||||
protected Agent() {
|
||||
//
|
||||
}
|
||||
|
||||
private Agent(final Client client, final String agentCode, final String agentName, final String agentAddress1,
|
||||
final String agentAddress2, final String agentCity, final String agentPinCode, final String agentTelephone,
|
||||
final String agentSavingAccountType, final String agentSavingAccountNo, final LocalDate agentOpeningDate,
|
||||
final LocalDate agentBirthDate, final BigDecimal agentOpeningBalance, final JsonCommand command) {
|
||||
this.client = client;
|
||||
this.agentCode = agentCode;
|
||||
this.agentName = agentName;
|
||||
this.agentAddress1 = agentAddress1;
|
||||
this.agentAddress2 = StringUtils.defaultIfEmpty(agentAddress2, null);
|
||||
this.agentCity = agentCity;
|
||||
this.agentPinCode = agentPinCode;
|
||||
this.agentTelephone = StringUtils.defaultIfEmpty(agentTelephone, null);
|
||||
this.agentSavingAccountType = agentSavingAccountType;
|
||||
this.agentSavingAccountNo = agentSavingAccountNo;
|
||||
this.agentOpeningDate = agentOpeningDate;
|
||||
this.agentBirthDate = agentBirthDate;
|
||||
this.agentOpeningBalance = agentOpeningBalance == null ? BigDecimal.ZERO : agentOpeningBalance;
|
||||
this.agentCurrentBalance = this.agentOpeningBalance;
|
||||
|
||||
this.homeBranch = command.stringValueOfParameterNamed("homeBranch");
|
||||
this.savingBranch = command.stringValueOfParameterNamed("savingBranch");
|
||||
this.tdsApplicable = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable");
|
||||
this.tdsPercent = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("tdsPercent");
|
||||
this.currentTds = BigDecimal.ZERO;
|
||||
this.tdsAmount = BigDecimal.ZERO;
|
||||
this.transferBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferBranch"), null);
|
||||
this.transferAmountAccountType = StringUtils
|
||||
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAmountAccountType"), null);
|
||||
this.transferAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAccountNo"), null);
|
||||
this.commissionCalculation = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation");
|
||||
this.limitAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("limit"));
|
||||
this.machineRent = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("machineRent"));
|
||||
this.agentSecurityBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityBranch"),
|
||||
null);
|
||||
this.agentSecurityAccountType = StringUtils
|
||||
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountType"), null);
|
||||
this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountNo"),
|
||||
null);
|
||||
this.commission = BigDecimal.ZERO;
|
||||
this.otherCommission = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherCommission"));
|
||||
this.agentDeposit = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentDeposit"));
|
||||
this.festivalAdvance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("festivalAdvance"));
|
||||
this.otherDeduction = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherDeduction"));
|
||||
}
|
||||
|
||||
public static Agent fromJson(final Client client, final String agentCode, final JsonCommand command) {
|
||||
final String agentName = command.stringValueOfParameterNamed("agentNameId");
|
||||
final String agentAddress1 = command.stringValueOfParameterNamed("agentAddress1");
|
||||
final String agentAddress2 = command.stringValueOfParameterNamedAllowingNull("agentAddress2");
|
||||
final String agentCity = command.stringValueOfParameterNamed("agentCity");
|
||||
final String agentPinCode = command.stringValueOfParameterNamed("agentPinCode");
|
||||
final String agentTelephone = command.stringValueOfParameterNamedAllowingNull("agentTelephone");
|
||||
final String agentSavingAccountType = command.stringValueOfParameterNamed("agentSavingAccountType");
|
||||
final String agentSavingAccountNo = command.stringValueOfParameterNamed("agentSavingAccountNo");
|
||||
final LocalDate agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
|
||||
final LocalDate agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
|
||||
final BigDecimal agentOpeningBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentOpeningBalance");
|
||||
|
||||
return new Agent(client, agentCode, agentName, agentAddress1, agentAddress2, agentCity, agentPinCode, agentTelephone,
|
||||
agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentOpeningBalance, command);
|
||||
}
|
||||
|
||||
private static BigDecimal orZero(final BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
public void addGuarantor(final AgentGuarantor guarantor) {
|
||||
guarantor.setAgent(this);
|
||||
this.guarantors.add(guarantor);
|
||||
}
|
||||
|
||||
public void resetGuarantors() {
|
||||
this.guarantors.clear();
|
||||
}
|
||||
|
||||
public void addLimit(final AgentLimit limit) {
|
||||
limit.setAgent(this);
|
||||
this.limits.add(limit);
|
||||
}
|
||||
|
||||
public void resetLimits() {
|
||||
this.limits.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Commission Posting's amount to this agent's running commission total and stamps the date
|
||||
* commission was last calculated through, so the next Commission Calculation batch only covers
|
||||
* collections since then.
|
||||
*/
|
||||
public void postCommission(final BigDecimal amount, final LocalDate throughDate) {
|
||||
this.commission = this.commission.add(amount);
|
||||
this.lastCommissionDate = throughDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cash this agent has collected from parties but not yet handed over to a branch (an Agent Tran
|
||||
* settlement). Raised by a Party Tran collection, lowered by an Agent Tran settlement - this is the
|
||||
* figure an Agent's collection limit is enforced against.
|
||||
*/
|
||||
public BigDecimal credit(final BigDecimal amount) {
|
||||
this.agentCurrentBalance = this.agentCurrentBalance.add(amount);
|
||||
return this.agentCurrentBalance;
|
||||
}
|
||||
|
||||
public BigDecimal settle(final BigDecimal amount) {
|
||||
this.agentCurrentBalance = this.agentCurrentBalance.subtract(amount);
|
||||
return this.agentCurrentBalance;
|
||||
}
|
||||
|
||||
public void changeClient(final Client newClient) {
|
||||
this.client = newClient;
|
||||
}
|
||||
|
||||
public Long clientId() {
|
||||
return this.client.getId();
|
||||
}
|
||||
|
||||
public Map<String, Object> update(final JsonCommand command) {
|
||||
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(10);
|
||||
|
||||
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
|
||||
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentNameId", this.agentName)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentNameId");
|
||||
actualChanges.put("agentNameId", newValue);
|
||||
this.agentName = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentAddress1", this.agentAddress1)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentAddress1");
|
||||
actualChanges.put("agentAddress1", newValue);
|
||||
this.agentAddress1 = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentAddress2", this.agentAddress2)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentAddress2");
|
||||
actualChanges.put("agentAddress2", newValue);
|
||||
this.agentAddress2 = StringUtils.defaultIfEmpty(newValue, null);
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentCity", this.agentCity)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentCity");
|
||||
actualChanges.put("agentCity", newValue);
|
||||
this.agentCity = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentPinCode", this.agentPinCode)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentPinCode");
|
||||
actualChanges.put("agentPinCode", newValue);
|
||||
this.agentPinCode = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentTelephone", this.agentTelephone)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentTelephone");
|
||||
actualChanges.put("agentTelephone", newValue);
|
||||
this.agentTelephone = StringUtils.defaultIfEmpty(newValue, null);
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentSavingAccountType", this.agentSavingAccountType)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountType");
|
||||
actualChanges.put("agentSavingAccountType", newValue);
|
||||
this.agentSavingAccountType = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInStringParameterNamed("agentSavingAccountNo", this.agentSavingAccountNo)) {
|
||||
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountNo");
|
||||
actualChanges.put("agentSavingAccountNo", newValue);
|
||||
this.agentSavingAccountNo = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("agentOpeningDate", this.agentOpeningDate)) {
|
||||
actualChanges.put("agentOpeningDate", command.stringValueOfParameterNamed("agentOpeningDate"));
|
||||
this.agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("agentBirthDate", this.agentBirthDate)) {
|
||||
actualChanges.put("agentBirthDate", command.stringValueOfParameterNamed("agentBirthDate"));
|
||||
this.agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "homeBranch", this.homeBranch, v -> this.homeBranch = v);
|
||||
stringChange(command, actualChanges, "savingBranch", this.savingBranch, v -> this.savingBranch = v);
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("tdsApplicable", this.tdsApplicable)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable");
|
||||
actualChanges.put("tdsApplicable", newValue);
|
||||
this.tdsApplicable = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("tdsPercent", this.tdsPercent)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("tdsPercent");
|
||||
actualChanges.put("tdsPercent", newValue);
|
||||
this.tdsPercent = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "transferBranch", this.transferBranch,
|
||||
v -> this.transferBranch = StringUtils.defaultIfEmpty(v, null));
|
||||
stringChange(command, actualChanges, "transferAmountAccountType", this.transferAmountAccountType,
|
||||
v -> this.transferAmountAccountType = StringUtils.defaultIfEmpty(v, null));
|
||||
stringChange(command, actualChanges, "transferAccountNo", this.transferAccountNo,
|
||||
v -> this.transferAccountNo = StringUtils.defaultIfEmpty(v, null));
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("commissionCalculation", this.commissionCalculation)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation");
|
||||
actualChanges.put("commissionCalculation", newValue);
|
||||
this.commissionCalculation = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("limit", this.limitAmount)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("limit");
|
||||
actualChanges.put("limit", newValue);
|
||||
this.limitAmount = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("machineRent", this.machineRent)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("machineRent");
|
||||
actualChanges.put("machineRent", newValue);
|
||||
this.machineRent = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "agentSecurityBranch", this.agentSecurityBranch,
|
||||
v -> this.agentSecurityBranch = StringUtils.defaultIfEmpty(v, null));
|
||||
stringChange(command, actualChanges, "agentSecurityAccountType", this.agentSecurityAccountType,
|
||||
v -> this.agentSecurityAccountType = StringUtils.defaultIfEmpty(v, null));
|
||||
stringChange(command, actualChanges, "agentSecurityAccountNo", this.agentSecurityAccountNo,
|
||||
v -> this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(v, null));
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("otherCommission", this.otherCommission)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherCommission");
|
||||
actualChanges.put("otherCommission", newValue);
|
||||
this.otherCommission = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("agentDeposit", this.agentDeposit)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("agentDeposit");
|
||||
actualChanges.put("agentDeposit", newValue);
|
||||
this.agentDeposit = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("festivalAdvance", this.festivalAdvance)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("festivalAdvance");
|
||||
actualChanges.put("festivalAdvance", newValue);
|
||||
this.festivalAdvance = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("otherDeduction", this.otherDeduction)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherDeduction");
|
||||
actualChanges.put("otherDeduction", newValue);
|
||||
this.otherDeduction = newValue;
|
||||
}
|
||||
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
private void stringChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
|
||||
final String existingValue, final java.util.function.Consumer<String> setter) {
|
||||
if (command.isChangeInStringParameterNamed(paramName, existingValue)) {
|
||||
final String newValue = command.stringValueOfParameterNamed(paramName);
|
||||
actualChanges.put(paramName, newValue);
|
||||
setter.accept(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.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 org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* A single guarantor recorded against an {@link Agent} (or, reused with the same shape, against a
|
||||
* SubAgent). Always created/replaced as part of its parent agent's own create/update command - it has
|
||||
* no independent API of its own.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_agent_guarantor")
|
||||
public class AgentGuarantor extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "agent_id", nullable = false)
|
||||
private Agent agent;
|
||||
|
||||
@Column(name = "client_id", nullable = false)
|
||||
private Long clientId;
|
||||
|
||||
@Column(name = "guarantor_name", length = 100, nullable = false)
|
||||
private String guarantorName;
|
||||
|
||||
@Column(name = "address1", length = 200, nullable = false)
|
||||
private String address1;
|
||||
|
||||
@Column(name = "address2", length = 200)
|
||||
private String address2;
|
||||
|
||||
@Column(name = "city", length = 100, nullable = false)
|
||||
private String city;
|
||||
|
||||
@Column(name = "pin_code", length = 20, nullable = false)
|
||||
private String pinCode;
|
||||
|
||||
@Column(name = "telephone", length = 20)
|
||||
private String telephone;
|
||||
|
||||
protected AgentGuarantor() {
|
||||
//
|
||||
}
|
||||
|
||||
public AgentGuarantor(final Long clientId, final String guarantorName, final String address1, final String address2,
|
||||
final String city, final String pinCode, final String telephone) {
|
||||
this.clientId = clientId;
|
||||
this.guarantorName = guarantorName;
|
||||
this.address1 = address1;
|
||||
this.address2 = StringUtils.defaultIfEmpty(address2, null);
|
||||
this.city = city;
|
||||
this.pinCode = pinCode;
|
||||
this.telephone = StringUtils.defaultIfEmpty(telephone, null);
|
||||
}
|
||||
|
||||
void setAgent(final Agent agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.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 org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* One row of an Agent's time-bound collection-limit history (From Date / To Date / Limit) - an
|
||||
* Agent's authorized collection limit can change over time and every change is kept, not overwritten.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_agent_limit")
|
||||
public class AgentLimit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "agent_id", nullable = false)
|
||||
private Agent agent;
|
||||
|
||||
@Column(name = "from_date", nullable = false)
|
||||
private LocalDate fromDate;
|
||||
|
||||
@Column(name = "to_date", nullable = false)
|
||||
private LocalDate toDate;
|
||||
|
||||
@Column(name = "limit_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal limitAmount;
|
||||
|
||||
protected AgentLimit() {
|
||||
//
|
||||
}
|
||||
|
||||
public AgentLimit(final LocalDate fromDate, final LocalDate toDate, final BigDecimal limitAmount) {
|
||||
this.fromDate = fromDate;
|
||||
this.toDate = toDate;
|
||||
this.limitAmount = limitAmount;
|
||||
}
|
||||
|
||||
void setAgent(final Agent agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
/**
|
||||
* A {@link RuntimeException} thrown when a DDS agent resource is not found.
|
||||
*/
|
||||
public class AgentNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public AgentNotFoundException(final Long id) {
|
||||
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id);
|
||||
}
|
||||
|
||||
public AgentNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id, e);
|
||||
}
|
||||
|
||||
public AgentNotFoundException(final String agentCode) {
|
||||
super("error.msg.agent.code.invalid", "Agent with code " + agentCode + " does not exist", agentCode);
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class CommissionSlabData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final String slabType;
|
||||
private final BigDecimal slabFrom;
|
||||
private final BigDecimal slabTo;
|
||||
private final BigDecimal commissionPercent;
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* One row of the agent commission chart: an ACCOUNT slab (commission percentage on collection
|
||||
* volume) or a COMMISSION slab (the override a senior agent earns on a sub-agent's commission).
|
||||
* Append-only reference data, like the legacy screen it replaces - there is deliberately no update or
|
||||
* delete API; a superseded slab range is retired by adding a new row, not editing history.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_commission_slab")
|
||||
public class CommissionSlab extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "slab_type", length = 20, nullable = false)
|
||||
private String slabType;
|
||||
|
||||
@Column(name = "slab_from", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal slabFrom;
|
||||
|
||||
@Column(name = "slab_to", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal slabTo;
|
||||
|
||||
@Column(name = "commission_percent", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal commissionPercent;
|
||||
|
||||
protected CommissionSlab() {
|
||||
//
|
||||
}
|
||||
|
||||
public CommissionSlab(final String slabType, final BigDecimal slabFrom, final BigDecimal slabTo, final BigDecimal commissionPercent) {
|
||||
this.slabType = slabType;
|
||||
this.slabFrom = slabFrom;
|
||||
this.slabTo = slabTo;
|
||||
this.commissionPercent = commissionPercent;
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -72,11 +72,26 @@ public class SavingsAccountSummaryData {
|
||||
// differentiate deposit accounts Savings, FD and RD accounts
|
||||
private final EnumOptionData depositType;
|
||||
|
||||
/**
|
||||
* For a Fixed Deposit, the stable customer-facing FD number: the account number of the first account in the renewal
|
||||
* chain, which stays the same across every renewal even though each term is its own account row. Equals accountNo
|
||||
* for an FD that has never been renewed, and is null for non-deposit accounts.
|
||||
*/
|
||||
private final String fdAccountNumber;
|
||||
/**
|
||||
* Which renewal cycle of that FD this row is: 0 for the originally opened account, 1 for the first renewal, and so
|
||||
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
||||
*/
|
||||
private final Integer renewalCycleNo;
|
||||
|
||||
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
||||
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
||||
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
||||
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
||||
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate) {
|
||||
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate,
|
||||
final String fdAccountNumber, final Integer renewalCycleNo) {
|
||||
this.fdAccountNumber = fdAccountNumber;
|
||||
this.renewalCycleNo = renewalCycleNo;
|
||||
this.id = id;
|
||||
this.accountNo = accountNo;
|
||||
this.externalId = externalId;
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder(toBuilder = true)
|
||||
public final class BatchRunData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final String jobType;
|
||||
private final String branchScope;
|
||||
private final LocalDate jobDate;
|
||||
private final Long agentFrom;
|
||||
private final Long agentTo;
|
||||
private final Long partyFrom;
|
||||
private final Long partyTo;
|
||||
private final String status;
|
||||
private final Integer totalRows;
|
||||
private final BigDecimal totalAmount;
|
||||
private final LocalDate generatedOnDate;
|
||||
private final LocalDate postedOnDate;
|
||||
private final Collection<BatchRunRowData> rows;
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class BatchRunRowData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final Long partyId;
|
||||
private final String partyCode;
|
||||
private final String partyName;
|
||||
private final Long agentId;
|
||||
private final String agentCode;
|
||||
private final BigDecimal amount;
|
||||
}
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.domain;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* One Calculate-then-Post batch run - the deliberate two-step control from the legacy Interest Part
|
||||
* menu (Interest Calculation -> Interest Posting, OD Interest Calculation -> OD Interest Posting,
|
||||
* Commission Calculation -> Commission Posting): a run is generated (rows computed and saved as a
|
||||
* preview, nothing else changes), then posted (a separate, explicit step that applies exactly the
|
||||
* rows that were previewed - never a fresh recalculation, so what gets applied is always what was
|
||||
* seen).
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_batch_run")
|
||||
public class BatchRun extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@Column(name = "job_type", length = 40, nullable = false)
|
||||
private String jobType;
|
||||
|
||||
@Column(name = "branch_scope", length = 20, nullable = false)
|
||||
private String branchScope;
|
||||
|
||||
@Column(name = "job_date", nullable = false)
|
||||
private LocalDate jobDate;
|
||||
|
||||
@Column(name = "agent_from", nullable = false)
|
||||
private Long agentFrom;
|
||||
|
||||
@Column(name = "agent_to", nullable = false)
|
||||
private Long agentTo;
|
||||
|
||||
@Column(name = "party_from")
|
||||
private Long partyFrom;
|
||||
|
||||
@Column(name = "party_to")
|
||||
private Long partyTo;
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
private String status;
|
||||
|
||||
@Column(name = "total_rows", nullable = false)
|
||||
private Integer totalRows;
|
||||
|
||||
@Column(name = "total_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Column(name = "generated_on_date", nullable = false)
|
||||
private LocalDate generatedOnDate;
|
||||
|
||||
@Column(name = "posted_on_date")
|
||||
private LocalDate postedOnDate;
|
||||
|
||||
@OneToMany(mappedBy = "batchRun", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@OrderBy("id")
|
||||
private List<BatchRunRow> rows = new ArrayList<>();
|
||||
|
||||
protected BatchRun() {
|
||||
//
|
||||
}
|
||||
|
||||
public BatchRun(final String jobType, final String branchScope, final LocalDate jobDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo, final LocalDate generatedOnDate) {
|
||||
this.jobType = jobType;
|
||||
this.branchScope = branchScope;
|
||||
this.jobDate = jobDate;
|
||||
this.agentFrom = agentFrom;
|
||||
this.agentTo = agentTo;
|
||||
this.partyFrom = partyFrom;
|
||||
this.partyTo = partyTo;
|
||||
this.status = "CALCULATED";
|
||||
this.totalRows = 0;
|
||||
this.totalAmount = BigDecimal.ZERO;
|
||||
this.generatedOnDate = generatedOnDate;
|
||||
}
|
||||
|
||||
public void addRow(final BatchRunRow row) {
|
||||
row.setBatchRun(this);
|
||||
this.rows.add(row);
|
||||
this.totalRows = this.rows.size();
|
||||
this.totalAmount = this.totalAmount.add(row.getAmount());
|
||||
}
|
||||
|
||||
public boolean isPosted() {
|
||||
return "POSTED".equals(this.status);
|
||||
}
|
||||
|
||||
public void markPosted(final LocalDate postedOnDate) {
|
||||
this.status = "POSTED";
|
||||
this.postedOnDate = postedOnDate;
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.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 lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* One previewed/posted line of a {@link BatchRun}: for an interest job, one Party's accrued interest;
|
||||
* for a commission job, one Agent's commission on their collected volume (partyId is null there).
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_batch_run_row")
|
||||
public class BatchRunRow extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "batch_run_id", nullable = false)
|
||||
private BatchRun batchRun;
|
||||
|
||||
@Column(name = "party_id")
|
||||
private Long partyId;
|
||||
|
||||
@Column(name = "agent_id", nullable = false)
|
||||
private Long agentId;
|
||||
|
||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
protected BatchRunRow() {
|
||||
//
|
||||
}
|
||||
|
||||
public BatchRunRow(final Long partyId, final Long agentId, final BigDecimal amount) {
|
||||
this.partyId = partyId;
|
||||
this.agentId = agentId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
void setBatchRun(final BatchRun batchRun) {
|
||||
this.batchRun = batchRun;
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when Post is attempted on a batch run that has already been posted - a run applies once,
|
||||
* matching the calculate-then-post safety control (never re-post the same preview).
|
||||
*/
|
||||
public class BatchRunAlreadyPostedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public BatchRunAlreadyPostedException(final Long batchRunId) {
|
||||
super("error.msg.ddsbatchrun.already.posted", "DDS batch run " + batchRunId + " has already been posted", batchRunId);
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class BatchRunNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public BatchRunNotFoundException(final Long id) {
|
||||
super("error.msg.ddsbatchrun.id.invalid", "DDS batch run with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Immutable data object representing a DDS Party (customer scheme account), including its nominees.
|
||||
* Built via the Lombok builder rather than a long positional constructor - the field count here makes
|
||||
* positional args unreadable and error-prone to keep in sync.
|
||||
*/
|
||||
@Getter
|
||||
@Builder(toBuilder = true)
|
||||
public final class PartyData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientName;
|
||||
private final Long agentId;
|
||||
private final String agentCode;
|
||||
private final String agentName;
|
||||
private final Long savingsAccountId;
|
||||
private final String subType;
|
||||
private final String partyCode;
|
||||
private final String lastAccount;
|
||||
private final Boolean nominal;
|
||||
private final String aadharNo;
|
||||
private final String partyName;
|
||||
private final String partyName1;
|
||||
private final String address1;
|
||||
private final String address2;
|
||||
private final String address3;
|
||||
private final String city;
|
||||
private final String pincode;
|
||||
private final String gender;
|
||||
private final String panCardNo;
|
||||
private final String telephone;
|
||||
private final String mobileNoSms;
|
||||
private final String operation;
|
||||
private final LocalDate birthDate;
|
||||
private final String occupation;
|
||||
private final String risk;
|
||||
private final String ckycId;
|
||||
private final String adultMinor;
|
||||
private final LocalDate minorDate;
|
||||
private final Long guardianClientId;
|
||||
private final String minAccountOpt;
|
||||
private final String relation;
|
||||
private final LocalDate openingDate;
|
||||
private final Integer month;
|
||||
private final LocalDate maturityDate;
|
||||
private final LocalDate reopenDate;
|
||||
private final BigDecimal depositAmount;
|
||||
private final BigDecimal dailyDepositAmount;
|
||||
private final Integer days;
|
||||
private final String ledgerNo;
|
||||
private final BigDecimal maturityAmount;
|
||||
private final BigDecimal interestRate;
|
||||
private final String withdrawType;
|
||||
private final Boolean od1Flag;
|
||||
private final Boolean od2Flag;
|
||||
private final Boolean lien;
|
||||
private final LocalDate payDate;
|
||||
private final BigDecimal paymentAmount;
|
||||
private final BigDecimal currentBalance;
|
||||
private final BigDecimal openingBalance;
|
||||
private final Boolean isSmsActive;
|
||||
private final Boolean isEmailActive;
|
||||
private final BigDecimal interestCalc;
|
||||
private final LocalDate interestDate;
|
||||
private final String spDescription;
|
||||
private final String freezeCode;
|
||||
private final String advCode;
|
||||
private final String creditToBranch;
|
||||
private final String creditToAccountType;
|
||||
private final String creditToAccount;
|
||||
private final Boolean nomineeApplicable;
|
||||
private final String nomineeSplitMode;
|
||||
private final String status;
|
||||
private final LocalDate closeDate;
|
||||
private final Collection<PartyNomineeData> nominees;
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class PartyNomineeData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final LocalDate nominationDate;
|
||||
private final String name;
|
||||
private final String mobileNo;
|
||||
private final LocalDate birthDate;
|
||||
private final Boolean minor;
|
||||
private final String relation;
|
||||
private final String gender;
|
||||
private final String guardianName;
|
||||
|
||||
public static PartyNomineeData instance(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
|
||||
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) {
|
||||
return new PartyNomineeData(id, nominationDate, name, mobileNo, birthDate, minor, relation, gender, guardianName);
|
||||
}
|
||||
|
||||
private PartyNomineeData(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
|
||||
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) {
|
||||
this.id = id;
|
||||
this.nominationDate = nominationDate;
|
||||
this.name = name;
|
||||
this.mobileNo = mobileNo;
|
||||
this.birthDate = birthDate;
|
||||
this.minor = minor;
|
||||
this.relation = relation;
|
||||
this.gender = gender;
|
||||
this.guardianName = guardianName;
|
||||
}
|
||||
}
|
||||
-569
@@ -1,569 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.domain;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
|
||||
/**
|
||||
* A DDS Party - the customer's daily-deposit scheme account, linked to the Agent who collects for it.
|
||||
* Carries the scheme's KYC, terms and two-stage overdue flags, plus a nominee list.
|
||||
*
|
||||
* The underlying RD (recurring deposit) account this scheme conceptually reuses is not wired up yet -
|
||||
* {@code savingsAccountId} is a nullable placeholder for that future linkage (see the DDS architecture
|
||||
* decision to reuse the RD engine for the deposit half). Until then the scheme/balance fields below
|
||||
* are this record's own, not mirrored from any m_savings_account row.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
|
||||
public class Party extends AbstractPersistableCustom<Long> {
|
||||
|
||||
/** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */
|
||||
private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2");
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_CLOSED = "CLOSED";
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "agent_id", nullable = false)
|
||||
private Agent agent;
|
||||
|
||||
@Column(name = "savings_account_id")
|
||||
private Long savingsAccountId;
|
||||
|
||||
@Column(name = "sub_type", length = 60, nullable = false)
|
||||
private String subType;
|
||||
|
||||
@Column(name = "party_code", length = 20, nullable = false)
|
||||
private String partyCode;
|
||||
|
||||
@Column(name = "last_account", length = 50)
|
||||
private String lastAccount;
|
||||
|
||||
@Column(name = "nominal", nullable = false)
|
||||
private boolean nominal;
|
||||
|
||||
@Column(name = "aadhar_no", length = 20)
|
||||
private String aadharNo;
|
||||
|
||||
@Column(name = "party_name", length = 100, nullable = false)
|
||||
private String partyName;
|
||||
|
||||
@Column(name = "party_name1", length = 100)
|
||||
private String partyName1;
|
||||
|
||||
@Column(name = "address1", length = 200, nullable = false)
|
||||
private String address1;
|
||||
|
||||
@Column(name = "address2", length = 200)
|
||||
private String address2;
|
||||
|
||||
@Column(name = "address3", length = 200)
|
||||
private String address3;
|
||||
|
||||
@Column(name = "city", length = 100, nullable = false)
|
||||
private String city;
|
||||
|
||||
@Column(name = "pincode", length = 20, nullable = false)
|
||||
private String pincode;
|
||||
|
||||
@Column(name = "gender", length = 20)
|
||||
private String gender;
|
||||
|
||||
@Column(name = "pan_card_no", length = 20)
|
||||
private String panCardNo;
|
||||
|
||||
@Column(name = "telephone", length = 20)
|
||||
private String telephone;
|
||||
|
||||
@Column(name = "mobile_no_sms", length = 20, nullable = false)
|
||||
private String mobileNoSms;
|
||||
|
||||
@Column(name = "operation", length = 20, nullable = false)
|
||||
private String operation;
|
||||
|
||||
@Column(name = "birth_date")
|
||||
private LocalDate birthDate;
|
||||
|
||||
@Column(name = "occupation", length = 40)
|
||||
private String occupation;
|
||||
|
||||
@Column(name = "risk", length = 20)
|
||||
private String risk;
|
||||
|
||||
@Column(name = "ckyc_id", length = 40)
|
||||
private String ckycId;
|
||||
|
||||
@Column(name = "adult_minor", length = 20, nullable = false)
|
||||
private String adultMinor;
|
||||
|
||||
@Column(name = "minor_date")
|
||||
private LocalDate minorDate;
|
||||
|
||||
@Column(name = "guardian_client_id")
|
||||
private Long guardianClientId;
|
||||
|
||||
@Column(name = "min_account_opt", length = 40)
|
||||
private String minAccountOpt;
|
||||
|
||||
@Column(name = "relation", length = 40)
|
||||
private String relation;
|
||||
|
||||
@Column(name = "opening_date", nullable = false)
|
||||
private LocalDate openingDate;
|
||||
|
||||
@Column(name = "months")
|
||||
private Integer month;
|
||||
|
||||
@Column(name = "maturity_date")
|
||||
private LocalDate maturityDate;
|
||||
|
||||
@Column(name = "reopen_date")
|
||||
private LocalDate reopenDate;
|
||||
|
||||
@Column(name = "deposit_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal depositAmount;
|
||||
|
||||
@Column(name = "daily_deposit_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal dailyDepositAmount;
|
||||
|
||||
@Column(name = "days", nullable = false)
|
||||
private Integer days;
|
||||
|
||||
@Column(name = "ledger_no", length = 40)
|
||||
private String ledgerNo;
|
||||
|
||||
@Column(name = "maturity_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal maturityAmount;
|
||||
|
||||
@Column(name = "interest_rate", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal interestRate;
|
||||
|
||||
@Column(name = "withdraw_type", length = 20)
|
||||
private String withdrawType;
|
||||
|
||||
@Column(name = "od1_flag", nullable = false)
|
||||
private boolean od1Flag;
|
||||
|
||||
@Column(name = "od2_flag", nullable = false)
|
||||
private boolean od2Flag;
|
||||
|
||||
@Column(name = "lien", nullable = false)
|
||||
private boolean lien;
|
||||
|
||||
@Column(name = "pay_date")
|
||||
private LocalDate payDate;
|
||||
|
||||
@Column(name = "payment_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal paymentAmount;
|
||||
|
||||
@Column(name = "current_balance", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal currentBalance;
|
||||
|
||||
@Column(name = "opening_balance", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal openingBalance;
|
||||
|
||||
@Column(name = "is_sms_active", nullable = false)
|
||||
private boolean isSmsActive;
|
||||
|
||||
@Column(name = "is_email_active", nullable = false)
|
||||
private boolean isEmailActive;
|
||||
|
||||
@Column(name = "interest_calc", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal interestCalc;
|
||||
|
||||
@Column(name = "interest_date")
|
||||
private LocalDate interestDate;
|
||||
|
||||
@Column(name = "sp_description", length = 500)
|
||||
private String spDescription;
|
||||
|
||||
@Column(name = "freeze_code", length = 20, nullable = false)
|
||||
private String freezeCode;
|
||||
|
||||
@Column(name = "adv_code", length = 40, nullable = false)
|
||||
private String advCode;
|
||||
|
||||
@Column(name = "credit_to_branch", length = 100)
|
||||
private String creditToBranch;
|
||||
|
||||
@Column(name = "credit_to_account_type", length = 100)
|
||||
private String creditToAccountType;
|
||||
|
||||
@Column(name = "credit_to_account", length = 50)
|
||||
private String creditToAccount;
|
||||
|
||||
@Column(name = "nominee_applicable", nullable = false)
|
||||
private boolean nomineeApplicable;
|
||||
|
||||
@Column(name = "nominee_split_mode", length = 20)
|
||||
private String nomineeSplitMode;
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
private String status;
|
||||
|
||||
@Column(name = "close_date")
|
||||
private LocalDate closeDate;
|
||||
|
||||
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@OrderBy("id")
|
||||
private List<PartyNominee> nominees = new ArrayList<>();
|
||||
|
||||
protected Party() {
|
||||
//
|
||||
}
|
||||
|
||||
private Party(final Client client, final Agent agent, final String partyCode, final JsonCommand command) {
|
||||
this.client = client;
|
||||
this.agent = agent;
|
||||
this.partyCode = partyCode;
|
||||
this.subType = command.stringValueOfParameterNamed("subType");
|
||||
this.lastAccount = command.stringValueOfParameterNamedAllowingNull("lastAccount");
|
||||
this.nominal = command.booleanPrimitiveValueOfParameterNamed("nominal");
|
||||
this.aadharNo = command.stringValueOfParameterNamedAllowingNull("aadharNo");
|
||||
this.partyName = command.stringValueOfParameterNamed("partyName");
|
||||
this.partyName1 = command.stringValueOfParameterNamedAllowingNull("partyName1");
|
||||
this.address1 = command.stringValueOfParameterNamed("address1");
|
||||
this.address2 = command.stringValueOfParameterNamedAllowingNull("address2");
|
||||
this.address3 = command.stringValueOfParameterNamedAllowingNull("address3");
|
||||
this.city = command.stringValueOfParameterNamed("city");
|
||||
this.pincode = command.stringValueOfParameterNamed("pincode");
|
||||
this.gender = command.stringValueOfParameterNamedAllowingNull("gender");
|
||||
this.panCardNo = command.stringValueOfParameterNamedAllowingNull("panCardNo");
|
||||
this.telephone = command.stringValueOfParameterNamedAllowingNull("telephone");
|
||||
this.mobileNoSms = command.stringValueOfParameterNamed("mobileNoSms");
|
||||
this.operation = command.stringValueOfParameterNamed("operation");
|
||||
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
|
||||
this.occupation = command.stringValueOfParameterNamedAllowingNull("occupation");
|
||||
this.risk = command.stringValueOfParameterNamedAllowingNull("risk");
|
||||
this.ckycId = command.stringValueOfParameterNamedAllowingNull("ckycId");
|
||||
this.adultMinor = command.stringValueOfParameterNamed("adultMinor");
|
||||
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
|
||||
this.guardianClientId = command.longValueOfParameterNamed("guardianClientId");
|
||||
this.minAccountOpt = command.stringValueOfParameterNamedAllowingNull("minAccountOpt");
|
||||
this.relation = command.stringValueOfParameterNamedAllowingNull("relation");
|
||||
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
|
||||
this.month = command.integerValueOfParameterNamedDefaultToNullIfZero("month");
|
||||
this.maturityDate = command.localDateValueOfParameterNamed("maturityDate");
|
||||
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
|
||||
this.depositAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("depositAmount"));
|
||||
this.dailyDepositAmount = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
|
||||
this.days = command.integerValueOfParameterNamed("days");
|
||||
this.ledgerNo = command.stringValueOfParameterNamedAllowingNull("ledgerNo");
|
||||
this.maturityAmount = BigDecimal.ZERO;
|
||||
this.interestRate = command.parameterExists("interestRate")
|
||||
? command.bigDecimalValueOfParameterNamed("interestRate")
|
||||
: DEFAULT_INTEREST_RATE;
|
||||
this.withdrawType = command.stringValueOfParameterNamedAllowingNull("withdrawType");
|
||||
this.od1Flag = false;
|
||||
this.od2Flag = false;
|
||||
this.lien = command.booleanPrimitiveValueOfParameterNamed("lien");
|
||||
this.payDate = command.localDateValueOfParameterNamed("payDate");
|
||||
this.paymentAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("paymentAmount"));
|
||||
this.openingBalance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("openingBalance"));
|
||||
this.currentBalance = this.openingBalance;
|
||||
this.isSmsActive = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
|
||||
this.isEmailActive = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
|
||||
this.interestCalc = BigDecimal.ZERO;
|
||||
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
|
||||
this.spDescription = command.stringValueOfParameterNamedAllowingNull("spDescription");
|
||||
this.freezeCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("freezeCode"), "Normal");
|
||||
this.advCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("advCode"), "None");
|
||||
this.creditToBranch = command.stringValueOfParameterNamedAllowingNull("creditToBranch");
|
||||
this.creditToAccountType = command.stringValueOfParameterNamedAllowingNull("creditToAccountType");
|
||||
this.creditToAccount = command.stringValueOfParameterNamedAllowingNull("creditToAccount");
|
||||
this.nomineeApplicable = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
|
||||
this.nomineeSplitMode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("nomineeSplitMode"),
|
||||
"Percentage");
|
||||
this.status = STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
public static Party fromJson(final Client client, final Agent agent, final String partyCode, final JsonCommand command) {
|
||||
return new Party(client, agent, partyCode, command);
|
||||
}
|
||||
|
||||
private static BigDecimal orZero(final BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
public void addNominee(final PartyNominee nominee) {
|
||||
nominee.setParty(this);
|
||||
this.nominees.add(nominee);
|
||||
}
|
||||
|
||||
public void resetNominees() {
|
||||
this.nominees.clear();
|
||||
}
|
||||
|
||||
public void changeClient(final Client newClient) {
|
||||
this.client = newClient;
|
||||
}
|
||||
|
||||
public void changeAgent(final Agent newAgent) {
|
||||
this.agent = newAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links this Party to an existing real Savings Account, resolved by account number in the write
|
||||
* service. There is no unlink - once a Party's DDS scheme is tied to a real account, it stays
|
||||
* tied.
|
||||
*/
|
||||
public void linkSavingsAccount(final Long savingsAccountId) {
|
||||
this.savingsAccountId = savingsAccountId;
|
||||
}
|
||||
|
||||
public BigDecimal credit(final BigDecimal amount) {
|
||||
this.currentBalance = this.currentBalance.add(amount);
|
||||
return this.currentBalance;
|
||||
}
|
||||
|
||||
public BigDecimal debit(final BigDecimal amount) {
|
||||
this.currentBalance = this.currentBalance.subtract(amount);
|
||||
return this.currentBalance;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return STATUS_CLOSED.equals(this.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this Party's account as of the given date. The balance must already be zero - a Party
|
||||
* is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money
|
||||
* still owed to it.
|
||||
*/
|
||||
public void close(final LocalDate closeDate) {
|
||||
this.status = STATUS_CLOSED;
|
||||
this.closeDate = closeDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamps the date interest (regular or OD) was last calculated up to, so the next batch run
|
||||
* accrues only the days since. Set on POST, not on calculate/preview.
|
||||
*/
|
||||
public void markInterestCalculatedThrough(final LocalDate date) {
|
||||
this.interestDate = date;
|
||||
}
|
||||
|
||||
public Long clientId() {
|
||||
return this.client.getId();
|
||||
}
|
||||
|
||||
public Long agentId() {
|
||||
return this.agent.getId();
|
||||
}
|
||||
|
||||
public Map<String, Object> update(final JsonCommand command) {
|
||||
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(40);
|
||||
|
||||
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
|
||||
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
|
||||
}
|
||||
if (command.isChangeInLongParameterNamed("agentId", this.agent.getId())) {
|
||||
actualChanges.put("agentId", command.longValueOfParameterNamed("agentId"));
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "subType", this.subType, v -> this.subType = v);
|
||||
stringChange(command, actualChanges, "lastAccount", this.lastAccount, v -> this.lastAccount = v);
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("nominal", this.nominal)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nominal");
|
||||
actualChanges.put("nominal", newValue);
|
||||
this.nominal = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "aadharNo", this.aadharNo, v -> this.aadharNo = v);
|
||||
stringChange(command, actualChanges, "partyName", this.partyName, v -> this.partyName = v);
|
||||
stringChange(command, actualChanges, "partyName1", this.partyName1, v -> this.partyName1 = v);
|
||||
stringChange(command, actualChanges, "address1", this.address1, v -> this.address1 = v);
|
||||
stringChange(command, actualChanges, "address2", this.address2, v -> this.address2 = v);
|
||||
stringChange(command, actualChanges, "address3", this.address3, v -> this.address3 = v);
|
||||
stringChange(command, actualChanges, "city", this.city, v -> this.city = v);
|
||||
stringChange(command, actualChanges, "pincode", this.pincode, v -> this.pincode = v);
|
||||
stringChange(command, actualChanges, "gender", this.gender, v -> this.gender = v);
|
||||
stringChange(command, actualChanges, "panCardNo", this.panCardNo, v -> this.panCardNo = v);
|
||||
stringChange(command, actualChanges, "telephone", this.telephone, v -> this.telephone = v);
|
||||
stringChange(command, actualChanges, "mobileNoSms", this.mobileNoSms, v -> this.mobileNoSms = v);
|
||||
stringChange(command, actualChanges, "operation", this.operation, v -> this.operation = v);
|
||||
|
||||
if (command.isChangeInDateParameterNamed("birthDate", this.birthDate)) {
|
||||
actualChanges.put("birthDate", command.stringValueOfParameterNamed("birthDate"));
|
||||
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "occupation", this.occupation, v -> this.occupation = v);
|
||||
stringChange(command, actualChanges, "risk", this.risk, v -> this.risk = v);
|
||||
stringChange(command, actualChanges, "ckycId", this.ckycId, v -> this.ckycId = v);
|
||||
stringChange(command, actualChanges, "adultMinor", this.adultMinor, v -> this.adultMinor = v);
|
||||
|
||||
if (command.isChangeInDateParameterNamed("minorDate", this.minorDate)) {
|
||||
actualChanges.put("minorDate", command.stringValueOfParameterNamed("minorDate"));
|
||||
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInLongParameterNamed("guardianClientId", this.guardianClientId)) {
|
||||
final Long newValue = command.longValueOfParameterNamed("guardianClientId");
|
||||
actualChanges.put("guardianClientId", newValue);
|
||||
this.guardianClientId = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "minAccountOpt", this.minAccountOpt, v -> this.minAccountOpt = v);
|
||||
stringChange(command, actualChanges, "relation", this.relation, v -> this.relation = v);
|
||||
|
||||
if (command.isChangeInDateParameterNamed("openingDate", this.openingDate)) {
|
||||
actualChanges.put("openingDate", command.stringValueOfParameterNamed("openingDate"));
|
||||
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInIntegerParameterNamed("month", this.month)) {
|
||||
final Integer newValue = command.integerValueOfParameterNamed("month");
|
||||
actualChanges.put("month", newValue);
|
||||
this.month = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("maturityDate", this.maturityDate)) {
|
||||
actualChanges.put("maturityDate", command.stringValueOfParameterNamed("maturityDate"));
|
||||
this.maturityDate = command.localDateValueOfParameterNamed("maturityDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("reopenDate", this.reopenDate)) {
|
||||
actualChanges.put("reopenDate", command.stringValueOfParameterNamed("reopenDate"));
|
||||
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("depositAmount", this.depositAmount)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("depositAmount");
|
||||
actualChanges.put("depositAmount", newValue);
|
||||
this.depositAmount = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("dailyDepositAmount", this.dailyDepositAmount)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
|
||||
actualChanges.put("dailyDepositAmount", newValue);
|
||||
this.dailyDepositAmount = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInIntegerParameterNamed("days", this.days)) {
|
||||
final Integer newValue = command.integerValueOfParameterNamed("days");
|
||||
actualChanges.put("days", newValue);
|
||||
this.days = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("interestRate", this.interestRate)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("interestRate");
|
||||
actualChanges.put("interestRate", newValue);
|
||||
this.interestRate = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "ledgerNo", this.ledgerNo, v -> this.ledgerNo = v);
|
||||
stringChange(command, actualChanges, "withdrawType", this.withdrawType, v -> this.withdrawType = v);
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("lien", this.lien)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("lien");
|
||||
actualChanges.put("lien", newValue);
|
||||
this.lien = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("payDate", this.payDate)) {
|
||||
actualChanges.put("payDate", command.stringValueOfParameterNamed("payDate"));
|
||||
this.payDate = command.localDateValueOfParameterNamed("payDate");
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("paymentAmount", this.paymentAmount)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("paymentAmount");
|
||||
actualChanges.put("paymentAmount", newValue);
|
||||
this.paymentAmount = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBigDecimalParameterNamed("openingBalance", this.openingBalance)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("openingBalance");
|
||||
actualChanges.put("openingBalance", newValue);
|
||||
this.openingBalance = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("isSmsActive", this.isSmsActive)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
|
||||
actualChanges.put("isSmsActive", newValue);
|
||||
this.isSmsActive = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("isEmailActive", this.isEmailActive)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
|
||||
actualChanges.put("isEmailActive", newValue);
|
||||
this.isEmailActive = newValue;
|
||||
}
|
||||
|
||||
if (command.isChangeInDateParameterNamed("interestDate", this.interestDate)) {
|
||||
actualChanges.put("interestDate", command.stringValueOfParameterNamed("interestDate"));
|
||||
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "spDescription", this.spDescription, v -> this.spDescription = v);
|
||||
stringChange(command, actualChanges, "freezeCode", this.freezeCode, v -> this.freezeCode = v);
|
||||
stringChange(command, actualChanges, "advCode", this.advCode, v -> this.advCode = v);
|
||||
stringChange(command, actualChanges, "creditToBranch", this.creditToBranch, v -> this.creditToBranch = v);
|
||||
stringChange(command, actualChanges, "creditToAccountType", this.creditToAccountType, v -> this.creditToAccountType = v);
|
||||
stringChange(command, actualChanges, "creditToAccount", this.creditToAccount, v -> this.creditToAccount = v);
|
||||
|
||||
if (command.isChangeInBooleanParameterNamed("nomineeApplicable", this.nomineeApplicable)) {
|
||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
|
||||
actualChanges.put("nomineeApplicable", newValue);
|
||||
this.nomineeApplicable = newValue;
|
||||
}
|
||||
|
||||
stringChange(command, actualChanges, "nomineeSplitMode", this.nomineeSplitMode, v -> this.nomineeSplitMode = v);
|
||||
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
private void stringChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
|
||||
final String existingValue, final Consumer<String> setter) {
|
||||
if (command.isChangeInStringParameterNamed(paramName, existingValue)) {
|
||||
final String newValue = command.stringValueOfParameterNamed(paramName);
|
||||
actualChanges.put(paramName, newValue);
|
||||
setter.accept(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
|
||||
/**
|
||||
* A single nominee recorded against a {@link Party}. Maker-checker on nominee edits is enforced at
|
||||
* the command layer (the whole Party update goes through the standard maker-checker command flow),
|
||||
* not on this child row individually.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_party_nominee")
|
||||
public class PartyNominee extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "party_id", nullable = false)
|
||||
private Party party;
|
||||
|
||||
@Column(name = "nomination_date", nullable = false)
|
||||
private LocalDate nominationDate;
|
||||
|
||||
@Column(name = "name", length = 100, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "mobile_no", length = 20)
|
||||
private String mobileNo;
|
||||
|
||||
@Column(name = "birth_date")
|
||||
private LocalDate birthDate;
|
||||
|
||||
@Column(name = "minor", nullable = false)
|
||||
private boolean minor;
|
||||
|
||||
@Column(name = "relation", length = 40, nullable = false)
|
||||
private String relation;
|
||||
|
||||
@Column(name = "gender", length = 20, nullable = false)
|
||||
private String gender;
|
||||
|
||||
@Column(name = "guardian_name", length = 100)
|
||||
private String guardianName;
|
||||
|
||||
protected PartyNominee() {
|
||||
//
|
||||
}
|
||||
|
||||
public PartyNominee(final LocalDate nominationDate, final String name, final String mobileNo, final LocalDate birthDate,
|
||||
final boolean minor, final String relation, final String gender, final String guardianName) {
|
||||
this.nominationDate = nominationDate;
|
||||
this.name = name;
|
||||
this.mobileNo = StringUtils.defaultIfEmpty(mobileNo, null);
|
||||
this.birthDate = birthDate;
|
||||
this.minor = minor;
|
||||
this.relation = relation;
|
||||
this.gender = gender;
|
||||
this.guardianName = StringUtils.defaultIfEmpty(guardianName, null);
|
||||
}
|
||||
|
||||
void setParty(final Party party) {
|
||||
this.party = party;
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when a Close Party command, or a Party Tran collection, targets a Party that is already
|
||||
* closed.
|
||||
*/
|
||||
public class PartyAlreadyClosedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public PartyAlreadyClosedException(final String partyCode) {
|
||||
super("error.msg.party.already.closed", "Party " + partyCode + " is already closed", partyCode);
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when a Close Party command is attempted while the Party's balance is still non-zero - the
|
||||
* remaining balance must be paid out (a Withdraw Deposite to zero) before the account can be closed.
|
||||
*/
|
||||
public class PartyHasBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public PartyHasBalanceException(final String partyCode) {
|
||||
super("error.msg.party.has.balance", "Party " + partyCode + " still has a non-zero balance and cannot be closed", partyCode);
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
/**
|
||||
* A {@link RuntimeException} thrown when a DDS party resource is not found.
|
||||
*/
|
||||
public class PartyNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public PartyNotFoundException(final Long id) {
|
||||
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id);
|
||||
}
|
||||
|
||||
public PartyNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id, e);
|
||||
}
|
||||
|
||||
public PartyNotFoundException(final String partyCode) {
|
||||
super("error.msg.party.code.invalid", "Party with code " + partyCode + " does not exist", partyCode);
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when the savingsAccountNo given for a Party belongs to a different client than the Party's
|
||||
* own clientId - a Party can only be linked to its own client's savings account.
|
||||
*/
|
||||
public class PartySavingsAccountClientMismatchException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public PartySavingsAccountClientMismatchException(final String savingsAccountNo) {
|
||||
super("error.msg.party.savings.account.client.mismatch",
|
||||
"Savings account " + savingsAccountNo + " does not belong to this party's client", savingsAccountNo);
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
/**
|
||||
* Thrown when a Party's savingsAccountNo does not match any non-closed Savings Account.
|
||||
*/
|
||||
public class PartySavingsAccountNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public PartySavingsAccountNotFoundException(final String savingsAccountNo) {
|
||||
super("error.msg.party.savings.account.not.found", "No open savings account found with account number " + savingsAccountNo,
|
||||
savingsAccountNo);
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 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.dds.report.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* A generic tabular result shape shared by every DDS report - one column list plus one row list, both
|
||||
* keyed the same way, so a single report viewer screen can render any of them without a bespoke
|
||||
* result type per report.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public final class DdsReportData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String reportKey;
|
||||
private final boolean available;
|
||||
private final String message;
|
||||
private final List<String> columns;
|
||||
private final List<Map<String, Object>> rows;
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public final class PartyTransactionData implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final Long partyId;
|
||||
private final String partyCode;
|
||||
private final String partyName;
|
||||
private final Long agentId;
|
||||
private final String agentCode;
|
||||
private final LocalDate transactionDate;
|
||||
private final String transactionType;
|
||||
private final String direction;
|
||||
private final String transactionMode;
|
||||
private final String accountType;
|
||||
private final String accountNo;
|
||||
private final BigDecimal amount;
|
||||
private final String chequeNo;
|
||||
private final String tokenNo;
|
||||
private final String description;
|
||||
private final BigDecimal balanceAfterTransaction;
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.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 org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||
|
||||
/**
|
||||
* A single posted entry on the DDS ledger: a Party's day's collection ("Party Tran", direction
|
||||
* CREDIT), one leg of a "Withdraw Deposite" transfer between two parties (the creditor leg CREDIT,
|
||||
* the debtor leg DEBIT), or an Agent's settlement of collected cash to its branch ("Agent Tran",
|
||||
* direction DEBIT - no {@link Party} involved, so {@code party} is null for this type).
|
||||
* {@code balanceAfterTransaction} holds the Party's balance for Party-linked rows, or the Agent's
|
||||
* balance for an Agent Tran row. Immutable once posted - like any transaction, it is reversed with
|
||||
* an opposing entry, not edited, so there is deliberately no update API for this entity.
|
||||
*/
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_dds_party_transaction")
|
||||
public class PartyTransaction extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "party_id")
|
||||
private Party party;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "agent_id", nullable = false)
|
||||
private Agent agent;
|
||||
|
||||
@Column(name = "transaction_date", nullable = false)
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@Column(name = "transaction_type", length = 30, nullable = false)
|
||||
private String transactionType;
|
||||
|
||||
@Column(name = "direction", length = 10, nullable = false)
|
||||
private String direction;
|
||||
|
||||
@Column(name = "transaction_mode", length = 20)
|
||||
private String transactionMode;
|
||||
|
||||
@Column(name = "account_type", length = 100)
|
||||
private String accountType;
|
||||
|
||||
@Column(name = "account_no", length = 50)
|
||||
private String accountNo;
|
||||
|
||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "cheque_no", length = 40)
|
||||
private String chequeNo;
|
||||
|
||||
@Column(name = "token_no", length = 40)
|
||||
private String tokenNo;
|
||||
|
||||
@Column(name = "description", length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal balanceAfterTransaction;
|
||||
|
||||
protected PartyTransaction() {
|
||||
//
|
||||
}
|
||||
|
||||
public PartyTransaction(final Party party, final Agent agent, final LocalDate transactionDate, final String transactionType,
|
||||
final String direction, final String transactionMode, final String accountType, final String accountNo,
|
||||
final BigDecimal amount, final String chequeNo, final String tokenNo, final String description,
|
||||
final BigDecimal balanceAfterTransaction) {
|
||||
this.party = party;
|
||||
this.agent = agent;
|
||||
this.transactionDate = transactionDate;
|
||||
this.transactionType = transactionType;
|
||||
this.direction = direction;
|
||||
this.transactionMode = transactionMode;
|
||||
this.accountType = accountType;
|
||||
this.accountNo = accountNo;
|
||||
this.amount = amount;
|
||||
this.chequeNo = chequeNo;
|
||||
this.tokenNo = tokenNo;
|
||||
this.description = description;
|
||||
this.balanceAfterTransaction = balanceAfterTransaction;
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when an Agent Tran settlement amount exceeds the Agent's current (collected but not yet
|
||||
* settled) balance.
|
||||
*/
|
||||
public class InsufficientAgentBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public InsufficientAgentBalanceException(final String agentCode) {
|
||||
super("error.msg.agent.insufficient.balance",
|
||||
"Agent " + agentCode + " does not have sufficient collected balance for this settlement", agentCode);
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when a Withdraw Deposite transfer would take the debtor Party's balance below zero.
|
||||
*/
|
||||
public class InsufficientPartyBalanceException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public InsufficientPartyBalanceException(final String partyCode) {
|
||||
super("error.msg.party.insufficient.balance", "Party " + partyCode + " does not have sufficient balance for this transfer",
|
||||
partyCode);
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
/**
|
||||
* A {@link RuntimeException} thrown when a DDS party transaction resource is not found.
|
||||
*/
|
||||
public class PartyTransactionNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public PartyTransactionNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||
super("error.msg.partytransaction.id.invalid", "Party transaction with identifier " + id + " does not exist", id, e);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -31,7 +31,8 @@ public enum DepositAccountOnClosureType {
|
||||
WITHDRAW_DEPOSIT(100, "depositAccountClosureType.withdrawDeposit"), //
|
||||
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
||||
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"); //
|
||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
||||
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
||||
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
@@ -51,6 +52,7 @@ public enum DepositAccountOnClosureType {
|
||||
case 200 -> TRANSFER_TO_SAVINGS;
|
||||
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
||||
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
||||
case 500 -> PARTIAL_REINVEST;
|
||||
default -> INVALID;
|
||||
};
|
||||
}
|
||||
@@ -85,6 +87,10 @@ public enum DepositAccountOnClosureType {
|
||||
return this.equals(INVALID);
|
||||
}
|
||||
|
||||
public boolean isPartialReinvest() {
|
||||
return this.equals(PARTIAL_REINVEST);
|
||||
}
|
||||
|
||||
// TODO: do we really need this?!?
|
||||
public static Object[] integerValues() {
|
||||
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
||||
|
||||
+1
@@ -208,6 +208,7 @@ public final class DepositsApiConstants {
|
||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||
public static final String transferDescriptionParamName = "transferDescription";
|
||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||
public static final String reinvestAmountParamName = "reinvestAmount";
|
||||
public static final String savingsAccounts = "savingsAccounts";
|
||||
public static final String maturityInstructionIdParamName = "maturityInstructionId";
|
||||
public static final String transferToSavingsIdParamName = "transferToSavingsId";
|
||||
|
||||
-1
@@ -275,7 +275,6 @@ public class SavingsApiConstants {
|
||||
// Additional custom fields
|
||||
public static final String balanceFrequencyParamName = "balanceFrequency";
|
||||
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
||||
public static final String productCategoryParamName = "productCategory";
|
||||
|
||||
// Current Product — new UI fields
|
||||
public static final String balanceSwapOptionParamName = "balanceSwapOption";
|
||||
|
||||
-1
@@ -144,7 +144,6 @@ public final class SavingsProductData implements Serializable {
|
||||
@Getter @Setter private Boolean enableLockinPeriod;
|
||||
@Getter @Setter private String balanceFrequency;
|
||||
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
||||
@Getter @Setter private String productCategory;
|
||||
|
||||
// New current product fields
|
||||
@Getter @Setter private Boolean balanceSwapOption;
|
||||
|
||||
+4
@@ -797,6 +797,10 @@ public final class SavingsEnumerations {
|
||||
optionData = new EnumOptionData(DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getValue().longValue(),
|
||||
DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getCode(), "Re-Invest Principal Only");
|
||||
break;
|
||||
case PARTIAL_REINVEST:
|
||||
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
||||
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
||||
break;
|
||||
}
|
||||
return optionData;
|
||||
}
|
||||
|
||||
+5
@@ -1263,6 +1263,11 @@ public class AccountingProcessorHelper {
|
||||
accountMapping = paymentChannelSpecificAccountMapping;
|
||||
}
|
||||
}
|
||||
|
||||
if (accountMapping == null) {
|
||||
throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.SAVING, savingsProductId,
|
||||
CashAccountsForSavings.fromInt(accountMappingTypeId).toString());
|
||||
}
|
||||
glAccount = accountMapping.getGlAccount();
|
||||
}
|
||||
return glAccount;
|
||||
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.agent.data.AgentData;
|
||||
import org.apache.fineract.organisation.agent.service.AgentReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/agents")
|
||||
@Component
|
||||
@Tag(name = "DDS Agents", description = "A DDS collection agent - either a person or, at the top of the hierarchy, the bank itself - with its own settlement savings account and guarantors.")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentsApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "AGENT";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final AgentReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve DDS Agents", description = "Returns the list of DDS agents.")
|
||||
public Collection<AgentData> retrieveAll() {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{agentId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a DDS Agent", description = "Returns the details of a DDS agent, including its guarantors.")
|
||||
public AgentData retrieveOne(@PathParam("agentId") @Parameter(description = "agentId") final Long agentId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveOne(agentId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a DDS Agent", description = "Mandatory fields: clientId, agentNameId, agentAddress1, agentCity, "
|
||||
+ "agentPinCode, agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, homeBranch, "
|
||||
+ "savingBranch, tdsApplicable, commissionCalculation\n" + "\n"
|
||||
+ "Optional fields: agentAddress2, agentTelephone, agentOpeningBalance, tdsPercent, transferBranch, "
|
||||
+ "transferAmountAccountType, transferAccountNo, limit, machineRent, agentSecurityBranch, agentSecurityAccountType, "
|
||||
+ "agentSecurityAccountNo, otherCommission, agentDeposit, festivalAdvance, otherDeduction, limits, guarantors")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.createAgent() //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{agentId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update a DDS Agent", description = "Updates the details of a DDS agent.")
|
||||
public CommandProcessingResult update(@PathParam("agentId") @Parameter(description = "agentId") final Long agentId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.updateAgent(agentId) //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface AgentRepository extends JpaRepository<Agent, Long> {
|
||||
|
||||
@Query("select coalesce(max(cast(substring(a.agentCode, 3) as long)), 0) from Agent a where a.agentCode like 'AG%'")
|
||||
Long retrieveMaxAgentCodeSuffix();
|
||||
|
||||
Optional<Agent> findByAgentCode(String agentCode);
|
||||
|
||||
@Query("select a from Agent a where a.id between :agentFrom and :agentTo")
|
||||
List<Agent> findBetweenIds(@Param("agentFrom") Long agentFrom, @Param("agentTo") Long agentTo);
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.organisation.agent.service.AgentWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "AGENT", action = "CREATE")
|
||||
public class CreateAgentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final AgentWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public CreateAgentCommandHandler(final AgentWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createAgent(command);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.organisation.agent.service.AgentWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "AGENT", action = "UPDATE")
|
||||
public class UpdateAgentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final AgentWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public UpdateAgentCommandHandler(final AgentWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateAgent(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-409
@@ -1,409 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Validates create/update commands for an Agent - the DDS collection agent. As of 2026-08-24, Agent
|
||||
* does the daily collection work directly (the SubAgent tier was removed), so this deserializer carries
|
||||
* the collection/commission/limit/security-account fields SubAgent used to own, alongside Agent's
|
||||
* original identity/settlement-account fields.
|
||||
*/
|
||||
@Component
|
||||
public final class AgentCommandFromApiJsonDeserializer {
|
||||
|
||||
public static final String CLIENT_ID = "clientId";
|
||||
public static final String AGENT_NAME_ID = "agentNameId";
|
||||
public static final String AGENT_ADDRESS1 = "agentAddress1";
|
||||
public static final String AGENT_ADDRESS2 = "agentAddress2";
|
||||
public static final String AGENT_CITY = "agentCity";
|
||||
public static final String AGENT_PINCODE = "agentPinCode";
|
||||
public static final String AGENT_TELEPHONE = "agentTelephone";
|
||||
public static final String AGENT_SAVING_ACCOUNT_TYPE = "agentSavingAccountType";
|
||||
public static final String AGENT_SAVING_ACCOUNT_NO = "agentSavingAccountNo";
|
||||
public static final String AGENT_OPENING_DATE = "agentOpeningDate";
|
||||
public static final String AGENT_BIRTH_DATE = "agentBirthDate";
|
||||
public static final String AGENT_OPENING_BALANCE = "agentOpeningBalance";
|
||||
public static final String HOME_BRANCH = "homeBranch";
|
||||
public static final String SAVING_BRANCH = "savingBranch";
|
||||
public static final String TDS_APPLICABLE = "tdsApplicable";
|
||||
public static final String TDS_PERCENT = "tdsPercent";
|
||||
public static final String TRANSFER_BRANCH = "transferBranch";
|
||||
public static final String TRANSFER_AMOUNT_ACCOUNT_TYPE = "transferAmountAccountType";
|
||||
public static final String TRANSFER_ACCOUNT_NO = "transferAccountNo";
|
||||
public static final String COMMISSION_CALCULATION = "commissionCalculation";
|
||||
public static final String LIMIT = "limit";
|
||||
public static final String MACHINE_RENT = "machineRent";
|
||||
public static final String AGENT_SECURITY_BRANCH = "agentSecurityBranch";
|
||||
public static final String AGENT_SECURITY_ACCOUNT_TYPE = "agentSecurityAccountType";
|
||||
public static final String AGENT_SECURITY_ACCOUNT_NO = "agentSecurityAccountNo";
|
||||
public static final String OTHER_COMMISSION = "otherCommission";
|
||||
public static final String AGENT_DEPOSIT = "agentDeposit";
|
||||
public static final String FESTIVAL_ADVANCE = "festivalAdvance";
|
||||
public static final String OTHER_DEDUCTION = "otherDeduction";
|
||||
public static final String LIMITS = "limits";
|
||||
public static final String GUARANTORS = "guarantors";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
public static final String LOCALE = "locale";
|
||||
|
||||
private static final String GUARANTOR_CLIENT_ID = "clientId";
|
||||
private static final String GUARANTOR_NAME = "guarantorName";
|
||||
private static final String GUARANTOR_ADDRESS1 = "address1";
|
||||
private static final String GUARANTOR_ADDRESS2 = "address2";
|
||||
private static final String GUARANTOR_CITY = "city";
|
||||
private static final String GUARANTOR_PINCODE = "pinCode";
|
||||
private static final String GUARANTOR_TELEPHONE = "telephone";
|
||||
|
||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList(CLIENT_ID, AGENT_NAME_ID, AGENT_ADDRESS1,
|
||||
AGENT_ADDRESS2, AGENT_CITY, AGENT_PINCODE, AGENT_TELEPHONE, AGENT_SAVING_ACCOUNT_TYPE, AGENT_SAVING_ACCOUNT_NO,
|
||||
AGENT_OPENING_DATE, AGENT_BIRTH_DATE, AGENT_OPENING_BALANCE, HOME_BRANCH, SAVING_BRANCH, TDS_APPLICABLE, TDS_PERCENT,
|
||||
TRANSFER_BRANCH, TRANSFER_AMOUNT_ACCOUNT_TYPE, TRANSFER_ACCOUNT_NO, COMMISSION_CALCULATION, LIMIT, MACHINE_RENT,
|
||||
AGENT_SECURITY_BRANCH, AGENT_SECURITY_ACCOUNT_TYPE, AGENT_SECURITY_ACCOUNT_NO, OTHER_COMMISSION, AGENT_DEPOSIT,
|
||||
FESTIVAL_ADVANCE, OTHER_DEDUCTION, LIMITS, GUARANTORS, DATE_FORMAT, LOCALE));
|
||||
|
||||
private static final Set<String> SUPPORTED_LIMIT_PARAMETERS = new HashSet<>(Arrays.asList("fromDate", "toDate", "limit"));
|
||||
|
||||
private static final Set<String> SUPPORTED_GUARANTOR_PARAMETERS = new HashSet<>(Arrays.asList(GUARANTOR_CLIENT_ID, GUARANTOR_NAME,
|
||||
GUARANTOR_ADDRESS1, GUARANTOR_ADDRESS2, GUARANTOR_CITY, GUARANTOR_PINCODE, GUARANTOR_TELEPHONE));
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Autowired
|
||||
public AgentCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("agent");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final Long clientId = this.fromApiJsonHelper.extractLongNamed(CLIENT_ID, element);
|
||||
baseDataValidator.reset().parameter(CLIENT_ID).value(clientId).notNull().integerGreaterThanZero();
|
||||
|
||||
final String agentName = this.fromApiJsonHelper.extractStringNamed(AGENT_NAME_ID, element);
|
||||
baseDataValidator.reset().parameter(AGENT_NAME_ID).value(agentName).notBlank().notExceedingLengthOf(100);
|
||||
|
||||
final String agentAddress1 = this.fromApiJsonHelper.extractStringNamed(AGENT_ADDRESS1, element);
|
||||
baseDataValidator.reset().parameter(AGENT_ADDRESS1).value(agentAddress1).notBlank().notExceedingLengthOf(200);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_ADDRESS2, element)) {
|
||||
final String agentAddress2 = this.fromApiJsonHelper.extractStringNamed(AGENT_ADDRESS2, element);
|
||||
baseDataValidator.reset().parameter(AGENT_ADDRESS2).value(agentAddress2).ignoreIfNull().notExceedingLengthOf(200);
|
||||
}
|
||||
|
||||
final String agentCity = this.fromApiJsonHelper.extractStringNamed(AGENT_CITY, element);
|
||||
baseDataValidator.reset().parameter(AGENT_CITY).value(agentCity).notBlank().notExceedingLengthOf(100);
|
||||
|
||||
final String agentPinCode = this.fromApiJsonHelper.extractStringNamed(AGENT_PINCODE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_PINCODE).value(agentPinCode).notBlank().notExceedingLengthOf(20);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_TELEPHONE, element)) {
|
||||
final String agentTelephone = this.fromApiJsonHelper.extractStringNamed(AGENT_TELEPHONE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_TELEPHONE).value(agentTelephone).ignoreIfNull().notExceedingLengthOf(20);
|
||||
}
|
||||
|
||||
final String agentSavingAccountType = this.fromApiJsonHelper.extractStringNamed(AGENT_SAVING_ACCOUNT_TYPE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_SAVING_ACCOUNT_TYPE).value(agentSavingAccountType).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
|
||||
final String agentSavingAccountNo = this.fromApiJsonHelper.extractStringNamed(AGENT_SAVING_ACCOUNT_NO, element);
|
||||
baseDataValidator.reset().parameter(AGENT_SAVING_ACCOUNT_NO).value(agentSavingAccountNo).notBlank().notExceedingLengthOf(50);
|
||||
|
||||
final var agentOpeningDate = this.fromApiJsonHelper.extractLocalDateNamed(AGENT_OPENING_DATE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_OPENING_DATE).value(agentOpeningDate).notNull();
|
||||
|
||||
final var agentBirthDate = this.fromApiJsonHelper.extractLocalDateNamed(AGENT_BIRTH_DATE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_BIRTH_DATE).value(agentBirthDate).notNull();
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_OPENING_BALANCE, element)) {
|
||||
final var agentOpeningBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AGENT_OPENING_BALANCE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_OPENING_BALANCE).value(agentOpeningBalance).ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
|
||||
final String homeBranch = this.fromApiJsonHelper.extractStringNamed(HOME_BRANCH, element);
|
||||
baseDataValidator.reset().parameter(HOME_BRANCH).value(homeBranch).notBlank().notExceedingLengthOf(100);
|
||||
|
||||
final String savingBranch = this.fromApiJsonHelper.extractStringNamed(SAVING_BRANCH, element);
|
||||
baseDataValidator.reset().parameter(SAVING_BRANCH).value(savingBranch).notBlank().notExceedingLengthOf(100);
|
||||
|
||||
baseDataValidator.reset().parameter(TDS_APPLICABLE).trueOrFalseRequired(this.fromApiJsonHelper.extractStringNamed(TDS_APPLICABLE,
|
||||
element));
|
||||
baseDataValidator.reset().parameter(COMMISSION_CALCULATION)
|
||||
.trueOrFalseRequired(this.fromApiJsonHelper.extractStringNamed(COMMISSION_CALCULATION, element));
|
||||
|
||||
validateOptionalStrings(element, baseDataValidator);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(LIMITS, element)) {
|
||||
validateLimits(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(GUARANTORS, element)) {
|
||||
validateGuarantors(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateForUpdate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("agent");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(CLIENT_ID, element)) {
|
||||
final Long clientId = this.fromApiJsonHelper.extractLongNamed(CLIENT_ID, element);
|
||||
baseDataValidator.reset().parameter(CLIENT_ID).value(clientId).notNull().integerGreaterThanZero();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_NAME_ID, element)) {
|
||||
final String agentName = this.fromApiJsonHelper.extractStringNamed(AGENT_NAME_ID, element);
|
||||
baseDataValidator.reset().parameter(AGENT_NAME_ID).value(agentName).notBlank().notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_ADDRESS1, element)) {
|
||||
final String agentAddress1 = this.fromApiJsonHelper.extractStringNamed(AGENT_ADDRESS1, element);
|
||||
baseDataValidator.reset().parameter(AGENT_ADDRESS1).value(agentAddress1).notBlank().notExceedingLengthOf(200);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_ADDRESS2, element)) {
|
||||
final String agentAddress2 = this.fromApiJsonHelper.extractStringNamed(AGENT_ADDRESS2, element);
|
||||
baseDataValidator.reset().parameter(AGENT_ADDRESS2).value(agentAddress2).ignoreIfNull().notExceedingLengthOf(200);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_CITY, element)) {
|
||||
final String agentCity = this.fromApiJsonHelper.extractStringNamed(AGENT_CITY, element);
|
||||
baseDataValidator.reset().parameter(AGENT_CITY).value(agentCity).notBlank().notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_PINCODE, element)) {
|
||||
final String agentPinCode = this.fromApiJsonHelper.extractStringNamed(AGENT_PINCODE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_PINCODE).value(agentPinCode).notBlank().notExceedingLengthOf(20);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_TELEPHONE, element)) {
|
||||
final String agentTelephone = this.fromApiJsonHelper.extractStringNamed(AGENT_TELEPHONE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_TELEPHONE).value(agentTelephone).ignoreIfNull().notExceedingLengthOf(20);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_SAVING_ACCOUNT_TYPE, element)) {
|
||||
final String agentSavingAccountType = this.fromApiJsonHelper.extractStringNamed(AGENT_SAVING_ACCOUNT_TYPE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_SAVING_ACCOUNT_TYPE).value(agentSavingAccountType).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_SAVING_ACCOUNT_NO, element)) {
|
||||
final String agentSavingAccountNo = this.fromApiJsonHelper.extractStringNamed(AGENT_SAVING_ACCOUNT_NO, element);
|
||||
baseDataValidator.reset().parameter(AGENT_SAVING_ACCOUNT_NO).value(agentSavingAccountNo).notBlank().notExceedingLengthOf(50);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_OPENING_DATE, element)) {
|
||||
final var agentOpeningDate = this.fromApiJsonHelper.extractLocalDateNamed(AGENT_OPENING_DATE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_OPENING_DATE).value(agentOpeningDate).notNull();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_BIRTH_DATE, element)) {
|
||||
final var agentBirthDate = this.fromApiJsonHelper.extractLocalDateNamed(AGENT_BIRTH_DATE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_BIRTH_DATE).value(agentBirthDate).notNull();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_OPENING_BALANCE, element)) {
|
||||
final var agentOpeningBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AGENT_OPENING_BALANCE, element);
|
||||
baseDataValidator.reset().parameter(AGENT_OPENING_BALANCE).value(agentOpeningBalance).ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(HOME_BRANCH, element)) {
|
||||
final String homeBranch = this.fromApiJsonHelper.extractStringNamed(HOME_BRANCH, element);
|
||||
baseDataValidator.reset().parameter(HOME_BRANCH).value(homeBranch).notBlank().notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(SAVING_BRANCH, element)) {
|
||||
final String savingBranch = this.fromApiJsonHelper.extractStringNamed(SAVING_BRANCH, element);
|
||||
baseDataValidator.reset().parameter(SAVING_BRANCH).value(savingBranch).notBlank().notExceedingLengthOf(100);
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(TDS_APPLICABLE, element)) {
|
||||
baseDataValidator.reset().parameter(TDS_APPLICABLE)
|
||||
.trueOrFalseRequired(this.fromApiJsonHelper.extractStringNamed(TDS_APPLICABLE, element));
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(COMMISSION_CALCULATION, element)) {
|
||||
baseDataValidator.reset().parameter(COMMISSION_CALCULATION)
|
||||
.trueOrFalseRequired(this.fromApiJsonHelper.extractStringNamed(COMMISSION_CALCULATION, element));
|
||||
}
|
||||
|
||||
validateOptionalStrings(element, baseDataValidator);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(LIMITS, element)) {
|
||||
validateLimits(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(GUARANTORS, element)) {
|
||||
validateGuarantors(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void validateOptionalStrings(final JsonElement element, final DataValidatorBuilder v) {
|
||||
if (this.fromApiJsonHelper.parameterExists(TDS_PERCENT, element)) {
|
||||
v.reset().parameter(TDS_PERCENT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TDS_PERCENT, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(TRANSFER_BRANCH, element)) {
|
||||
v.reset().parameter(TRANSFER_BRANCH).value(this.fromApiJsonHelper.extractStringNamed(TRANSFER_BRANCH, element))
|
||||
.ignoreIfNull().notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(TRANSFER_AMOUNT_ACCOUNT_TYPE, element)) {
|
||||
v.reset().parameter(TRANSFER_AMOUNT_ACCOUNT_TYPE)
|
||||
.value(this.fromApiJsonHelper.extractStringNamed(TRANSFER_AMOUNT_ACCOUNT_TYPE, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(TRANSFER_ACCOUNT_NO, element)) {
|
||||
v.reset().parameter(TRANSFER_ACCOUNT_NO).value(this.fromApiJsonHelper.extractStringNamed(TRANSFER_ACCOUNT_NO, element))
|
||||
.ignoreIfNull().notExceedingLengthOf(50);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(LIMIT, element)) {
|
||||
v.reset().parameter(LIMIT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LIMIT, element)).ignoreIfNull()
|
||||
.zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(MACHINE_RENT, element)) {
|
||||
v.reset().parameter(MACHINE_RENT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(MACHINE_RENT, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_SECURITY_BRANCH, element)) {
|
||||
v.reset().parameter(AGENT_SECURITY_BRANCH).value(this.fromApiJsonHelper.extractStringNamed(AGENT_SECURITY_BRANCH, element))
|
||||
.ignoreIfNull().notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_SECURITY_ACCOUNT_TYPE, element)) {
|
||||
v.reset().parameter(AGENT_SECURITY_ACCOUNT_TYPE)
|
||||
.value(this.fromApiJsonHelper.extractStringNamed(AGENT_SECURITY_ACCOUNT_TYPE, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_SECURITY_ACCOUNT_NO, element)) {
|
||||
v.reset().parameter(AGENT_SECURITY_ACCOUNT_NO)
|
||||
.value(this.fromApiJsonHelper.extractStringNamed(AGENT_SECURITY_ACCOUNT_NO, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(OTHER_COMMISSION, element)) {
|
||||
v.reset().parameter(OTHER_COMMISSION).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(OTHER_COMMISSION, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(AGENT_DEPOSIT, element)) {
|
||||
v.reset().parameter(AGENT_DEPOSIT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AGENT_DEPOSIT, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(FESTIVAL_ADVANCE, element)) {
|
||||
v.reset().parameter(FESTIVAL_ADVANCE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FESTIVAL_ADVANCE, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(OTHER_DEDUCTION, element)) {
|
||||
v.reset().parameter(OTHER_DEDUCTION).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(OTHER_DEDUCTION, element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLimits(final JsonObject topLevelJson, final List<ApiParameterError> dataValidationErrors) {
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("agent.limit");
|
||||
final var limitsArray = topLevelJson.getAsJsonArray(LIMITS);
|
||||
for (int i = 0; i < limitsArray.size(); i++) {
|
||||
final JsonObject limitJson = limitsArray.get(i).getAsJsonObject();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(limitJson, SUPPORTED_LIMIT_PARAMETERS);
|
||||
|
||||
v.reset().parameterAtIndexArray("fromDate", i).value(extractString(limitJson, "fromDate")).notBlank();
|
||||
v.reset().parameterAtIndexArray("toDate", i).value(extractString(limitJson, "toDate")).notBlank();
|
||||
v.reset().parameterAtIndexArray("limit", i).value(extractBigDecimal(limitJson, "limit")).notNull().zeroOrPositiveAmount();
|
||||
}
|
||||
}
|
||||
|
||||
private void validateGuarantors(final JsonObject topLevelJson, final List<ApiParameterError> dataValidationErrors) {
|
||||
final DataValidatorBuilder guarantorValidator = new DataValidatorBuilder(dataValidationErrors).resource("agent.guarantor");
|
||||
|
||||
final var guarantorsArray = topLevelJson.getAsJsonArray(GUARANTORS);
|
||||
for (int i = 0; i < guarantorsArray.size(); i++) {
|
||||
final JsonObject guarantorJson = guarantorsArray.get(i).getAsJsonObject();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(guarantorJson, SUPPORTED_GUARANTOR_PARAMETERS);
|
||||
|
||||
guarantorValidator.reset().parameterAtIndexArray(GUARANTOR_CLIENT_ID, i)
|
||||
.value(extractLong(guarantorJson, GUARANTOR_CLIENT_ID)).notNull().integerGreaterThanZero();
|
||||
guarantorValidator.reset().parameterAtIndexArray(GUARANTOR_NAME, i).value(extractString(guarantorJson, GUARANTOR_NAME))
|
||||
.notBlank().notExceedingLengthOf(100);
|
||||
guarantorValidator.reset().parameterAtIndexArray(GUARANTOR_ADDRESS1, i).value(extractString(guarantorJson, GUARANTOR_ADDRESS1))
|
||||
.notBlank().notExceedingLengthOf(200);
|
||||
guarantorValidator.reset().parameterAtIndexArray(GUARANTOR_CITY, i).value(extractString(guarantorJson, GUARANTOR_CITY))
|
||||
.notBlank().notExceedingLengthOf(100);
|
||||
guarantorValidator.reset().parameterAtIndexArray(GUARANTOR_PINCODE, i).value(extractString(guarantorJson, GUARANTOR_PINCODE))
|
||||
.notBlank().notExceedingLengthOf(20);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractString(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
|
||||
}
|
||||
|
||||
private Long extractLong(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsLong() : null;
|
||||
}
|
||||
|
||||
private java.math.BigDecimal extractBigDecimal(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsBigDecimal() : null;
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.organisation.agent.data.AgentData;
|
||||
|
||||
public interface AgentReadPlatformService {
|
||||
|
||||
Collection<AgentData> retrieveAll();
|
||||
|
||||
AgentData retrieveOne(Long agentId);
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.organisation.agent.data.AgentData;
|
||||
import org.apache.fineract.organisation.agent.data.AgentGuarantorData;
|
||||
import org.apache.fineract.organisation.agent.data.AgentLimitData;
|
||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class AgentReadPlatformServiceImpl implements AgentReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final AgentMapper AGENT_MAPPER = new AgentMapper();
|
||||
private static final AgentLimitMapper LIMIT_MAPPER = new AgentLimitMapper();
|
||||
private static final AgentGuarantorMapper GUARANTOR_MAPPER = new AgentGuarantorMapper();
|
||||
|
||||
private static final class AgentMapper implements RowMapper<AgentData> {
|
||||
|
||||
public String schema() {
|
||||
return " a.id as id, a.client_id as clientId, c.display_name as clientName, a.agent_code as agentCode, "
|
||||
+ " a.agent_name as agentNameId, a.agent_address1 as agentAddress1, a.agent_address2 as agentAddress2, "
|
||||
+ " a.agent_city as agentCity, a.agent_pincode as agentPinCode, a.agent_telephone as agentTelephone, "
|
||||
+ " a.agent_saving_account_type as agentSavingAccountType, a.agent_saving_account_no as agentSavingAccountNo, "
|
||||
+ " a.agent_opening_date as agentOpeningDate, a.agent_birth_date as agentBirthDate, "
|
||||
+ " a.agent_current_balance as agentCurrentBalance, a.agent_opening_balance as agentOpeningBalance, "
|
||||
+ " a.home_branch as homeBranch, a.saving_branch as savingBranch, a.tds_applicable as tdsApplicable, "
|
||||
+ " a.tds_percent as tdsPercent, a.current_tds as currentTds, a.tds_amount as tdsAmount, "
|
||||
+ " a.transfer_branch as transferBranch, a.transfer_amount_account_type as transferAmountAccountType, "
|
||||
+ " a.transfer_account_no as transferAccountNo, a.collector_commission_calculation as commissionCalculation, "
|
||||
+ " a.collector_limit_amount as limitAmount, a.machine_rent as machineRent, "
|
||||
+ " a.agent_security_branch as agentSecurityBranch, a.agent_security_account_type as agentSecurityAccountType, "
|
||||
+ " a.agent_security_account_no as agentSecurityAccountNo, a.commission as commission, "
|
||||
+ " a.other_commission as otherCommission, a.agent_deposit as agentDeposit, "
|
||||
+ " a.festival_advance as festivalAdvance, a.other_deduction as otherDeduction "
|
||||
+ " from m_dds_agent a join m_client c on c.id = a.client_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return AgentData.instance(rs.getLong("id"), rs.getLong("clientId"), rs.getString("clientName"), rs.getString("agentCode"),
|
||||
rs.getString("agentNameId"), rs.getString("agentAddress1"), rs.getString("agentAddress2"),
|
||||
rs.getString("agentCity"), rs.getString("agentPinCode"), rs.getString("agentTelephone"),
|
||||
rs.getString("agentSavingAccountType"), rs.getString("agentSavingAccountNo"),
|
||||
JdbcSupport.getLocalDate(rs, "agentOpeningDate"), JdbcSupport.getLocalDate(rs, "agentBirthDate"),
|
||||
rs.getBigDecimal("agentCurrentBalance"), rs.getBigDecimal("agentOpeningBalance"), rs.getString("homeBranch"),
|
||||
rs.getString("savingBranch"), rs.getBoolean("tdsApplicable"), rs.getBigDecimal("tdsPercent"),
|
||||
rs.getBigDecimal("currentTds"), rs.getBigDecimal("tdsAmount"), rs.getString("transferBranch"),
|
||||
rs.getString("transferAmountAccountType"), rs.getString("transferAccountNo"),
|
||||
rs.getBoolean("commissionCalculation"), rs.getBigDecimal("limitAmount"), rs.getBigDecimal("machineRent"),
|
||||
rs.getString("agentSecurityBranch"), rs.getString("agentSecurityAccountType"),
|
||||
rs.getString("agentSecurityAccountNo"), rs.getBigDecimal("commission"), rs.getBigDecimal("otherCommission"),
|
||||
rs.getBigDecimal("agentDeposit"), rs.getBigDecimal("festivalAdvance"), rs.getBigDecimal("otherDeduction"),
|
||||
new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AgentLimitMapper implements RowMapper<Map.Entry<Long, AgentLimitData>> {
|
||||
|
||||
public String schema() {
|
||||
return " l.agent_id as agentId, l.id as id, l.from_date as fromDate, l.to_date as toDate, "
|
||||
+ " l.limit_amount as limitAmount from m_dds_agent_limit l";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<Long, AgentLimitData> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Long agentId = rs.getLong("agentId");
|
||||
final AgentLimitData data = AgentLimitData.instance(rs.getLong("id"), JdbcSupport.getLocalDate(rs, "fromDate"),
|
||||
JdbcSupport.getLocalDate(rs, "toDate"), rs.getBigDecimal("limitAmount"));
|
||||
return Map.entry(agentId, data);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AgentGuarantorMapper implements RowMapper<Map.Entry<Long, AgentGuarantorData>> {
|
||||
|
||||
public String schema() {
|
||||
return " g.agent_id as agentId, g.id as id, g.client_id as clientId, g.guarantor_name as guarantorName, "
|
||||
+ " g.address1 as address1, g.address2 as address2, g.city as city, g.pin_code as pinCode, "
|
||||
+ " g.telephone as telephone from m_dds_agent_guarantor g";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<Long, AgentGuarantorData> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Long agentId = rs.getLong("agentId");
|
||||
final AgentGuarantorData data = AgentGuarantorData.instance(rs.getLong("id"), rs.getLong("clientId"),
|
||||
rs.getString("guarantorName"), rs.getString("address1"), rs.getString("address2"), rs.getString("city"),
|
||||
rs.getString("pinCode"), rs.getString("telephone"));
|
||||
return Map.entry(agentId, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AgentData> retrieveAll() {
|
||||
final String sql = "select " + AGENT_MAPPER.schema() + " order by a.id";
|
||||
final List<AgentData> agents = this.jdbcTemplate.query(sql, AGENT_MAPPER); // NOSONAR
|
||||
return attachChildren(agents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentData retrieveOne(final Long agentId) {
|
||||
try {
|
||||
final String sql = "select " + AGENT_MAPPER.schema() + " where a.id = ?";
|
||||
final AgentData agent = this.jdbcTemplate.queryForObject(sql, AGENT_MAPPER, agentId); // NOSONAR
|
||||
return attachChildren(List.of(agent)).iterator().next();
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new AgentNotFoundException(agentId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<AgentData> attachChildren(final List<AgentData> agents) {
|
||||
if (agents.isEmpty()) {
|
||||
return agents;
|
||||
}
|
||||
|
||||
final String limitSql = "select " + LIMIT_MAPPER.schema() + " order by l.id";
|
||||
final List<Map.Entry<Long, AgentLimitData>> limitRows = this.jdbcTemplate.query(limitSql, LIMIT_MAPPER); // NOSONAR
|
||||
final Map<Long, List<AgentLimitData>> limitsByAgentId = new LinkedHashMap<>();
|
||||
for (final Map.Entry<Long, AgentLimitData> row : limitRows) {
|
||||
limitsByAgentId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue());
|
||||
}
|
||||
|
||||
final String guarantorSql = "select " + GUARANTOR_MAPPER.schema() + " order by g.id";
|
||||
final List<Map.Entry<Long, AgentGuarantorData>> guarantorRows = this.jdbcTemplate.query(guarantorSql, GUARANTOR_MAPPER); // NOSONAR
|
||||
final Map<Long, List<AgentGuarantorData>> guarantorsByAgentId = new LinkedHashMap<>();
|
||||
for (final Map.Entry<Long, AgentGuarantorData> row : guarantorRows) {
|
||||
guarantorsByAgentId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue());
|
||||
}
|
||||
|
||||
final List<AgentData> result = new ArrayList<>(agents.size());
|
||||
for (final AgentData agent : agents) {
|
||||
final List<AgentLimitData> limits = limitsByAgentId.getOrDefault(agent.getId(), new ArrayList<>());
|
||||
final List<AgentGuarantorData> guarantors = guarantorsByAgentId.getOrDefault(agent.getId(), new ArrayList<>());
|
||||
result.add(AgentData.instance(agent.getId(), agent.getClientId(), agent.getClientName(), agent.getAgentCode(),
|
||||
agent.getAgentNameId(), agent.getAgentAddress1(), agent.getAgentAddress2(), agent.getAgentCity(),
|
||||
agent.getAgentPinCode(), agent.getAgentTelephone(), agent.getAgentSavingAccountType(),
|
||||
agent.getAgentSavingAccountNo(), agent.getAgentOpeningDate(), agent.getAgentBirthDate(),
|
||||
agent.getAgentCurrentBalance(), agent.getAgentOpeningBalance(), agent.getHomeBranch(), agent.getSavingBranch(),
|
||||
agent.getTdsApplicable(), agent.getTdsPercent(), agent.getCurrentTds(), agent.getTdsAmount(),
|
||||
agent.getTransferBranch(), agent.getTransferAmountAccountType(), agent.getTransferAccountNo(),
|
||||
agent.getCommissionCalculation(), agent.getLimit(), agent.getMachineRent(), agent.getAgentSecurityBranch(),
|
||||
agent.getAgentSecurityAccountType(), agent.getAgentSecurityAccountNo(), agent.getCommission(),
|
||||
agent.getOtherCommission(), agent.getAgentDeposit(), agent.getFestivalAdvance(), agent.getOtherDeduction(),
|
||||
limits, guarantors));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface AgentWritePlatformService {
|
||||
|
||||
CommandProcessingResult createAgent(JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateAgent(Long agentId, JsonCommand command);
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentGuarantor;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentLimit;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||
import org.apache.fineract.organisation.agent.serialization.AgentCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AgentWritePlatformServiceJpaRepositoryImpl implements AgentWritePlatformService {
|
||||
|
||||
private final AgentCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final AgentRepository agentRepository;
|
||||
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createAgent(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||
|
||||
final Long clientId = command.longValueOfParameterNamed("clientId");
|
||||
final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final String agentCode = generateNextAgentCode();
|
||||
final Agent agent = Agent.fromJson(client, agentCode, command);
|
||||
|
||||
addLimitsFromCommand(agent, command);
|
||||
addGuarantorsFromCommand(agent, command);
|
||||
|
||||
this.agentRepository.saveAndFlush(agent);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(agent.getId()) //
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleAgentDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||
return CommandProcessingResult.empty();
|
||||
} catch (final PersistenceException dve) {
|
||||
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
|
||||
handleAgentDataIntegrityIssues(throwable, dve);
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateAgent(final Long agentId, final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
||||
|
||||
final Agent agentForUpdate = this.agentRepository.findById(agentId).orElseThrow(() -> new AgentNotFoundException(agentId));
|
||||
final Map<String, Object> changesOnly = agentForUpdate.update(command);
|
||||
|
||||
if (changesOnly.containsKey("clientId")) {
|
||||
final Long newClientId = (Long) changesOnly.get("clientId");
|
||||
final Client newClient = this.clientRepositoryWrapper.findOneWithNotFoundDetection(newClientId);
|
||||
agentForUpdate.changeClient(newClient);
|
||||
}
|
||||
|
||||
if (command.parameterExists("limits")) {
|
||||
agentForUpdate.resetLimits();
|
||||
addLimitsFromCommand(agentForUpdate, command);
|
||||
changesOnly.put("limits", command.jsonFragment("limits"));
|
||||
}
|
||||
|
||||
if (command.parameterExists("guarantors")) {
|
||||
agentForUpdate.resetGuarantors();
|
||||
addGuarantorsFromCommand(agentForUpdate, command);
|
||||
changesOnly.put("guarantors", command.jsonFragment("guarantors"));
|
||||
}
|
||||
|
||||
this.agentRepository.saveAndFlush(agentForUpdate);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(agentId).with(changesOnly)
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleAgentDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||
return CommandProcessingResult.empty();
|
||||
} catch (final PersistenceException dve) {
|
||||
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
|
||||
handleAgentDataIntegrityIssues(throwable, dve);
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void addLimitsFromCommand(final Agent agent, final JsonCommand command) {
|
||||
if (!command.parameterExists("limits")) {
|
||||
return;
|
||||
}
|
||||
final JsonArray limitsArray = command.arrayOfParameterNamed("limits");
|
||||
if (limitsArray == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < limitsArray.size(); i++) {
|
||||
final JsonObject limitJson = limitsArray.get(i).getAsJsonObject();
|
||||
final LocalDate fromDate = LocalDate.parse(limitJson.get("fromDate").getAsString());
|
||||
final LocalDate toDate = LocalDate.parse(limitJson.get("toDate").getAsString());
|
||||
final BigDecimal limit = limitJson.get("limit").getAsBigDecimal();
|
||||
agent.addLimit(new AgentLimit(fromDate, toDate, limit));
|
||||
}
|
||||
}
|
||||
|
||||
private void addGuarantorsFromCommand(final Agent agent, final JsonCommand command) {
|
||||
if (!command.parameterExists("guarantors")) {
|
||||
return;
|
||||
}
|
||||
final JsonArray guarantorsArray = command.arrayOfParameterNamed("guarantors");
|
||||
if (guarantorsArray == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < guarantorsArray.size(); i++) {
|
||||
final JsonObject guarantorJson = guarantorsArray.get(i).getAsJsonObject();
|
||||
final Long guarantorClientId = guarantorJson.get("clientId").getAsLong();
|
||||
final String guarantorName = guarantorJson.get("guarantorName").getAsString();
|
||||
final String address1 = guarantorJson.get("address1").getAsString();
|
||||
final String address2 = guarantorJson.has("address2") && !guarantorJson.get("address2").isJsonNull()
|
||||
? guarantorJson.get("address2").getAsString()
|
||||
: null;
|
||||
final String city = guarantorJson.get("city").getAsString();
|
||||
final String pinCode = guarantorJson.get("pinCode").getAsString();
|
||||
final String telephone = guarantorJson.has("telephone") && !guarantorJson.get("telephone").isJsonNull()
|
||||
? guarantorJson.get("telephone").getAsString()
|
||||
: null;
|
||||
|
||||
agent.addGuarantor(new AgentGuarantor(guarantorClientId, guarantorName, address1, address2, city, pinCode, telephone));
|
||||
}
|
||||
}
|
||||
|
||||
private String generateNextAgentCode() {
|
||||
final Long maxSuffix = this.agentRepository.retrieveMaxAgentCodeSuffix();
|
||||
final long nextSuffix = (maxSuffix == null ? 0L : maxSuffix) + 1L;
|
||||
return String.format("AG%04d", nextSuffix);
|
||||
}
|
||||
|
||||
private void handleAgentDataIntegrityIssues(final Throwable realCause, final Exception dve) {
|
||||
if (realCause.getMessage() != null && realCause.getMessage().contains("agent_code")) {
|
||||
throw new PlatformDataIntegrityException("error.msg.agent.duplicate.agentCode", "An agent with the generated code already exists, please retry");
|
||||
}
|
||||
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.agent.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource: " + realCause.getMessage());
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.agent.starter;
|
||||
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.organisation.agent.serialization.AgentCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.organisation.agent.service.AgentReadPlatformService;
|
||||
import org.apache.fineract.organisation.agent.service.AgentReadPlatformServiceImpl;
|
||||
import org.apache.fineract.organisation.agent.service.AgentWritePlatformService;
|
||||
import org.apache.fineract.organisation.agent.service.AgentWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class OrganisationAgentConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(AgentReadPlatformService.class)
|
||||
public AgentReadPlatformService agentReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new AgentReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(AgentWritePlatformService.class)
|
||||
public AgentWritePlatformService agentWritePlatformService(AgentCommandFromApiJsonDeserializer fromApiJsonDeserializer,
|
||||
AgentRepository agentRepository, ClientRepositoryWrapper clientRepositoryWrapper) {
|
||||
return new AgentWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, agentRepository, clientRepositoryWrapper);
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.commissionslab.data.CommissionSlabData;
|
||||
import org.apache.fineract.organisation.commissionslab.service.CommissionSlabReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/commissionslabs")
|
||||
@Component
|
||||
@Tag(name = "DDS Commission Slabs", description = "The agent commission chart: ACCOUNT slabs (commission percentage on collection volume) and COMMISSION slabs (the override a senior agent earns on a sub-agent's commission). Append-only reference data - no update or delete API.")
|
||||
@RequiredArgsConstructor
|
||||
public class CommissionSlabsApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "COMMISSIONSLAB";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CommissionSlabReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve DDS Commission Slabs", description = "Returns the full commission slab chart.")
|
||||
public Collection<CommissionSlabData> retrieveAll() {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Add DDS Commission Slabs", description = "Mandatory fields: slabs (array of {slabType: ACCOUNT|COMMISSION, "
|
||||
+ "slabFrom, slabTo, commissionPercent}). Adds new slab rows to the chart - it does not replace or edit existing rows.")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.createCommissionSlabs() //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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;
|
||||
|
||||
public interface CommissionSlabRepository extends JpaRepository<CommissionSlab, Long> {
|
||||
|
||||
@Query("select s from CommissionSlab s where s.slabType = :slabType and :amount between s.slabFrom and s.slabTo")
|
||||
List<CommissionSlab> findMatchingSlabs(@Param("slabType") String slabType, @Param("amount") BigDecimal amount);
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class CommissionSlabCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final Set<String> SUPPORTED_SLAB_TYPES = new HashSet<>(Arrays.asList("ACCOUNT", "COMMISSION"));
|
||||
|
||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("slabs", "dateFormat", "locale"));
|
||||
|
||||
private static final Set<String> SUPPORTED_SLAB_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList("slabType", "slabFrom", "slabTo", "commissionPercent"));
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Autowired
|
||||
public CommissionSlabCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("commissionslab");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
if (!this.fromApiJsonHelper.parameterExists("slabs", element)) {
|
||||
v.reset().parameter("slabs").value(null).notNull();
|
||||
} else {
|
||||
final JsonObject topLevel = element.getAsJsonObject();
|
||||
final var slabsArray = topLevel.getAsJsonArray("slabs");
|
||||
v.reset().parameter("slabs").value(slabsArray).jsonArrayNotEmpty();
|
||||
|
||||
for (int i = 0; i < slabsArray.size(); i++) {
|
||||
final JsonObject slabJson = slabsArray.get(i).getAsJsonObject();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(slabJson, SUPPORTED_SLAB_PARAMETERS);
|
||||
|
||||
final String slabType = extractString(slabJson, "slabType");
|
||||
v.reset().parameterAtIndexArray("slabType", i).value(slabType).notBlank()
|
||||
.isOneOfTheseStringValues(SUPPORTED_SLAB_TYPES.toArray());
|
||||
v.reset().parameterAtIndexArray("slabFrom", i).value(extractBigDecimal(slabJson, "slabFrom")).notNull()
|
||||
.zeroOrPositiveAmount();
|
||||
v.reset().parameterAtIndexArray("slabTo", i).value(extractBigDecimal(slabJson, "slabTo")).notNull()
|
||||
.zeroOrPositiveAmount();
|
||||
v.reset().parameterAtIndexArray("commissionPercent", i).value(extractBigDecimal(slabJson, "commissionPercent"))
|
||||
.notNull().zeroOrPositiveAmount();
|
||||
}
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private String extractString(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
|
||||
}
|
||||
|
||||
private BigDecimal extractBigDecimal(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsBigDecimal() : null;
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.organisation.commissionslab.data.CommissionSlabData;
|
||||
|
||||
public interface CommissionSlabReadPlatformService {
|
||||
|
||||
Collection<CommissionSlabData> retrieveAll();
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.organisation.commissionslab.data.CommissionSlabData;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class CommissionSlabReadPlatformServiceImpl implements CommissionSlabReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final CommissionSlabMapper MAPPER = new CommissionSlabMapper();
|
||||
|
||||
private static final class CommissionSlabMapper implements RowMapper<CommissionSlabData> {
|
||||
|
||||
public String schema() {
|
||||
return " s.id as id, s.slab_type as slabType, s.slab_from as slabFrom, s.slab_to as slabTo, "
|
||||
+ " s.commission_percent as commissionPercent from m_dds_commission_slab s";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommissionSlabData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return CommissionSlabData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.slabType(rs.getString("slabType")) //
|
||||
.slabFrom(rs.getBigDecimal("slabFrom")) //
|
||||
.slabTo(rs.getBigDecimal("slabTo")) //
|
||||
.commissionPercent(rs.getBigDecimal("commissionPercent")) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommissionSlabData> retrieveAll() {
|
||||
final String sql = "select " + MAPPER.schema() + " order by s.slab_type, s.slab_from";
|
||||
return this.jdbcTemplate.query(sql, MAPPER); // NOSONAR
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface CommissionSlabWritePlatformService {
|
||||
|
||||
CommandProcessingResult createCommissionSlabs(JsonCommand command);
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlab;
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
||||
import org.apache.fineract.organisation.commissionslab.serialization.CommissionSlabCommandFromApiJsonDeserializer;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements CommissionSlabWritePlatformService {
|
||||
|
||||
private final CommissionSlabCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final CommissionSlabRepository commissionSlabRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createCommissionSlabs(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||
|
||||
final List<CommissionSlab> slabs = new ArrayList<>();
|
||||
final JsonArray slabsArray = command.arrayOfParameterNamed("slabs");
|
||||
for (int i = 0; i < slabsArray.size(); i++) {
|
||||
final JsonObject slabJson = slabsArray.get(i).getAsJsonObject();
|
||||
slabs.add(new CommissionSlab(slabJson.get("slabType").getAsString(), slabJson.get("slabFrom").getAsBigDecimal(),
|
||||
slabJson.get("slabTo").getAsBigDecimal(), slabJson.get("commissionPercent").getAsBigDecimal()));
|
||||
}
|
||||
|
||||
this.commissionSlabRepository.saveAllAndFlush(slabs);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(slabs.isEmpty() ? null : slabs.get(0).getId()) //
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.commissionslab.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource: " + ExceptionUtils.getRootCauseMessage(dve));
|
||||
} catch (final PersistenceException dve) {
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.commissionslab.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource: " + ExceptionUtils.getRootCauseMessage(dve));
|
||||
}
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* 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.organisation.commissionslab.starter;
|
||||
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
||||
import org.apache.fineract.organisation.commissionslab.serialization.CommissionSlabCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.organisation.commissionslab.service.CommissionSlabReadPlatformService;
|
||||
import org.apache.fineract.organisation.commissionslab.service.CommissionSlabReadPlatformServiceImpl;
|
||||
import org.apache.fineract.organisation.commissionslab.service.CommissionSlabWritePlatformService;
|
||||
import org.apache.fineract.organisation.commissionslab.service.CommissionSlabWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class OrganisationCommissionSlabConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CommissionSlabReadPlatformService.class)
|
||||
public CommissionSlabReadPlatformService commissionSlabReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new CommissionSlabReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CommissionSlabWritePlatformService.class)
|
||||
public CommissionSlabWritePlatformService commissionSlabWritePlatformService(
|
||||
CommissionSlabCommandFromApiJsonDeserializer fromApiJsonDeserializer, CommissionSlabRepository commissionSlabRepository) {
|
||||
return new CommissionSlabWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, commissionSlabRepository);
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -357,9 +357,14 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
||||
accountsSummary.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
|
||||
accountsSummary.append("curr.display_symbol as currencyDisplaySymbol, ");
|
||||
accountsSummary.append("sa.product_id as productId, p.name as productName, p.short_name as shortProductName, ");
|
||||
accountsSummary.append("sa.deposit_type_enum as depositType ");
|
||||
accountsSummary.append("sa.deposit_type_enum as depositType, ");
|
||||
// Renewal-chain identity for Fixed Deposits: the stable customer-facing FD number and
|
||||
// this row's cycle within the chain, so a client's account list can collapse a renewed
|
||||
// FD into one entry rather than listing every term as a separate account.
|
||||
accountsSummary.append("dtp.fd_account_number as fdAccountNumber, dtp.renewal_cycle_no as renewalCycleNo ");
|
||||
accountsSummary.append("from m_savings_account sa ");
|
||||
accountsSummary.append("join m_savings_product as p on p.id = sa.product_id ");
|
||||
accountsSummary.append("left join m_deposit_account_term_and_preclosure dtp on dtp.savings_account_id = sa.id ");
|
||||
accountsSummary.append("join m_currency curr on curr.code = sa.currency_code ");
|
||||
accountsSummary.append("left join m_appuser sbu on sbu.id = sa.submittedon_userid ");
|
||||
accountsSummary.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid ");
|
||||
@@ -453,9 +458,15 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
||||
approvedByUsername, approvedByFirstname, approvedByLastname, activatedOnDate, activatedByUsername, activatedByFirstname,
|
||||
activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname);
|
||||
|
||||
// Null for a savings/current account and for an FD opened before this column existed;
|
||||
// callers fall back to accountNo, so an FD that has never been renewed still reads as
|
||||
// its own FD number.
|
||||
final String fdAccountNumber = rs.getString("fdAccountNumber");
|
||||
final Integer renewalCycleNo = JdbcSupport.getInteger(rs, "renewalCycleNo");
|
||||
|
||||
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
||||
accountBalance, onHoldFunds, onHoldAmount, availableBalance, accountTypeData, timeline, depositTypeData, subStatus,
|
||||
lastActiveTransactionDate);
|
||||
lastActiveTransactionDate, fdAccountNumber, renewalCycleNo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,8 +578,10 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
||||
|
||||
final EnumOptionData depositTypeData = SavingsEnumerations.depositType(400);
|
||||
|
||||
// Current accounts have no deposit term, so no FD renewal chain to report.
|
||||
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
||||
accountBalance, onHoldFunds, null, accountBalance, accountTypeData, timeline, depositTypeData, subStatus, activatedOnDate);
|
||||
accountBalance, onHoldFunds, null, accountBalance, accountTypeData, timeline, depositTypeData, subStatus, activatedOnDate,
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/batchruns")
|
||||
@Component
|
||||
@Tag(name = "DDS Batch Runs", description = "The Interest/Commission calculate-then-post batch: Generate previews a run's rows without changing any balance; Post applies exactly those previewed rows. A run can only be posted once.")
|
||||
@RequiredArgsConstructor
|
||||
public class BatchRunsApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "DDSBATCH";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final BatchRunReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve DDS Batch Runs", description = "Returns the list of DDS batch runs, most recent first.")
|
||||
public Collection<BatchRunData> retrieveAll() {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{batchRunId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a DDS Batch Run", description = "Returns the details of a batch run, including its previewed/posted rows.")
|
||||
public BatchRunData retrieveOne(@PathParam("batchRunId") @Parameter(description = "batchRunId") final Long batchRunId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveOne(batchRunId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Generate a DDS Batch Run", description = "Mandatory fields: jobType (INTEREST_CALCULATION, "
|
||||
+ "OD_INTEREST_CALCULATION or COMMISSION_CALCULATION), branchScope (All or Selected), jobDate, agentFrom, agentTo\n"
|
||||
+ "\n" + "Optional fields: partyFrom, partyTo (interest jobs only)")
|
||||
public CommandProcessingResult generate(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.generateDdsBatchRun() //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{batchRunId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Post a DDS Batch Run", description = "Applies exactly the rows a prior Generate call previewed: credits "
|
||||
+ "each row's amount to the Party (interest jobs) or to the Agent's running commission total (commission jobs). "
|
||||
+ "Fails if the run has already been posted.")
|
||||
public CommandProcessingResult post(@PathParam("batchRunId") @Parameter(description = "batchRunId") final Long batchRunId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.postDdsBatchRun(batchRunId) //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "DDSBATCH", action = "GENERATE")
|
||||
public class GenerateBatchRunCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final BatchRunWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public GenerateBatchRunCommandHandler(final BatchRunWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.generateBatchRun(command);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "DDSBATCH", action = "POST")
|
||||
public class PostBatchRunCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final BatchRunWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public PostBatchRunCommandHandler(final BatchRunWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.postBatchRun(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class BatchRunCommandFromApiJsonDeserializer {
|
||||
|
||||
public static final Set<String> SUPPORTED_JOB_TYPES = new HashSet<>(
|
||||
Arrays.asList("INTEREST_CALCULATION", "OD_INTEREST_CALCULATION", "COMMISSION_CALCULATION"));
|
||||
|
||||
private static final Set<String> SUPPORTED_BRANCH_SCOPES = new HashSet<>(Arrays.asList("All", "Selected"));
|
||||
|
||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("jobType", "branchScope", "officeNames",
|
||||
"jobDate", "agentFrom", "agentTo", "partyFrom", "partyTo", "dateFormat", "locale"));
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Autowired
|
||||
public BatchRunCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateForGenerate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("ddsbatchrun");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
v.reset().parameter("jobType").value(this.fromApiJsonHelper.extractStringNamed("jobType", element)).notBlank()
|
||||
.isOneOfTheseStringValues(SUPPORTED_JOB_TYPES.toArray());
|
||||
final String branchScope = this.fromApiJsonHelper.extractStringNamed("branchScope", element);
|
||||
v.reset().parameter("branchScope").value(branchScope).notBlank().isOneOfTheseStringValues(SUPPORTED_BRANCH_SCOPES.toArray());
|
||||
|
||||
if ("Selected".equals(branchScope)) {
|
||||
v.reset().parameter("officeNames").value(this.fromApiJsonHelper.extractJsonArrayNamed("officeNames", element))
|
||||
.jsonArrayNotEmpty();
|
||||
}
|
||||
|
||||
v.reset().parameter("jobDate").value(this.fromApiJsonHelper.extractLocalDateNamed("jobDate", element)).notNull();
|
||||
v.reset().parameter("agentFrom").value(this.fromApiJsonHelper.extractLongNamed("agentFrom", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
v.reset().parameter("agentTo").value(this.fromApiJsonHelper.extractLongNamed("agentTo", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists("partyFrom", element)) {
|
||||
v.reset().parameter("partyFrom").value(this.fromApiJsonHelper.extractLongNamed("partyFrom", element)).ignoreIfNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("partyTo", element)) {
|
||||
v.reset().parameter("partyTo").value(this.fromApiJsonHelper.extractLongNamed("partyTo", element)).ignoreIfNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
|
||||
|
||||
public interface BatchRunReadPlatformService {
|
||||
|
||||
Collection<BatchRunData> retrieveAll();
|
||||
|
||||
BatchRunData retrieveOne(Long batchRunId);
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
|
||||
import org.apache.fineract.portfolio.dds.batch.data.BatchRunRowData;
|
||||
import org.apache.fineract.portfolio.dds.batch.exception.BatchRunNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class BatchRunReadPlatformServiceImpl implements BatchRunReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final BatchRunMapper BATCH_RUN_MAPPER = new BatchRunMapper();
|
||||
private static final BatchRunRowMapper ROW_MAPPER = new BatchRunRowMapper();
|
||||
|
||||
private static final class BatchRunMapper implements RowMapper<BatchRunData> {
|
||||
|
||||
public String schema() {
|
||||
return " b.id as id, b.job_type as jobType, b.branch_scope as branchScope, b.job_date as jobDate, "
|
||||
+ " b.agent_from as agentFrom, b.agent_to as agentTo, b.party_from as partyFrom, "
|
||||
+ " b.party_to as partyTo, b.status as status, b.total_rows as totalRows, b.total_amount as totalAmount, "
|
||||
+ " b.generated_on_date as generatedOnDate, b.posted_on_date as postedOnDate from m_dds_batch_run b";
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchRunData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return BatchRunData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.jobType(rs.getString("jobType")) //
|
||||
.branchScope(rs.getString("branchScope")) //
|
||||
.jobDate(JdbcSupport.getLocalDate(rs, "jobDate")) //
|
||||
.agentFrom(rs.getLong("agentFrom")) //
|
||||
.agentTo(rs.getLong("agentTo")) //
|
||||
.partyFrom(JdbcSupport.getLong(rs, "partyFrom")) //
|
||||
.partyTo(JdbcSupport.getLong(rs, "partyTo")) //
|
||||
.status(rs.getString("status")) //
|
||||
.totalRows(rs.getInt("totalRows")) //
|
||||
.totalAmount(rs.getBigDecimal("totalAmount")) //
|
||||
.generatedOnDate(JdbcSupport.getLocalDate(rs, "generatedOnDate")) //
|
||||
.postedOnDate(JdbcSupport.getLocalDate(rs, "postedOnDate")) //
|
||||
.rows(new ArrayList<>()) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BatchRunRowMapper implements RowMapper<Map.Entry<Long, BatchRunRowData>> {
|
||||
|
||||
public String schema() {
|
||||
return " r.batch_run_id as batchRunId, r.id as id, r.party_id as partyId, p.party_code as partyCode, "
|
||||
+ " p.party_name as partyName, r.agent_id as agentId, a.agent_code as agentCode, "
|
||||
+ " r.amount as amount from m_dds_batch_run_row r " + " left join m_dds_party p on p.id = r.party_id "
|
||||
+ " join m_dds_agent a on a.id = r.agent_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<Long, BatchRunRowData> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Long batchRunId = rs.getLong("batchRunId");
|
||||
final BatchRunRowData data = BatchRunRowData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.partyId(JdbcSupport.getLong(rs, "partyId")) //
|
||||
.partyCode(rs.getString("partyCode")) //
|
||||
.partyName(rs.getString("partyName")) //
|
||||
.agentId(rs.getLong("agentId")) //
|
||||
.agentCode(rs.getString("agentCode")) //
|
||||
.amount(rs.getBigDecimal("amount")) //
|
||||
.build();
|
||||
return Map.entry(batchRunId, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<BatchRunData> retrieveAll() {
|
||||
final String sql = "select " + BATCH_RUN_MAPPER.schema() + " order by b.id desc";
|
||||
final List<BatchRunData> batchRuns = this.jdbcTemplate.query(sql, BATCH_RUN_MAPPER); // NOSONAR
|
||||
return attachRows(batchRuns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchRunData retrieveOne(final Long batchRunId) {
|
||||
try {
|
||||
final String sql = "select " + BATCH_RUN_MAPPER.schema() + " where b.id = ?";
|
||||
final BatchRunData batchRun = this.jdbcTemplate.queryForObject(sql, BATCH_RUN_MAPPER, batchRunId); // NOSONAR
|
||||
return attachRows(List.of(batchRun)).iterator().next();
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new BatchRunNotFoundException(batchRunId);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<BatchRunData> attachRows(final List<BatchRunData> batchRuns) {
|
||||
if (batchRuns.isEmpty()) {
|
||||
return batchRuns;
|
||||
}
|
||||
|
||||
final String sql = "select " + ROW_MAPPER.schema() + " order by r.id";
|
||||
final List<Map.Entry<Long, BatchRunRowData>> rows = this.jdbcTemplate.query(sql, ROW_MAPPER); // NOSONAR
|
||||
|
||||
final Map<Long, List<BatchRunRowData>> rowsByBatchRunId = new LinkedHashMap<>();
|
||||
for (final Map.Entry<Long, BatchRunRowData> row : rows) {
|
||||
rowsByBatchRunId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue());
|
||||
}
|
||||
|
||||
final List<BatchRunData> result = new ArrayList<>(batchRuns.size());
|
||||
for (final BatchRunData b : batchRuns) {
|
||||
result.add(b.toBuilder().rows(rowsByBatchRunId.getOrDefault(b.getId(), new ArrayList<>())).build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface BatchRunWritePlatformService {
|
||||
|
||||
CommandProcessingResult generateBatchRun(JsonCommand command);
|
||||
|
||||
CommandProcessingResult postBatchRun(Long batchRunId, JsonCommand command);
|
||||
}
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlab;
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
||||
import org.apache.fineract.portfolio.dds.batch.domain.BatchRun;
|
||||
import org.apache.fineract.portfolio.dds.batch.domain.BatchRunRepository;
|
||||
import org.apache.fineract.portfolio.dds.batch.domain.BatchRunRow;
|
||||
import org.apache.fineract.portfolio.dds.batch.exception.BatchRunAlreadyPostedException;
|
||||
import org.apache.fineract.portfolio.dds.batch.exception.BatchRunNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.batch.serialization.BatchRunCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWritePlatformService {
|
||||
|
||||
private static final BigDecimal DAYS_IN_YEAR = BigDecimal.valueOf(365);
|
||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||
|
||||
private final BatchRunCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final BatchRunRepository batchRunRepository;
|
||||
private final PartyRepository partyRepository;
|
||||
private final AgentRepository agentRepository;
|
||||
private final PartyTransactionRepository partyTransactionRepository;
|
||||
private final CommissionSlabRepository commissionSlabRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult generateBatchRun(final JsonCommand command) {
|
||||
|
||||
this.fromApiJsonDeserializer.validateForGenerate(command.json());
|
||||
|
||||
final String jobType = command.stringValueOfParameterNamed("jobType");
|
||||
final String branchScope = command.stringValueOfParameterNamed("branchScope");
|
||||
final LocalDate jobDate = command.localDateValueOfParameterNamed("jobDate");
|
||||
final Long agentFrom = command.longValueOfParameterNamed("agentFrom");
|
||||
final Long agentTo = command.longValueOfParameterNamed("agentTo");
|
||||
final Long partyFrom = command.parameterExists("partyFrom") ? command.longValueOfParameterNamed("partyFrom") : 1L;
|
||||
final Long partyTo = command.parameterExists("partyTo") ? command.longValueOfParameterNamed("partyTo") : Long.MAX_VALUE;
|
||||
|
||||
final Set<String> officeNames = "Selected".equals(branchScope) ? extractOfficeNames(command) : null;
|
||||
|
||||
final BatchRun batchRun = new BatchRun(jobType, branchScope, jobDate, agentFrom, agentTo, partyFrom, partyTo, LocalDate.now());
|
||||
|
||||
switch (jobType) {
|
||||
case "INTEREST_CALCULATION" -> generateInterestRows(batchRun, agentFrom, agentTo, partyFrom, partyTo, jobDate, false,
|
||||
officeNames);
|
||||
case "OD_INTEREST_CALCULATION" -> generateInterestRows(batchRun, agentFrom, agentTo, partyFrom, partyTo, jobDate, true,
|
||||
officeNames);
|
||||
case "COMMISSION_CALCULATION" -> generateCommissionRows(batchRun, agentFrom, agentTo, jobDate, officeNames);
|
||||
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
|
||||
}
|
||||
|
||||
this.batchRunRepository.saveAndFlush(batchRun);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(batchRun.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Parses the "Branch" checklist selection - null/empty means no filter ("All"). Matched
|
||||
* against Agent.homeBranch by name, since that field is plain text (not a real Office FK)
|
||||
* mirroring the same free-text branch concept the legacy screen itself used. */
|
||||
private Set<String> extractOfficeNames(final JsonCommand command) {
|
||||
if (!command.parameterExists("officeNames")) {
|
||||
return null;
|
||||
}
|
||||
final JsonArray officeNamesArray = command.arrayOfParameterNamed("officeNames");
|
||||
final Set<String> officeNames = new HashSet<>();
|
||||
for (int i = 0; i < officeNamesArray.size(); i++) {
|
||||
officeNames.add(officeNamesArray.get(i).getAsString().trim().toUpperCase());
|
||||
}
|
||||
return officeNames;
|
||||
}
|
||||
|
||||
private boolean matchesSelectedBranch(final String homeBranch, final Set<String> officeNames) {
|
||||
if (officeNames == null || officeNames.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return homeBranch != null && officeNames.contains(homeBranch.trim().toUpperCase());
|
||||
}
|
||||
|
||||
private void generateInterestRows(final BatchRun batchRun, final Long agentFrom, final Long agentTo, final Long partyFrom,
|
||||
final Long partyTo, final LocalDate jobDate, final boolean odOnly, final Set<String> officeNames) {
|
||||
final List<Party> parties = odOnly
|
||||
? this.partyRepository.findForOdInterestBatch(agentFrom, agentTo, partyFrom, partyTo)
|
||||
: this.partyRepository.findForInterestBatch(agentFrom, agentTo, partyFrom, partyTo);
|
||||
|
||||
for (final Party party : parties) {
|
||||
if (!matchesSelectedBranch(party.getAgent().getHomeBranch(), officeNames)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BigDecimal rate = party.getInterestRate();
|
||||
final BigDecimal balance = party.getCurrentBalance();
|
||||
if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0 || balance.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final LocalDate accrueFrom = party.getInterestDate() != null ? party.getInterestDate() : party.getOpeningDate();
|
||||
final long days = ChronoUnit.DAYS.between(accrueFrom, jobDate);
|
||||
if (days <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BigDecimal amount = balance.multiply(rate).multiply(BigDecimal.valueOf(days))
|
||||
.divide(HUNDRED.multiply(DAYS_IN_YEAR), 6, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
batchRun.addRow(new BatchRunRow(party.getId(), party.getAgent().getId(), amount));
|
||||
}
|
||||
}
|
||||
|
||||
private void generateCommissionRows(final BatchRun batchRun, final Long agentFrom, final Long agentTo, final LocalDate jobDate,
|
||||
final Set<String> officeNames) {
|
||||
final List<Agent> agents = this.agentRepository.findBetweenIds(agentFrom, agentTo);
|
||||
|
||||
for (final Agent agent : agents) {
|
||||
if (!matchesSelectedBranch(agent.getHomeBranch(), officeNames)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final LocalDate fromDate = agent.getLastCommissionDate() != null ? agent.getLastCommissionDate().plusDays(1)
|
||||
: agent.getAgentOpeningDate();
|
||||
if (fromDate.isAfter(jobDate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BigDecimal collectionVolume = this.partyTransactionRepository.sumPartyTranCredits(agent.getId(), fromDate, jobDate);
|
||||
if (collectionVolume == null || collectionVolume.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final List<CommissionSlab> matchingSlabs = this.commissionSlabRepository.findMatchingSlabs("ACCOUNT", collectionVolume);
|
||||
if (matchingSlabs.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BigDecimal commissionPercent = matchingSlabs.get(0).getCommissionPercent();
|
||||
final BigDecimal amount = collectionVolume.multiply(commissionPercent).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
batchRun.addRow(new BatchRunRow(null, agent.getId(), amount));
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult postBatchRun(final Long batchRunId, final JsonCommand command) {
|
||||
|
||||
final BatchRun batchRun = this.batchRunRepository.findById(batchRunId).orElseThrow(() -> new BatchRunNotFoundException(batchRunId));
|
||||
if (batchRun.isPosted()) {
|
||||
throw new BatchRunAlreadyPostedException(batchRunId);
|
||||
}
|
||||
|
||||
final boolean isCommissionJob = "COMMISSION_CALCULATION".equals(batchRun.getJobType());
|
||||
final String transactionType = "OD_INTEREST_CALCULATION".equals(batchRun.getJobType()) ? "OD_INTEREST_POST" : "INTEREST_POST";
|
||||
|
||||
for (final BatchRunRow row : batchRun.getRows()) {
|
||||
if (isCommissionJob) {
|
||||
final Agent agent = this.agentRepository.findById(row.getAgentId())
|
||||
.orElseThrow(() -> new AgentNotFoundException(row.getAgentId()));
|
||||
agent.postCommission(row.getAmount(), batchRun.getJobDate());
|
||||
} else {
|
||||
final Party party = this.partyRepository.findById(row.getPartyId())
|
||||
.orElseThrow(() -> new PartyNotFoundException(row.getPartyId()));
|
||||
final Agent agent = party.getAgent();
|
||||
final BigDecimal newBalance = party.credit(row.getAmount());
|
||||
party.markInterestCalculatedThrough(batchRun.getJobDate());
|
||||
|
||||
final PartyTransaction transaction = new PartyTransaction(party, agent, batchRun.getJobDate(), transactionType,
|
||||
"CREDIT", null, null, null, row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(), newBalance);
|
||||
this.partyTransactionRepository.save(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
batchRun.markPosted(LocalDate.now());
|
||||
this.batchRunRepository.saveAndFlush(batchRun);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(batchRunId) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* 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.dds.batch.starter;
|
||||
|
||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.portfolio.dds.batch.domain.BatchRunRepository;
|
||||
import org.apache.fineract.portfolio.dds.batch.serialization.BatchRunCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformService;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformService;
|
||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class DdsBatchConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(BatchRunReadPlatformService.class)
|
||||
public BatchRunReadPlatformService batchRunReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new BatchRunReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(BatchRunWritePlatformService.class)
|
||||
public BatchRunWritePlatformService batchRunWritePlatformService(BatchRunCommandFromApiJsonDeserializer fromApiJsonDeserializer,
|
||||
BatchRunRepository batchRunRepository, PartyRepository partyRepository, AgentRepository agentRepository,
|
||||
PartyTransactionRepository partyTransactionRepository, CommissionSlabRepository commissionSlabRepository) {
|
||||
return new BatchRunWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, batchRunRepository, partyRepository,
|
||||
agentRepository, partyTransactionRepository, commissionSlabRepository);
|
||||
}
|
||||
}
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.dds.party.data.PartyData;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/parties")
|
||||
@Component
|
||||
@Tag(name = "DDS Parties", description = "A DDS Party is a customer's daily-deposit scheme account, linked to the Agent who collects for it. Carries the scheme's KYC, terms, two-stage overdue flags and a nominee list.")
|
||||
@RequiredArgsConstructor
|
||||
public class PartiesApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "PARTY";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final PartyReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve DDS Parties", description = "Returns the list of DDS parties.")
|
||||
public Collection<PartyData> retrieveAll() {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{partyId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a DDS Party", description = "Returns the details of a DDS party, including its nominees.")
|
||||
public PartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveOne(partyId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create a DDS Party", description = "Mandatory fields: clientId, agentId, subType, partyName, address1, "
|
||||
+ "city, pincode, mobileNoSms, operation, adultMinor, openingDate, dailyDepositAmount, days\n" + "\n"
|
||||
+ "Optional fields: lastAccount, nominal, aadharNo, partyName1, address2, address3, gender, panCardNo, telephone, "
|
||||
+ "birthDate, occupation, risk, ckycId, minorDate, guardianClientId, minAccountOpt, relation, month, maturityDate, "
|
||||
+ "reopenDate, depositAmount, ledgerNo, withdrawType, lien, payDate, paymentAmount, openingBalance, isSmsActive, "
|
||||
+ "isEmailActive, interestDate, spDescription, freezeCode, advCode, creditToBranch, creditToAccountType, "
|
||||
+ "creditToAccount, nomineeApplicable, nomineeSplitMode, nominees, savingsAccountNo (links this party to an existing, "
|
||||
+ "non-closed real savings account of the same client, resolved by account number - omit if the client has none)")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.createParty() //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{partyId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update a DDS Party", description = "Updates the details of a DDS party.")
|
||||
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.updateParty(partyId) //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{partyId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Close a DDS Party", description = "command=close is the only supported command. Mandatory field: "
|
||||
+ "closeDate. The party's balance must already be zero (settled with a full Withdraw Deposite payout) before it "
|
||||
+ "can be closed.")
|
||||
public CommandProcessingResult handleCommand(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||
@QueryParam("command") @Parameter(description = "command") final String commandParam,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
if (!"close".equals(commandParam)) {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.closeParty(partyId) //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface PartyRepository extends JpaRepository<Party, Long> {
|
||||
|
||||
@Query("select coalesce(max(cast(substring(p.partyCode, 3) as long)), 0) from Party p where p.partyCode like 'PT%'")
|
||||
Long retrieveMaxPartyCodeSuffix();
|
||||
|
||||
Optional<Party> findByPartyCode(String partyCode);
|
||||
|
||||
@Query("select p from Party p where p.agent.id between :agentFrom and :agentTo and p.id between :partyFrom and :partyTo")
|
||||
List<Party> findForInterestBatch(@Param("agentFrom") Long agentFrom, @Param("agentTo") Long agentTo,
|
||||
@Param("partyFrom") Long partyFrom, @Param("partyTo") Long partyTo);
|
||||
|
||||
@Query("select p from Party p where p.agent.id between :agentFrom and :agentTo and p.id between :partyFrom and :partyTo "
|
||||
+ "and (p.od1Flag = true or p.od2Flag = true)")
|
||||
List<Party> findForOdInterestBatch(@Param("agentFrom") Long agentFrom, @Param("agentTo") Long agentTo,
|
||||
@Param("partyFrom") Long partyFrom, @Param("partyTo") Long partyTo);
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "PARTY", action = "CLOSE")
|
||||
public class ClosePartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final PartyWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public ClosePartyCommandHandler(final PartyWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.closeParty(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "PARTY", action = "CREATE")
|
||||
public class CreatePartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final PartyWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public CreatePartyCommandHandler(final PartyWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createParty(command);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "PARTY", action = "UPDATE")
|
||||
public class UpdatePartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final PartyWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public UpdatePartyCommandHandler(final PartyWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateParty(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-294
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public final class PartyCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("clientId", "agentId", "subType",
|
||||
"lastAccount", "nominal", "aadharNo", "partyName", "partyName1", "address1", "address2", "address3", "city", "pincode",
|
||||
"gender", "panCardNo", "telephone", "mobileNoSms", "operation", "birthDate", "occupation", "risk", "ckycId", "adultMinor",
|
||||
"minorDate", "guardianClientId", "minAccountOpt", "relation", "openingDate", "month", "maturityDate", "reopenDate",
|
||||
"depositAmount", "dailyDepositAmount", "days", "ledgerNo", "interestRate", "withdrawType", "lien", "payDate",
|
||||
"paymentAmount", "openingBalance", "isSmsActive", "isEmailActive", "interestDate", "spDescription", "freezeCode", "advCode",
|
||||
"creditToBranch", "creditToAccountType", "creditToAccount", "nomineeApplicable", "nomineeSplitMode", "nominees",
|
||||
"savingsAccountNo", "dateFormat", "locale"));
|
||||
|
||||
private static final Set<String> SUPPORTED_NOMINEE_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList("nominationDate", "name", "mobileNo", "birthDate", "minor", "relation", "gender", "guardianName"));
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Autowired
|
||||
public PartyCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("party");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
v.reset().parameter("clientId").value(this.fromApiJsonHelper.extractLongNamed("clientId", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
v.reset().parameter("agentId").value(this.fromApiJsonHelper.extractLongNamed("agentId", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
v.reset().parameter("subType").value(this.fromApiJsonHelper.extractStringNamed("subType", element)).notBlank()
|
||||
.notExceedingLengthOf(60);
|
||||
v.reset().parameter("partyName").value(this.fromApiJsonHelper.extractStringNamed("partyName", element)).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
v.reset().parameter("address1").value(this.fromApiJsonHelper.extractStringNamed("address1", element)).notBlank()
|
||||
.notExceedingLengthOf(200);
|
||||
v.reset().parameter("city").value(this.fromApiJsonHelper.extractStringNamed("city", element)).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
v.reset().parameter("pincode").value(this.fromApiJsonHelper.extractStringNamed("pincode", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
v.reset().parameter("mobileNoSms").value(this.fromApiJsonHelper.extractStringNamed("mobileNoSms", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
v.reset().parameter("operation").value(this.fromApiJsonHelper.extractStringNamed("operation", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
v.reset().parameter("adultMinor").value(this.fromApiJsonHelper.extractStringNamed("adultMinor", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
v.reset().parameter("openingDate").value(this.fromApiJsonHelper.extractLocalDateNamed("openingDate", element)).notNull();
|
||||
v.reset().parameter("dailyDepositAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("dailyDepositAmount", element))
|
||||
.notNull().positiveAmount();
|
||||
v.reset().parameter("days").value(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("days", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
|
||||
validateOptionalFields(element, v);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists("nominees", element)) {
|
||||
validateNominees(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateForUpdate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("party");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists("clientId", element)) {
|
||||
v.reset().parameter("clientId").value(this.fromApiJsonHelper.extractLongNamed("clientId", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("agentId", element)) {
|
||||
v.reset().parameter("agentId").value(this.fromApiJsonHelper.extractLongNamed("agentId", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("subType", element)) {
|
||||
v.reset().parameter("subType").value(this.fromApiJsonHelper.extractStringNamed("subType", element)).notBlank()
|
||||
.notExceedingLengthOf(60);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("partyName", element)) {
|
||||
v.reset().parameter("partyName").value(this.fromApiJsonHelper.extractStringNamed("partyName", element)).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("address1", element)) {
|
||||
v.reset().parameter("address1").value(this.fromApiJsonHelper.extractStringNamed("address1", element)).notBlank()
|
||||
.notExceedingLengthOf(200);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("city", element)) {
|
||||
v.reset().parameter("city").value(this.fromApiJsonHelper.extractStringNamed("city", element)).notBlank()
|
||||
.notExceedingLengthOf(100);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("pincode", element)) {
|
||||
v.reset().parameter("pincode").value(this.fromApiJsonHelper.extractStringNamed("pincode", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("mobileNoSms", element)) {
|
||||
v.reset().parameter("mobileNoSms").value(this.fromApiJsonHelper.extractStringNamed("mobileNoSms", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("operation", element)) {
|
||||
v.reset().parameter("operation").value(this.fromApiJsonHelper.extractStringNamed("operation", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("adultMinor", element)) {
|
||||
v.reset().parameter("adultMinor").value(this.fromApiJsonHelper.extractStringNamed("adultMinor", element)).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("openingDate", element)) {
|
||||
v.reset().parameter("openingDate").value(this.fromApiJsonHelper.extractLocalDateNamed("openingDate", element)).notNull();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("dailyDepositAmount", element)) {
|
||||
v.reset().parameter("dailyDepositAmount")
|
||||
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("dailyDepositAmount", element)).notNull()
|
||||
.positiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("days", element)) {
|
||||
v.reset().parameter("days").value(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("days", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
|
||||
validateOptionalFields(element, v);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists("nominees", element)) {
|
||||
validateNominees(element.getAsJsonObject(), dataValidationErrors);
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateForClose(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, Set.of("closeDate", "dateFormat", "locale"));
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("party");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
v.reset().parameter("closeDate").value(this.fromApiJsonHelper.extractLocalDateNamed("closeDate", element)).notNull();
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void validateOptionalFields(final JsonElement element, final DataValidatorBuilder v) {
|
||||
optionalString(element, v, "lastAccount", 50);
|
||||
optionalString(element, v, "aadharNo", 20);
|
||||
optionalString(element, v, "partyName1", 100);
|
||||
optionalString(element, v, "address2", 200);
|
||||
optionalString(element, v, "address3", 200);
|
||||
optionalString(element, v, "gender", 20);
|
||||
optionalString(element, v, "panCardNo", 20);
|
||||
optionalString(element, v, "telephone", 20);
|
||||
optionalString(element, v, "occupation", 40);
|
||||
optionalString(element, v, "risk", 20);
|
||||
optionalString(element, v, "ckycId", 40);
|
||||
optionalString(element, v, "minAccountOpt", 40);
|
||||
optionalString(element, v, "relation", 40);
|
||||
optionalString(element, v, "ledgerNo", 40);
|
||||
optionalString(element, v, "withdrawType", 20);
|
||||
optionalString(element, v, "spDescription", 500);
|
||||
optionalString(element, v, "freezeCode", 20);
|
||||
optionalString(element, v, "advCode", 40);
|
||||
optionalString(element, v, "creditToBranch", 100);
|
||||
optionalString(element, v, "creditToAccountType", 100);
|
||||
optionalString(element, v, "creditToAccount", 50);
|
||||
optionalString(element, v, "nomineeSplitMode", 20);
|
||||
optionalString(element, v, "savingsAccountNo", 20);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists("guardianClientId", element)) {
|
||||
v.reset().parameter("guardianClientId").value(this.fromApiJsonHelper.extractLongNamed("guardianClientId", element))
|
||||
.ignoreIfNull().integerGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("month", element)) {
|
||||
v.reset().parameter("month").value(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("month", element)).ignoreIfNull()
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("depositAmount", element)) {
|
||||
v.reset().parameter("depositAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("depositAmount", element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("paymentAmount", element)) {
|
||||
v.reset().parameter("paymentAmount")
|
||||
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("paymentAmount", element)).ignoreIfNull()
|
||||
.zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("openingBalance", element)) {
|
||||
v.reset().parameter("openingBalance")
|
||||
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("openingBalance", element)).ignoreIfNull()
|
||||
.zeroOrPositiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("interestRate", element)) {
|
||||
v.reset().parameter("interestRate").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRate", element))
|
||||
.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
}
|
||||
|
||||
private void optionalString(final JsonElement element, final DataValidatorBuilder v, final String paramName, final int maxLength) {
|
||||
if (this.fromApiJsonHelper.parameterExists(paramName, element)) {
|
||||
v.reset().parameter(paramName).value(this.fromApiJsonHelper.extractStringNamed(paramName, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNominees(final JsonObject topLevelJson, final List<ApiParameterError> dataValidationErrors) {
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("party.nominee");
|
||||
final var nomineesArray = topLevelJson.getAsJsonArray("nominees");
|
||||
for (int i = 0; i < nomineesArray.size(); i++) {
|
||||
final JsonObject nomineeJson = nomineesArray.get(i).getAsJsonObject();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(nomineeJson, SUPPORTED_NOMINEE_PARAMETERS);
|
||||
|
||||
v.reset().parameterAtIndexArray("nominationDate", i).value(extractString(nomineeJson, "nominationDate")).notBlank();
|
||||
v.reset().parameterAtIndexArray("name", i).value(extractString(nomineeJson, "name")).notBlank().notExceedingLengthOf(100);
|
||||
v.reset().parameterAtIndexArray("relation", i).value(extractString(nomineeJson, "relation")).notBlank()
|
||||
.notExceedingLengthOf(40);
|
||||
v.reset().parameterAtIndexArray("gender", i).value(extractString(nomineeJson, "gender")).notBlank()
|
||||
.notExceedingLengthOf(20);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractString(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.portfolio.dds.party.data.PartyData;
|
||||
|
||||
public interface PartyReadPlatformService {
|
||||
|
||||
Collection<PartyData> retrieveAll();
|
||||
|
||||
PartyData retrieveOne(Long partyId);
|
||||
}
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.portfolio.dds.party.data.PartyData;
|
||||
import org.apache.fineract.portfolio.dds.party.data.PartyNomineeData;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final PartyMapper PARTY_MAPPER = new PartyMapper();
|
||||
private static final PartyNomineeMapper NOMINEE_MAPPER = new PartyNomineeMapper();
|
||||
|
||||
private static final class PartyMapper implements RowMapper<PartyData> {
|
||||
|
||||
public String schema() {
|
||||
return " p.id as id, p.client_id as clientId, c.display_name as clientName, p.agent_id as agentId, "
|
||||
+ " a.agent_code as agentCode, a.agent_name as agentName, p.savings_account_id as savingsAccountId, "
|
||||
+ " p.sub_type as subType, p.party_code as partyCode, p.last_account as lastAccount, p.nominal as nominal, "
|
||||
+ " p.aadhar_no as aadharNo, p.party_name as partyName, p.party_name1 as partyName1, p.address1 as address1, "
|
||||
+ " p.address2 as address2, p.address3 as address3, p.city as city, p.pincode as pincode, p.gender as gender, "
|
||||
+ " p.pan_card_no as panCardNo, p.telephone as telephone, p.mobile_no_sms as mobileNoSms, p.operation as operation, "
|
||||
+ " p.birth_date as birthDate, p.occupation as occupation, p.risk as risk, p.ckyc_id as ckycId, "
|
||||
+ " p.adult_minor as adultMinor, p.minor_date as minorDate, p.guardian_client_id as guardianClientId, "
|
||||
+ " p.min_account_opt as minAccountOpt, p.relation as relation, p.opening_date as openingDate, "
|
||||
+ " p.months as month, p.maturity_date as maturityDate, p.reopen_date as reopenDate, "
|
||||
+ " p.deposit_amount as depositAmount, p.daily_deposit_amount as dailyDepositAmount, p.days as days, "
|
||||
+ " p.ledger_no as ledgerNo, p.maturity_amount as maturityAmount, p.interest_rate as interestRate, "
|
||||
+ " p.withdraw_type as withdrawType, p.od1_flag as od1Flag, p.od2_flag as od2Flag, p.lien as lien, "
|
||||
+ " p.pay_date as payDate, p.payment_amount as paymentAmount, p.current_balance as currentBalance, "
|
||||
+ " p.opening_balance as openingBalance, p.is_sms_active as isSmsActive, p.is_email_active as isEmailActive, "
|
||||
+ " p.interest_calc as interestCalc, p.interest_date as interestDate, p.sp_description as spDescription, "
|
||||
+ " p.freeze_code as freezeCode, p.adv_code as advCode, p.credit_to_branch as creditToBranch, "
|
||||
+ " p.credit_to_account_type as creditToAccountType, p.credit_to_account as creditToAccount, "
|
||||
+ " p.nominee_applicable as nomineeApplicable, p.nominee_split_mode as nomineeSplitMode, "
|
||||
+ " p.status as status, p.close_date as closeDate "
|
||||
+ " from m_dds_party p join m_client c on c.id = p.client_id join m_dds_agent a on a.id = p.agent_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartyData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return PartyData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.clientId(rs.getLong("clientId")) //
|
||||
.clientName(rs.getString("clientName")) //
|
||||
.agentId(rs.getLong("agentId")) //
|
||||
.agentCode(rs.getString("agentCode")) //
|
||||
.agentName(rs.getString("agentName")) //
|
||||
.savingsAccountId(JdbcSupport.getLong(rs, "savingsAccountId")) //
|
||||
.subType(rs.getString("subType")) //
|
||||
.partyCode(rs.getString("partyCode")) //
|
||||
.lastAccount(rs.getString("lastAccount")) //
|
||||
.nominal(rs.getBoolean("nominal")) //
|
||||
.aadharNo(rs.getString("aadharNo")) //
|
||||
.partyName(rs.getString("partyName")) //
|
||||
.partyName1(rs.getString("partyName1")) //
|
||||
.address1(rs.getString("address1")) //
|
||||
.address2(rs.getString("address2")) //
|
||||
.address3(rs.getString("address3")) //
|
||||
.city(rs.getString("city")) //
|
||||
.pincode(rs.getString("pincode")) //
|
||||
.gender(rs.getString("gender")) //
|
||||
.panCardNo(rs.getString("panCardNo")) //
|
||||
.telephone(rs.getString("telephone")) //
|
||||
.mobileNoSms(rs.getString("mobileNoSms")) //
|
||||
.operation(rs.getString("operation")) //
|
||||
.birthDate(JdbcSupport.getLocalDate(rs, "birthDate")) //
|
||||
.occupation(rs.getString("occupation")) //
|
||||
.risk(rs.getString("risk")) //
|
||||
.ckycId(rs.getString("ckycId")) //
|
||||
.adultMinor(rs.getString("adultMinor")) //
|
||||
.minorDate(JdbcSupport.getLocalDate(rs, "minorDate")) //
|
||||
.guardianClientId(JdbcSupport.getLong(rs, "guardianClientId")) //
|
||||
.minAccountOpt(rs.getString("minAccountOpt")) //
|
||||
.relation(rs.getString("relation")) //
|
||||
.openingDate(JdbcSupport.getLocalDate(rs, "openingDate")) //
|
||||
.month(JdbcSupport.getInteger(rs, "month")) //
|
||||
.maturityDate(JdbcSupport.getLocalDate(rs, "maturityDate")) //
|
||||
.reopenDate(JdbcSupport.getLocalDate(rs, "reopenDate")) //
|
||||
.depositAmount(rs.getBigDecimal("depositAmount")) //
|
||||
.dailyDepositAmount(rs.getBigDecimal("dailyDepositAmount")) //
|
||||
.days(JdbcSupport.getInteger(rs, "days")) //
|
||||
.ledgerNo(rs.getString("ledgerNo")) //
|
||||
.maturityAmount(rs.getBigDecimal("maturityAmount")) //
|
||||
.interestRate(rs.getBigDecimal("interestRate")) //
|
||||
.withdrawType(rs.getString("withdrawType")) //
|
||||
.od1Flag(rs.getBoolean("od1Flag")) //
|
||||
.od2Flag(rs.getBoolean("od2Flag")) //
|
||||
.lien(rs.getBoolean("lien")) //
|
||||
.payDate(JdbcSupport.getLocalDate(rs, "payDate")) //
|
||||
.paymentAmount(rs.getBigDecimal("paymentAmount")) //
|
||||
.currentBalance(rs.getBigDecimal("currentBalance")) //
|
||||
.openingBalance(rs.getBigDecimal("openingBalance")) //
|
||||
.isSmsActive(rs.getBoolean("isSmsActive")) //
|
||||
.isEmailActive(rs.getBoolean("isEmailActive")) //
|
||||
.interestCalc(rs.getBigDecimal("interestCalc")) //
|
||||
.interestDate(JdbcSupport.getLocalDate(rs, "interestDate")) //
|
||||
.spDescription(rs.getString("spDescription")) //
|
||||
.freezeCode(rs.getString("freezeCode")) //
|
||||
.advCode(rs.getString("advCode")) //
|
||||
.creditToBranch(rs.getString("creditToBranch")) //
|
||||
.creditToAccountType(rs.getString("creditToAccountType")) //
|
||||
.creditToAccount(rs.getString("creditToAccount")) //
|
||||
.nomineeApplicable(rs.getBoolean("nomineeApplicable")) //
|
||||
.nomineeSplitMode(rs.getString("nomineeSplitMode")) //
|
||||
.status(rs.getString("status")) //
|
||||
.closeDate(JdbcSupport.getLocalDate(rs, "closeDate")) //
|
||||
.nominees(new ArrayList<>()) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PartyNomineeMapper implements RowMapper<Map.Entry<Long, PartyNomineeData>> {
|
||||
|
||||
public String schema() {
|
||||
return " n.party_id as partyId, n.id as id, n.nomination_date as nominationDate, n.name as name, "
|
||||
+ " n.mobile_no as mobileNo, n.birth_date as birthDate, n.minor as minor, n.relation as relation, "
|
||||
+ " n.gender as gender, n.guardian_name as guardianName from m_dds_party_nominee n";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map.Entry<Long, PartyNomineeData> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
final Long partyId = rs.getLong("partyId");
|
||||
final PartyNomineeData data = PartyNomineeData.instance(rs.getLong("id"), JdbcSupport.getLocalDate(rs, "nominationDate"),
|
||||
rs.getString("name"), rs.getString("mobileNo"), JdbcSupport.getLocalDate(rs, "birthDate"), rs.getBoolean("minor"),
|
||||
rs.getString("relation"), rs.getString("gender"), rs.getString("guardianName"));
|
||||
return Map.entry(partyId, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PartyData> retrieveAll() {
|
||||
final String sql = "select " + PARTY_MAPPER.schema() + " order by p.id";
|
||||
final List<PartyData> parties = this.jdbcTemplate.query(sql, PARTY_MAPPER); // NOSONAR
|
||||
return attachNominees(parties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartyData retrieveOne(final Long partyId) {
|
||||
try {
|
||||
final String sql = "select " + PARTY_MAPPER.schema() + " where p.id = ?";
|
||||
final PartyData party = this.jdbcTemplate.queryForObject(sql, PARTY_MAPPER, partyId); // NOSONAR
|
||||
return attachNominees(List.of(party)).iterator().next();
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new PartyNotFoundException(partyId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<PartyData> attachNominees(final List<PartyData> parties) {
|
||||
if (parties.isEmpty()) {
|
||||
return parties;
|
||||
}
|
||||
|
||||
final String sql = "select " + NOMINEE_MAPPER.schema() + " order by n.id";
|
||||
final List<Map.Entry<Long, PartyNomineeData>> rows = this.jdbcTemplate.query(sql, NOMINEE_MAPPER); // NOSONAR
|
||||
|
||||
final Map<Long, List<PartyNomineeData>> nomineesByPartyId = new LinkedHashMap<>();
|
||||
for (final Map.Entry<Long, PartyNomineeData> row : rows) {
|
||||
nomineesByPartyId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue());
|
||||
}
|
||||
|
||||
final List<PartyData> result = new ArrayList<>(parties.size());
|
||||
for (final PartyData p : parties) {
|
||||
result.add(p.toBuilder().nominees(nomineesByPartyId.getOrDefault(p.getId(), new ArrayList<>())).build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface PartyWritePlatformService {
|
||||
|
||||
CommandProcessingResult createParty(JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateParty(Long partyId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult closeParty(Long partyId, JsonCommand command);
|
||||
}
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyNominee;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyAlreadyClosedException;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyHasBalanceException;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartySavingsAccountClientMismatchException;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartySavingsAccountNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.party.serialization.PartyCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePlatformService {
|
||||
|
||||
private final PartyCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final PartyRepository partyRepository;
|
||||
private final AgentRepository agentRepository;
|
||||
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||
private final SavingsAccountRepository savingsAccountRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createParty(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||
|
||||
final Long clientId = command.longValueOfParameterNamed("clientId");
|
||||
final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final Long agentId = command.longValueOfParameterNamed("agentId");
|
||||
final Agent agent = this.agentRepository.findById(agentId)
|
||||
.orElseThrow(() -> new AgentNotFoundException(agentId));
|
||||
|
||||
final String partyCode = generateNextPartyCode();
|
||||
final Party party = Party.fromJson(client, agent, partyCode, command);
|
||||
|
||||
linkSavingsAccountIfProvided(party, clientId, command);
|
||||
addNomineesFromCommand(party, command);
|
||||
|
||||
this.partyRepository.saveAndFlush(party);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(party.getId()) //
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||
return CommandProcessingResult.empty();
|
||||
} catch (final PersistenceException dve) {
|
||||
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
|
||||
handleDataIntegrityIssues(throwable, dve);
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
||||
|
||||
final Party partyForUpdate = this.partyRepository.findById(partyId).orElseThrow(() -> new PartyNotFoundException(partyId));
|
||||
final Map<String, Object> changesOnly = partyForUpdate.update(command);
|
||||
|
||||
if (changesOnly.containsKey("clientId")) {
|
||||
final Long newClientId = (Long) changesOnly.get("clientId");
|
||||
final Client newClient = this.clientRepositoryWrapper.findOneWithNotFoundDetection(newClientId);
|
||||
partyForUpdate.changeClient(newClient);
|
||||
}
|
||||
|
||||
if (changesOnly.containsKey("agentId")) {
|
||||
final Long newAgentId = (Long) changesOnly.get("agentId");
|
||||
final Agent newAgent = this.agentRepository.findById(newAgentId)
|
||||
.orElseThrow(() -> new AgentNotFoundException(newAgentId));
|
||||
partyForUpdate.changeAgent(newAgent);
|
||||
}
|
||||
|
||||
if (command.parameterExists("nominees")) {
|
||||
partyForUpdate.resetNominees();
|
||||
addNomineesFromCommand(partyForUpdate, command);
|
||||
changesOnly.put("nominees", command.jsonFragment("nominees"));
|
||||
}
|
||||
|
||||
linkSavingsAccountIfProvided(partyForUpdate, partyForUpdate.clientId(), command);
|
||||
|
||||
this.partyRepository.saveAndFlush(partyForUpdate);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).with(changesOnly)
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||
return CommandProcessingResult.empty();
|
||||
} catch (final PersistenceException dve) {
|
||||
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
|
||||
handleDataIntegrityIssues(throwable, dve);
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult closeParty(final Long partyId, final JsonCommand command) {
|
||||
|
||||
this.fromApiJsonDeserializer.validateForClose(command.json());
|
||||
|
||||
final Party party = this.partyRepository.findById(partyId).orElseThrow(() -> new PartyNotFoundException(partyId));
|
||||
|
||||
if (party.isClosed()) {
|
||||
throw new PartyAlreadyClosedException(party.getPartyCode());
|
||||
}
|
||||
if (party.getCurrentBalance().compareTo(BigDecimal.ZERO) != 0) {
|
||||
throw new PartyHasBalanceException(party.getPartyCode());
|
||||
}
|
||||
|
||||
final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate");
|
||||
party.close(closeDate);
|
||||
this.partyRepository.saveAndFlush(party);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the caller gave a savingsAccountNo, links this Party to that real Savings Account - resolved
|
||||
* by account number since that's what an officer actually has to hand, not the internal id. Left
|
||||
* unlinked (as before) when the parameter isn't given: a Party doesn't require a real account,
|
||||
* this is opportunistic linking for a client that already happens to have one.
|
||||
*/
|
||||
private void linkSavingsAccountIfProvided(final Party party, final Long expectedClientId, final JsonCommand command) {
|
||||
if (!command.parameterExists("savingsAccountNo")) {
|
||||
return;
|
||||
}
|
||||
final String savingsAccountNo = command.stringValueOfParameterNamed("savingsAccountNo");
|
||||
final SavingsAccount savingsAccount = this.savingsAccountRepository.findNonClosedAccountByAccountNumber(savingsAccountNo);
|
||||
if (savingsAccount == null) {
|
||||
throw new PartySavingsAccountNotFoundException(savingsAccountNo);
|
||||
}
|
||||
if (!expectedClientId.equals(savingsAccount.clientId())) {
|
||||
throw new PartySavingsAccountClientMismatchException(savingsAccountNo);
|
||||
}
|
||||
party.linkSavingsAccount(savingsAccount.getId());
|
||||
}
|
||||
|
||||
private void addNomineesFromCommand(final Party party, final JsonCommand command) {
|
||||
if (!command.parameterExists("nominees")) {
|
||||
return;
|
||||
}
|
||||
final JsonArray nomineesArray = command.arrayOfParameterNamed("nominees");
|
||||
if (nomineesArray == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < nomineesArray.size(); i++) {
|
||||
final JsonObject nomineeJson = nomineesArray.get(i).getAsJsonObject();
|
||||
final LocalDate nominationDate = LocalDate.parse(nomineeJson.get("nominationDate").getAsString());
|
||||
final String name = nomineeJson.get("name").getAsString();
|
||||
final String mobileNo = nomineeJson.has("mobileNo") && !nomineeJson.get("mobileNo").isJsonNull()
|
||||
? nomineeJson.get("mobileNo").getAsString()
|
||||
: null;
|
||||
final LocalDate birthDate = nomineeJson.has("birthDate") && !nomineeJson.get("birthDate").isJsonNull()
|
||||
? LocalDate.parse(nomineeJson.get("birthDate").getAsString())
|
||||
: null;
|
||||
final boolean minor = nomineeJson.has("minor") && !nomineeJson.get("minor").isJsonNull() && nomineeJson.get("minor")
|
||||
.getAsBoolean();
|
||||
final String relation = nomineeJson.get("relation").getAsString();
|
||||
final String gender = nomineeJson.get("gender").getAsString();
|
||||
final String guardianName = nomineeJson.has("guardianName") && !nomineeJson.get("guardianName").isJsonNull()
|
||||
? nomineeJson.get("guardianName").getAsString()
|
||||
: null;
|
||||
|
||||
party.addNominee(new PartyNominee(nominationDate, name, mobileNo, birthDate, minor, relation, gender, guardianName));
|
||||
}
|
||||
}
|
||||
|
||||
private String generateNextPartyCode() {
|
||||
final Long maxSuffix = this.partyRepository.retrieveMaxPartyCodeSuffix();
|
||||
final long nextSuffix = (maxSuffix == null ? 0L : maxSuffix) + 1L;
|
||||
return String.format("PT%04d", nextSuffix);
|
||||
}
|
||||
|
||||
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
|
||||
if (realCause.getMessage() != null && realCause.getMessage().contains("party_code")) {
|
||||
throw new PlatformDataIntegrityException("error.msg.party.duplicate.partyCode",
|
||||
"A party with the generated code already exists, please retry");
|
||||
}
|
||||
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.party.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource: " + realCause.getMessage());
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* 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.dds.party.starter;
|
||||
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.party.serialization.PartyCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyReadPlatformService;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyWritePlatformService;
|
||||
import org.apache.fineract.portfolio.dds.party.service.PartyWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class DdsPartyConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PartyReadPlatformService.class)
|
||||
public PartyReadPlatformService partyReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new PartyReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PartyWritePlatformService.class)
|
||||
public PartyWritePlatformService partyWritePlatformService(PartyCommandFromApiJsonDeserializer fromApiJsonDeserializer,
|
||||
PartyRepository partyRepository, AgentRepository agentRepository, ClientRepositoryWrapper clientRepositoryWrapper,
|
||||
SavingsAccountRepository savingsAccountRepository) {
|
||||
return new PartyWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, partyRepository, agentRepository,
|
||||
clientRepositoryWrapper, savingsAccountRepository);
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* 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.dds.report.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
|
||||
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/reports/{reportKey}")
|
||||
@Component
|
||||
@Tag(name = "DDS Reports", description = "The DDS reports suite - one shared query engine behind a generic column/row shape, filtered by date range and agent/party id range.")
|
||||
@RequiredArgsConstructor
|
||||
public class DdsReportsApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "DDSREPORT";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final DdsReportReadPlatformService readPlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Run a DDS Report", description = "reportKey is one of: AGENT_CHECK_LIST, PARTY_CHECK_LIST, PARTY_BALANCE, "
|
||||
+ "AGENT_WISE_BALANCE, AGENT_WISE_MONTHLY_COLLECTION, COMMISSION_REGISTER, COLLECTION_SUMMARY_AGENT_WISE, "
|
||||
+ "AGENT_PARTYWISE_DAILY_COLLECTION, DAILY_COLLECTION_REPORT, PARTY_INTEREST_REGISTER, BALANCE_SUMMARY, "
|
||||
+ "TRANSACTION_REGISTER, MATURE_DATE_WISE, INTEREST_CERTIFICATE, AGENT_COMMISSION_STATEMENT, SUPPLEMENTARY_DDS, "
|
||||
+ "CLOSE_PARTY_AC_REGISTER, MASTER_DATA_EXPORT (the last three always return available=false - no matching data exists "
|
||||
+ "in this system yet). "
|
||||
+ "fromDate/toDate are ISO (yyyy-MM-dd); agentFrom/To and partyFrom/To default to the full id range.")
|
||||
public DdsReportData runReport(@PathParam("reportKey") @Parameter(description = "reportKey") final String reportKey,
|
||||
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate,
|
||||
@DefaultValue("1") @QueryParam("agentFrom") @Parameter(description = "agentFrom") final Long agentFrom,
|
||||
@DefaultValue("9223372036854775807") @QueryParam("agentTo") @Parameter(description = "agentTo") final Long agentTo,
|
||||
@DefaultValue("1") @QueryParam("partyFrom") @Parameter(description = "partyFrom") final Long partyFrom,
|
||||
@DefaultValue("9223372036854775807") @QueryParam("partyTo") @Parameter(description = "partyTo") final Long partyTo) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
|
||||
final LocalDate from = fromDate != null ? LocalDate.parse(fromDate) : LocalDate.of(1900, 1, 1);
|
||||
final LocalDate to = toDate != null ? LocalDate.parse(toDate) : LocalDate.of(9999, 12, 31);
|
||||
|
||||
return readPlatformService.runReport(reportKey, from, to, agentFrom, agentTo, partyFrom, partyTo);
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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.dds.report.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
|
||||
|
||||
public interface DdsReportReadPlatformService {
|
||||
|
||||
DdsReportData runReport(String reportKey, LocalDate fromDate, LocalDate toDate, Long agentFrom, Long agentTo, Long partyFrom,
|
||||
Long partyTo);
|
||||
}
|
||||
-313
@@ -1,313 +0,0 @@
|
||||
/**
|
||||
* 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.dds.report.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
|
||||
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
/**
|
||||
* One switch over every DDS report key. Each branch is a real query against data this system
|
||||
* actually has (Agent/Party/PartyTransaction/CommissionSlab/BatchRun).
|
||||
*
|
||||
* Since SubAgent was folded into Agent (2026-08-24, Agent now does the daily collection work
|
||||
* directly), the queries here dropped a join level wherever they used to go Party/Transaction ->
|
||||
* SubAgent -> Agent - they now join straight to Agent.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class DdsReportReadPlatformServiceImpl implements DdsReportReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public DdsReportData runReport(final String reportKey, final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
|
||||
return switch (reportKey) {
|
||||
case "AGENT_CHECK_LIST" -> agentCheckList();
|
||||
case "PARTY_CHECK_LIST", "PARTY_BALANCE" -> partyList(agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "AGENT_WISE_BALANCE" -> agentWiseBalance(agentFrom, agentTo);
|
||||
case "AGENT_WISE_MONTHLY_COLLECTION" -> agentWiseCollection(fromDate, toDate, agentFrom, agentTo);
|
||||
case "COMMISSION_REGISTER" -> commissionRegister(fromDate, toDate, agentFrom, agentTo);
|
||||
case "COLLECTION_SUMMARY_AGENT_WISE" -> collectionSummaryAgentWise(fromDate, toDate, agentFrom, agentTo);
|
||||
case "AGENT_PARTYWISE_DAILY_COLLECTION" -> agentPartywiseDailyCollection(fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
case "DAILY_COLLECTION_REPORT" -> dailyCollectionReport(fromDate, toDate, agentFrom, agentTo);
|
||||
case "PARTY_INTEREST_REGISTER" -> partyInterestRegister(fromDate, toDate, partyFrom, partyTo);
|
||||
case "BALANCE_SUMMARY" -> balanceSummary(agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "TRANSACTION_REGISTER" -> transactionRegister(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "MATURE_DATE_WISE" -> matureDateWise(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "INTEREST_CERTIFICATE" -> interestCertificate(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "AGENT_COMMISSION_STATEMENT" -> agentCommissionStatement(fromDate, toDate);
|
||||
case "SUPPLEMENTARY_DDS" -> supplementaryDds(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "CLOSE_PARTY_AC_REGISTER" -> closePartyAcRegister(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
|
||||
case "MASTER_DATA_EXPORT" -> masterDataExport(agentFrom, agentTo, partyFrom, partyTo);
|
||||
default -> throw new UnrecognizedQueryParamException("reportKey", reportKey);
|
||||
};
|
||||
}
|
||||
|
||||
private DdsReportData agentCheckList() {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", a.agent_city as \"City\", "
|
||||
+ "a.agent_current_balance as \"Current Balance\" from m_dds_agent a order by a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql);
|
||||
return available("AGENT_CHECK_LIST", List.of("Agent Code", "Agent Name", "City", "Current Balance"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData partyList(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
|
||||
+ "a.agent_code as \"Agent Code\", p.current_balance as \"Current Balance\", "
|
||||
+ "p.opening_date as \"Opening Date\" from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
|
||||
+ "where a.id between ? and ? and p.id between ? and ? order by p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo, partyFrom, partyTo);
|
||||
return available("PARTY_CHECK_LIST", List.of("Party Code", "Party Name", "Agent Code", "Current Balance", "Opening Date"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData agentWiseBalance(final Long agentFrom, final Long agentTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", "
|
||||
+ "a.agent_current_balance as \"Current Balance\" from m_dds_agent a where a.id between ? and ? "
|
||||
+ "order by a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo);
|
||||
return available("AGENT_WISE_BALANCE", List.of("Agent Code", "Name", "Current Balance"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData agentWiseCollection(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", "
|
||||
+ "coalesce(sum(t.amount), 0) as \"Total Collection\" from m_dds_agent a "
|
||||
+ "left join m_dds_party_transaction t on t.agent_id = a.id and t.transaction_type = 'PARTY_TRAN' "
|
||||
+ "and t.direction = 'CREDIT' and t.transaction_date between ? and ? " + "where a.id between ? and ? "
|
||||
+ "group by a.id, a.agent_code, a.agent_name order by a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
|
||||
return available("AGENT_WISE_MONTHLY_COLLECTION", List.of("Agent Code", "Name", "Total Collection"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData commissionRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", b.job_date as \"Job Date\", "
|
||||
+ "b.posted_on_date as \"Posted On\", r.amount as \"Commission Amount\" from m_dds_batch_run_row r "
|
||||
+ "join m_dds_batch_run b on b.id = r.batch_run_id join m_dds_agent a on a.id = r.agent_id "
|
||||
+ "where b.job_type = 'COMMISSION_CALCULATION' and b.status = 'POSTED' and b.posted_on_date between ? and ? "
|
||||
+ "and a.id between ? and ? order by b.posted_on_date, a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
|
||||
return available("COMMISSION_REGISTER", List.of("Agent Code", "Name", "Job Date", "Posted On", "Commission Amount"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData collectionSummaryAgentWise(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
|
||||
+ "coalesce(sum(t.amount), 0) as \"Total Collection\" from m_dds_agent a "
|
||||
+ "left join m_dds_party_transaction t on t.agent_id = a.id and t.transaction_type = 'PARTY_TRAN' "
|
||||
+ "and t.direction = 'CREDIT' and t.transaction_date between ? and ? " + "where a.id between ? and ? "
|
||||
+ "group by a.id, a.agent_code, a.agent_name order by a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
|
||||
return available("COLLECTION_SUMMARY_AGENT_WISE", List.of("Agent Code", "Agent Name", "Total Collection"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData agentPartywiseDailyCollection(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", p.party_code as \"Party Code\", "
|
||||
+ "t.transaction_date as \"Transaction Date\", t.amount as \"Amount\", "
|
||||
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
|
||||
+ "join m_dds_party p on p.id = t.party_id join m_dds_agent a on a.id = t.agent_id "
|
||||
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
|
||||
+ "and a.id between ? and ? and p.id between ? and ? order by t.transaction_date, a.agent_code, p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("AGENT_PARTYWISE_DAILY_COLLECTION",
|
||||
List.of("Agent Code", "Party Code", "Transaction Date", "Amount", "Balance After"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData dailyCollectionReport(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo) {
|
||||
final String sql = "select t.transaction_date as \"Transaction Date\", a.agent_code as \"Agent Code\", "
|
||||
+ "coalesce(sum(t.amount), 0) as \"Total Collection\", count(*) as \"Transaction Count\" "
|
||||
+ "from m_dds_party_transaction t join m_dds_agent a on a.id = t.agent_id "
|
||||
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
|
||||
+ "and a.id between ? and ? group by t.transaction_date, a.id, a.agent_code "
|
||||
+ "order by t.transaction_date, a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
|
||||
return available("DAILY_COLLECTION_REPORT",
|
||||
List.of("Transaction Date", "Agent Code", "Total Collection", "Transaction Count"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData partyInterestRegister(final LocalDate fromDate, final LocalDate toDate, final Long partyFrom,
|
||||
final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", t.transaction_date as \"Transaction Date\", "
|
||||
+ "t.transaction_type as \"Transaction Type\", t.amount as \"Amount\", "
|
||||
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
|
||||
+ "join m_dds_party p on p.id = t.party_id "
|
||||
+ "where t.transaction_type in ('INTEREST_POST', 'OD_INTEREST_POST') and t.transaction_date between ? and ? "
|
||||
+ "and p.id between ? and ? order by t.transaction_date, p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, partyFrom, partyTo);
|
||||
return available("PARTY_INTEREST_REGISTER",
|
||||
List.of("Party Code", "Transaction Date", "Transaction Type", "Amount", "Balance After"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData balanceSummary(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final Integer totalAgents = this.jdbcTemplate.queryForObject("select count(*) from m_dds_agent where id between ? and ?",
|
||||
Integer.class, agentFrom, agentTo);
|
||||
final Integer totalParties = this.jdbcTemplate.queryForObject(
|
||||
"select count(*) from m_dds_party where agent_id between ? and ? and id between ? and ?", Integer.class, agentFrom,
|
||||
agentTo, partyFrom, partyTo);
|
||||
final BigDecimal totalBalance = this.jdbcTemplate.queryForObject(
|
||||
"select coalesce(sum(current_balance), 0) from m_dds_party where agent_id between ? and ? and id between ? and ?",
|
||||
BigDecimal.class, agentFrom, agentTo, partyFrom, partyTo);
|
||||
|
||||
final Map<String, Object> row = Map.of("Total Agents", totalAgents, "Total Parties", totalParties, "Total Party Balance",
|
||||
totalBalance);
|
||||
return available("BALANCE_SUMMARY", List.of("Total Agents", "Total Parties", "Total Party Balance"), List.of(row));
|
||||
}
|
||||
|
||||
private DdsReportData transactionRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", a.agent_code as \"Agent Code\", "
|
||||
+ "t.transaction_date as \"Transaction Date\", t.transaction_type as \"Transaction Type\", "
|
||||
+ "t.direction as \"Direction\", t.amount as \"Amount\", t.description as \"Description\" "
|
||||
+ "from m_dds_party_transaction t join m_dds_party p on p.id = t.party_id "
|
||||
+ "join m_dds_agent a on a.id = t.agent_id "
|
||||
+ "where t.transaction_date between ? and ? and a.id between ? and ? and p.id between ? and ? "
|
||||
+ "order by t.transaction_date, t.id";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("TRANSACTION_REGISTER",
|
||||
List.of("Party Code", "Agent Code", "Transaction Date", "Transaction Type", "Direction", "Amount", "Description"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData matureDateWise(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom, final Long agentTo,
|
||||
final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", p.maturity_date as \"Maturity Date\", "
|
||||
+ "p.current_balance as \"Current Balance\" from m_dds_party p "
|
||||
+ "where p.maturity_date is not null and p.maturity_date between ? and ? and p.agent_id between ? and ? "
|
||||
+ "and p.id between ? and ? order by p.maturity_date";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("MATURE_DATE_WISE", List.of("Party Code", "Maturity Date", "Current Balance"), rows);
|
||||
}
|
||||
|
||||
private DdsReportData interestCertificate(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
|
||||
+ "p.opening_date as \"Opening Date\", p.daily_deposit_amount as \"Daily Deposit Amount\", "
|
||||
+ "p.current_balance as \"Current Balance\", coalesce((select sum(t.amount) from m_dds_party_transaction t "
|
||||
+ "where t.party_id = p.id and t.transaction_type in ('INTEREST_POST', 'OD_INTEREST_POST') "
|
||||
+ "and t.transaction_date between ? and ?), 0) as \"Interest Earned\" from m_dds_party p "
|
||||
+ "where p.agent_id between ? and ? and p.id between ? and ? order by p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("INTEREST_CERTIFICATE",
|
||||
List.of("Party Code", "Party Name", "Opening Date", "Daily Deposit Amount", "Current Balance", "Interest Earned"), rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-agent settlement statement for a date range: collection volume, commission actually
|
||||
* posted in that window (via the same BatchRunRow join as COMMISSION_REGISTER), and the
|
||||
* deposit/advance/deduction fields tracked directly on the agent. "Deduct Commission" and
|
||||
* "PC Diff" have no corresponding data anywhere in this schema yet, so they report as 0 rather
|
||||
* than a guessed figure - matching the legacy sample where most of these columns were 0.00 too.
|
||||
* No agent id range filter is applied here: the legacy screenshot for this report only ever
|
||||
* showed a date range, no agent-range inputs.
|
||||
*/
|
||||
private DdsReportData agentCommissionStatement(final LocalDate fromDate, final LocalDate toDate) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
|
||||
+ "a.agent_saving_account_no as \"A/c No\", coalesce(collection.total, 0) as \"Total Collection\", "
|
||||
+ "coalesce(commission.total, 0) as \"Commission\", 0 as \"Deduct Commission\", "
|
||||
+ "a.agent_deposit as \"Agent Deposit\", a.festival_advance as \"Festival Advance\", "
|
||||
+ "a.other_deduction as \"Other Deduction\", "
|
||||
+ "(coalesce(commission.total, 0) - a.agent_deposit - a.festival_advance - a.other_deduction) as \"Total\", "
|
||||
+ "0 as \"PC Diff\" " + "from m_dds_agent a "
|
||||
+ "left join (select t.agent_id as agent_id, sum(t.amount) as total from m_dds_party_transaction t "
|
||||
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
|
||||
+ "group by t.agent_id) collection on collection.agent_id = a.id "
|
||||
+ "left join (select r.agent_id as agent_id, sum(r.amount) as total from m_dds_batch_run_row r "
|
||||
+ "join m_dds_batch_run b on b.id = r.batch_run_id "
|
||||
+ "where b.job_type = 'COMMISSION_CALCULATION' and b.status = 'POSTED' and b.posted_on_date between ? and ? "
|
||||
+ "group by r.agent_id) commission on commission.agent_id = a.id " + "order by a.agent_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, fromDate, toDate);
|
||||
return available("AGENT_COMMISSION_STATEMENT",
|
||||
List.of("Agent Code", "Agent Name", "A/c No", "Total Collection", "Commission", "Deduct Commission", "Agent Deposit",
|
||||
"Festival Advance", "Other Deduction", "Total", "PC Diff"),
|
||||
rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy Supplementary-DDS (As On) report: every regular collection (PARTY_TRAN, CREDIT)
|
||||
* posted in the given date range - clarified with the business as "the regular transactions
|
||||
* going on" (2026-08-24), same shape as the other collection-listing reports.
|
||||
*/
|
||||
private DdsReportData supplementaryDds(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom, final Long agentTo,
|
||||
final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
|
||||
+ "t.transaction_date as \"Transaction Date\", t.transaction_mode as \"Mode\", t.amount as \"Amount\", "
|
||||
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
|
||||
+ "join m_dds_party p on p.id = t.party_id join m_dds_agent a on a.id = t.agent_id "
|
||||
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
|
||||
+ "and a.id between ? and ? and p.id between ? and ? order by t.transaction_date, a.agent_code, p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("SUPPLEMENTARY_DDS",
|
||||
List.of("Agent Code", "Party Code", "Party Name", "Transaction Date", "Mode", "Amount", "Balance After"), rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy Close Party Ac Register report: Parties closed (via the Close Party command) in the
|
||||
* given date range, with their close date and final (zero) balance.
|
||||
*/
|
||||
private DdsReportData closePartyAcRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
|
||||
final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", a.agent_code as \"Agent Code\", "
|
||||
+ "p.opening_date as \"Opening Date\", p.close_date as \"Close Date\", p.current_balance as \"Final Balance\" "
|
||||
+ "from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
|
||||
+ "where p.status = 'CLOSED' and p.close_date between ? and ? and a.id between ? and ? and p.id between ? and ? "
|
||||
+ "order by p.close_date, p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
|
||||
partyTo);
|
||||
return available("CLOSE_PARTY_AC_REGISTER",
|
||||
List.of("Party Code", "Party Name", "Agent Code", "Opening Date", "Close Date", "Final Balance"), rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy Master Data Export report: clarified with the business (2026-08-24) as "select an
|
||||
* agent, show the full detail of every party under that agent" - filtered by the same agent
|
||||
* range as every other report here (a single agent is just agentFrom == agentTo).
|
||||
*/
|
||||
private DdsReportData masterDataExport(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
|
||||
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
|
||||
+ "p.party_code as \"Party Code\", p.party_name as \"Party Name\", p.address1 as \"Address\", "
|
||||
+ "p.city as \"City\", p.pincode as \"Pincode\", p.mobile_no_sms as \"Mobile No\", "
|
||||
+ "p.aadhar_no as \"Aadhar No\", p.pan_card_no as \"PAN No\", p.opening_date as \"Opening Date\", "
|
||||
+ "p.maturity_date as \"Maturity Date\", p.daily_deposit_amount as \"Daily Deposit Amount\", "
|
||||
+ "p.interest_rate as \"Interest Rate\", p.current_balance as \"Current Balance\" "
|
||||
+ "from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
|
||||
+ "where a.id between ? and ? and p.id between ? and ? order by a.agent_code, p.party_code";
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo, partyFrom, partyTo);
|
||||
return available("MASTER_DATA_EXPORT",
|
||||
List.of("Agent Code", "Agent Name", "Party Code", "Party Name", "Address", "City", "Pincode", "Mobile No", "Aadhar No",
|
||||
"PAN No", "Opening Date", "Maturity Date", "Daily Deposit Amount", "Interest Rate", "Current Balance"),
|
||||
rows);
|
||||
}
|
||||
|
||||
private DdsReportData available(final String reportKey, final List<String> columns, final List<Map<String, Object>> rows) {
|
||||
return DdsReportData.builder().reportKey(reportKey).available(true).columns(columns).rows(rows).build();
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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.dds.report.starter;
|
||||
|
||||
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformService;
|
||||
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformServiceImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class DdsReportConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DdsReportReadPlatformService.class)
|
||||
public DdsReportReadPlatformService ddsReportReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new DdsReportReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/dds/partytransactions")
|
||||
@Component
|
||||
@Tag(name = "DDS Party Transactions", description = "The Daily Transaction Entry ledger: a day's PARTY_TRAN collection rows, the two legs of a WITHDRAW_DEPOSITE transfer between two parties, or an AGENT_TRAN settlement of an Agent's collected cash to its branch. Immutable once posted - reversed with an opposing entry, never edited.")
|
||||
@RequiredArgsConstructor
|
||||
public class PartyTransactionsApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "PARTYTRANSACTION";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final PartyTransactionReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve DDS Party Transactions", description = "Returns the list of posted DDS party transactions, most recent first. Optionally filter by agentId and/or partyId.")
|
||||
public Collection<PartyTransactionData> retrieveAll(@QueryParam("agentId") @Parameter(description = "agentId") final Long agentId,
|
||||
@QueryParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveAll(agentId, partyId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{transactionId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve a DDS Party Transaction", description = "Returns the details of a single posted DDS party transaction.")
|
||||
public PartyTransactionData retrieveOne(@PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId) {
|
||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
return readPlatformService.retrieveOne(transactionId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Post DDS Party Transactions", description = "Mandatory fields: agentId, transactionType (PARTY_TRAN, "
|
||||
+ "WITHDRAW_DEPOSITE or AGENT_TRAN), transactionDate\n" + "\n"
|
||||
+ "For PARTY_TRAN: collections (array of {partyCode, transactionMode, accountType, accountNo, amount, chequeNo, tokenNo, "
|
||||
+ "description})\n" + "\n" + "For WITHDRAW_DEPOSITE: creditorPartyCode, debitPartyCode, withdrawAmount, withdrawDescription\n"
|
||||
+ "\n" + "For AGENT_TRAN: amount, transactionMode, accountType, accountNo, description")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.createPartyTransaction() //
|
||||
.withJson(apiRequestBodyAsJson) //
|
||||
.build();
|
||||
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface PartyTransactionRepository extends JpaRepository<PartyTransaction, Long> {
|
||||
|
||||
@Query("select coalesce(sum(t.amount), 0) from PartyTransaction t where t.agent.id = :agentId "
|
||||
+ "and t.transactionType = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transactionDate between :fromDate and :toDate")
|
||||
BigDecimal sumPartyTranCredits(@Param("agentId") Long agentId, @Param("fromDate") LocalDate fromDate,
|
||||
@Param("toDate") LocalDate toDate);
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.handler;
|
||||
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "PARTYTRANSACTION", action = "CREATE")
|
||||
public class CreatePartyTransactionCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final PartyTransactionWritePlatformService writePlatformService;
|
||||
|
||||
@Autowired
|
||||
public CreatePartyTransactionCommandHandler(final PartyTransactionWritePlatformService writePlatformService) {
|
||||
this.writePlatformService = writePlatformService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createPartyTransaction(command);
|
||||
}
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.serialization;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* PARTY_TRAN (batch collection rows), WITHDRAW_DEPOSITE (a transfer between two parties) and
|
||||
* AGENT_TRAN (an Agent settling its collected cash to a branch, no party involved) are
|
||||
* validated/processed - the three transaction types the Daily Transaction Entry screen actually
|
||||
* collects fields for today. PARTY_PAYMENT / OD_ENTRY / OD_PAYMENT are listed on the frontend as
|
||||
* not-yet-implemented placeholders, so they are rejected here rather than silently accepted with no
|
||||
* real effect.
|
||||
*/
|
||||
@Component
|
||||
public final class PartyTransactionCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final Set<String> SUPPORTED_TRANSACTION_TYPES = new HashSet<>(
|
||||
Arrays.asList("PARTY_TRAN", "WITHDRAW_DEPOSITE", "AGENT_TRAN"));
|
||||
|
||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("agentId", "transactionType",
|
||||
"transactionDate", "collections", "creditorPartyCode", "debitPartyCode", "withdrawAmount", "withdrawDescription",
|
||||
"amount", "transactionMode", "accountType", "accountNo", "description", "dateFormat", "locale"));
|
||||
|
||||
private static final Set<String> SUPPORTED_COLLECTION_PARAMETERS = new HashSet<>(Arrays.asList("partyCode", "transactionMode",
|
||||
"accountType", "accountNo", "amount", "chequeNo", "tokenNo", "description"));
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Autowired
|
||||
public PartyTransactionCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
|
||||
|
||||
}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("partytransaction");
|
||||
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
v.reset().parameter("agentId").value(this.fromApiJsonHelper.extractLongNamed("agentId", element)).notNull()
|
||||
.integerGreaterThanZero();
|
||||
|
||||
final String transactionType = this.fromApiJsonHelper.extractStringNamed("transactionType", element);
|
||||
v.reset().parameter("transactionType").value(transactionType).notBlank().isOneOfTheseStringValues(
|
||||
SUPPORTED_TRANSACTION_TYPES.toArray());
|
||||
|
||||
v.reset().parameter("transactionDate").value(this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element)).notNull();
|
||||
|
||||
if ("PARTY_TRAN".equals(transactionType)) {
|
||||
if (!this.fromApiJsonHelper.parameterExists("collections", element)) {
|
||||
v.reset().parameter("collections").value(null).notNull();
|
||||
} else {
|
||||
final JsonObject topLevel = element.getAsJsonObject();
|
||||
final var collectionsArray = topLevel.getAsJsonArray("collections");
|
||||
v.reset().parameter("collections").value(collectionsArray).jsonArrayNotEmpty();
|
||||
for (int i = 0; i < collectionsArray.size(); i++) {
|
||||
final JsonObject row = collectionsArray.get(i).getAsJsonObject();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(row, SUPPORTED_COLLECTION_PARAMETERS);
|
||||
|
||||
v.reset().parameterAtIndexArray("partyCode", i).value(extractString(row, "partyCode")).notBlank();
|
||||
v.reset().parameterAtIndexArray("accountNo", i).value(extractString(row, "accountNo")).notBlank();
|
||||
v.reset().parameterAtIndexArray("amount", i).value(extractBigDecimal(row, "amount")).notNull().positiveAmount();
|
||||
}
|
||||
}
|
||||
} else if ("WITHDRAW_DEPOSITE".equals(transactionType)) {
|
||||
v.reset().parameter("creditorPartyCode").value(this.fromApiJsonHelper.extractStringNamed("creditorPartyCode", element))
|
||||
.notBlank();
|
||||
v.reset().parameter("debitPartyCode").value(this.fromApiJsonHelper.extractStringNamed("debitPartyCode", element))
|
||||
.notBlank();
|
||||
v.reset().parameter("withdrawAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element))
|
||||
.notNull().positiveAmount();
|
||||
} else if ("AGENT_TRAN".equals(transactionType)) {
|
||||
v.reset().parameter("amount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element)).notNull()
|
||||
.positiveAmount();
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private String extractString(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
|
||||
}
|
||||
|
||||
private java.math.BigDecimal extractBigDecimal(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsBigDecimal() : null;
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
|
||||
|
||||
public interface PartyTransactionReadPlatformService {
|
||||
|
||||
Collection<PartyTransactionData> retrieveAll(Long agentId, Long partyId);
|
||||
|
||||
PartyTransactionData retrieveOne(Long transactionId);
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLBuilder;
|
||||
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
|
||||
import org.apache.fineract.portfolio.dds.transaction.exception.PartyTransactionNotFoundException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class PartyTransactionReadPlatformServiceImpl implements PartyTransactionReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final PartyTransactionMapper MAPPER = new PartyTransactionMapper();
|
||||
|
||||
private static final class PartyTransactionMapper implements RowMapper<PartyTransactionData> {
|
||||
|
||||
public String schema() {
|
||||
return " t.id as id, t.party_id as partyId, p.party_code as partyCode, p.party_name as partyName, "
|
||||
+ " t.agent_id as agentId, a.agent_code as agentCode, t.transaction_date as transactionDate, "
|
||||
+ " t.transaction_type as transactionType, t.direction as direction, t.transaction_mode as transactionMode, "
|
||||
+ " t.account_type as accountType, t.account_no as accountNo, t.amount as amount, t.cheque_no as chequeNo, "
|
||||
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction "
|
||||
+ " from m_dds_party_transaction t left join m_dds_party p on p.id = t.party_id "
|
||||
+ " join m_dds_agent a on a.id = t.agent_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartyTransactionData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||
return PartyTransactionData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.partyId(JdbcSupport.getLongDefaultToNullIfZero(rs, "partyId")) //
|
||||
.partyCode(rs.getString("partyCode")) //
|
||||
.partyName(rs.getString("partyName")) //
|
||||
.agentId(rs.getLong("agentId")) //
|
||||
.agentCode(rs.getString("agentCode")) //
|
||||
.transactionDate(JdbcSupport.getLocalDate(rs, "transactionDate")) //
|
||||
.transactionType(rs.getString("transactionType")) //
|
||||
.direction(rs.getString("direction")) //
|
||||
.transactionMode(rs.getString("transactionMode")) //
|
||||
.accountType(rs.getString("accountType")) //
|
||||
.accountNo(rs.getString("accountNo")) //
|
||||
.amount(rs.getBigDecimal("amount")) //
|
||||
.chequeNo(rs.getString("chequeNo")) //
|
||||
.tokenNo(rs.getString("tokenNo")) //
|
||||
.description(rs.getString("description")) //
|
||||
.balanceAfterTransaction(rs.getBigDecimal("balanceAfterTransaction")) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PartyTransactionData> retrieveAll(final Long agentId, final Long partyId) {
|
||||
final SQLBuilder extraCriteria = new SQLBuilder();
|
||||
extraCriteria.addNonNullCriteria(" t.agent_id = ", agentId);
|
||||
extraCriteria.addNonNullCriteria(" t.party_id = ", partyId);
|
||||
|
||||
String sql = "select " + MAPPER.schema() + " " + extraCriteria.getSQLTemplate();
|
||||
sql = sql + " order by t.id desc";
|
||||
|
||||
return this.jdbcTemplate.query(sql, MAPPER, extraCriteria.getArguments()); // NOSONAR
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartyTransactionData retrieveOne(final Long transactionId) {
|
||||
try {
|
||||
final String sql = "select " + MAPPER.schema() + " where t.id = ?";
|
||||
return this.jdbcTemplate.queryForObject(sql, MAPPER, transactionId); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new PartyTransactionNotFoundException(transactionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface PartyTransactionWritePlatformService {
|
||||
|
||||
CommandProcessingResult createPartyTransaction(JsonCommand command);
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.service;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyAlreadyClosedException;
|
||||
import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientAgentBalanceException;
|
||||
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientPartyBalanceException;
|
||||
import org.apache.fineract.portfolio.dds.transaction.serialization.PartyTransactionCommandFromApiJsonDeserializer;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PartyTransactionWritePlatformServiceJpaRepositoryImpl implements PartyTransactionWritePlatformService {
|
||||
|
||||
private final PartyTransactionCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
private final PartyTransactionRepository partyTransactionRepository;
|
||||
private final PartyRepository partyRepository;
|
||||
private final AgentRepository agentRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult createPartyTransaction(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||
|
||||
final Long agentId = command.longValueOfParameterNamed("agentId");
|
||||
final Agent agent = this.agentRepository.findById(agentId)
|
||||
.orElseThrow(() -> new AgentNotFoundException(agentId));
|
||||
|
||||
final String transactionType = command.stringValueOfParameterNamed("transactionType");
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
|
||||
final List<PartyTransaction> posted = new ArrayList<>();
|
||||
if ("PARTY_TRAN".equals(transactionType)) {
|
||||
posted.addAll(postCollections(agent, transactionDate, command));
|
||||
} else if ("AGENT_TRAN".equals(transactionType)) {
|
||||
posted.add(postAgentSettlement(agent, transactionDate, command));
|
||||
} else {
|
||||
posted.addAll(postWithdrawDeposite(agent, transactionDate, command));
|
||||
}
|
||||
|
||||
this.partyTransactionRepository.saveAllAndFlush(posted);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(posted.isEmpty() ? null : posted.get(0).getId()) //
|
||||
.build();
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleDataIntegrityIssues(dve.getMostSpecificCause(), dve);
|
||||
return CommandProcessingResult.empty();
|
||||
} catch (final PersistenceException dve) {
|
||||
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
|
||||
handleDataIntegrityIssues(throwable, dve);
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private List<PartyTransaction> postCollections(final Agent agent, final LocalDate transactionDate, final JsonCommand command) {
|
||||
final List<PartyTransaction> transactions = new ArrayList<>();
|
||||
final JsonArray collectionsArray = command.arrayOfParameterNamed("collections");
|
||||
for (int i = 0; i < collectionsArray.size(); i++) {
|
||||
final JsonObject row = collectionsArray.get(i).getAsJsonObject();
|
||||
final String partyCode = row.get("partyCode").getAsString();
|
||||
final Party party = this.partyRepository.findByPartyCode(partyCode)
|
||||
.orElseThrow(() -> new PartyNotFoundException(partyCode));
|
||||
if (party.isClosed()) {
|
||||
throw new PartyAlreadyClosedException(partyCode);
|
||||
}
|
||||
final BigDecimal amount = row.get("amount").getAsBigDecimal();
|
||||
final BigDecimal newBalance = party.credit(amount);
|
||||
agent.credit(amount);
|
||||
|
||||
transactions.add(new PartyTransaction(party, agent, transactionDate, "PARTY_TRAN", "CREDIT",
|
||||
stringOrNull(row, "transactionMode"), stringOrNull(row, "accountType"), stringOrNull(row, "accountNo"), amount,
|
||||
stringOrNull(row, "chequeNo"), stringOrNull(row, "tokenNo"), stringOrNull(row, "description"), newBalance));
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private PartyTransaction postAgentSettlement(final Agent agent, final LocalDate transactionDate, final JsonCommand command) {
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount");
|
||||
if (agent.getAgentCurrentBalance().compareTo(amount) < 0) {
|
||||
throw new InsufficientAgentBalanceException(agent.getAgentCode());
|
||||
}
|
||||
final BigDecimal newBalance = agent.settle(amount);
|
||||
|
||||
return new PartyTransaction(null, agent, transactionDate, "AGENT_TRAN", "DEBIT",
|
||||
command.stringValueOfParameterNamedAllowingNull("transactionMode"),
|
||||
command.stringValueOfParameterNamedAllowingNull("accountType"),
|
||||
command.stringValueOfParameterNamedAllowingNull("accountNo"), amount, null, null,
|
||||
command.stringValueOfParameterNamedAllowingNull("description"), newBalance);
|
||||
}
|
||||
|
||||
private List<PartyTransaction> postWithdrawDeposite(final Agent agent, final LocalDate transactionDate,
|
||||
final JsonCommand command) {
|
||||
final String creditorPartyCode = command.stringValueOfParameterNamed("creditorPartyCode");
|
||||
final String debitPartyCode = command.stringValueOfParameterNamed("debitPartyCode");
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("withdrawAmount");
|
||||
final String description = command.stringValueOfParameterNamedAllowingNull("withdrawDescription");
|
||||
|
||||
final Party creditorParty = this.partyRepository.findByPartyCode(creditorPartyCode)
|
||||
.orElseThrow(() -> new PartyNotFoundException(creditorPartyCode));
|
||||
final Party debitParty = this.partyRepository.findByPartyCode(debitPartyCode)
|
||||
.orElseThrow(() -> new PartyNotFoundException(debitPartyCode));
|
||||
|
||||
if (debitParty.getCurrentBalance().compareTo(amount) < 0) {
|
||||
throw new InsufficientPartyBalanceException(debitPartyCode);
|
||||
}
|
||||
|
||||
final BigDecimal debtorNewBalance = debitParty.debit(amount);
|
||||
final BigDecimal creditorNewBalance = creditorParty.credit(amount);
|
||||
|
||||
final List<PartyTransaction> transactions = new ArrayList<>();
|
||||
transactions.add(new PartyTransaction(debitParty, agent, transactionDate, "WITHDRAW_DEPOSITE", "DEBIT", null, null, null,
|
||||
amount, null, null, description, debtorNewBalance));
|
||||
transactions.add(new PartyTransaction(creditorParty, agent, transactionDate, "WITHDRAW_DEPOSITE", "CREDIT", null, null, null,
|
||||
amount, null, null, description, creditorNewBalance));
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private String stringOrNull(final JsonObject json, final String paramName) {
|
||||
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
|
||||
}
|
||||
|
||||
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
|
||||
if (realCause.getMessage() != null && realCause.getMessage().contains("party_code")) {
|
||||
throw new PlatformDataIntegrityException("error.msg.partytransaction.invalid.party",
|
||||
"One of the party codes in this transaction does not exist");
|
||||
}
|
||||
|
||||
log.error("Error occured.", dve);
|
||||
throw ErrorHandler.getMappable(dve, "error.msg.partytransaction.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource: " + realCause.getMessage());
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* 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.dds.transaction.starter;
|
||||
|
||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||
import org.apache.fineract.portfolio.dds.transaction.serialization.PartyTransactionCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformService;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformService;
|
||||
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformServiceJpaRepositoryImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@Configuration
|
||||
public class DdsPartyTransactionConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PartyTransactionReadPlatformService.class)
|
||||
public PartyTransactionReadPlatformService partyTransactionReadPlatformService(JdbcTemplate jdbcTemplate) {
|
||||
return new PartyTransactionReadPlatformServiceImpl(jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PartyTransactionWritePlatformService.class)
|
||||
public PartyTransactionWritePlatformService partyTransactionWritePlatformService(
|
||||
PartyTransactionCommandFromApiJsonDeserializer fromApiJsonDeserializer, PartyTransactionRepository partyTransactionRepository,
|
||||
PartyRepository partyRepository, AgentRepository agentRepository) {
|
||||
return new PartyTransactionWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, partyTransactionRepository,
|
||||
partyRepository, agentRepository);
|
||||
}
|
||||
}
|
||||
+62
@@ -57,14 +57,23 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
"9752123456", "9752345678", "9752987654"
|
||||
);
|
||||
|
||||
/** Demo numbers that always return DISCONNECTED (found on the MNRL) without calling the external API. */
|
||||
private static final Set<String> DEMO_DISCONNECTED_NUMBERS = Set.of("9000000001", "9000000002");
|
||||
|
||||
/** Demo numbers that always return SUSPECTED_AND_DISCONNECTED without calling the external API. */
|
||||
private static final Set<String> DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS = Set.of("9000000003");
|
||||
|
||||
private final String baseUrl;
|
||||
private final boolean enabled;
|
||||
private final RestTemplate restTemplate;
|
||||
private final MobileLookupLogRepository logRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl,
|
||||
@Value("${fineract.mobile-lookup.enabled:true}") boolean enabled,
|
||||
MobileLookupLogRepository logRepository, JdbcTemplate jdbcTemplate) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.enabled = enabled;
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.logRepository = logRepository;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
@@ -103,6 +112,39 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
return MobileLookupResult.clean(logId, trimmed);
|
||||
}
|
||||
|
||||
// Demo numbers — return DISCONNECTED (on the MNRL) without calling the external API
|
||||
if (isDemoDisconnectedNumber(trimmed)) {
|
||||
LOG.info("Demo disconnected number detected: {}", trimmed);
|
||||
final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected(
|
||||
"Number revoked and listed on MNRL (demo data)", "DEMO");
|
||||
final Long logId = saveLog(trimmed, "DISCONNECTED", false, true,
|
||||
"Number is DISCONNECTED — found in MNRL records", null, source, clientId);
|
||||
return MobileLookupResult.disconnected(logId, trimmed, disconnectedDetail);
|
||||
}
|
||||
|
||||
// Demo numbers — return SUSPECTED_AND_DISCONNECTED without calling the external API
|
||||
if (isDemoSuspectedAndDisconnectedNumber(trimmed)) {
|
||||
LOG.info("Demo suspected+disconnected number detected: {}", trimmed);
|
||||
final MobileLookupDetailData suspectedDetail = MobileLookupDetailData.forSuspected(
|
||||
"active", "Demo DIP suspected number", "DEMO");
|
||||
final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected(
|
||||
"Number revoked and listed on MNRL (demo data)", "DEMO");
|
||||
final Long logId = saveLog(trimmed, "SUSPECTED_AND_DISCONNECTED", true, true,
|
||||
"Number is SUSPECTED and DISCONNECTED — found in both DIP and MNRL records", null, source, clientId);
|
||||
return MobileLookupResult.suspectedAndDisconnected(logId, trimmed, suspectedDetail, disconnectedDetail);
|
||||
}
|
||||
|
||||
// Lookup switched off by config (fineract.mobile-lookup.enabled=false) -- skip the external
|
||||
// call entirely rather than fail with a connection error. Logged distinctly from a genuine
|
||||
// CLEAN result so it can never be mistaken for one in an audit trail.
|
||||
if (!enabled) {
|
||||
LOG.info("DIP/MNRL lookup is disabled by configuration — skipping check for {}", trimmed);
|
||||
final Long logId = saveLog(trimmed, MobileLookupLog.STATUS_CLEAN, false, false,
|
||||
"DIP/MNRL lookup is disabled by configuration — the number was not actually checked.",
|
||||
"{\"provider\":\"LOOKUP_DISABLED_BY_CONFIG\"}", source, clientId);
|
||||
return MobileLookupResult.clean(logId, trimmed);
|
||||
}
|
||||
|
||||
final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed;
|
||||
|
||||
LOG.info("Calling DIP/MNRL lookup API: {}", url);
|
||||
@@ -402,6 +444,26 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDemoDisconnectedNumber(final String mobileNumber) {
|
||||
if (DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber)) {
|
||||
return true;
|
||||
}
|
||||
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||
return DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDemoSuspectedAndDisconnectedNumber(final String mobileNumber) {
|
||||
if (DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber)) {
|
||||
return true;
|
||||
}
|
||||
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||
return DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getStringOrNull(JsonObject json, String key) {
|
||||
JsonElement el = json.get(key);
|
||||
return (el != null && !el.isJsonNull()) ? el.getAsString() : null;
|
||||
|
||||
+69
@@ -18,6 +18,11 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.api;
|
||||
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DESCRIPTION;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_NAME;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -26,6 +31,7 @@ import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.HeaderParam;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -34,9 +40,11 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -50,6 +58,10 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiResource;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
@@ -63,6 +75,9 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/currentaccounts")
|
||||
@@ -84,6 +99,10 @@ public class CurrentAccountsApiResource {
|
||||
private final CurrentAccountEditWritePlatformService currentAccountEditWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<CurrentAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||
private final DocumentApiResource documentApiResource;
|
||||
|
||||
/** Matches the {@code entityType} current-account document requests are stored/looked up under. */
|
||||
private static final String DOCUMENT_ENTITY_TYPE = "currentaccounts";
|
||||
|
||||
@GET
|
||||
@Path("template")
|
||||
@@ -337,4 +356,54 @@ public class CurrentAccountsApiResource {
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
// ── Documents ────────────────────────────────────────────────────────────
|
||||
// The generic /v1/{entityType}/{entityId}/documents resource (DocumentApiResource) never gets a
|
||||
// chance to handle current-account requests: this class's own literal /v1/currentaccounts path
|
||||
// is matched first for anything under {accountId}, and it has no "documents" method, so the
|
||||
// request dead-ends in a 404 instead of falling through. These delegate straight to the same
|
||||
// DocumentApiResource so current accounts get identical document-management behaviour.
|
||||
|
||||
@GET
|
||||
@Path("{accountId}/documents")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List documents attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public List<DocumentData> retrieveDocuments(@PathParam("accountId") final Long accountId) {
|
||||
return documentApiResource.retrieveAllDocuments(DOCUMENT_ENTITY_TYPE, accountId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{accountId}/documents/{documentId}/attachment")
|
||||
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
|
||||
@Operation(summary = "Download a document attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown: The corresponding Binary file")
|
||||
public Response downloadDocument(@PathParam("accountId") final Long accountId, @PathParam("documentId") final Long documentId) {
|
||||
return documentApiResource.downloadFile(DOCUMENT_ENTITY_TYPE, accountId, documentId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{accountId}/documents")
|
||||
@Consumes({ MediaType.MULTIPART_FORM_DATA })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Attach a document to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
|
||||
public DocumentCreateResponse createDocument(@PathParam("accountId") final Long accountId,
|
||||
@HeaderParam(CONTENT_LENGTH) final Long fileSize, @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream inputStream,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_NAME) final String name,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_DESCRIPTION) final String description) {
|
||||
return documentApiResource.createDocument(DOCUMENT_ENTITY_TYPE, accountId, fileSize, inputStream, fileDetails, filePart, name,
|
||||
description);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{accountId}/documents/{documentId}")
|
||||
@Operation(summary = "Remove a document attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public DocumentDeleteResponse deleteDocument(@PathParam("accountId") final Long accountId,
|
||||
@PathParam("documentId") final Long documentId) {
|
||||
return documentApiResource.deleteDocument(DOCUMENT_ENTITY_TYPE, accountId, documentId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-1
@@ -440,6 +440,9 @@ public class FixedDepositAccountsApiResource {
|
||||
} else if (is(commandParam, "prematureClose")) {
|
||||
final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "payment")) {
|
||||
final CommandWrapper commandRequest = builder.paymentFixedDepositAccount(accountId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "calculatePrematureAmount")) {
|
||||
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
|
||||
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
|
||||
@@ -453,7 +456,7 @@ public class FixedDepositAccountsApiResource {
|
||||
if (result == null) {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam,
|
||||
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
|
||||
"postInterest", "close", "prematureClose", "calculatePrematureAmount" });
|
||||
"postInterest", "close", "prematureClose", "payment", "calculatePrematureAmount" });
|
||||
}
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
@@ -489,6 +492,8 @@ public class FixedDepositAccountsApiResource {
|
||||
DepositAccountData account = null;
|
||||
if (is(commandParam, "close")) {
|
||||
account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId);
|
||||
} else if (is(commandParam, "payment")) {
|
||||
account = this.depositAccountReadPlatformService.retrieveOneWithPaymentTemplate(DepositAccountType.FIXED_DEPOSIT, accountId);
|
||||
}
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
|
||||
+65
-1
@@ -97,6 +97,7 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -105,11 +106,14 @@ public class DepositAccountDataValidator {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DepositProductDataValidator productDataValidator;
|
||||
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
|
||||
|
||||
@Autowired
|
||||
public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator) {
|
||||
public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator,
|
||||
final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
this.productDataValidator = productDataValidator;
|
||||
this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper;
|
||||
}
|
||||
|
||||
public void validateFixedDepositForSubmit(final String json) {
|
||||
@@ -351,10 +355,40 @@ public class DepositAccountDataValidator {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||
|
||||
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
|
||||
// moment the account matures -- before the customer has actually come in to collect it. For
|
||||
// maturities processed automatically by the scheduled job (not a live counter transaction),
|
||||
// that books cash as disbursed while it is still physically in the vault, so this option is
|
||||
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
||||
"maturity.instruction.withdraw.deposit.not.allowed",
|
||||
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
|
||||
}
|
||||
|
||||
// Partial Reinvest needs a specific kept-amount decided at the moment of closure -- there is
|
||||
// nothing sensible to pre-commit to years in advance as a standing instruction, so it is only
|
||||
// ever chosen manually via the Payment action, never set here.
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue())) {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
||||
"maturity.instruction.partial.reinvest.not.allowed",
|
||||
"Partial Re-Invest is not a valid standing maturity instruction -- it can only be chosen manually at closure time.");
|
||||
}
|
||||
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
||||
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
||||
.longGreaterThanZero();
|
||||
|
||||
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
|
||||
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
|
||||
if (transferToSavingsId != null && transferToSavingsId > 0
|
||||
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT)) {
|
||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
|
||||
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
|
||||
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,10 +527,40 @@ public class DepositAccountDataValidator {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
|
||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||
|
||||
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
|
||||
// moment the account matures -- before the customer has actually come in to collect it. For
|
||||
// maturities processed automatically by the scheduled job (not a live counter transaction),
|
||||
// that books cash as disbursed while it is still physically in the vault, so this option is
|
||||
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
||||
"maturity.instruction.withdraw.deposit.not.allowed",
|
||||
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
|
||||
}
|
||||
|
||||
// Partial Reinvest needs a specific kept-amount decided at the moment of closure -- there is
|
||||
// nothing sensible to pre-commit to years in advance as a standing instruction, so it is only
|
||||
// ever chosen manually via the Payment action, never set here.
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue())) {
|
||||
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
|
||||
"maturity.instruction.partial.reinvest.not.allowed",
|
||||
"Partial Re-Invest is not a valid standing maturity instruction -- it can only be chosen manually at closure time.");
|
||||
}
|
||||
|
||||
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
|
||||
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
|
||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
|
||||
.longGreaterThanZero();
|
||||
|
||||
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
|
||||
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
|
||||
if (transferToSavingsId != null && transferToSavingsId > 0
|
||||
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT)) {
|
||||
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
|
||||
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
|
||||
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+83
-1
@@ -82,6 +82,13 @@ public class DepositAccountTransactionDataValidator {
|
||||
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
|
||||
|
||||
private static final Set<String> DEPOSIT_ACCOUNT_PAYMENT_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
||||
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
||||
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName,
|
||||
DepositsApiConstants.reinvestAmountParamName));
|
||||
|
||||
@Autowired
|
||||
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
@@ -222,7 +229,15 @@ public class DepositAccountTransactionDataValidator {
|
||||
|
||||
if (onAccountClosureId != null) {
|
||||
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
if (accountOnClosureType.isTransferToSavings()) {
|
||||
if (accountOnClosureType.isPartialReinvest()) {
|
||||
// Partial Re-Invest needs a kept-amount and a destination account that only the Payment
|
||||
// command collects, and only its handler implements. Neither the FD nor the RD closure
|
||||
// handler has a branch for it, so without this it would fall through to their terminal
|
||||
// "withdraw the whole balance" case and silently pay the deposit out in cash.
|
||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCode(
|
||||
"partial.reinvest.not.allowed",
|
||||
"Partial Re-Invest is only supported through the Payment action, not account closure");
|
||||
} else if (accountOnClosureType.isTransferToSavings()) {
|
||||
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
||||
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
||||
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
|
||||
@@ -247,6 +262,73 @@ public class DepositAccountTransactionDataValidator {
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the FD "Payment" command -- kept independent of {@link #validateClosing}, which stays exactly as-is
|
||||
* for Close/Premature Close, so this never changes either of their existing behavior. Unlike
|
||||
* {@code validateClosing}, reinvest is always allowed here (there is no {@code isPreMatureClose} restriction --
|
||||
* Payment is meant to work on an Active account too), and a new Partial Reinvest closure type needs its own
|
||||
* amount and destination-account rules.
|
||||
*/
|
||||
public void validatePayment(final JsonCommand command) {
|
||||
final String json = command.json();
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_PAYMENT_REQUEST_DATA_PARAMETERS);
|
||||
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
.resource(DepositAccountType.FIXED_DEPOSIT.resourceName());
|
||||
|
||||
final JsonElement element = command.parsedJson();
|
||||
|
||||
final LocalDate closedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
||||
baseDataValidator.reset().parameter(closedOnDateParamName).value(closedOnDate).notNull();
|
||||
|
||||
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
|
||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
|
||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||
|
||||
if (onAccountClosureId != null) {
|
||||
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
if (accountOnClosureType.isWithdarwDeposit()) {
|
||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).failWithCodeAndMessage(
|
||||
"withdraw.deposit.not.allowed.for.payment",
|
||||
"Withdraw Deposit is not supported via Payment -- choose Transfer to Savings or a Reinvest option.");
|
||||
}
|
||||
// Interest earned under Reinvest Principal Only has to leave the deposit somewhere -- the same
|
||||
// destination-account rule Transfer to Savings and Partial Reinvest already need.
|
||||
if (accountOnClosureType.isTransferToSavings() || accountOnClosureType.isPartialReinvest()
|
||||
|| accountOnClosureType.isReinvestPrincipal()) {
|
||||
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
||||
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
||||
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, accountOnClosureType.getCode());
|
||||
}
|
||||
if (accountOnClosureType.isPartialReinvest()) {
|
||||
final BigDecimal reinvestAmount = this.fromApiJsonHelper
|
||||
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.reinvestAmountParamName, element);
|
||||
baseDataValidator.reset().parameter(DepositsApiConstants.reinvestAmountParamName).value(reinvestAmount).notNull()
|
||||
.positiveAmount();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all string payment detail fields for max length
|
||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
||||
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
||||
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
||||
.notExceedingLengthOf(50);
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
//
|
||||
|
||||
+29
-1
@@ -87,6 +87,7 @@ import org.apache.fineract.portfolio.group.exception.CenterNotActiveException;
|
||||
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
|
||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetailAssembler;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||
@@ -97,9 +98,11 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
|
||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -123,6 +126,7 @@ public class DepositAccountAssembler {
|
||||
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
||||
|
||||
@Autowired
|
||||
public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
|
||||
@@ -134,7 +138,8 @@ public class DepositAccountAssembler {
|
||||
final RecurringDepositProductRepository recurringDepositProductRepository,
|
||||
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context,
|
||||
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory,
|
||||
final SavingsAccountAssembler savingsAccountAssembler) {
|
||||
final SavingsAccountAssembler savingsAccountAssembler,
|
||||
final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService) {
|
||||
|
||||
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
|
||||
this.clientRepository = clientRepository;
|
||||
@@ -151,6 +156,7 @@ public class DepositAccountAssembler {
|
||||
this.paymentDetailAssembler = paymentDetailAssembler;
|
||||
this.externalIdFactory = externalIdFactory;
|
||||
this.savingsAccountAssembler = savingsAccountAssembler;
|
||||
this.fdBaseInterestRateReadPlatformService = fdBaseInterestRateReadPlatformService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +328,28 @@ public class DepositAccountAssembler {
|
||||
accountChart = DepositAccountInterestRateChart.from(productChart);
|
||||
}
|
||||
|
||||
// A new FD priced from its product's own chart never carries a senior-citizen premium -- that
|
||||
// concept exists only on the CBS Base Interest Rate master. So whenever staff hasn't picked a
|
||||
// specific chart, price the account from that master's currently active version instead (the same
|
||||
// one the Base Interest Rates screen shows and renewals already use), so a senior-citizen FD shows
|
||||
// its premium from day one rather than only after its first renewal. RD is unaffected: this only
|
||||
// runs when depositAccountType.isFixedDeposit(), and it's a no-op wherever no active base-rate
|
||||
// version exists for the product's FD type, in which case the product's own chart above stands.
|
||||
if (depositAccountType.isFixedDeposit() && accountChart != null && !command.parameterExists(chartIdParamName)
|
||||
&& product instanceof FixedDepositProduct) {
|
||||
final String fdType = ((FixedDepositProduct) product).getFdType();
|
||||
if (fdType != null) {
|
||||
final FdInterestRateEffectiveData activeBaseRates = this.fdBaseInterestRateReadPlatformService
|
||||
.retrieveActiveInterestRatesByType(fdType).stream().findFirst().orElse(null);
|
||||
if (activeBaseRates != null && activeBaseRates.getSlabs() != null && !activeBaseRates.getSlabs().isEmpty()) {
|
||||
final InterestRateChartFields baseChartFields = DepositAccountDomainServiceJpa.baseChartFieldsOf(activeBaseRates);
|
||||
final Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabs = DepositAccountDomainServiceJpa
|
||||
.baseChartSlabsOf(activeBaseRates, product.currency().getCode());
|
||||
accountChart.refreshFromSlabSnapshots(baseChartFields, baseChartSlabs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean withHoldTax = product.withHoldTax();
|
||||
if (command.parameterExists(withHoldTaxParamName)) {
|
||||
withHoldTax = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName);
|
||||
|
||||
+11
@@ -35,6 +35,14 @@ public interface DepositAccountDomainService {
|
||||
SavingsAccountTransaction handleFDDeposit(FixedDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail);
|
||||
|
||||
/**
|
||||
* As above, but lets the caller declare the deposit as an internal account-to-account movement rather
|
||||
* than cash taken over the counter. Used by reinvestment on maturity, where the funds come straight
|
||||
* from the matured deposit and must post against the transfer-suspense GL instead of cash/vault.
|
||||
*/
|
||||
SavingsAccountTransaction handleFDDeposit(FixedDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer);
|
||||
|
||||
SavingsAccountTransaction handleRDDeposit(RecurringDepositAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isRegularTransaction);
|
||||
|
||||
@@ -56,4 +64,7 @@ public interface DepositAccountDomainService {
|
||||
|
||||
Long handleRDAccountPreMatureClosure(RecurringDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
||||
Map<String, Object> changes);
|
||||
|
||||
Long handleFDAccountPayment(FixedDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
||||
Map<String, Object> changes);
|
||||
}
|
||||
|
||||
+629
-24
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.reinvestAmountParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferDescriptionParamName;
|
||||
|
||||
@@ -26,6 +27,9 @@ import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -55,12 +59,18 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
|
||||
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
|
||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartSlabFields;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
|
||||
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
|
||||
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -80,13 +90,28 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
|
||||
private final CalendarInstanceRepository calendarInstanceRepository;
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final FixedDepositRenewalHistoryRepository fixedDepositRenewalHistoryRepository;
|
||||
private final FixedDepositRenewalChartSlabHistoryRepository fixedDepositRenewalChartSlabHistoryRepository;
|
||||
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean applyWithdrawFee, final boolean isRegularTransaction) {
|
||||
boolean isAccountTransfer = false;
|
||||
final boolean isAccountTransfer = false;
|
||||
return handleWithdrawal(account, fmt, transactionDate, transactionAmount, paymentDetail, applyWithdrawFee, isRegularTransaction,
|
||||
isAccountTransfer);
|
||||
}
|
||||
|
||||
/**
|
||||
* As above, but lets the caller declare the withdrawal as an internal account-to-account movement
|
||||
* rather than cash paid out over the counter. Used by reinvestment on maturity, where the balance
|
||||
* moves straight into the new deposit and must post against the transfer-suspense GL, not cash/vault.
|
||||
*/
|
||||
private SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean applyWithdrawFee, final boolean isRegularTransaction, final boolean isAccountTransfer) {
|
||||
boolean isInterestTransfer = false;
|
||||
boolean isWithdrawBalance = false;
|
||||
final boolean backdatedTxnsAllowedTill = false;
|
||||
@@ -100,7 +125,15 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
@Override
|
||||
public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail) {
|
||||
boolean isAccountTransfer = false;
|
||||
final boolean isAccountTransfer = false;
|
||||
return handleFDDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleFDDeposit(final FixedDepositAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer) {
|
||||
boolean isRegularTransaction = false;
|
||||
final boolean backdatedTxnsAllowedTill = false;
|
||||
return this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail,
|
||||
@@ -200,16 +233,26 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
if (onClosureType.isReinvest()) {
|
||||
// Captured before the withdrawal below empties the account, so the renewal history can
|
||||
// record what this term actually matured at.
|
||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||
ExternalId externalId = this.externalIdFactory.create();
|
||||
FixedDepositAccount reinvestedDeposit = account.reInvest(account.getAccountBalance(), externalId);
|
||||
FixedDepositAccount reinvestedDeposit = account.reInvest(maturedBalance, externalId);
|
||||
this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit);
|
||||
reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||
autoGenerateAccountNumber(reinvestedDeposit);
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||
paymentDetail, false, isRegularTransaction);
|
||||
// Renewal is an internal movement into the new deposit, not cash paid out -- post it
|
||||
// against the transfer-suspense GL rather than crediting cash/vault.
|
||||
final boolean isInternalTransferLeg = true;
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, maturedBalance, paymentDetail,
|
||||
false, isRegularTransaction, isInternalTransferLeg);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
// Reached when a user closes the account with a re-invest instruction, rather than the
|
||||
// maturity job acting on a standing instruction.
|
||||
recordRenewal(account, reinvestedDeposit, maturedBalance, maturedBalance, closedDate,
|
||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user);
|
||||
} else if (onClosureType.isTransferToSavings()) {
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
@@ -260,13 +303,58 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
account.setClosedOnDate(closedDate);
|
||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
if (onClosureType.isReinvest()) {
|
||||
BigDecimal reInvestAmount;
|
||||
if (onClosureType.isReinvestPrincipal()) {
|
||||
reInvestAmount = account.getDepositAmount();
|
||||
} else {
|
||||
reInvestAmount = account.getAccountBalance();
|
||||
}
|
||||
if (onClosureType.isReinvestPrincipalAndInterest()) {
|
||||
// Renews on the SAME account, so the customer keeps one account number however many times
|
||||
// the deposit rolls over. Nothing moves: the whole matured balance simply becomes the new
|
||||
// term's principal, so there is no withdrawal, no deposit and no journal entry for the
|
||||
// rollover itself -- which also removes the transfer-suspense round trip the old
|
||||
// close-and-reopen flow needed.
|
||||
final BigDecimal closingPrincipal = account.getDepositAmount();
|
||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||
final BigDecimal interestEarned = maturedBalance == null || closingPrincipal == null ? null
|
||||
: maturedBalance.subtract(closingPrincipal);
|
||||
|
||||
// The active CBS base interest-rate version for this FD type: the same rate card the Base
|
||||
// Interest Rates screen shows and the account-opening wizard prices from. Used for both the
|
||||
// new term's slabs and its senior-citizen premium, so screen and account agree.
|
||||
final FdInterestRateEffectiveData activeBaseRates = account.fdType() == null ? null
|
||||
: this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(account.fdType()).stream().findFirst()
|
||||
.orElse(null);
|
||||
|
||||
final BigDecimal currentSeniorCitizenRate = account.isSeniorCitizen()
|
||||
? seniorCitizenRateFor(activeBaseRates, account.depositPeriod(), account.depositPeriodFrequency())
|
||||
: null;
|
||||
|
||||
// Captured before renewInPlace overwrites the account's chart, so the closing term's full
|
||||
// rate-by-tenure chart (including its senior-citizen premiums) can be archived rather than lost
|
||||
// -- renewInPlace mutates the same chart row every time, it does not create a new one.
|
||||
final String closingChartName = account.currentChartName();
|
||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs = account.currentChartSlabFields();
|
||||
// Likewise captured before renewInPlace overwrites the account's own rate/tenure with the new
|
||||
// term's -- needed only if this is the closing term's first-ever history row (see
|
||||
// recordInPlaceRenewal), so that row records what this term actually ran at instead of blank.
|
||||
final BigDecimal closingInterestRate = account.getNominalAnnualInterestRate();
|
||||
final Integer closingTenure = account.depositPeriod();
|
||||
final Integer closingTenureFrequencyEnum = account.depositPeriodFrequency();
|
||||
|
||||
account.renewInPlace(maturedBalance, closedDate, currentSeniorCitizenRate, baseChartFieldsOf(activeBaseRates),
|
||||
baseChartSlabsOf(activeBaseRates, account.getCurrency().getCode()));
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
recordInPlaceRenewal(account, closingPrincipal, interestEarned, maturedBalance, maturedBalance, closedDate,
|
||||
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user, closingChartName, closingChartSlabs, closingInterestRate,
|
||||
closingTenure, closingTenureFrequencyEnum);
|
||||
changes.put("renewedInPlace", Boolean.TRUE);
|
||||
changes.put("renewalCycleNo", account.renewalCycleNo());
|
||||
|
||||
} else if (onClosureType.isReinvest()) {
|
||||
// "Re-invest principal only" still closes and reopens, because only the principal carries
|
||||
// forward and the interest has to leave the deposit -- which needs a payout destination that
|
||||
// an in-place renewal has nowhere to send. Left on the original flow rather than guessing.
|
||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||
final BigDecimal reInvestAmount = account.getDepositAmount();
|
||||
ExternalId externalId = this.externalIdFactory.create();
|
||||
|
||||
FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
||||
@@ -276,18 +364,26 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
|
||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||
autoGenerateAccountNumber(reinvestedDeposit);
|
||||
// Renewal moves the balance internally into the new deposit -- no cash leaves the branch --
|
||||
// so this leg is posted as an account transfer (against the transfer-suspense GL) rather
|
||||
// than as a counter withdrawal, which would wrongly credit the cash/vault account.
|
||||
final boolean isInternalTransferLeg = true;
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, reInvestAmount, paymentDetail,
|
||||
false, isRegularTransaction);
|
||||
false, isRegularTransaction, isInternalTransferLeg);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
// handleWithdrawal() posts this withdrawal's own journal entries, so it has to be marked as
|
||||
// already accounted for -- exactly as the transfer-to-savings branch below does -- or the
|
||||
// postJournalEntries() call at the end of this method posts it a second time.
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
|
||||
if (onClosureType.isReinvestPrincipalAndInterest()) {
|
||||
account.updateClosedStatus();
|
||||
account.updateOnAccountClosureStatus(onClosureType);
|
||||
}
|
||||
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
||||
reinvestedDeposit.approveAndActivateApplication(closedDate, user);
|
||||
this.savingsAccountRepository.save(reinvestedDeposit);
|
||||
|
||||
// Reached from the maturity job processing the account's standing maturity instruction.
|
||||
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
||||
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user);
|
||||
|
||||
} else if (onClosureType.isTransferToSavings()) {
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
@@ -304,6 +400,7 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||
paymentDetail, false, isRegularTransaction);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
}
|
||||
|
||||
// if(!processMaturityInstructionOnly)
|
||||
@@ -429,6 +526,274 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the closing term's full rate-by-tenure chart -- each slab together with its senior-citizen premium --
|
||||
* against its {@link FixedDepositRenewalHistory} row.
|
||||
* <p>
|
||||
* Slabs missing a rate or a from-period are skipped -- they cannot describe a tenure band. Writes nothing when the
|
||||
* closing term had no chart (e.g. a plain flat-rate deposit), rather than persisting an empty, meaningless archive.
|
||||
*/
|
||||
private void archiveChartSlabs(final Long fdRenewalHistoryId, final String chartName,
|
||||
final List<DepositAccountInterestRateChart.SlabSnapshot> snapshots, final String currencyCode) {
|
||||
if (snapshots == null || snapshots.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (final DepositAccountInterestRateChart.SlabSnapshot snapshot : snapshots) {
|
||||
final InterestRateChartSlabFields fields = snapshot.getFields();
|
||||
if (fields.annualInterestRate() == null || fields.fromPeriod() == null) {
|
||||
continue;
|
||||
}
|
||||
this.fixedDepositRenewalChartSlabHistoryRepository
|
||||
.save(FixedDepositRenewalChartSlabHistory.create(fdRenewalHistoryId, chartName, fields.description(),
|
||||
fields.periodType(), fields.fromPeriod(), fields.toPeriod(), fields.getAmountRangeFrom(),
|
||||
fields.getAmountRangeTo(), fields.annualInterestRate(), currencyCode, snapshot.getSeniorCitizenRate()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chart identity for the account snapshot, so it names the base rate version it was priced from. Package-private
|
||||
* and static (no instance state used) so {@link DepositAccountAssembler} can reuse the exact same mapping when a
|
||||
* new FD account is first priced from this master, instead of duplicating the logic.
|
||||
*/
|
||||
static InterestRateChartFields baseChartFieldsOf(final FdInterestRateEffectiveData baseRates) {
|
||||
// name and from_date are NOT NULL on the embeddable. A base version missing either would abort the
|
||||
// whole renewal (or account creation) with a constraint violation, so treat it as "no identity to
|
||||
// apply" instead: refreshFromSlabSnapshots keeps the chart's existing fields when handed null, and
|
||||
// the slabs -- the part that actually prices the deposit -- still get applied.
|
||||
if (baseRates == null || baseRates.getVersionLabel() == null || baseRates.getEffectiveFromDate() == null) {
|
||||
return null;
|
||||
}
|
||||
final boolean isPrimaryGroupingByAmount = false; // the base rate card bands by tenure, never by amount
|
||||
return InterestRateChartFields.createNew(baseRates.getVersionLabel(), baseRates.getRemarks(), baseRates.getEffectiveFromDate(),
|
||||
baseRates.getEffectiveToDate(), isPrimaryGroupingByAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the base rate card's slabs into the shape an account chart stores, each paired with its senior-citizen
|
||||
* premium so that figure is persisted on the account (and later archived) rather than only ever being looked up live.
|
||||
* <p>
|
||||
* The base master bands purely by tenure, so amount ranges are left unset -- which
|
||||
* {@code InterestRateChartSlabFields.isAmountBetween} then treats as "any amount", exactly the intent. Slabs with no
|
||||
* from-period are dropped, since a slab that does not say where it starts cannot be matched against a tenure.
|
||||
*/
|
||||
static Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabsOf(final FdInterestRateEffectiveData baseRates,
|
||||
final String currencyCode) {
|
||||
if (baseRates == null || baseRates.getSlabs() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<DepositAccountInterestRateChart.SlabSnapshot> snapshots = new ArrayList<>();
|
||||
for (final FdInterestRateSlabData slab : baseRates.getSlabs()) {
|
||||
if (slab.getFromPeriod() == null || slab.getAnnualInterestRate() == null) {
|
||||
continue;
|
||||
}
|
||||
final InterestRateChartSlabFields fields = InterestRateChartSlabFields.createNew(slab.getDescription(),
|
||||
SavingsPeriodFrequencyType.fromInt(slab.getPeriodTypeEnum()), slab.getFromPeriod(), slab.getToPeriod(), null, null,
|
||||
slab.getAnnualInterestRate(), currencyCode);
|
||||
snapshots.add(new DepositAccountInterestRateChart.SlabSnapshot(fields, slab.getSeniorCitizenRate()));
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
/**
|
||||
* The senior-citizen premium from the active base rate card for a given tenure.
|
||||
* <p>
|
||||
* The tenure and the slab bounds are converted to a common unit before comparing, so a rate card published in years
|
||||
* still matches a deposit taken for a number of months. Conversion stays inside a unit family -- days/weeks, or
|
||||
* months/years -- because only those are exact; a days-based card is therefore still never matched against a
|
||||
* months-based tenure, since "1 month" is not a fixed number of days and guessing would land the deposit in the
|
||||
* wrong band. Null when no slab covers the tenure or the matching slab carries no premium, which leaves the
|
||||
* account's existing rate untouched rather than silently dropping the depositor's premium.
|
||||
*/
|
||||
private BigDecimal seniorCitizenRateFor(final FdInterestRateEffectiveData baseRates, final Integer tenure,
|
||||
final Integer tenurePeriodTypeEnum) {
|
||||
if (baseRates == null || baseRates.getSlabs() == null || tenure == null) {
|
||||
return null;
|
||||
}
|
||||
final Integer tenureInBaseUnit = inFamilyBaseUnit(tenure, tenurePeriodTypeEnum);
|
||||
if (tenureInBaseUnit == null) {
|
||||
return null;
|
||||
}
|
||||
for (final FdInterestRateSlabData slab : baseRates.getSlabs()) {
|
||||
if (!sameUnitFamily(tenurePeriodTypeEnum, slab.getPeriodTypeEnum())) {
|
||||
continue;
|
||||
}
|
||||
final Integer slabFrom = inFamilyBaseUnit(slab.getFromPeriod(), slab.getPeriodTypeEnum());
|
||||
if (slabFrom == null || tenureInBaseUnit < slabFrom) {
|
||||
continue;
|
||||
}
|
||||
if (slab.getToPeriod() != null) {
|
||||
final Integer slabTo = inFamilyBaseUnit(slab.getToPeriod(), slab.getPeriodTypeEnum());
|
||||
if (slabTo == null || tenureInBaseUnit > slabTo) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
final BigDecimal rate = slab.getSeniorCitizenRate();
|
||||
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return rate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code value} restated in the finest unit of its own family -- days for DAYS/WEEKS, months for MONTHS/YEARS -- so
|
||||
* two periods written in different but related units can be compared without losing precision. Null for an unknown
|
||||
* unit. Enum codes are {@link SavingsPeriodFrequencyType}: DAYS(0), WEEKS(1), MONTHS(2), YEARS(3).
|
||||
*/
|
||||
private Integer inFamilyBaseUnit(final Integer value, final Integer periodTypeEnum) {
|
||||
if (value == null || periodTypeEnum == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (SavingsPeriodFrequencyType.fromInt(periodTypeEnum)) {
|
||||
case DAYS, MONTHS -> value;
|
||||
case WEEKS -> value * 7;
|
||||
case YEARS -> value * 12;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether two period types convert exactly into one another. Days and weeks do; months and years do. Across those
|
||||
* two families they do not, because a month is not a fixed number of days -- so a days-based rate card is never
|
||||
* matched against a months-based tenure rather than guessing and landing the deposit in the wrong band.
|
||||
*/
|
||||
private boolean sameUnitFamily(final Integer firstPeriodTypeEnum, final Integer secondPeriodTypeEnum) {
|
||||
if (firstPeriodTypeEnum == null || secondPeriodTypeEnum == null) {
|
||||
return false;
|
||||
}
|
||||
return isDayFamily(firstPeriodTypeEnum) == isDayFamily(secondPeriodTypeEnum);
|
||||
}
|
||||
|
||||
private boolean isDayFamily(final Integer periodTypeEnum) {
|
||||
final SavingsPeriodFrequencyType type = SavingsPeriodFrequencyType.fromInt(periodTypeEnum);
|
||||
return type == SavingsPeriodFrequencyType.DAYS || type == SavingsPeriodFrequencyType.WEEKS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a renewal that happened on the SAME account, where there is no second account row to describe the new term.
|
||||
* <p>
|
||||
* Called after {@link FixedDepositAccount#renewInPlace}, which has already advanced the account's cycle number,
|
||||
* principal and maturity date -- so the closing term's figures have to be passed in rather than read back off the
|
||||
* account, where they no longer exist. Every row for one FD shares that FD's single account id and account number;
|
||||
* they are told apart by renewal_no, which is what makes this a one-to-many history of one account.
|
||||
*
|
||||
* @param closingChartName
|
||||
* the name of the chart the closing term was priced on, captured before renewInPlace overwrote it; null if
|
||||
* unavailable
|
||||
* @param closingChartSlabs
|
||||
* the closing term's full rate-by-tenure chart, likewise captured before it was overwritten. Archived
|
||||
* against the closing term's row so its whole rate card remains queryable, not only the single effective
|
||||
* rate {@link FixedDepositRenewalHistory} records.
|
||||
* @param closingInterestRate
|
||||
* the closing term's own {@code nominalAnnualInterestRate}, captured before renewInPlace overwrote it --
|
||||
* needed only when this term has no existing row yet (see below), since the account itself no longer
|
||||
* carries it once renewInPlace has run.
|
||||
* @param closingTenure
|
||||
* the closing term's own deposit period, captured the same way and needed for the same reason.
|
||||
* @param closingTenureFrequencyEnum
|
||||
* the closing term's own deposit period frequency, captured the same way and needed for the same reason.
|
||||
*/
|
||||
private void recordInPlaceRenewal(final FixedDepositAccount account, final BigDecimal closingPrincipal,
|
||||
final BigDecimal closingInterestEarned, final BigDecimal maturedBalance, final BigDecimal renewedAmount,
|
||||
final LocalDate renewedOn, final String renewalType, final AppUser user, final String closingChartName,
|
||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs, final BigDecimal closingInterestRate,
|
||||
final Integer closingTenure, final Integer closingTenureFrequencyEnum) {
|
||||
|
||||
// The CHAIN's number, not this account's own. They differ for an FD that was renewed under the old
|
||||
// close-and-reopen model before renewing in place: its rows are keyed on the original account's
|
||||
// number, so keying new rows on this account's number would split the history in two and re-create
|
||||
// a duplicate row for the term that already has one. fdAccountNumber() falls back to this account's
|
||||
// own number when there is no chain, which is what the read side resolves to as well.
|
||||
final String fdAccountNumber = account.fdAccountNumber();
|
||||
final int newCycleNo = account.renewalCycleNo();
|
||||
final Long createdById = user == null ? null : user.getId();
|
||||
|
||||
final List<FixedDepositRenewalHistory> existingRows = this.fixedDepositRenewalHistoryRepository
|
||||
.findByFdAccountNumberOrderByRenewalNoAsc(fdAccountNumber);
|
||||
FixedDepositRenewalHistory closingRow = existingRows.stream()
|
||||
.filter(row -> Integer.valueOf(newCycleNo - 1).equals(row.getRenewalNo())).findFirst().orElse(null);
|
||||
|
||||
// The term that just closed has no row when this FD renews for the first time since the history
|
||||
// table existed. Written now, from the figures captured before the rollover, so the history
|
||||
// always begins with the term the customer originally opened -- rate and tenure included, rather
|
||||
// than left blank, since the caller captured them before renewInPlace overwrote the account with
|
||||
// the new term's own values.
|
||||
if (closingRow == null) {
|
||||
closingRow = this.fixedDepositRenewalHistoryRepository.saveAndFlush(FixedDepositRenewalHistory.create(fdAccountNumber,
|
||||
account.getId(), account.getId(), null, null, newCycleNo - 1, closingPrincipal, null, maturedBalance,
|
||||
closingPrincipal, closingInterestRate, account.getActivationDate(), renewedOn, closingTenure,
|
||||
closingTenureFrequencyEnum, renewalType, account.getActivationDate(), createdById));
|
||||
}
|
||||
closingRow.recordOutcome(closingInterestEarned, maturedBalance);
|
||||
closingRow.markMatured();
|
||||
this.fixedDepositRenewalHistoryRepository.save(closingRow);
|
||||
|
||||
archiveChartSlabs(closingRow.getId(), closingChartName, closingChartSlabs, account.getCurrency().getCode());
|
||||
|
||||
// The new term's own interest/maturity stay unset until it closes in turn.
|
||||
this.fixedDepositRenewalHistoryRepository.save(FixedDepositRenewalHistory.create(fdAccountNumber, account.getId(),
|
||||
account.getId(), null, closingRow.getId(), newCycleNo, renewedAmount, null, null, renewedAmount,
|
||||
account.getNominalAnnualInterestRate(), renewedOn, account.maturityDate(), account.depositPeriod(),
|
||||
account.depositPeriodFrequency(), renewalType, renewedOn, createdById));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the renewal that started {@code newTerm}, and closes off the previous term's history row.
|
||||
* <p>
|
||||
* Each term of an FD runs as its own deposit instance so it keeps an immutable ledger, which means the rollover event
|
||||
* itself -- what the previous term earned, what carried forward, at what rate, automatically or by whom -- is not
|
||||
* recoverable from the account rows alone. This writes it down explicitly.
|
||||
* <p>
|
||||
* The chain's first term is backfilled here too when it has no row yet: an FD opened before this table existed only
|
||||
* gets discovered at its first renewal, and without its opening row the history would start at "Renewal 1" with no
|
||||
* original.
|
||||
*/
|
||||
private void recordRenewal(final FixedDepositAccount previousTerm, final FixedDepositAccount newTerm,
|
||||
final BigDecimal maturityAmount, final BigDecimal renewedAmount, final LocalDate renewedOn, final String renewalType,
|
||||
final AppUser user) {
|
||||
|
||||
final String fdAccountNumber = previousTerm.fdAccountNumber();
|
||||
final Long originalAccountId = this.fixedDepositRenewalHistoryRepository
|
||||
.findByFdAccountNumberOrderByRenewalNoAsc(fdAccountNumber).stream().findFirst()
|
||||
.map(FixedDepositRenewalHistory::getOriginalSavingsAccountId)
|
||||
.orElseGet(() -> previousTerm.reinvestedFromSavingsId() == null ? previousTerm.getId() : null);
|
||||
|
||||
FixedDepositRenewalHistory previousRow = this.fixedDepositRenewalHistoryRepository
|
||||
.findBySavingsAccountIdOrderByRenewalNoDesc(previousTerm.getId()).stream().findFirst().orElse(null);
|
||||
|
||||
// Backfill the opening term when the chain started before this table existed, so the history
|
||||
// always begins with the account the customer actually opened.
|
||||
if (previousRow == null && originalAccountId != null && originalAccountId.equals(previousTerm.getId())) {
|
||||
previousRow = this.fixedDepositRenewalHistoryRepository.saveAndFlush(FixedDepositRenewalHistory.create(fdAccountNumber,
|
||||
originalAccountId, previousTerm.getId(), null, null, previousTerm.renewalCycleNo(),
|
||||
previousTerm.getDepositAmount(), null, previousTerm.maturityAmount(), previousTerm.getDepositAmount(),
|
||||
previousTerm.getNominalAnnualInterestRate(), previousTerm.getActivationDate(), previousTerm.maturityDate(),
|
||||
previousTerm.depositPeriod(), previousTerm.depositPeriodFrequency(), renewalType, previousTerm.getActivationDate(),
|
||||
user == null ? null : user.getId()));
|
||||
}
|
||||
|
||||
// What the closing term actually earned. maturityAmount is passed in rather than read off the
|
||||
// account because the caller has already withdrawn the balance out of it by this point, which
|
||||
// would make it read as zero. Recorded on the CLOSING term's own row -- every row states its own
|
||||
// term's outcome, so a reader never has to look at the next row to find out how one term did.
|
||||
final BigDecimal previousPrincipal = previousTerm.getDepositAmount();
|
||||
final BigDecimal interestEarned = maturityAmount == null || previousPrincipal == null ? null
|
||||
: maturityAmount.subtract(previousPrincipal);
|
||||
|
||||
if (previousRow != null) {
|
||||
previousRow.recordOutcome(interestEarned, maturityAmount);
|
||||
previousRow.markMatured();
|
||||
this.fixedDepositRenewalHistoryRepository.save(previousRow);
|
||||
}
|
||||
|
||||
// The new term's own interest/maturity stay unset until it closes in turn.
|
||||
this.fixedDepositRenewalHistoryRepository.save(FixedDepositRenewalHistory.create(fdAccountNumber,
|
||||
originalAccountId == null ? previousTerm.getId() : originalAccountId, newTerm.getId(), previousTerm.getId(),
|
||||
previousRow == null ? null : previousRow.getId(), newTerm.renewalCycleNo(), renewedAmount, null, null, renewedAmount,
|
||||
newTerm.getNominalAnnualInterestRate(), renewedOn, newTerm.maturityDate(), newTerm.depositPeriod(),
|
||||
newTerm.depositPeriodFrequency(), renewalType, renewedOn, user == null ? null : user.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Long handleFDAccountPreMatureClosure(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user,
|
||||
@@ -474,6 +839,9 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||
paymentDetail, false, isRegularTransaction);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
// handleWithdrawal() posts this withdrawal's journal entries itself, so mark it as already
|
||||
// accounted for or the postJournalEntries() call below posts it a second time.
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
}
|
||||
|
||||
account.prematureClosure(user, command, changes);
|
||||
@@ -484,6 +852,238 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
return savingsTransactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A staff-triggered, always-available alternative to Premature Close (Active-only, no reinvest) and Close
|
||||
* (Matured-only): works on an FD in either state, and adds a Partial Reinvest option neither of those two support
|
||||
* -- part of the matured amount renews on this same account via {@link FixedDepositAccount#renewInPlace}, the rest
|
||||
* transfers out to a savings account picked at the moment of the request. Full reinvest here also goes through
|
||||
* {@code renewInPlace} rather than the legacy close-and-reopen {@link FixedDepositAccount#reInvest}, which is what
|
||||
* the existing manual Close command still uses today.
|
||||
* <p>
|
||||
* Deliberately independent of {@link #handleFDAccountClosure} and {@link #handleFDAccountPreMatureClosure} --
|
||||
* neither of those two methods is touched, so the existing Close/Premature Close buttons keep behaving exactly as
|
||||
* before.
|
||||
*/
|
||||
@Transactional
|
||||
@Override
|
||||
public Long handleFDAccountPayment(final FixedDepositAccount account, final PaymentDetail paymentDetail, final AppUser user,
|
||||
final JsonCommand command, final Map<String, Object> changes) {
|
||||
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
|
||||
boolean isRegularTransaction = false;
|
||||
boolean isAccountTransfer = false;
|
||||
final boolean isPreMatureClosure = false;
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
final Locale locale = command.extractLocale();
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName);
|
||||
Long savingsTransactionId = null;
|
||||
|
||||
// account.isMatured() only reflects whether the overnight scheduler has already flipped this
|
||||
// account's status -- not whether the customer actually showed up before or after the maturity
|
||||
// date. A customer walking in ON the maturity date (or any day after) is not premature just
|
||||
// because that job hasn't run yet today, so the real test is the date, not the status flag.
|
||||
if (!account.isMatured()) {
|
||||
if (!DateUtils.isBefore(closedDate, account.maturityDate())) {
|
||||
// Genuinely due, the scheduler just hasn't caught up yet -- do exactly what it would:
|
||||
// flip to Matured and post final interest at the full contracted rate, no penalty.
|
||||
account.updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
} else {
|
||||
// The three closing closure types re-check this themselves inside prematureClosure()
|
||||
// below, but the two reinvest branches never call it (they keep the account open on the
|
||||
// same term), so it is enforced once, up front, for every genuinely-premature Payment --
|
||||
// a locked-in FD (e.g. a Tax Saver with its statutory lock-in) cannot be touched early via
|
||||
// this action either.
|
||||
if (account.isLockedOnDate(closedDate)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddeposit.payment.lockin.period.not.elapsed",
|
||||
"This account is still within its lock-in period and cannot be processed before it elapses.", closedDate);
|
||||
}
|
||||
// Same penalty-aware interest calculation prematureClosure() already relies on, so a
|
||||
// genuinely early Payment settles interest exactly as a Premature Close would.
|
||||
account.postPreMaturityInterest(closedDate, true, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
}
|
||||
} else {
|
||||
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
}
|
||||
|
||||
// Same as the closure and maturity paths do: the legacy reinvest branch's reInvest() reads
|
||||
// getClosedOnDate() to pick the new account's chart and submitted date, so it has to be set
|
||||
// before branching. renewInPlace() clears it again for the branches that keep the account open.
|
||||
account.setClosedOnDate(closedDate);
|
||||
|
||||
// Re-read after the block above: updateMaturityStatus() may just have flipped it, so this now
|
||||
// correctly decides close() (needs Matured) vs prematureClosure() (needs Active) either way.
|
||||
final boolean alreadyMatured = account.isMatured();
|
||||
|
||||
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
|
||||
if (onClosureType.isReinvestPrincipalAndInterest() || onClosureType.isPartialReinvest()) {
|
||||
// These two branches keep the account open, so they never reach close()/prematureClosure() and
|
||||
// would otherwise accept any date at all -- including one behind already-posted interest, which
|
||||
// the renewal would then turn into a pivot that reverses and re-posts it.
|
||||
account.validateRenewalDate(closedDate);
|
||||
|
||||
final BigDecimal closingPrincipal = account.getDepositAmount();
|
||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||
final BigDecimal interestEarned = maturedBalance == null || closingPrincipal == null ? null
|
||||
: maturedBalance.subtract(closingPrincipal);
|
||||
|
||||
final BigDecimal renewedAmount;
|
||||
if (onClosureType.isPartialReinvest()) {
|
||||
renewedAmount = command.bigDecimalValueOfParameterNamed(reinvestAmountParamName);
|
||||
if (renewedAmount == null || renewedAmount.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| renewedAmount.compareTo(maturedBalance) >= 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.fixeddeposit.partial.reinvest.amount.invalid",
|
||||
"The reinvest amount must be greater than zero and less than the matured balance.", renewedAmount);
|
||||
}
|
||||
} else {
|
||||
renewedAmount = maturedBalance;
|
||||
}
|
||||
|
||||
final FdInterestRateEffectiveData activeBaseRates = account.fdType() == null ? null
|
||||
: this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(account.fdType()).stream().findFirst()
|
||||
.orElse(null);
|
||||
final BigDecimal currentSeniorCitizenRate = account.isSeniorCitizen()
|
||||
? seniorCitizenRateFor(activeBaseRates, account.depositPeriod(), account.depositPeriodFrequency())
|
||||
: null;
|
||||
|
||||
final String closingChartName = account.currentChartName();
|
||||
final List<DepositAccountInterestRateChart.SlabSnapshot> closingChartSlabs = account.currentChartSlabFields();
|
||||
// Likewise captured before renewInPlace overwrites the account's own rate/tenure with the new
|
||||
// term's -- needed only if this is the closing term's first-ever history row (see
|
||||
// recordInPlaceRenewal), so that row records what this term actually ran at instead of blank.
|
||||
final BigDecimal closingInterestRate = account.getNominalAnnualInterestRate();
|
||||
final Integer closingTenure = account.depositPeriod();
|
||||
final Integer closingTenureFrequencyEnum = account.depositPeriodFrequency();
|
||||
|
||||
account.renewInPlace(renewedAmount, closedDate, currentSeniorCitizenRate, baseChartFieldsOf(activeBaseRates),
|
||||
baseChartSlabsOf(activeBaseRates, account.getCurrency().getCode()));
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
recordInPlaceRenewal(account, closingPrincipal, interestEarned, maturedBalance, renewedAmount, closedDate,
|
||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user, closingChartName, closingChartSlabs, closingInterestRate,
|
||||
closingTenure, closingTenureFrequencyEnum);
|
||||
changes.put("renewedInPlace", Boolean.TRUE);
|
||||
changes.put("renewalCycleNo", account.renewalCycleNo());
|
||||
|
||||
if (onClosureType.isPartialReinvest()) {
|
||||
// The kept portion never left the account -- renewInPlace() just relabels it as the new
|
||||
// term's principal -- so only the remainder is an actual money movement, posted the same
|
||||
// way the Transfer to Savings branch below posts its full-balance transfer.
|
||||
final BigDecimal remainder = maturedBalance.subtract(renewedAmount);
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, remainder, PortfolioAccountType.SAVINGS,
|
||||
PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null, null, null, null,
|
||||
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, account,
|
||||
isAccountTransfer, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
}
|
||||
|
||||
} else if (onClosureType.isReinvestPrincipal()) {
|
||||
// Legacy close-and-reopen, same shape as handleFDAccountClosure()'s isReinvest() branch --
|
||||
// the interest has to leave the deposit, which needs a payout destination an in-place renewal
|
||||
// has nowhere to send.
|
||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||
final BigDecimal reInvestAmount = account.getDepositAmount();
|
||||
final ExternalId externalId = this.externalIdFactory.create();
|
||||
|
||||
final FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
||||
this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit);
|
||||
reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
|
||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||
autoGenerateAccountNumber(reinvestedDeposit);
|
||||
final boolean isInternalTransferLeg = true;
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, reInvestAmount, paymentDetail,
|
||||
false, isRegularTransaction, isInternalTransferLeg);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
|
||||
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
||||
reinvestedDeposit.approveAndActivateApplication(closedDate, user);
|
||||
this.savingsAccountRepository.save(reinvestedDeposit);
|
||||
|
||||
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
||||
FixedDepositRenewalHistory.RENEWAL_TYPE_MANUAL, user);
|
||||
|
||||
// Only the principal moved into the new deposit -- the interest earned on this term is still
|
||||
// sitting in this account's balance. Pay it out to the chosen savings account now, before
|
||||
// closing, rather than stranding it here with nowhere left to go.
|
||||
final BigDecimal interestRemainder = maturedBalance.subtract(reInvestAmount);
|
||||
if (interestRemainder.compareTo(BigDecimal.ZERO) > 0) {
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, interestRemainder,
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null,
|
||||
null, null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
||||
toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
}
|
||||
|
||||
if (alreadyMatured) {
|
||||
account.close(user, command, changes);
|
||||
} else {
|
||||
account.prematureClosure(user, command, changes);
|
||||
}
|
||||
|
||||
} else if (onClosureType.isTransferToSavings()) {
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(),
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null,
|
||||
null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
||||
toSavingsAccount, account, isAccountTransfer, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
|
||||
if (alreadyMatured) {
|
||||
account.close(user, command, changes);
|
||||
} else {
|
||||
account.prematureClosure(user, command, changes);
|
||||
}
|
||||
|
||||
} else {
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||
paymentDetail, false, isRegularTransaction);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
|
||||
if (alreadyMatured) {
|
||||
account.close(user, command, changes);
|
||||
} else {
|
||||
account.prematureClosure(user, command, changes);
|
||||
}
|
||||
}
|
||||
|
||||
this.savingsAccountRepository.save(account);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
|
||||
|
||||
return savingsTransactionId;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Long handleRDAccountPreMatureClosure(final RecurringDepositAccount account, final PaymentDetail paymentDetail,
|
||||
@@ -555,14 +1155,19 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
}
|
||||
|
||||
private void updateAlreadyPostedTransactions(final Set<Long> existingTransactionIds, final SavingsAccount savingsAccount) {
|
||||
List<SavingsAccountTransaction> transactions = savingsAccount.getTransactions();
|
||||
int size = transactions.size();
|
||||
for (int i = size - 1;; i--) {
|
||||
SavingsAccountTransaction transaction = transactions.get(i);
|
||||
if (transaction.isWithdrawal() || transaction.isWithdrawalFee()) {
|
||||
// accountTransfersWritePlatformService.transferFunds() already posts accounting for the
|
||||
// withdrawal it creates on this account (via SavingsAccountDomainServiceJpa#handleWithdrawal),
|
||||
// so that withdrawal must be added to existingTransactionIds here or the caller's own
|
||||
// postJournalEntries() re-derives and re-posts it a second time. The previous implementation
|
||||
// found it by walking the transaction list backwards from the end while the type stayed
|
||||
// withdrawal/withdrawal-fee -- but @OneToMany collections only guarantee @OrderBy ordering on
|
||||
// a fresh fetch, not after an in-memory append, so a same-day interest posting or any other
|
||||
// ordering quirk could put the new withdrawal anywhere in the list and break the walk
|
||||
// silently. Scanning every transaction and checking against the original snapshot is correct
|
||||
// regardless of collection order.
|
||||
for (final SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
|
||||
if ((transaction.isWithdrawal() || transaction.isWithdrawalFee()) && !existingTransactionIds.contains(transaction.getId())) {
|
||||
existingTransactionIds.add(transaction.getId());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+288
-21
@@ -53,6 +53,7 @@ import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.group.domain.Group;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart;
|
||||
import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChartFields;
|
||||
import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
@@ -276,7 +277,9 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
|
||||
public LocalDate calculateMaturityDate() {
|
||||
|
||||
final LocalDate startDate = accountSubmittedOrActivationDate();
|
||||
// The CURRENT term's start, not the account's activation date -- on a renewed FD these differ,
|
||||
// and measuring the term from activation would put maturity in the past.
|
||||
final LocalDate startDate = depositStartDate();
|
||||
LocalDate maturityDate = null;
|
||||
final Integer depositPeriod = this.accountTermAndPreClosure.depositPeriod();
|
||||
switch (this.accountTermAndPreClosure.depositPeriodFrequencyType()) {
|
||||
@@ -312,13 +315,19 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
.fromInt(this.interestCalculationDaysInYearType);
|
||||
List<LocalDate> postedAsOnTransactionDates = getManualPostingDates();
|
||||
|
||||
// Scoped to the current term, so a renewed FD's projection covers only the term now running --
|
||||
// matching what postInterest() will actually post -- rather than replaying the whole account.
|
||||
final List<LocalDateInterval> postingPeriodIntervals = this.savingsHelper.determineInterestPostingPeriods(
|
||||
accountSubmittedOrActivationDate(), maturityDate, postingPeriodType, financialYearBeginningMonth,
|
||||
postedAsOnTransactionDates);
|
||||
depositStartDate(), maturityDate, postingPeriodType, financialYearBeginningMonth, postedAsOnTransactionDates);
|
||||
|
||||
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
|
||||
|
||||
Money periodStartingBalance = Money.zero(currency);
|
||||
// On a first term the opening deposit falls inside the first posting period, so the period can
|
||||
// start from zero and pick the balance up from that transaction. On a renewed term that deposit
|
||||
// is BEFORE the period range and would be missed, leaving the term to earn interest on nothing,
|
||||
// so the term's own principal seeds the opening balance instead.
|
||||
Money periodStartingBalance = isRenewedTerm() ? Money.of(currency, this.accountTermAndPreClosure.depositAmount())
|
||||
: Money.zero(currency);
|
||||
|
||||
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
|
||||
final BigDecimal interestRateAsFraction = getEffectiveInterestRateAsFraction(mc, maturityDate, isPreMatureClosure);
|
||||
@@ -435,7 +444,10 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
|
||||
@Override
|
||||
public Money activateWithBalance() {
|
||||
return Money.of(this.currency, this.accountTermAndPreClosure.depositAmount());
|
||||
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||
// transaction (posted by a clerk) after the account is already active, regardless of the
|
||||
// deposit amount configured on the account's term/pre-closure details.
|
||||
return Money.zero(this.currency);
|
||||
}
|
||||
|
||||
public void close(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges) {
|
||||
@@ -683,6 +695,60 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
return this.accountTermAndPreClosure.maturityAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* The stable customer-facing FD number: the account number of the first account in this renewal chain. Falls back to
|
||||
* this account's own number when it has never been renewed, so callers always get a usable FD number.
|
||||
*/
|
||||
public String fdAccountNumber() {
|
||||
final String carried = this.accountTermAndPreClosure.getFdAccountNumber();
|
||||
return carried != null ? carried : getAccountNumber();
|
||||
}
|
||||
|
||||
/** 0 for the originally opened term, 1 for the first renewal, and so on. */
|
||||
public Integer renewalCycleNo() {
|
||||
return this.accountTermAndPreClosure.getRenewalCycleNo();
|
||||
}
|
||||
|
||||
public Integer depositPeriod() {
|
||||
return this.accountTermAndPreClosure.depositPeriod();
|
||||
}
|
||||
|
||||
public Integer depositPeriodFrequency() {
|
||||
return this.accountTermAndPreClosure.depositPeriodFrequency();
|
||||
}
|
||||
|
||||
public Long reinvestedFromSavingsId() {
|
||||
return this.accountTermAndPreClosure.getReinvestedFromSavingsId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the chart currently priced on this account. Read before a renewal overwrites it, so the closing term's
|
||||
* chart identity can be archived rather than lost.
|
||||
*/
|
||||
public String currentChartName() {
|
||||
return this.chart == null ? null : this.chart.chartFields().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* The slabs of the chart currently priced on this account -- each paired with its senior-citizen premium -- detached
|
||||
* from the chart's own persistence lifecycle.
|
||||
* <p>
|
||||
* Read before a renewal overwrites the account's chart in place ({@link #renewInPlace}), so the closing term's full
|
||||
* rate-by-tenure breakdown (including the premium) can be archived -- not just the single effective rate the renewal
|
||||
* history records -- rather than being silently deleted when the new term's slabs replace it.
|
||||
*/
|
||||
public List<DepositAccountInterestRateChart.SlabSnapshot> currentChartSlabFields() {
|
||||
return this.chart == null ? java.util.Collections.emptyList() : this.chart.currentSlabSnapshots();
|
||||
}
|
||||
|
||||
/**
|
||||
* This deposit's FD type (SINGLE / DOUBLE / SPECIAL), which lives on the product rather than the account. Null when
|
||||
* the product is not a Fixed Deposit product or has no type set.
|
||||
*/
|
||||
public String fdType() {
|
||||
return this.product instanceof FixedDepositProduct ? ((FixedDepositProduct) this.product).getFdType() : null;
|
||||
}
|
||||
|
||||
private Money totalInterestPosted() {
|
||||
Money interestPostedToDate = Money.zero(this.currency);
|
||||
for (final SavingsAccountTransaction transaction : this.transactions) {
|
||||
@@ -712,11 +778,26 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* The start of the term currently running, which is what the deposit's tenure and rate must be measured from.
|
||||
* <p>
|
||||
* For an FD that has never been renewed this is the activation date, exactly as before. A renewal advances
|
||||
* startInterestCalculationDate to the renewal date, so from then on the interest-rate chart slab is chosen by THIS
|
||||
* term's tenure rather than the cumulative tenure since the account was opened, and maturity is measured from the
|
||||
* renewal rather than from the original activation.
|
||||
*/
|
||||
private LocalDate depositStartDate() {
|
||||
// TODO: Support to add deposit start date which can be a date after
|
||||
// account activation date.
|
||||
final LocalDate depositStartDate = accountSubmittedOrActivationDate();
|
||||
return depositStartDate;
|
||||
final LocalDate currentTermStartDate = getStartInterestCalculationDate();
|
||||
return currentTermStartDate != null ? currentTermStartDate : accountSubmittedOrActivationDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* True once this FD has been renewed at least once, i.e. the term now running started later than the account did.
|
||||
* Everything keyed off this keeps its original, pre-renewal behaviour for an FD still on its first term.
|
||||
*/
|
||||
private boolean isRenewedTerm() {
|
||||
final LocalDate termStart = getStartInterestCalculationDate();
|
||||
return termStart != null && getActivationDate() != null && !termStart.equals(getActivationDate());
|
||||
}
|
||||
|
||||
private LocalDate interestCalculatedUpto() {
|
||||
@@ -811,28 +892,142 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
return this.accountTermAndPreClosure.getTransferToSavingsAccountId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls this FD into a new term on the SAME account, so the customer keeps one account number however many times it
|
||||
* renews. No new account is opened and no money moves: the balance simply carries into the next term.
|
||||
* <p>
|
||||
* What changes is the term itself. startInterestCalculationDate advances to the renewal date, which is what confines
|
||||
* interest posting to the new term and thereby leaves the closing term's postings untouched -- they fall outside the
|
||||
* posting periods the engine now generates, so nothing recalculates or reverses them. The term's principal becomes
|
||||
* {@code renewedAmount}, and every interest-mechanics setting plus the rate chart is re-taken from the product as it
|
||||
* stands today, so a renewal is priced on current terms rather than the ones frozen at account opening.
|
||||
* <p>
|
||||
* The caller must have posted the closing term's interest first, and must record the renewal in the FD's renewal
|
||||
* history so each term's own figures remain reportable.
|
||||
*
|
||||
* @param renewedAmount
|
||||
* what carries into the new term: the full maturity amount, or the principal only
|
||||
* @param renewedOn
|
||||
* the renewal date, which becomes the new term's start
|
||||
* @param currentSeniorCitizenRate
|
||||
* the senior-citizen premium the bank publishes on the renewal date for this FD type and tenure, or null to
|
||||
* keep the rate the account already holds. Only applied to an account already flagged senior-citizen; the
|
||||
* renewal never turns that flag on or off.
|
||||
* @param baseChartFields
|
||||
* identity of the CBS base rate version being applied, or null to keep the existing chart's
|
||||
* @param baseChartSlabs
|
||||
* the slabs of the active CBS base interest-rate version, each paired with its senior-citizen premium -- the
|
||||
* rate card staff maintain and see on the Base Interest Rates screen. When empty or null the account falls
|
||||
* back to the product's own chart, so a product with no base rate version still renews on something.
|
||||
*/
|
||||
public void renewInPlace(final BigDecimal renewedAmount, final LocalDate renewedOn, final BigDecimal currentSeniorCitizenRate,
|
||||
final InterestRateChartFields baseChartFields, final Collection<DepositAccountInterestRateChart.SlabSnapshot> baseChartSlabs) {
|
||||
|
||||
final SavingsProduct product = this.product;
|
||||
|
||||
// Confines interest posting to the new term. Set before recomputing maturity, since maturity is
|
||||
// measured from the current term's start.
|
||||
setStartInterestCalculationDate(renewedOn);
|
||||
|
||||
this.accountTermAndPreClosure.updateDepositAmount(renewedAmount);
|
||||
this.accountTermAndPreClosure.setRenewalCycleNo(this.accountTermAndPreClosure.getRenewalCycleNo() + 1);
|
||||
// Pin the chain number now that this account has renewed at least once, so a non-zero cycle number
|
||||
// is never paired with a blank FD number. fdAccountNumber() resolves to the chain's original number
|
||||
// if there is one and to this account's own otherwise, which is the same value every reader derives.
|
||||
this.accountTermAndPreClosure.setFdAccountNumber(fdAccountNumber());
|
||||
|
||||
// Re-price off the CURRENT product, exactly as opening a fresh deposit today would.
|
||||
this.nominalAnnualInterestRate = product.nominalAnnualInterestRate();
|
||||
this.interestCompoundingPeriodType = product.interestCompoundingPeriodType().getValue();
|
||||
this.interestPostingPeriodType = product.interestPostingPeriodType().getValue();
|
||||
this.interestCalculationType = product.interestCalculationType().getValue();
|
||||
this.interestCalculationDaysInYearType = product.interestCalculationDaysInYearType().getValue();
|
||||
this.withHoldTax = product.withHoldTax();
|
||||
|
||||
// Lock-in is re-taken from the product and re-measured from this renewal, exactly as the legacy
|
||||
// close-and-reopen reInvest() does for the account it creates. Left alone, a renewed term would
|
||||
// carry the lock-in window computed at the original activation -- long expired -- so a product with
|
||||
// a statutory lock-in (a Tax Saver, say) would hold the customer for the first term only.
|
||||
final SavingsPeriodFrequencyType renewedLockinPeriodFrequencyType = product.lockinPeriodFrequencyType();
|
||||
this.lockinPeriodFrequency = product.lockinPeriodFrequency();
|
||||
this.lockinPeriodFrequencyType = renewedLockinPeriodFrequencyType == null ? null
|
||||
: renewedLockinPeriodFrequencyType.getValue();
|
||||
this.lockedInUntilDate = this.lockinPeriodFrequencyType == null || this.lockinPeriodFrequency == null ? null
|
||||
: calculateDateAccountIsLockedUntil(renewedOn);
|
||||
|
||||
// The new term is priced off the CBS base interest-rate master -- the rate card staff maintain on
|
||||
// the Base Interest Rates screen, per FD type, with its own active version. That master is what
|
||||
// the bank publishes and what the account-opening wizard already shows, so pricing a renewal off
|
||||
// anything else would put the deposit on rates nobody chose.
|
||||
if (this.chart != null && baseChartSlabs != null && !baseChartSlabs.isEmpty()) {
|
||||
this.chart.refreshFromSlabSnapshots(baseChartFields, baseChartSlabs);
|
||||
} else if (this.chart != null) {
|
||||
// No active base version for this FD type: fall back to the product's own chart so the
|
||||
// renewal still gets current product rates rather than the ones frozen at opening.
|
||||
// activeChart, not applicableChart -- the latter treats a chart with no end date as valid only
|
||||
// up to today's business date, so a renewal dated in the future would find nothing.
|
||||
final InterestRateChart productChart = product instanceof FixedDepositProduct
|
||||
? ((FixedDepositProduct) product).activeChart(renewedOn)
|
||||
: null;
|
||||
if (productChart != null) {
|
||||
this.chart.refreshFrom(productChart);
|
||||
}
|
||||
}
|
||||
|
||||
// The senior-citizen premium is stored per account and takes priority over the chart in
|
||||
// getEffectiveInterestRateAsFraction(), so leaving it alone would price every future term of a
|
||||
// senior-citizen deposit at the rate captured when the account was opened. Only overwritten when
|
||||
// the caller found a currently published rate -- otherwise the existing one stands, so a missing
|
||||
// rate version can never silently drop the depositor's premium.
|
||||
if (isSeniorCitizen() && currentSeniorCitizenRate != null && currentSeniorCitizenRate.compareTo(BigDecimal.ZERO) > 0) {
|
||||
setSeniorCitizenInterestRate(currentSeniorCitizenRate);
|
||||
}
|
||||
|
||||
// The account never left ACTIVE, but updateMaturityStatus() flipped it to MATURED just before
|
||||
// this ran, so the new term has to put it back.
|
||||
this.status = SavingsAccountStatusType.ACTIVE.getValue();
|
||||
setClosedOnDate(null);
|
||||
}
|
||||
|
||||
public FixedDepositAccount reInvest(BigDecimal depositAmount, ExternalId externalId) {
|
||||
|
||||
final DepositAccountTermAndPreClosure newAccountTermAndPreClosure = this.accountTermAndPreClosure.copy(depositAmount);
|
||||
final SavingsProduct product = this.product;
|
||||
final InterestRateChart productChart = product.applicableChart(getClosedOnDate());
|
||||
// Effective-dated pick, same as renewInPlace: an open-ended chart stays valid past today's
|
||||
// business date, and the most recently effective chart wins when more than one covers the date.
|
||||
// Falls back to applicableChart so a product whose charts have no usable from-date still resolves
|
||||
// as it did before.
|
||||
InterestRateChart productChart = product instanceof FixedDepositProduct
|
||||
? ((FixedDepositProduct) product).activeChart(getClosedOnDate())
|
||||
: null;
|
||||
if (productChart == null) {
|
||||
productChart = product.applicableChart(getClosedOnDate());
|
||||
}
|
||||
final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart);
|
||||
|
||||
final AccountType accountType = AccountType.fromInt(this.accountType);
|
||||
final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType);
|
||||
final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType
|
||||
.fromInt(this.interestCompoundingPeriodType);
|
||||
final SavingsInterestCalculationType interestCalculationType = SavingsInterestCalculationType.fromInt(this.interestCalculationType);
|
||||
final SavingsInterestCalculationDaysInYearType daysInYearType = SavingsInterestCalculationDaysInYearType
|
||||
.fromInt(this.interestCalculationDaysInYearType);
|
||||
// Re-price every interest-mechanics setting off the CURRENT product, not this (old) account's
|
||||
// frozen-at-opening snapshot -- same reasoning as interestRate/newChart below. Without this, a
|
||||
// renewed FD kept running on whatever compounding/posting/calculation-type/lock-in/tax setting
|
||||
// was in force when the ORIGINAL account was opened, silently ignoring every product edit made
|
||||
// since (e.g. Daily->Quarterly compounding, or a lock-in added later).
|
||||
final SavingsPostingInterestPeriodType postingPeriodType = product.interestPostingPeriodType();
|
||||
final SavingsCompoundingInterestPeriodType compoundingPeriodType = product.interestCompoundingPeriodType();
|
||||
final SavingsInterestCalculationType interestCalculationType = product.interestCalculationType();
|
||||
final SavingsInterestCalculationDaysInYearType daysInYearType = product.interestCalculationDaysInYearType();
|
||||
final BigDecimal minRequiredOpeningBalance = null;
|
||||
final BigDecimal interestRate = BigDecimal.ZERO;
|
||||
// Falls back to the product's own flat rate -- same fallback DepositAccountAssembler uses
|
||||
// for a brand-new account -- so a product with no chart slab covering this tenure/amount
|
||||
// doesn't silently reinvest the customer at 0%. getEffectiveInterestRateAsFraction() still
|
||||
// re-prices off newChart (the product's CURRENT chart, reflecting any edits made since the
|
||||
// original account was opened) whenever a matching slab exists.
|
||||
final BigDecimal interestRate = product.nominalAnnualInterestRate();
|
||||
final Set<SavingsAccountCharge> savingsAccountCharges = null;
|
||||
final SavingsPeriodFrequencyType lockinPeriodFrequencyType = SavingsPeriodFrequencyType.fromInt(this.lockinPeriodFrequencyType);
|
||||
final Integer lockinPeriodFrequency = this.lockinPeriodFrequency;
|
||||
final SavingsPeriodFrequencyType lockinPeriodFrequencyType = product.lockinPeriodFrequencyType();
|
||||
final Integer lockinPeriodFrequency = product.lockinPeriodFrequency();
|
||||
final boolean withdrawalFeeApplicableForTransfer = false;
|
||||
final String accountNumber = null;
|
||||
final boolean withHoldTax = this.withHoldTax;
|
||||
final boolean withHoldTax = product.withHoldTax();
|
||||
final FixedDepositAccount reInvestedAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product,
|
||||
savingsOfficer, accountNumber, externalId, accountType, getClosedOnDate(), closedBy, interestRate, compoundingPeriodType,
|
||||
postingPeriodType, interestCalculationType, daysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency,
|
||||
@@ -842,6 +1037,30 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
newAccountTermAndPreClosure.updateAccountReference(reInvestedAccount);
|
||||
newChart.updateDepositAccountReference(reInvestedAccount);
|
||||
|
||||
// Carry the senior-citizen premium forward -- without this, a senior-citizen depositor's
|
||||
// reinvested FD silently drops back to the standard rate on the new term.
|
||||
reInvestedAccount.setSeniorCitizen(this.isSeniorCitizen());
|
||||
reInvestedAccount.setSeniorCitizenInterestRate(this.getSeniorCitizenInterestRate());
|
||||
|
||||
// Track which account this was reinvested from, so staff/UI can trace the two accounts to
|
||||
// each other instead of the link disappearing the moment the old one closes.
|
||||
newAccountTermAndPreClosure.setReinvestedFromSavingsId(this.getId());
|
||||
|
||||
// Carry the ORIGINAL account's number forward across every renewal, so the customer keeps
|
||||
// seeing one recognisable FD number even though each term is its own account/ledger row.
|
||||
final String rootFdAccountNumber = this.accountTermAndPreClosure.getFdAccountNumber() != null
|
||||
? this.accountTermAndPreClosure.getFdAccountNumber()
|
||||
: this.getAccountNumber();
|
||||
newAccountTermAndPreClosure.setFdAccountNumber(rootFdAccountNumber);
|
||||
|
||||
// Number this term within the chain, so the UI can label it "Renewal 2 of FD ...048".
|
||||
newAccountTermAndPreClosure.setRenewalCycleNo(this.accountTermAndPreClosure.getRenewalCycleNo() + 1);
|
||||
|
||||
// Without this the renewed deposit has no maturity instruction, so it would mature once more and
|
||||
// then sit idle rather than rolling over again as the customer asked. Done here rather than in
|
||||
// the shared copy(), which Recurring Deposit renewal also uses.
|
||||
newAccountTermAndPreClosure.carryForwardStandingInstructionsFrom(this.accountTermAndPreClosure);
|
||||
|
||||
return reInvestedAccount;
|
||||
|
||||
}
|
||||
@@ -883,7 +1102,9 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
@Override
|
||||
public void loadLazyCollections() {
|
||||
super.loadLazyCollections();
|
||||
this.chart.getId();
|
||||
if (this.chart != null) {
|
||||
this.chart.getId();
|
||||
}
|
||||
}
|
||||
|
||||
public BigDecimal getDepositAmount() {
|
||||
@@ -899,6 +1120,52 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
return SavingsAccountStatusType.MATURED.getValue().equals(this.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the parent class's protected lock-in check to callers outside this class hierarchy. Needed by the
|
||||
* Payment action's domain-service method, which enforces this itself for its reinvest branches rather than going
|
||||
* through {@link #prematureClosure}, since those branches keep the account open on the same term instead of
|
||||
* closing it.
|
||||
*/
|
||||
public boolean isLockedOnDate(final LocalDate transactionDate) {
|
||||
return isAccountLocked(transactionDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* The date guards {@link #close} and {@link #prematureClosure} apply, minus the maturity-date bound, for a Payment
|
||||
* that renews the deposit instead of closing it and so never reaches either of those methods.
|
||||
* <p>
|
||||
* Dropping the maturity bound is the point: Payment is allowed on an already-matured deposit. The other three
|
||||
* matter more here than they do on a closure, because a renewal turns this date into the new term's interest
|
||||
* pivot -- backdate it behind interest that has already been posted and the engine regenerates those posting
|
||||
* periods, reverses the settled postings and re-posts them, which is exactly what the pivot exists to prevent.
|
||||
*/
|
||||
public void validateRenewalDate(final LocalDate renewedOn) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
.resource(FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction);
|
||||
|
||||
if (DateUtils.isAfterBusinessDate(renewedOn)) {
|
||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
||||
.failWithCode("cannot.be.a.future.date");
|
||||
}
|
||||
if (DateUtils.isBefore(renewedOn, getActivationDate())) {
|
||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
||||
.failWithCode("must.be.after.activation.date");
|
||||
}
|
||||
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
|
||||
if (!savingsAccountTransactions.isEmpty()) {
|
||||
final SavingsAccountTransaction lastTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
|
||||
if (lastTransaction.isAfter(renewedOn)) {
|
||||
baseDataValidator.reset().parameter(SavingsApiConstants.closedOnDateParamName).value(renewedOn)
|
||||
.failWithCode("must.be.after.last.transaction.date");
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public void setClosedOnDate(final LocalDate closedOnDate) {
|
||||
this.closedOnDate = closedOnDate;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user