Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c961f713 | ||
|
|
9eac40bb5c | ||
|
|
14b2585cf5 | ||
|
|
a2fa8688ae | ||
|
|
a8ca7dd948 | ||
|
|
c8833367b9 |
+72
@@ -664,6 +664,78 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
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() {
|
public CommandWrapperBuilder createOfficeTransaction() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "OFFICETRANSACTION";
|
this.entityName = "OFFICETRANSACTION";
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -191,6 +191,7 @@ public class ClientApiConstants {
|
|||||||
public static final String constitutionIdParamName = "constitutionId";
|
public static final String constitutionIdParamName = "constitutionId";
|
||||||
public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
|
public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
|
||||||
public static final String authorizedSignatoryParamName = "authorizedSignatory";
|
public static final String authorizedSignatoryParamName = "authorizedSignatory";
|
||||||
|
public static final String authorizedSignatoryClientIdsParamName = "authorizedSignatoryClientIds";
|
||||||
public static final String additionalInfoParamName = "additionalInfo";
|
public static final String additionalInfoParamName = "additionalInfo";
|
||||||
public static final String dateOfRegistrationParamName = "dateOfRegistration";
|
public static final String dateOfRegistrationParamName = "dateOfRegistration";
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
private Boolean alternateDipVerified;
|
private Boolean alternateDipVerified;
|
||||||
private Long alternateDipLookupLogId;
|
private Long alternateDipLookupLogId;
|
||||||
private String alternateDipTimestamp;
|
private String alternateDipTimestamp;
|
||||||
|
/**
|
||||||
|
* Live DIP/MNRL status of the number, resolved from the latest lookup rather than the flag
|
||||||
|
* captured at onboarding. A customer cleared at onboarding whose number is later reported or
|
||||||
|
* reassigned would otherwise keep showing "Verified" forever.
|
||||||
|
*/
|
||||||
|
private String dipLiveStatus;
|
||||||
|
private String alternateDipLiveStatus;
|
||||||
|
/**
|
||||||
|
* Disposition of the client's most recent flagged sanctions alert, or null when they carry
|
||||||
|
* none. Like the DIP status this is resolved live: nameVerified is captured at onboarding and
|
||||||
|
* would keep reading "Clear" after the client was added to a later list snapshot.
|
||||||
|
*/
|
||||||
|
private String sanctionsAlertStatus;
|
||||||
private Boolean nameVerified;
|
private Boolean nameVerified;
|
||||||
private Long screeningLogId;
|
private Long screeningLogId;
|
||||||
private Boolean isStaff;
|
private Boolean isStaff;
|
||||||
@@ -436,6 +449,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
@@ -498,6 +516,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
@@ -550,6 +573,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+14
@@ -34,6 +34,8 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
private final CodeValueData mainBusinessLine;
|
private final CodeValueData mainBusinessLine;
|
||||||
private final String remarks;
|
private final String remarks;
|
||||||
private final String authorizedSignatory;
|
private final String authorizedSignatory;
|
||||||
|
/** Client ids behind {@link #authorizedSignatory}, comma separated and in the same order. */
|
||||||
|
private final String authorizedSignatoryClientIds;
|
||||||
private final String additionalInfo;
|
private final String additionalInfo;
|
||||||
private final LocalDate dateOfRegistration;
|
private final LocalDate dateOfRegistration;
|
||||||
|
|
||||||
@@ -62,13 +64,24 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
this.constitution = null;
|
this.constitution = null;
|
||||||
this.mainBusinessLine = null;
|
this.mainBusinessLine = null;
|
||||||
this.authorizedSignatory = null;
|
this.authorizedSignatory = null;
|
||||||
|
this.authorizedSignatoryClientIds = null;
|
||||||
this.additionalInfo = null;
|
this.additionalInfo = null;
|
||||||
this.dateOfRegistration = null;
|
this.dateOfRegistration = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kept for the list/search mappers, whose SQL does not select the signatory client ids.
|
||||||
|
*/
|
||||||
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
||||||
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
||||||
LocalDate dateOfRegistration) {
|
LocalDate dateOfRegistration) {
|
||||||
|
this(constitution, incorpNo, incorpValidityTillDate, mainBusinessLine, remarks, authorizedSignatory, additionalInfo,
|
||||||
|
dateOfRegistration, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
||||||
|
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
||||||
|
LocalDate dateOfRegistration, String authorizedSignatoryClientIds) {
|
||||||
|
|
||||||
this.constitution = constitution;
|
this.constitution = constitution;
|
||||||
this.incorpNumber = incorpNo;
|
this.incorpNumber = incorpNo;
|
||||||
@@ -76,6 +89,7 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
this.mainBusinessLine = mainBusinessLine;
|
this.mainBusinessLine = mainBusinessLine;
|
||||||
this.remarks = remarks;
|
this.remarks = remarks;
|
||||||
this.authorizedSignatory = authorizedSignatory;
|
this.authorizedSignatory = authorizedSignatory;
|
||||||
|
this.authorizedSignatoryClientIds = authorizedSignatoryClientIds;
|
||||||
this.additionalInfo = additionalInfo;
|
this.additionalInfo = additionalInfo;
|
||||||
this.dateOfRegistration = dateOfRegistration;
|
this.dateOfRegistration = dateOfRegistration;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,17 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "alternate_dip_timestamp")
|
@Column(name = "alternate_dip_timestamp")
|
||||||
private String alternateDipTimestamp;
|
private String alternateDipTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DIP/MNRL status the number actually returned — CLEAN, SUSPECTED, DISCONNECTED or
|
||||||
|
* SERVICE_UNAVAILABLE. dipVerified alone cannot tell "checked and clean" apart from "the DoT
|
||||||
|
* service was down", which previously stored both as verified.
|
||||||
|
*/
|
||||||
|
@Column(name = "dip_status", length = 40)
|
||||||
|
private String dipStatus;
|
||||||
|
|
||||||
|
@Column(name = "alternate_dip_status", length = 40)
|
||||||
|
private String alternateDipStatus;
|
||||||
|
|
||||||
@Column(name = "name_verified")
|
@Column(name = "name_verified")
|
||||||
private Boolean nameVerified;
|
private Boolean nameVerified;
|
||||||
|
|
||||||
|
|||||||
+52
@@ -25,11 +25,15 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.Transient;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
@@ -61,6 +65,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@JoinColumn(name = "client_id", nullable = false)
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
private Client client;
|
private Client client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scratch record of what the in-flight {@link #update} replaced, read back via
|
||||||
|
* {@link #getFieldsOverwrittenByLastUpdate()}. Never persisted and never read outside the
|
||||||
|
* transaction that called update(), which clears it first.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
private final transient Set<String> fieldsOverwrittenByLastUpdate = new LinkedHashSet<>();
|
||||||
|
|
||||||
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
|
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
|
||||||
@Column(name = "kyc_status", nullable = false)
|
@Column(name = "kyc_status", nullable = false)
|
||||||
private Integer kycStatus;
|
private Integer kycStatus;
|
||||||
@@ -440,11 +452,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
|
|
||||||
public Map<String, Object> update(final JsonCommand command) {
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||||
|
this.fieldsOverwrittenByLastUpdate.clear();
|
||||||
|
|
||||||
final String panNumberParam = "panNumber";
|
final String panNumberParam = "panNumber";
|
||||||
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
|
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
|
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
|
||||||
actualChanges.put(panNumberParam, newValue);
|
actualChanges.put(panNumberParam, newValue);
|
||||||
|
recordOverwrite(panNumberParam, this.panNumber);
|
||||||
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
|
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +466,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
|
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
|
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
|
||||||
actualChanges.put(aadhaarParam, newValue);
|
actualChanges.put(aadhaarParam, newValue);
|
||||||
|
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
|
||||||
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +474,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
|
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(gstinParam);
|
final String newValue = command.stringValueOfParameterNamed(gstinParam);
|
||||||
actualChanges.put(gstinParam, newValue);
|
actualChanges.put(gstinParam, newValue);
|
||||||
|
recordOverwrite(gstinParam, this.gstinNumber);
|
||||||
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
|
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,9 +482,12 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
|
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
|
||||||
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
|
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
|
||||||
actualChanges.put(riskParam, newValue);
|
actualChanges.put(riskParam, newValue);
|
||||||
|
recordOverwrite(riskParam, this.riskCategory);
|
||||||
this.riskCategory = newValue;
|
this.riskCategory = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remarks is the audit justification itself, so a change to it is deliberately not recorded
|
||||||
|
// as an overwrite — requiring remarks in order to write remarks would be circular.
|
||||||
final String remarksParam = "remarks";
|
final String remarksParam = "remarks";
|
||||||
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
|
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(remarksParam);
|
final String newValue = command.stringValueOfParameterNamed(remarksParam);
|
||||||
@@ -551,6 +570,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(param, current)) {
|
if (command.isChangeInStringParameterNamed(param, current)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(param);
|
final String newValue = command.stringValueOfParameterNamed(param);
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
|
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,6 +580,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInLocalDateParameterNamed(param, current)) {
|
if (command.isChangeInLocalDateParameterNamed(param, current)) {
|
||||||
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
|
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(newValue);
|
setter.accept(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,6 +591,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
|
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
|
||||||
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
|
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(newValue);
|
setter.accept(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,10 +602,40 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInBigDecimalParameterNamed(param, current)) {
|
if (command.isChangeInBigDecimalParameterNamed(param, current)) {
|
||||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(param);
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(param);
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(newValue);
|
setter.accept(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notes that {@code param} replaced a value the record already held. A field going from empty to
|
||||||
|
* populated is the record being completed rather than altered, so it is deliberately not
|
||||||
|
* recorded — see {@link #getFieldsOverwrittenByLastUpdate()}.
|
||||||
|
*/
|
||||||
|
private void recordOverwrite(final String param, final Object currentValue) {
|
||||||
|
final boolean wasPopulated = currentValue instanceof String text
|
||||||
|
? StringUtils.isNotBlank(text)
|
||||||
|
: currentValue != null;
|
||||||
|
if (wasPopulated) {
|
||||||
|
this.fieldsOverwrittenByLastUpdate.add(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters whose already-populated value the most recent {@link #update} replaced.
|
||||||
|
*
|
||||||
|
* <p>Distinguishes altering a record from finishing one: a non-individual KYC auto-verifies as
|
||||||
|
* soon as PAN and GSTIN pass, which happens part-way through the create wizard, and the officer
|
||||||
|
* still has to save the KYC document details afterwards. Those fields are empty at that point,
|
||||||
|
* so filling them in leaves this set empty and no audit justification is demanded — while
|
||||||
|
* changing a document number that was already on the verified record does populate it.
|
||||||
|
*
|
||||||
|
* <p>Reset at the start of every {@link #update} and never persisted: it describes one call.
|
||||||
|
*/
|
||||||
|
public Set<String> getFieldsOverwrittenByLastUpdate() {
|
||||||
|
return Collections.unmodifiableSet(this.fieldsOverwrittenByLastUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
/** Called when documents are submitted and 3rd-party verification is initiated. */
|
/** Called when documents are submitted and 3rd-party verification is initiated. */
|
||||||
public void markInProgress() {
|
public void markInProgress() {
|
||||||
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
|
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,569 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -65,6 +65,29 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
@Column(name = "looked_up_on", nullable = false)
|
@Column(name = "looked_up_on", nullable = false)
|
||||||
private OffsetDateTime lookedUpOn;
|
private OffsetDateTime lookedUpOn;
|
||||||
|
|
||||||
|
// ── Officer decision on a non-clean result ────────────────────────────────
|
||||||
|
// A DIP hit is a fraud-risk signal rather than a legal bar, so onboarding may continue — but
|
||||||
|
// the officer has to say why in writing, and that reasoning is kept with the lookup.
|
||||||
|
|
||||||
|
/** PROCEEDED | REJECTED. Null while no decision is required or none has been taken. */
|
||||||
|
@Column(name = "override_decision", length = 20)
|
||||||
|
private String overrideDecision;
|
||||||
|
|
||||||
|
@Column(name = "override_reason")
|
||||||
|
private String overrideReason;
|
||||||
|
|
||||||
|
@Column(name = "decided_by")
|
||||||
|
private Long decidedBy;
|
||||||
|
|
||||||
|
@Column(name = "decided_on")
|
||||||
|
private OffsetDateTime decidedOn;
|
||||||
|
|
||||||
|
public static final String STATUS_CLEAN = "CLEAN";
|
||||||
|
public static final String STATUS_ERROR = "ERROR";
|
||||||
|
public static final String STATUS_SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE";
|
||||||
|
public static final String DECISION_PROCEEDED = "PROCEEDED";
|
||||||
|
public static final String DECISION_REJECTED = "REJECTED";
|
||||||
|
|
||||||
protected MobileLookupLog() {}
|
protected MobileLookupLog() {}
|
||||||
|
|
||||||
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
|
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
|
||||||
@@ -81,4 +104,27 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
|
this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void recordDecision(final String decision, final String reason, final Long userId) {
|
||||||
|
this.overrideDecision = decision;
|
||||||
|
this.overrideReason = reason;
|
||||||
|
this.decidedBy = userId;
|
||||||
|
this.decidedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the lookup produced something the officer must consciously decide about. */
|
||||||
|
public boolean requiresDecision() {
|
||||||
|
return this.suspected || this.disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number may be used only when it came back clean, or when an officer explicitly chose to
|
||||||
|
* proceed. An unreachable DoT service is NOT a pass — it means nothing was checked.
|
||||||
|
*/
|
||||||
|
public boolean isUsable() {
|
||||||
|
if (STATUS_CLEAN.equals(this.lookupStatus)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return DECISION_PROCEEDED.equals(this.overrideDecision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -19,6 +19,8 @@
|
|||||||
package org.apache.fineract.portfolio.mobilelookup.domain;
|
package org.apache.fineract.portfolio.mobilelookup.domain;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -35,4 +37,29 @@ public interface MobileLookupLogRepository extends JpaRepository<MobileLookupLog
|
|||||||
|
|
||||||
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
|
||||||
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
|
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
|
||||||
|
|
||||||
|
/** Non-clean lookups still waiting for an officer's decision — the DIP review queue. */
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE (l.suspected = true OR l.disconnected = true) "
|
||||||
|
+ "AND l.overrideDecision IS NULL ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findPendingDecisions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most recent lookup for a number, used by the server-side gate to confirm the browser's
|
||||||
|
* claim that a number was verified.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Paged rather than a JPQL {@code LIMIT}, which EclipseLink does not accept — the caller
|
||||||
|
* takes the first row.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findLatestByMobileNumber(@Param("mobileNumber") String mobileNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earlier rejections of the same number, on any client. A number turned away once is a
|
||||||
|
* standing signal: without this the next officer screens it fresh and sees no history.
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber "
|
||||||
|
+ "AND l.overrideDecision = 'REJECTED' ORDER BY l.decidedOn DESC")
|
||||||
|
List<MobileLookupLog> findRejections(@Param("mobileNumber") String mobileNumber, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
+139
@@ -61,6 +61,65 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
@Column(name = "screened_on", nullable = false)
|
@Column(name = "screened_on", nullable = false)
|
||||||
private OffsetDateTime screenedOn;
|
private OffsetDateTime screenedOn;
|
||||||
|
|
||||||
|
// ── Alert disposition ─────────────────────────────────────────────────────
|
||||||
|
// A FLAGGED screening opens a case. It stays PENDING_REVIEW until an officer either
|
||||||
|
// documents it as a different person (FALSE_POSITIVE, effective only once the Principal
|
||||||
|
// Officer approves) or confirms it (CONFIRMED_MATCH, which freezes and gets reported).
|
||||||
|
|
||||||
|
@Column(name = "disposition", length = 30)
|
||||||
|
private String disposition;
|
||||||
|
|
||||||
|
@Column(name = "disposition_reason")
|
||||||
|
private String dispositionReason;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_by")
|
||||||
|
private Long reviewedBy;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_on")
|
||||||
|
private OffsetDateTime reviewedOn;
|
||||||
|
|
||||||
|
@Column(name = "approved_by")
|
||||||
|
private Long approvedBy;
|
||||||
|
|
||||||
|
@Column(name = "approved_on")
|
||||||
|
private OffsetDateTime approvedOn;
|
||||||
|
|
||||||
|
@Column(name = "fiu_str_reference", length = 100)
|
||||||
|
private String fiuStrReference;
|
||||||
|
|
||||||
|
@Column(name = "nodal_officer_informed_on")
|
||||||
|
private OffsetDateTime nodalOfficerInformedOn;
|
||||||
|
|
||||||
|
@Column(name = "frozen_account_ids", length = 1000)
|
||||||
|
private String frozenAccountIds;
|
||||||
|
|
||||||
|
@Column(name = "frozen_on")
|
||||||
|
private OffsetDateTime frozenOn;
|
||||||
|
|
||||||
|
// ── STR tracking ──────────────────────────────────────────────────────────
|
||||||
|
// A confirmed match is reportable to FIU-IND. Tracked here rather than as an AML alert: an
|
||||||
|
// AML alert is built around a monetary transaction, whereas this is triggered by who the
|
||||||
|
// customer is, so the two are surfaced as separate streams on the FIU screen.
|
||||||
|
|
||||||
|
/** NULL until approval; then PENDING_FILING → FILED. */
|
||||||
|
@Column(name = "str_status", length = 30)
|
||||||
|
private String strStatus;
|
||||||
|
|
||||||
|
@Column(name = "str_due_date")
|
||||||
|
private java.time.LocalDate strDueDate;
|
||||||
|
|
||||||
|
@Column(name = "str_raised_on")
|
||||||
|
private OffsetDateTime strRaisedOn;
|
||||||
|
|
||||||
|
public static final String STR_PENDING_FILING = "PENDING_FILING";
|
||||||
|
public static final String STR_FILED = "FILED";
|
||||||
|
|
||||||
|
public static final String RESULT_FLAGGED = "FLAGGED";
|
||||||
|
public static final String RESULT_CLEAR = "CLEAR";
|
||||||
|
public static final String DISPOSITION_PENDING = "PENDING_REVIEW";
|
||||||
|
public static final String DISPOSITION_FALSE_POSITIVE = "FALSE_POSITIVE";
|
||||||
|
public static final String DISPOSITION_CONFIRMED = "CONFIRMED_MATCH";
|
||||||
|
|
||||||
protected SanctionsScreeningLog() {}
|
protected SanctionsScreeningLog() {}
|
||||||
|
|
||||||
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
|
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
|
||||||
@@ -75,8 +134,75 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.ipAddress = ipAddress;
|
this.ipAddress = ipAddress;
|
||||||
this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
|
this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
// A hit opens a case; a clear screening has no case to close.
|
||||||
|
this.disposition = RESULT_FLAGGED.equals(screeningResult) ? DISPOSITION_PENDING : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Alert lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the officer's assessment of the hit. Deliberately does NOT clear the block: a
|
||||||
|
* false positive only takes effect once {@link #approve(Long)} is called by the Principal
|
||||||
|
* Officer, so the person who raised it cannot wave their own match through.
|
||||||
|
*/
|
||||||
|
public void recordDisposition(final String newDisposition, final String reason, final Long userId) {
|
||||||
|
this.disposition = newDisposition;
|
||||||
|
this.dispositionReason = reason;
|
||||||
|
this.reviewedBy = userId;
|
||||||
|
this.reviewedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
// Re-opening a previously approved alert must drop the old approval with it.
|
||||||
|
this.approvedBy = null;
|
||||||
|
this.approvedOn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final Long approverId) {
|
||||||
|
this.approvedBy = approverId;
|
||||||
|
this.approvedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records the FIU-IND STR reference and the date the nodal officer was informed. */
|
||||||
|
public void recordReporting(final String strReference, final OffsetDateTime nodalOfficerInformedOn) {
|
||||||
|
if (strReference != null) {
|
||||||
|
this.fiuStrReference = strReference;
|
||||||
|
// Recording the FIU-IND acknowledgement is what closes the filing obligation, so the
|
||||||
|
// case stops appearing as outstanding on the FIU screen.
|
||||||
|
if (!strReference.isBlank()) {
|
||||||
|
this.strStatus = STR_FILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodalOfficerInformedOn != null) {
|
||||||
|
this.nodalOfficerInformedOn = nodalOfficerInformedOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordFreeze(final String accountIds) {
|
||||||
|
this.frozenAccountIds = accountIds;
|
||||||
|
this.frozenOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the STR obligation on approval of a confirmed match. PMLA allows seven working days
|
||||||
|
* from forming the suspicion; seven calendar days is the stricter reading.
|
||||||
|
*/
|
||||||
|
public void raiseStr() {
|
||||||
|
if (this.strStatus != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.strStatus = STR_PENDING_FILING;
|
||||||
|
this.strRaisedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
this.strDueDate = DateUtils.getBusinessLocalDate().plusDays(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStrPending() { return STR_PENDING_FILING.equals(this.strStatus); }
|
||||||
|
|
||||||
|
public boolean isFlagged() { return RESULT_FLAGGED.equals(this.screeningResult); }
|
||||||
|
|
||||||
|
public boolean isApprovedFalsePositive() {
|
||||||
|
return DISPOSITION_FALSE_POSITIVE.equals(this.disposition) && this.approvedBy != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfirmedMatch() { return DISPOSITION_CONFIRMED.equals(this.disposition); }
|
||||||
|
|
||||||
// ── Accessors ─────────────────────────────────────────────────────────────
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public String getScreenedName() { return screenedName; }
|
public String getScreenedName() { return screenedName; }
|
||||||
@@ -89,4 +215,17 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
public void setClientId(Long clientId) { this.clientId = clientId; }
|
public void setClientId(Long clientId) { this.clientId = clientId; }
|
||||||
public String getIpAddress() { return ipAddress; }
|
public String getIpAddress() { return ipAddress; }
|
||||||
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
||||||
|
public String getDisposition() { return disposition; }
|
||||||
|
public String getDispositionReason() { return dispositionReason; }
|
||||||
|
public Long getReviewedBy() { return reviewedBy; }
|
||||||
|
public OffsetDateTime getReviewedOn() { return reviewedOn; }
|
||||||
|
public Long getApprovedBy() { return approvedBy; }
|
||||||
|
public OffsetDateTime getApprovedOn() { return approvedOn; }
|
||||||
|
public String getFiuStrReference() { return fiuStrReference; }
|
||||||
|
public OffsetDateTime getNodalOfficerInformedOn() { return nodalOfficerInformedOn; }
|
||||||
|
public String getFrozenAccountIds() { return frozenAccountIds; }
|
||||||
|
public OffsetDateTime getFrozenOn() { return frozenOn; }
|
||||||
|
public String getStrStatus() { return strStatus; }
|
||||||
|
public java.time.LocalDate getStrDueDate() { return strDueDate; }
|
||||||
|
public OffsetDateTime getStrRaisedOn() { return strRaisedOn; }
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -39,4 +39,32 @@ public interface SanctionsScreeningLogRepository extends JpaRepository<Sanctions
|
|||||||
|
|
||||||
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
|
||||||
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
|
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition = :disposition ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findByDisposition(@Param("disposition") String disposition);
|
||||||
|
|
||||||
|
/** Every open alert — the Principal Officer's review queue. */
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition IS NOT NULL ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findAllAlerts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A name is only cleared to onboard once a false positive on that exact name has been
|
||||||
|
* approved by the Principal Officer — the maker's own assessment is not enough.
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(l) FROM SanctionsScreeningLog l WHERE LOWER(TRIM(l.screenedName)) = LOWER(TRIM(:name)) "
|
||||||
|
+ "AND l.disposition = 'FALSE_POSITIVE' AND l.approvedBy IS NOT NULL")
|
||||||
|
long countApprovedFalsePositives(@Param("name") String name);
|
||||||
|
|
||||||
|
/** Latest alert for a client, used to decide whether their accounts must stay frozen. */
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.clientId = :clientId AND l.screeningResult = 'FLAGGED' "
|
||||||
|
+ "ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findFlaggedByClientId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmed matches carrying an STR obligation — the sanctions stream on the FIU-IND screen.
|
||||||
|
* Pass null for every STR regardless of whether it has been filed.
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.strStatus IS NOT NULL "
|
||||||
|
+ "AND (:status IS NULL OR l.strStatus = :status) ORDER BY l.strDueDate ASC, l.strRaisedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findStrCases(@Param("status") String status);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -275,6 +275,7 @@ public class SavingsApiConstants {
|
|||||||
// Additional custom fields
|
// Additional custom fields
|
||||||
public static final String balanceFrequencyParamName = "balanceFrequency";
|
public static final String balanceFrequencyParamName = "balanceFrequency";
|
||||||
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
||||||
|
public static final String productCategoryParamName = "productCategory";
|
||||||
|
|
||||||
// Current Product — new UI fields
|
// Current Product — new UI fields
|
||||||
public static final String balanceSwapOptionParamName = "balanceSwapOption";
|
public static final String balanceSwapOptionParamName = "balanceSwapOption";
|
||||||
|
|||||||
+1
@@ -144,6 +144,7 @@ public final class SavingsProductData implements Serializable {
|
|||||||
@Getter @Setter private Boolean enableLockinPeriod;
|
@Getter @Setter private Boolean enableLockinPeriod;
|
||||||
@Getter @Setter private String balanceFrequency;
|
@Getter @Setter private String balanceFrequency;
|
||||||
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
||||||
|
@Getter @Setter private String productCategory;
|
||||||
|
|
||||||
// New current product fields
|
// New current product fields
|
||||||
@Getter @Setter private Boolean balanceSwapOption;
|
@Getter @Setter private Boolean balanceSwapOption;
|
||||||
|
|||||||
+10
-1
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.contentstore.policy;
|
package org.apache.fineract.infrastructure.contentstore.policy;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.Strings;
|
import org.apache.commons.lang3.Strings;
|
||||||
@@ -31,6 +32,10 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
public class MimeContentPolicy implements ContentPolicy {
|
public class MimeContentPolicy implements ContentPolicy {
|
||||||
|
|
||||||
|
// CSV has no content-level magic bytes, so byte-sniffing a delimited text file legitimately
|
||||||
|
// detects it as text/plain even though it was uploaded (and named) as text/csv.
|
||||||
|
private static final Set<String> INTERCHANGEABLE_TEXT_MIME_TYPES = Set.of("text/csv", "text/plain");
|
||||||
|
|
||||||
private final ContentDetectorManager contentDetectorManager;
|
private final ContentDetectorManager contentDetectorManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,7 +44,11 @@ public class MimeContentPolicy implements ContentPolicy {
|
|||||||
var result = contentDetectorManager
|
var result = contentDetectorManager
|
||||||
.detect(ContentDetectorContext.builder().inputStream(ctx.getInputStream()).inputStreamEnabled(true).build());
|
.detect(ContentDetectorContext.builder().inputStream(ctx.getInputStream()).inputStreamEnabled(true).build());
|
||||||
|
|
||||||
if (!Strings.CI.equals(result.getMimeType(), ctx.getMimeType())) {
|
boolean matches = Strings.CI.equals(result.getMimeType(), ctx.getMimeType())
|
||||||
|
|| (INTERCHANGEABLE_TEXT_MIME_TYPES.contains(result.getMimeType())
|
||||||
|
&& INTERCHANGEABLE_TEXT_MIME_TYPES.contains(ctx.getMimeType()));
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
throw new ContentPolicyException(String.format("Detected file type (%s), but mime type (%s) was provided. Mismatch!",
|
throw new ContentPolicyException(String.format("Detected file type (%s), but mime type (%s) was provided. Mismatch!",
|
||||||
result.getMimeType(), ctx.getMimeType()));
|
result.getMimeType(), ctx.getMimeType()));
|
||||||
}
|
}
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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.infrastructure.databaseexport.api;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
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 jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.databaseexport.service.DatabaseExportService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/databaseexport")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Database Export", description = "Approval-gated full export of the tenant database.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseExportApiResource {
|
||||||
|
|
||||||
|
private final DatabaseExportService service;
|
||||||
|
private final FromJsonHelper fromJsonHelper;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List export requests, optionally filtered by status")
|
||||||
|
public List<Map<String, Object>> retrieveAll(@QueryParam("status") @Parameter(description = "status") final String status) {
|
||||||
|
return this.service.retrieveAll(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{requestId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a single export request")
|
||||||
|
public Map<String, Object> retrieveOne(@PathParam("requestId") final Long requestId) {
|
||||||
|
return this.service.retrieveOne(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Raise a request for a full database export")
|
||||||
|
public Map<String, Object> requestExport(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final JsonElement element = this.fromJsonHelper.parse(apiRequestBodyAsJson);
|
||||||
|
final String reason = this.fromJsonHelper.extractStringNamed("reason", element);
|
||||||
|
final Long id = this.service.requestExport(reason);
|
||||||
|
return Map.of("resourceId", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{requestId}/approve")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Approve an export request (admin only, never the requester)")
|
||||||
|
public Map<String, Object> approve(@PathParam("requestId") final Long requestId) {
|
||||||
|
this.service.approve(requestId);
|
||||||
|
return Map.of("resourceId", requestId, "status", "APPROVED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{requestId}/reject")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Reject an export request with a reason")
|
||||||
|
public Map<String, Object> reject(@PathParam("requestId") final Long requestId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final JsonElement element = this.fromJsonHelper.parse(apiRequestBodyAsJson);
|
||||||
|
final JsonObject json = element == null ? new JsonObject() : element.getAsJsonObject();
|
||||||
|
final String reason = json.has("rejectionReason") && !json.get("rejectionReason").isJsonNull()
|
||||||
|
? json.get("rejectionReason").getAsString()
|
||||||
|
: null;
|
||||||
|
this.service.reject(requestId, reason);
|
||||||
|
return Map.of("resourceId", requestId, "status", "REJECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{requestId}/download")
|
||||||
|
@Produces({ "application/zip" })
|
||||||
|
@Operation(summary = "Download the approved export as a ZIP of CSV files")
|
||||||
|
public Response download(@PathParam("requestId") final Long requestId) {
|
||||||
|
final byte[] archive = this.service.generateExport(requestId);
|
||||||
|
return Response.ok(archive)
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"cbsnexora-database-export.zip\"")
|
||||||
|
.header("Content-Length", archive.length)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* 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.infrastructure.databaseexport.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request to take a full export of the tenant database. The export carries every client's personal, KYC and
|
||||||
|
* account data, so it is deliberately a two-person action: whoever asks for it cannot also approve it.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_database_export_request")
|
||||||
|
public class DatabaseExportRequest extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING_APPROVAL = "PENDING_APPROVAL";
|
||||||
|
public static final String STATUS_APPROVED = "APPROVED";
|
||||||
|
public static final String STATUS_REJECTED = "REJECTED";
|
||||||
|
/** Approved and the dump has actually been downloaded at least once. */
|
||||||
|
public static final String STATUS_GENERATED = "GENERATED";
|
||||||
|
|
||||||
|
@Column(name = "status", length = 30, nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "reason", nullable = false)
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Column(name = "requested_by", nullable = false)
|
||||||
|
private Long requestedBy;
|
||||||
|
|
||||||
|
@Column(name = "requested_by_username", length = 100)
|
||||||
|
private String requestedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "requested_on", nullable = false)
|
||||||
|
private LocalDateTime requestedOn;
|
||||||
|
|
||||||
|
@Column(name = "approved_by")
|
||||||
|
private Long approvedBy;
|
||||||
|
|
||||||
|
@Column(name = "approved_by_username", length = 100)
|
||||||
|
private String approvedByUsername;
|
||||||
|
|
||||||
|
@Column(name = "approved_on")
|
||||||
|
private LocalDateTime approvedOn;
|
||||||
|
|
||||||
|
@Column(name = "rejection_reason")
|
||||||
|
private String rejectionReason;
|
||||||
|
|
||||||
|
@Column(name = "generated_on")
|
||||||
|
private LocalDateTime generatedOn;
|
||||||
|
|
||||||
|
@Column(name = "generated_by")
|
||||||
|
private Long generatedBy;
|
||||||
|
|
||||||
|
@Column(name = "file_name", length = 255)
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Column(name = "file_size_bytes")
|
||||||
|
private Long fileSizeBytes;
|
||||||
|
|
||||||
|
@Column(name = "row_count")
|
||||||
|
private Long rowCount;
|
||||||
|
|
||||||
|
protected DatabaseExportRequest() {
|
||||||
|
// Default constructor for JPA
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseExportRequest(final String reason, final Long requestedBy, final String requestedByUsername,
|
||||||
|
final LocalDateTime requestedOn) {
|
||||||
|
this.status = STATUS_PENDING_APPROVAL;
|
||||||
|
this.reason = reason;
|
||||||
|
this.requestedBy = requestedBy;
|
||||||
|
this.requestedByUsername = requestedByUsername;
|
||||||
|
this.requestedOn = requestedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DatabaseExportRequest createNew(final String reason, final Long requestedBy, final String requestedByUsername,
|
||||||
|
final LocalDateTime requestedOn) {
|
||||||
|
return new DatabaseExportRequest(reason, requestedBy, requestedByUsername, requestedOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRequestedBy() {
|
||||||
|
return this.requestedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPendingApproval() {
|
||||||
|
return STATUS_PENDING_APPROVAL.equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An approval buys exactly one download. Once the archive has been handed over the request is spent, and a
|
||||||
|
* second copy has to be asked for and approved again — so every copy of the database that exists outside
|
||||||
|
* the system can be traced back to an approval that was granted for it specifically.
|
||||||
|
*/
|
||||||
|
public boolean isDownloadable() {
|
||||||
|
return STATUS_APPROVED.equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlreadyDownloaded() {
|
||||||
|
return STATUS_GENERATED.equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final Long approvedBy, final String approvedByUsername, final LocalDateTime approvedOn) {
|
||||||
|
this.status = STATUS_APPROVED;
|
||||||
|
this.approvedBy = approvedBy;
|
||||||
|
this.approvedByUsername = approvedByUsername;
|
||||||
|
this.approvedOn = approvedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject(final Long approvedBy, final String approvedByUsername, final LocalDateTime approvedOn,
|
||||||
|
final String rejectionReason) {
|
||||||
|
this.status = STATUS_REJECTED;
|
||||||
|
this.approvedBy = approvedBy;
|
||||||
|
this.approvedByUsername = approvedByUsername;
|
||||||
|
this.approvedOn = approvedOn;
|
||||||
|
this.rejectionReason = rejectionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records that the dump actually left the system, which is the moment that matters for an audit. */
|
||||||
|
public void markGenerated(final Long generatedBy, final LocalDateTime generatedOn, final String fileName,
|
||||||
|
final Long fileSizeBytes, final Long rowCount) {
|
||||||
|
this.status = STATUS_GENERATED;
|
||||||
|
this.generatedBy = generatedBy;
|
||||||
|
this.generatedOn = generatedOn;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.fileSizeBytes = fileSizeBytes;
|
||||||
|
this.rowCount = rowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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.infrastructure.databaseexport.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.LockModeType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Lock;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface DatabaseExportRequestRepository extends JpaRepository<DatabaseExportRequest, Long> {
|
||||||
|
|
||||||
|
List<DatabaseExportRequest> findByStatusOrderByIdDesc(String status);
|
||||||
|
|
||||||
|
long countByRequestedByAndStatus(Long requestedBy, String status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An approval is worth exactly one download, so the row is locked while that single use is spent. Without
|
||||||
|
* the lock two concurrent calls could both read APPROVED and both walk away with a copy of the database.
|
||||||
|
*/
|
||||||
|
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
@Query("select request from DatabaseExportRequest request where request.id = :requestId")
|
||||||
|
Optional<DatabaseExportRequest> findByIdWithLock(@Param("requestId") Long requestId);
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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.infrastructure.databaseexport.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Requesting, approving and generating a full export of the tenant database. */
|
||||||
|
public interface DatabaseExportService {
|
||||||
|
|
||||||
|
Long requestExport(String reason);
|
||||||
|
|
||||||
|
List<Map<String, Object>> retrieveAll(String status);
|
||||||
|
|
||||||
|
Map<String, Object> retrieveOne(Long requestId);
|
||||||
|
|
||||||
|
void approve(Long requestId);
|
||||||
|
|
||||||
|
void reject(Long requestId, String rejectionReason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the approved export as a ZIP of CSV files, one per table. Only callable against a request that an
|
||||||
|
* admin has already approved.
|
||||||
|
*/
|
||||||
|
byte[] generateExport(Long requestId);
|
||||||
|
}
|
||||||
+318
@@ -0,0 +1,318 @@
|
|||||||
|
/**
|
||||||
|
* 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.infrastructure.databaseexport.service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.databaseexport.domain.DatabaseExportRequest;
|
||||||
|
import org.apache.fineract.infrastructure.databaseexport.domain.DatabaseExportRequestRepository;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DatabaseExportServiceImpl implements DatabaseExportService {
|
||||||
|
|
||||||
|
private static final String PERMISSION = "DATABASEEXPORT";
|
||||||
|
/** Guards against a runaway export: a single table beyond this is truncated rather than exhausting heap. */
|
||||||
|
private static final int MAX_ROWS_PER_TABLE = 200_000;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final DatabaseExportRequestRepository repository;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Long requestExport(final String reason) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
user.validateHasPermissionTo("CREATE_" + PERMISSION);
|
||||||
|
|
||||||
|
if (reason == null || reason.trim().length() < 10) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.reason.too.short",
|
||||||
|
"A reason of at least 10 characters is required for a database export request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// One open request per user at a time, so a queue of forgotten requests cannot build up for an
|
||||||
|
// approver to rubber-stamp in bulk.
|
||||||
|
if (this.repository.countByRequestedByAndStatus(user.getId(), DatabaseExportRequest.STATUS_PENDING_APPROVAL) > 0) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.request.already.pending",
|
||||||
|
"You already have a database export request awaiting approval.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final DatabaseExportRequest request = DatabaseExportRequest.createNew(reason.trim(), user.getId(), user.getUsername(),
|
||||||
|
LocalDateTime.now());
|
||||||
|
this.repository.saveAndFlush(request);
|
||||||
|
|
||||||
|
log.warn("Database export requested by user {} ({}), request id {}", user.getUsername(), user.getId(), request.getId());
|
||||||
|
return request.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> retrieveAll(final String status) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(PERMISSION);
|
||||||
|
|
||||||
|
// Aliases are quoted because PostgreSQL folds unquoted identifiers to lower case, which would hand the
|
||||||
|
// client `requestedbyusername` and leave every camelCase field on the screen empty.
|
||||||
|
final StringBuilder sql = new StringBuilder("SELECT id, status, reason, requested_by AS \"requestedBy\", "
|
||||||
|
+ "requested_by_username AS \"requestedByUsername\", requested_on AS \"requestedOn\", "
|
||||||
|
+ "approved_by AS \"approvedBy\", approved_by_username AS \"approvedByUsername\", "
|
||||||
|
+ "approved_on AS \"approvedOn\", rejection_reason AS \"rejectionReason\", "
|
||||||
|
+ "generated_on AS \"generatedOn\", generated_by AS \"generatedBy\", file_name AS \"fileName\", "
|
||||||
|
+ "file_size_bytes AS \"fileSizeBytes\", row_count AS \"rowCount\" "
|
||||||
|
+ "FROM m_database_export_request ");
|
||||||
|
|
||||||
|
if (status != null && !status.trim().isEmpty()) {
|
||||||
|
sql.append("WHERE status = ? ORDER BY id DESC");
|
||||||
|
return this.jdbcTemplate.queryForList(sql.toString(), status.trim().toUpperCase());
|
||||||
|
}
|
||||||
|
sql.append("ORDER BY id DESC");
|
||||||
|
return this.jdbcTemplate.queryForList(sql.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> retrieveOne(final Long requestId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(PERMISSION);
|
||||||
|
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(
|
||||||
|
"SELECT id, status, reason, requested_by AS \"requestedBy\", "
|
||||||
|
+ "requested_by_username AS \"requestedByUsername\", requested_on AS \"requestedOn\", "
|
||||||
|
+ "approved_by_username AS \"approvedByUsername\", approved_on AS \"approvedOn\", "
|
||||||
|
+ "rejection_reason AS \"rejectionReason\", generated_on AS \"generatedOn\", "
|
||||||
|
+ "file_name AS \"fileName\", file_size_bytes AS \"fileSizeBytes\", row_count AS \"rowCount\" "
|
||||||
|
+ "FROM m_database_export_request WHERE id = ?",
|
||||||
|
requestId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.not.found", "Export request not found.");
|
||||||
|
}
|
||||||
|
return rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void approve(final Long requestId) {
|
||||||
|
final AppUser approver = this.context.authenticatedUser();
|
||||||
|
approver.validateHasPermissionTo("APPROVE_" + PERMISSION);
|
||||||
|
|
||||||
|
final DatabaseExportRequest request = findOneOrThrow(requestId);
|
||||||
|
validatePending(request);
|
||||||
|
validateNotSelfApproval(request, approver);
|
||||||
|
|
||||||
|
request.approve(approver.getId(), approver.getUsername(), LocalDateTime.now());
|
||||||
|
this.repository.saveAndFlush(request);
|
||||||
|
|
||||||
|
log.warn("Database export request {} approved by {} ({})", requestId, approver.getUsername(), approver.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void reject(final Long requestId, final String rejectionReason) {
|
||||||
|
final AppUser approver = this.context.authenticatedUser();
|
||||||
|
approver.validateHasPermissionTo("APPROVE_" + PERMISSION);
|
||||||
|
|
||||||
|
final DatabaseExportRequest request = findOneOrThrow(requestId);
|
||||||
|
validatePending(request);
|
||||||
|
|
||||||
|
if (rejectionReason == null || rejectionReason.trim().isEmpty()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.rejection.reason.required",
|
||||||
|
"A reason is required when rejecting a database export request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.reject(approver.getId(), approver.getUsername(), LocalDateTime.now(), rejectionReason.trim());
|
||||||
|
this.repository.saveAndFlush(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public byte[] generateExport(final Long requestId) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
user.validateHasPermissionTo("DOWNLOAD_" + PERMISSION);
|
||||||
|
|
||||||
|
// Locked for the duration of the transaction: the approval is worth one download, and two concurrent
|
||||||
|
// calls must not both find it unspent.
|
||||||
|
final DatabaseExportRequest request = this.repository.findByIdWithLock(requestId)
|
||||||
|
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.databaseexport.not.found",
|
||||||
|
"Export request not found."));
|
||||||
|
// Checked before ownership so the requester coming back for a second copy gets told why, rather than
|
||||||
|
// being turned away with a message about approval that no longer describes their situation.
|
||||||
|
if (request.isAlreadyDownloaded()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.already.downloaded",
|
||||||
|
"This export has already been downloaded. Raise a new request to take another copy.");
|
||||||
|
}
|
||||||
|
if (!request.isDownloadable()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.not.approved",
|
||||||
|
"This export request has not been approved. Current status: " + request.getStatus() + ".");
|
||||||
|
}
|
||||||
|
validateIsRequester(request, user);
|
||||||
|
|
||||||
|
final List<String> tables = listTenantTables();
|
||||||
|
long totalRows = 0;
|
||||||
|
|
||||||
|
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream zip = new ZipOutputStream(buffer, StandardCharsets.UTF_8)) {
|
||||||
|
for (final String table : tables) {
|
||||||
|
try {
|
||||||
|
totalRows += writeTableAsCsv(zip, table);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
// One unreadable table (a view, a permissions quirk) must not abandon the whole export.
|
||||||
|
log.error("Database export: skipping table {} — {}", table, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeManifest(zip, request, tables, totalRows, user);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.generation.failed",
|
||||||
|
"Failed to generate the database export: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] archive = buffer.toByteArray();
|
||||||
|
final String fileName = "cbsnexora-export-" + DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now())
|
||||||
|
+ ".zip";
|
||||||
|
|
||||||
|
request.markGenerated(user.getId(), LocalDateTime.now(), fileName, (long) archive.length, totalRows);
|
||||||
|
this.repository.saveAndFlush(request);
|
||||||
|
|
||||||
|
log.warn("Database export request {} downloaded by {} ({}) — {} tables, {} rows, {} bytes", requestId, user.getUsername(),
|
||||||
|
user.getId(), tables.size(), totalRows, archive.length);
|
||||||
|
return archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> listTenantTables() {
|
||||||
|
return this.jdbcTemplate.queryForList("SELECT table_name FROM information_schema.tables "
|
||||||
|
+ "WHERE table_schema = current_schema() AND table_type = 'BASE TABLE' ORDER BY table_name", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes one table as a CSV entry and returns how many rows it contained. */
|
||||||
|
private long writeTableAsCsv(final ZipOutputStream zip, final String table) throws IOException {
|
||||||
|
// The table name comes from information_schema, never from user input, so it cannot carry injection.
|
||||||
|
final List<Map<String, Object>> rows = this.jdbcTemplate
|
||||||
|
.queryForList("SELECT * FROM \"" + table + "\" LIMIT " + MAX_ROWS_PER_TABLE);
|
||||||
|
|
||||||
|
zip.putNextEntry(new ZipEntry("data/" + table + ".csv"));
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
// Still emit the header so an empty table is visibly empty rather than missing.
|
||||||
|
final List<String> columns = this.jdbcTemplate.queryForList("SELECT column_name FROM information_schema.columns "
|
||||||
|
+ "WHERE table_schema = current_schema() AND table_name = ? ORDER BY ordinal_position", String.class, table);
|
||||||
|
zip.write(String.join(",", columns).getBytes(StandardCharsets.UTF_8));
|
||||||
|
zip.write('\n');
|
||||||
|
zip.closeEntry();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> columns = new ArrayList<>(rows.get(0).keySet());
|
||||||
|
zip.write(String.join(",", columns).getBytes(StandardCharsets.UTF_8));
|
||||||
|
zip.write('\n');
|
||||||
|
|
||||||
|
for (final Map<String, Object> row : rows) {
|
||||||
|
final StringBuilder line = new StringBuilder();
|
||||||
|
for (int i = 0; i < columns.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
line.append(',');
|
||||||
|
}
|
||||||
|
line.append(csvEscape(row.get(columns.get(i))));
|
||||||
|
}
|
||||||
|
line.append('\n');
|
||||||
|
zip.write(line.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
zip.closeEntry();
|
||||||
|
return rows.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records who authorised the export inside the archive, so the file carries its own provenance. */
|
||||||
|
private void writeManifest(final ZipOutputStream zip, final DatabaseExportRequest request, final List<String> tables,
|
||||||
|
final long totalRows, final AppUser downloadedBy) throws IOException {
|
||||||
|
final Map<String, Object> details = retrieveOne(request.getId());
|
||||||
|
final StringBuilder manifest = new StringBuilder();
|
||||||
|
manifest.append("CBSNexora — Full Database Export\n");
|
||||||
|
manifest.append("=================================\n\n");
|
||||||
|
manifest.append("CONFIDENTIAL. This archive contains customer personal, KYC and account data.\n");
|
||||||
|
manifest.append("Handle under your institution's data protection policy.\n\n");
|
||||||
|
manifest.append("Request ID : ").append(request.getId()).append('\n');
|
||||||
|
manifest.append("Requested by : ").append(details.get("requestedByUsername")).append('\n');
|
||||||
|
manifest.append("Requested on : ").append(details.get("requestedOn")).append('\n');
|
||||||
|
manifest.append("Reason : ").append(details.get("reason")).append('\n');
|
||||||
|
manifest.append("Approved by : ").append(details.get("approvedByUsername")).append('\n');
|
||||||
|
manifest.append("Approved on : ").append(details.get("approvedOn")).append('\n');
|
||||||
|
manifest.append("Downloaded by : ").append(downloadedBy.getUsername()).append('\n');
|
||||||
|
manifest.append("Downloaded on : ").append(LocalDateTime.now()).append('\n');
|
||||||
|
manifest.append("Tables exported : ").append(tables.size()).append('\n');
|
||||||
|
manifest.append("Rows exported : ").append(totalRows).append('\n');
|
||||||
|
manifest.append("Row cap / table : ").append(MAX_ROWS_PER_TABLE).append('\n');
|
||||||
|
|
||||||
|
zip.putNextEntry(new ZipEntry("MANIFEST.txt"));
|
||||||
|
zip.write(manifest.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
zip.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String csvEscape(final Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
final String text = String.valueOf(value);
|
||||||
|
if (text.indexOf(',') >= 0 || text.indexOf('"') >= 0 || text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) {
|
||||||
|
return '"' + text.replace("\"", "\"\"") + '"';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseExportRequest findOneOrThrow(final Long requestId) {
|
||||||
|
return this.repository.findById(requestId).orElseThrow(() -> new PlatformDataIntegrityException(
|
||||||
|
"error.msg.databaseexport.not.found", "Export request not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePending(final DatabaseExportRequest request) {
|
||||||
|
if (!request.isPendingApproval()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.already.actioned",
|
||||||
|
"This request has already been actioned. Current status: " + request.getStatus() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The archive goes to the person who asked for it and stated the reason, and to nobody else. An approver
|
||||||
|
* authorises the export; that is not the same as being entitled to take a copy of it, and letting the
|
||||||
|
* approver download would collapse the two-person control back into one.
|
||||||
|
*/
|
||||||
|
private void validateIsRequester(final DatabaseExportRequest request, final AppUser user) {
|
||||||
|
if (!user.getId().equals(request.getRequestedBy())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.not.requester",
|
||||||
|
"Only the user who raised this export request can download the archive.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The entire control is that two different people are involved, so this is the check that enforces it. */
|
||||||
|
private void validateNotSelfApproval(final DatabaseExportRequest request, final AppUser approver) {
|
||||||
|
if (approver.getId().equals(request.getRequestedBy())) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.databaseexport.self.approval",
|
||||||
|
"A database export cannot be approved by the same user who requested it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,409 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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.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.commissionslab.service.CommissionSlabWritePlatformService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "COMMISSIONSLAB", action = "CREATE")
|
||||||
|
public class CreateCommissionSlabsCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final CommissionSlabWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CreateCommissionSlabsCommandHandler(final CommissionSlabWritePlatformService writePlatformService) {
|
||||||
|
this.writePlatformService = writePlatformService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createCommissionSlabs(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-3
@@ -797,7 +797,9 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the token number is unique for today's date among non-rejected requests.
|
* Ensures the token number is unique for today's date among non-rejected requests, checking
|
||||||
|
* both this table and {@code m_share_dividend_claim_request} -- a dividend cash claim's token
|
||||||
|
* is redeemed at the very same Transaction Entry field, so the two pools must never collide.
|
||||||
* {@code excludeId} is set during resubmit to skip the record being edited.
|
* {@code excludeId} is set during resubmit to skip the record being edited.
|
||||||
*/
|
*/
|
||||||
private void validateTokenUniqueToday(final String tokNo, final Long excludeId) {
|
private void validateTokenUniqueToday(final String tokNo, final Long excludeId) {
|
||||||
@@ -810,10 +812,16 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
sql.append(" AND id <> ?");
|
sql.append(" AND id <> ?");
|
||||||
params.add(excludeId);
|
params.add(excludeId);
|
||||||
}
|
}
|
||||||
final long count = Optional.ofNullable(
|
final long ownCount = Optional.ofNullable(
|
||||||
jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray())
|
jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray())
|
||||||
).orElse(0L);
|
).orElse(0L);
|
||||||
if (count > 0) {
|
final long otherCount = Optional.ofNullable(
|
||||||
|
jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_share_dividend_claim_request "
|
||||||
|
+ "WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE AND status <> 'REJECTED'",
|
||||||
|
Long.class, tokNo)
|
||||||
|
).orElse(0L);
|
||||||
|
if (ownCount + otherCount > 0) {
|
||||||
throw apiError(400,
|
throw apiError(400,
|
||||||
"Token number '" + tokNo + "' is already used today. Please enter a different token number.");
|
"Token number '" + tokNo + "' is already used today. Please enter a different token number.");
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -111,12 +111,16 @@ public class ClientContactOtpServiceImpl implements ClientContactOtpService {
|
|||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private final org.apache.fineract.portfolio.mobilelookup.service.MobileLookupService mobileLookupService;
|
||||||
|
|
||||||
public ClientContactOtpServiceImpl(final OtpLogRepository otpLogRepository, final EmailSenderService emailSenderService,
|
public ClientContactOtpServiceImpl(final OtpLogRepository otpLogRepository, final EmailSenderService emailSenderService,
|
||||||
final EmailTemplateService emailTemplateService) {
|
final EmailTemplateService emailTemplateService,
|
||||||
|
final org.apache.fineract.portfolio.mobilelookup.service.MobileLookupService mobileLookupService) {
|
||||||
this.restTemplate = new RestTemplate();
|
this.restTemplate = new RestTemplate();
|
||||||
this.otpLogRepository = otpLogRepository;
|
this.otpLogRepository = otpLogRepository;
|
||||||
this.emailSenderService = emailSenderService;
|
this.emailSenderService = emailSenderService;
|
||||||
this.emailTemplateService = emailTemplateService;
|
this.emailTemplateService = emailTemplateService;
|
||||||
|
this.mobileLookupService = mobileLookupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generate ─────────────────────────────────────────────────────────────
|
// ── Generate ─────────────────────────────────────────────────────────────
|
||||||
@@ -128,6 +132,18 @@ public class ClientContactOtpServiceImpl implements ClientContactOtpService {
|
|||||||
return OtpGenerateResponse.builder().success(false).message("mobileNumber is required").build();
|
return OtpGenerateResponse.builder().success(false).message("mobileNumber is required").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A number the DoT flagged may already be in someone else's hands — an MNRL number is one
|
||||||
|
// that was disconnected and can be reassigned. Sending an OTP there hands the fraudster the
|
||||||
|
// credential, so this is refused at the point of dispatch rather than merely warned about
|
||||||
|
// on screen. Only an officer's recorded PROCEEDED decision reopens it.
|
||||||
|
if (!this.mobileLookupService.isMobileNumberUsable(mobileNumber.trim())) {
|
||||||
|
LOG.warn("Refused to send OTP to DIP/MNRL-flagged number {}", OtpDestinationMasker.maskMobile(mobileNumber));
|
||||||
|
return OtpGenerateResponse.builder().success(false)
|
||||||
|
.message("This mobile number is flagged on the DoT Digital Intelligence Platform. "
|
||||||
|
+ "Contact the customer through an alternate channel and obtain a new number.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
final OtpLog log = new OtpLog();
|
final OtpLog log = new OtpLog();
|
||||||
log.setPurpose(PURPOSE_CLIENT_CONTACT_CHANGE);
|
log.setPurpose(PURPOSE_CLIENT_CONTACT_CHANGE);
|
||||||
log.setProvider(PROVIDER_LINCPAY);
|
log.setProvider(PROVIDER_LINCPAY);
|
||||||
|
|||||||
+4
-3
@@ -46,7 +46,8 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
|||||||
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
|
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
|
||||||
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName,
|
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName,
|
||||||
authorizedSignatoryParamName, additionalInfoParamName, dateOfRegistrationParamName, datatables));
|
authorizedSignatoryParamName, authorizedSignatoryClientIdsParamName, additionalInfoParamName,
|
||||||
|
dateOfRegistrationParamName, datatables));
|
||||||
|
|
||||||
protected static final Set<String> CLIENT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
|
protected static final Set<String> CLIENT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
|
||||||
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, alternateMobileNoParamName, emailAddressParamName, firstnameParamName,
|
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, alternateMobileNoParamName, emailAddressParamName, firstnameParamName,
|
||||||
@@ -70,8 +71,8 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
|
|||||||
|
|
||||||
protected static final Set<String> CLIENT_NON_PERSON_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
protected static final Set<String> CLIENT_NON_PERSON_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName, incorpValidityTillParamName,
|
Arrays.asList(localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName, incorpValidityTillParamName,
|
||||||
constitutionIdParamName, mainBusinessLineIdParamName, authorizedSignatoryParamName, additionalInfoParamName,
|
constitutionIdParamName, mainBusinessLineIdParamName, authorizedSignatoryParamName,
|
||||||
dateOfRegistrationParamName));
|
authorizedSignatoryClientIdsParamName, additionalInfoParamName, dateOfRegistrationParamName));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These parameters will match the class level parameters of {@link ClientData}. Where possible, we try to get
|
* These parameters will match the class level parameters of {@link ClientData}. Where possible, we try to get
|
||||||
|
|||||||
+5
@@ -87,6 +87,11 @@ public final class ClientKycData {
|
|||||||
private final Boolean cinVerified;
|
private final Boolean cinVerified;
|
||||||
private final LocalDate cinVerifiedOn;
|
private final LocalDate cinVerifiedOn;
|
||||||
private final LocalDate incorporationDate;
|
private final LocalDate incorporationDate;
|
||||||
|
// DIN of a director — read back so an entity re-opened for edit can show the verification
|
||||||
|
// already obtained for each authorized signatory instead of asking for it again.
|
||||||
|
private final String dinNumber;
|
||||||
|
private final Boolean dinVerified;
|
||||||
|
private final LocalDate dinVerifiedOn;
|
||||||
|
|
||||||
// ── KYC Document ─────────────────────────────────────────────────────────
|
// ── KYC Document ─────────────────────────────────────────────────────────
|
||||||
private final String kycDocumentName;
|
private final String kycDocumentName;
|
||||||
|
|||||||
+22
-3
@@ -75,6 +75,14 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "authorized_signatory", length = 500, nullable = true)
|
@Column(name = "authorized_signatory", length = 500, nullable = true)
|
||||||
private String authorizedSignatory;
|
private String authorizedSignatory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client ids of the signatories named in {@link #authorizedSignatory}, comma separated and in the
|
||||||
|
* same order. Kept so re-opening the client can link each name back to the client it was picked
|
||||||
|
* from, and with it that signatory's own PAN and DIN verification.
|
||||||
|
*/
|
||||||
|
@Column(name = "authorized_signatory_client_ids", length = 500, nullable = true)
|
||||||
|
private String authorizedSignatoryClientIds;
|
||||||
|
|
||||||
@Column(name = "additional_info", length = 500, nullable = true)
|
@Column(name = "additional_info", length = 500, nullable = true)
|
||||||
private String additionalInfo;
|
private String additionalInfo;
|
||||||
|
|
||||||
@@ -99,9 +107,9 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
|
|||||||
|
|
||||||
public static ClientNonPerson createNew(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine,
|
public static ClientNonPerson createNew(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine,
|
||||||
String incorpNumber, LocalDate incorpValidityTill, String remarks, String authorizedSignatory, String additionalInfo,
|
String incorpNumber, LocalDate incorpValidityTill, String remarks, String authorizedSignatory, String additionalInfo,
|
||||||
LocalDate dateOfRegistration) {
|
LocalDate dateOfRegistration, String authorizedSignatoryClientIds) {
|
||||||
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory,
|
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory,
|
||||||
additionalInfo, dateOfRegistration);
|
additionalInfo, dateOfRegistration, authorizedSignatoryClientIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ClientNonPerson() {
|
protected ClientNonPerson() {
|
||||||
@@ -110,7 +118,7 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
|
|||||||
|
|
||||||
private ClientNonPerson(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine, final String incorpNumber,
|
private ClientNonPerson(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine, final String incorpNumber,
|
||||||
final LocalDate incorpValidityTill, final String remarks, final String authorizedSignatory, final String additionalInfo,
|
final LocalDate incorpValidityTill, final String remarks, final String authorizedSignatory, final String additionalInfo,
|
||||||
final LocalDate dateOfRegistration) {
|
final LocalDate dateOfRegistration, final String authorizedSignatoryClientIds) {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
@@ -137,6 +145,10 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
|
|||||||
this.authorizedSignatory = authorizedSignatory.trim();
|
this.authorizedSignatory = authorizedSignatory.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(authorizedSignatoryClientIds)) {
|
||||||
|
this.authorizedSignatoryClientIds = authorizedSignatoryClientIds.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(additionalInfo)) {
|
if (StringUtils.isNotBlank(additionalInfo)) {
|
||||||
this.additionalInfo = additionalInfo.trim();
|
this.additionalInfo = additionalInfo.trim();
|
||||||
}
|
}
|
||||||
@@ -258,6 +270,13 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
|
|||||||
this.additionalInfo = StringUtils.defaultIfEmpty(newValue, null);
|
this.additionalInfo = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed(ClientApiConstants.authorizedSignatoryClientIdsParamName,
|
||||||
|
this.authorizedSignatoryClientIds)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.authorizedSignatoryClientIdsParamName);
|
||||||
|
actualChanges.put(ClientApiConstants.authorizedSignatoryClientIdsParamName, newValue);
|
||||||
|
this.authorizedSignatoryClientIds = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.isChangeInLocalDateParameterNamed(ClientApiConstants.dateOfRegistrationParamName, this.dateOfRegistration)) {
|
if (command.isChangeInLocalDateParameterNamed(ClientApiConstants.dateOfRegistrationParamName, this.dateOfRegistration)) {
|
||||||
final String valueAsInput = command.stringValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
|
final String valueAsInput = command.stringValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
|
||||||
actualChanges.put(ClientApiConstants.dateOfRegistrationParamName, valueAsInput);
|
actualChanges.put(ClientApiConstants.dateOfRegistrationParamName, valueAsInput);
|
||||||
|
|||||||
+22
-1
@@ -48,7 +48,10 @@ public class KycVerificationResult {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience factory for a failed verification. */
|
/**
|
||||||
|
* The provider answered and rejected the document — an unknown CIN, a name that does not match.
|
||||||
|
* A data problem the officer can act on, not an outage.
|
||||||
|
*/
|
||||||
public static KycVerificationResult failure(final String errorMessage, final String rawResponse) {
|
public static KycVerificationResult failure(final String errorMessage, final String rawResponse) {
|
||||||
return KycVerificationResult.builder()
|
return KycVerificationResult.builder()
|
||||||
.status(Status.FAILURE)
|
.status(Status.FAILURE)
|
||||||
@@ -57,7 +60,25 @@ public class KycVerificationResult {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provider could not be reached, or answered with something unusable. Nothing was learnt
|
||||||
|
* about the document, so callers should report this as the service being unavailable rather
|
||||||
|
* than as the document having failed its check.
|
||||||
|
*/
|
||||||
|
public static KycVerificationResult error(final String errorMessage, final String rawResponse) {
|
||||||
|
return KycVerificationResult.builder()
|
||||||
|
.status(Status.ERROR)
|
||||||
|
.errorMessage(errorMessage)
|
||||||
|
.rawResponse(rawResponse)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSuccess() {
|
public boolean isSuccess() {
|
||||||
return Status.SUCCESS == this.status;
|
return Status.SUCCESS == this.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Distinguishes an unreachable provider from one that answered with a rejection. */
|
||||||
|
public boolean isServiceError() {
|
||||||
|
return Status.ERROR == this.status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -322,7 +322,9 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
|
|||||||
final JsonObject response = parseJson(rawResponse);
|
final JsonObject response = parseJson(rawResponse);
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return KycVerificationResult.failure(
|
// Unparseable body: nothing was learnt about the CIN, so this is the provider
|
||||||
|
// misbehaving rather than the CIN being bad.
|
||||||
|
return KycVerificationResult.error(
|
||||||
"CIN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
|
"CIN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
|
||||||
rawResponse);
|
rawResponse);
|
||||||
}
|
}
|
||||||
@@ -338,12 +340,12 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
|
|||||||
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
} catch (final HttpClientErrorException | HttpServerErrorException e) {
|
||||||
LOG.error("LincPay CIN validation HTTP error for CIN {}: {} {}", cinNumber,
|
LOG.error("LincPay CIN validation HTTP error for CIN {}: {} {}", cinNumber,
|
||||||
e.getStatusCode(), e.getResponseBodyAsString());
|
e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
return KycVerificationResult.failure(
|
return KycVerificationResult.error(
|
||||||
"CIN verification failed (HTTP " + e.getStatusCode() + "): "
|
"CIN verification failed (HTTP " + e.getStatusCode() + "): "
|
||||||
+ e.getResponseBodyAsString(), null);
|
+ e.getResponseBodyAsString(), null);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
LOG.error("LincPay CIN validation unexpected error for CIN: {}", cinNumber, e);
|
LOG.error("LincPay CIN validation unexpected error for CIN: {}", cinNumber, e);
|
||||||
return KycVerificationResult.failure("CIN verification failed: " + e.getMessage(), null);
|
return KycVerificationResult.error("CIN verification failed: " + e.getMessage(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -34,7 +34,7 @@ import org.mapstruct.Mapper;
|
|||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
import org.mapstruct.MappingTarget;
|
import org.mapstruct.MappingTarget;
|
||||||
import org.mapstruct.Named;
|
import org.mapstruct.Named;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
@Mapper(config = MapstructMapperConfig.class)
|
@Mapper(config = MapstructMapperConfig.class)
|
||||||
public interface ClientMapper {
|
public interface ClientMapper {
|
||||||
@@ -107,6 +107,11 @@ public interface ClientMapper {
|
|||||||
@Mapping(target = "legalFormId", ignore = true)
|
@Mapping(target = "legalFormId", ignore = true)
|
||||||
@Mapping(target = "clientCollateralManagements", ignore = true)
|
@Mapping(target = "clientCollateralManagements", ignore = true)
|
||||||
@Mapping(target = "groups", ignore = true)
|
@Mapping(target = "groups", ignore = true)
|
||||||
|
// Resolved from the latest DIP/MNRL lookup after mapping, not from a Client column — see
|
||||||
|
// ClientReadPlatformServiceImpl#retrieveOne.
|
||||||
|
@Mapping(target = "dipLiveStatus", ignore = true)
|
||||||
|
@Mapping(target = "alternateDipLiveStatus", ignore = true)
|
||||||
|
@Mapping(target = "sanctionsAlertStatus", ignore = true)
|
||||||
ClientData map(Client source);
|
ClientData map(Client source);
|
||||||
|
|
||||||
@Named("clientTypeCode")
|
@Named("clientTypeCode")
|
||||||
|
|||||||
+3
@@ -107,6 +107,9 @@ public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformSe
|
|||||||
.cinVerified(kyc.isCinVerified())
|
.cinVerified(kyc.isCinVerified())
|
||||||
.cinVerifiedOn(kyc.getCinVerifiedOn())
|
.cinVerifiedOn(kyc.getCinVerifiedOn())
|
||||||
.incorporationDate(kyc.getIncorporationDate())
|
.incorporationDate(kyc.getIncorporationDate())
|
||||||
|
.dinNumber(kyc.getDinNumber())
|
||||||
|
.dinVerified(kyc.isDinVerified())
|
||||||
|
.dinVerifiedOn(kyc.getDinVerifiedOn())
|
||||||
.kycDocumentName(kyc.getKycDocumentName())
|
.kycDocumentName(kyc.getKycDocumentName())
|
||||||
.kycDocumentNumber(kyc.getKycDocumentNumber())
|
.kycDocumentNumber(kyc.getKycDocumentNumber())
|
||||||
.kycDocIssueDate(kyc.getKycDocIssueDate())
|
.kycDocIssueDate(kyc.getKycDocIssueDate())
|
||||||
|
|||||||
+64
-19
@@ -93,9 +93,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
kycCommand.validateForInitiate(Boolean.TRUE.equals(clientForValidation.getIsMinor()));
|
kycCommand.validateForInitiate(Boolean.TRUE.equals(clientForValidation.getIsMinor()));
|
||||||
|
|
||||||
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
||||||
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
|
enforceNonIndividualOnlyFields(command, isIndividualClient(clientForValidation));
|
||||||
final boolean isIndividualInitiate = kycTypeOnInitiate != null && kycTypeOnInitiate == 1;
|
|
||||||
enforceNonIndividualOnlyFields(command, isIndividualInitiate);
|
|
||||||
|
|
||||||
if (kycRepositoryWrapper.existsByClientId(clientId)) {
|
if (kycRepositoryWrapper.existsByClientId(clientId)) {
|
||||||
LOG.info("KYC record already exists for client {}. Returning existing record.", clientId);
|
LOG.info("KYC record already exists for client {}. Returning existing record.", clientId);
|
||||||
@@ -140,23 +138,29 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
|
||||||
|
|
||||||
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
||||||
final boolean isIndividualExisting = kyc.getKycType() != null && kyc.getKycType().isIndividual();
|
enforceNonIndividualOnlyFields(command, isIndividualClient(client));
|
||||||
enforceNonIndividualOnlyFields(command, isIndividualExisting);
|
|
||||||
|
|
||||||
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
||||||
|
|
||||||
// When re-editing a VERIFIED record, remarks are mandatory (audit trail)
|
final Map<String, Object> changes = kyc.update(command);
|
||||||
if (currentStatus.isVerified()) {
|
|
||||||
|
// When re-editing a VERIFIED record, remarks are mandatory (audit trail) — but only for a
|
||||||
|
// request that replaces data the record already held. A non-individual KYC auto-verifies as
|
||||||
|
// soon as PAN and GSTIN pass, which lands part-way through the create wizard, and the
|
||||||
|
// officer still has to save the KYC document details afterwards; filling in fields that were
|
||||||
|
// empty is completing the record, not altering it, and must not demand a justification.
|
||||||
|
// Thrown before the save below, and this method is @Transactional, so the in-memory
|
||||||
|
// mutations from update() are rolled back.
|
||||||
|
if (currentStatus.isVerified() && !kyc.getFieldsOverwrittenByLastUpdate().isEmpty()) {
|
||||||
final String remarks = command.stringValueOfParameterNamed("remarks");
|
final String remarks = command.stringValueOfParameterNamed("remarks");
|
||||||
if (StringUtils.isBlank(remarks)) {
|
if (StringUtils.isBlank(remarks)) {
|
||||||
throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(
|
throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(
|
||||||
"error.msg.client.kyc.remarks.required.for.reverification",
|
"error.msg.client.kyc.remarks.required.for.reverification",
|
||||||
"Remarks are mandatory when updating a verified KYC record.", "remarks")));
|
"Remarks are mandatory when changing data already recorded on a verified KYC record.",
|
||||||
|
"remarks")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Object> changes = kyc.update(command);
|
|
||||||
|
|
||||||
if (!changes.isEmpty()) {
|
if (!changes.isEmpty()) {
|
||||||
kycRepositoryWrapper.saveAndFlush(kyc);
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
}
|
}
|
||||||
@@ -335,7 +339,11 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
// firstname/middlename/lastname ← individual columns; NULL for entities
|
// firstname/middlename/lastname ← individual columns; NULL for entities
|
||||||
// fullname ← the entity name lives here
|
// fullname ← the entity name lives here
|
||||||
// dateOfBirth ← only set for individuals
|
// dateOfBirth ← only set for individuals
|
||||||
// ClientNonPerson.dateOfRegistration ← the incorporation date for entities
|
// ClientKyc.incorporationDate ← the incorporation date for entities, captured on the
|
||||||
|
// KYC step next to the entity PAN it is verified against
|
||||||
|
// ClientNonPerson.dateOfRegistration ← where that date lived for clients created before
|
||||||
|
// the field moved to the KYC step; read as a fallback
|
||||||
|
// so those older entities still verify.
|
||||||
// LincPay's /validate-pan rejects empty/missing DOB with a PLAIN-TEXT "Invalid DOB format"
|
// LincPay's /validate-pan rejects empty/missing DOB with a PLAIN-TEXT "Invalid DOB format"
|
||||||
// response (not JSON) — Gson then throws MalformedJsonException at column 10. Build the
|
// response (not JSON) — Gson then throws MalformedJsonException at column 10. Build the
|
||||||
// right (name, dob) pair per legal form so we always send LincPay the data it expects.
|
// right (name, dob) pair per legal form so we always send LincPay the data it expects.
|
||||||
@@ -343,10 +351,13 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
final String dob;
|
final String dob;
|
||||||
if (client.getLegalForm() != null && LegalForm.fromInt(client.getLegalForm()).isEntity()) {
|
if (client.getLegalForm() != null && LegalForm.fromInt(client.getLegalForm()).isEntity()) {
|
||||||
clientNameForPan = client.getFullname() != null ? client.getFullname().trim() : "";
|
clientNameForPan = client.getFullname() != null ? client.getFullname().trim() : "";
|
||||||
final ClientNonPerson np = this.clientNonPersonRepository.findOneByClientId(client.getId());
|
java.time.LocalDate incorpDate = kyc.getIncorporationDate();
|
||||||
final java.time.LocalDate regDate = (np != null) ? np.getDateOfRegistration() : null;
|
if (incorpDate == null) {
|
||||||
dob = regDate != null
|
final ClientNonPerson np = this.clientNonPersonRepository.findOneByClientId(client.getId());
|
||||||
? regDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
|
incorpDate = (np != null) ? np.getDateOfRegistration() : null;
|
||||||
|
}
|
||||||
|
dob = incorpDate != null
|
||||||
|
? incorpDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
|
||||||
: null;
|
: null;
|
||||||
} else {
|
} else {
|
||||||
dob = client.dateOfBirth() != null
|
dob = client.dateOfBirth() != null
|
||||||
@@ -521,15 +532,36 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
"CIN number is not set on this KYC record. Update the KYC record first.");
|
"CIN number is not set on this KYC record. Update the KYC record first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final KycVerificationResult result = kycVerificationService.verifyCin(kyc.getCinNumber());
|
final String cin = kyc.getCinNumber().toUpperCase().trim();
|
||||||
|
if (!cin.matches(ClientKycApiConstants.CIN_REGEX)) {
|
||||||
|
throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(
|
||||||
|
"error.msg.client.kyc.cin.invalid.format",
|
||||||
|
"Invalid CIN format. Expected 21 characters, e.g. L17110MH1973PLC019786.",
|
||||||
|
ClientKycApiConstants.cinNumberParam)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.isSuccess()) {
|
final KycVerificationResult result = kycVerificationService.verifyCin(cin);
|
||||||
|
|
||||||
|
if (!result.isSuccess() && result.isServiceError()) {
|
||||||
throw new PlatformServiceUnavailableException(
|
throw new PlatformServiceUnavailableException(
|
||||||
"error.msg.client.kyc.cin.verification.failed",
|
"error.msg.client.kyc.cin.verification.failed",
|
||||||
"CIN verification failed: " + result.getErrorMessage(),
|
"CIN verification failed: " + result.getErrorMessage(),
|
||||||
result.getErrorMessage());
|
result.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A CIN whose structure is valid is accepted even when the registry lookup returns no match.
|
||||||
|
// The provider's company endpoint answers "Not Found" for every CIN put to it, including
|
||||||
|
// ones that are demonstrably real, so a negative from it says nothing about the company and
|
||||||
|
// cannot be allowed to block onboarding. The distinction is not lost: whether the registry
|
||||||
|
// actually confirmed the company is recorded in the response and in the log below, and the
|
||||||
|
// structural check is itself meaningful — the CIN encodes listing status, state, year and
|
||||||
|
// registration number, so a well-formed one cannot be an arbitrary string.
|
||||||
|
final boolean confirmedByRegistry = result.isSuccess();
|
||||||
|
if (!confirmedByRegistry) {
|
||||||
|
LOG.warn("verifyCin: accepting CIN {} on format for clientId={} — registry did not confirm it: {}",
|
||||||
|
cin, clientId, result.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
kyc.markCinVerified();
|
kyc.markCinVerified();
|
||||||
kyc.tryAutoVerify();
|
kyc.tryAutoVerify();
|
||||||
kycRepositoryWrapper.saveAndFlush(kyc);
|
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||||
@@ -537,6 +569,10 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
|
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
|
||||||
changes.put("cinVerified", true);
|
changes.put("cinVerified", true);
|
||||||
changes.put("cinVerifiedOn", kyc.getCinVerifiedOn());
|
changes.put("cinVerifiedOn", kyc.getCinVerifiedOn());
|
||||||
|
changes.put("cinConfirmedByRegistry", confirmedByRegistry);
|
||||||
|
if (!confirmedByRegistry) {
|
||||||
|
changes.put("cinVerificationMessage", "CIN format validated. MCA records could not confirm this company.");
|
||||||
|
}
|
||||||
changes.put("kycStatus", kyc.getKycStatusValue());
|
changes.put("kycStatus", kyc.getKycStatusValue());
|
||||||
return buildResult(command, client, clientId, kycId, changes);
|
return buildResult(command, client, clientId, kycId, changes);
|
||||||
}
|
}
|
||||||
@@ -701,6 +737,16 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
return s.trim().toLowerCase().replaceAll("[^a-z0-9]", "");
|
return s.trim().toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the client itself is a natural person. Read off the client's legal form rather than
|
||||||
|
* the KYC record's kycType or the request's kycType parameter: the legal form is the fact that
|
||||||
|
* decides whether the entity-only fields apply, it cannot be mislabelled by a caller, and it
|
||||||
|
* still classifies correctly for KYC rows written before the caller sent the right kycType.
|
||||||
|
*/
|
||||||
|
private boolean isIndividualClient(final Client client) {
|
||||||
|
return client.getLegalForm() == null || !LegalForm.fromInt(client.getLegalForm()).isEntity();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CIN, GSTIN and incorporation-date columns on m_client_kyc are only meaningful
|
* CIN, GSTIN and incorporation-date columns on m_client_kyc are only meaningful
|
||||||
* for NON_INDIVIDUAL clients. Reject any request that tries to set them on an
|
* for NON_INDIVIDUAL clients. Reject any request that tries to set them on an
|
||||||
@@ -976,11 +1022,10 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
|||||||
final ClientKyc kyc = kycRepositoryWrapper.findByClientIdWithNotFoundDetection(client.getId());
|
final ClientKyc kyc = kycRepositoryWrapper.findByClientIdWithNotFoundDetection(client.getId());
|
||||||
|
|
||||||
// Non-Individual-only fields (CIN / GSTIN / DIN etc) must not be passed for INDIVIDUAL KYC.
|
// Non-Individual-only fields (CIN / GSTIN / DIN etc) must not be passed for INDIVIDUAL KYC.
|
||||||
final boolean isIndividualExisting = kyc.getKycType() != null && kyc.getKycType().isIndividual();
|
|
||||||
final JsonElement parsedJson = this.fromApiJsonHelper.parse(kycJson);
|
final JsonElement parsedJson = this.fromApiJsonHelper.parse(kycJson);
|
||||||
final JsonCommand jsonCommand = JsonCommand.from(kycJson, parsedJson, this.fromApiJsonHelper,
|
final JsonCommand jsonCommand = JsonCommand.from(kycJson, parsedJson, this.fromApiJsonHelper,
|
||||||
"CLIENTKYC", null, null, null, client.getId(), null, null, null, null, null, null, null, null, null);
|
"CLIENTKYC", null, null, null, client.getId(), null, null, null, null, null, null, null, null, null);
|
||||||
enforceNonIndividualOnlyFields(jsonCommand, isIndividualExisting);
|
enforceNonIndividualOnlyFields(jsonCommand, isIndividualClient(client));
|
||||||
|
|
||||||
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
final ClientKycStatus currentStatus = kyc.getKycStatus();
|
||||||
final Map<String, Object> changes = kyc.update(jsonCommand);
|
final Map<String, Object> changes = kyc.update(jsonCommand);
|
||||||
|
|||||||
+72
@@ -79,10 +79,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
|||||||
private final ClientLookupMapper lookupMapper = new ClientLookupMapper();
|
private final ClientLookupMapper lookupMapper = new ClientLookupMapper();
|
||||||
private final ClientMembersOfGroupMapper membersOfGroupMapper = new ClientMembersOfGroupMapper();
|
private final ClientMembersOfGroupMapper membersOfGroupMapper = new ClientMembersOfGroupMapper();
|
||||||
private final ParentGroupsMapper clientGroupsMapper = new ParentGroupsMapper();
|
private final ParentGroupsMapper clientGroupsMapper = new ParentGroupsMapper();
|
||||||
|
private final ClientNonPersonDataMapper clientNonPersonMapper = new ClientNonPersonDataMapper();
|
||||||
|
|
||||||
private final ColumnValidator columnValidator;
|
private final ColumnValidator columnValidator;
|
||||||
private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper;
|
private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper;
|
||||||
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||||
|
private final org.apache.fineract.portfolio.mobilelookup.service.MobileLookupService mobileLookupService;
|
||||||
|
private final org.apache.fineract.portfolio.sanctions.service.SanctionsScreeningService sanctionsScreeningService;
|
||||||
private final ClientMapper clientMapper;
|
private final ClientMapper clientMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -227,6 +230,24 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
|||||||
final Client client = clientRepositoryWrapper.getClientByClientIdAndHierarchy(clientId, hierarchySearchString);
|
final Client client = clientRepositoryWrapper.getClientByClientIdAndHierarchy(clientId, hierarchySearchString);
|
||||||
final ClientData clientData = clientMapper.map(client);
|
final ClientData clientData = clientMapper.map(client);
|
||||||
|
|
||||||
|
// ClientMapper maps the Client entity, which holds no association to ClientNonPerson, so
|
||||||
|
// it declares clientNonPersonDetails as ignored. Without this the constitution, business
|
||||||
|
// line, incorporation number and the rest of an entity client's details are absent from
|
||||||
|
// GET /clients/{id} — the payload the edit and view screens load from — and every one of
|
||||||
|
// those fields comes back blank however carefully they were filled in at creation.
|
||||||
|
clientData.setClientNonPersonDetails(retrieveClientNonPersonDetails(clientId));
|
||||||
|
|
||||||
|
// dipVerified is the flag captured when the client was onboarded and never changes
|
||||||
|
// afterwards. Resolve the CURRENT DIP/MNRL status from the latest lookup so a number
|
||||||
|
// reported for fraud or reassigned since then shows as flagged on the client's page,
|
||||||
|
// instead of the page continuing to display the old "Verified".
|
||||||
|
clientData.setDipLiveStatus(this.mobileLookupService.getLatestStatus(client.getMobileNo()));
|
||||||
|
clientData.setAlternateDipLiveStatus(this.mobileLookupService.getLatestStatus(client.getAlternateMobileNo()));
|
||||||
|
|
||||||
|
// Same reasoning for sanctions: nameVerified records the outcome at onboarding, so a
|
||||||
|
// client added to a later UNSC snapshot would keep showing "Clear" on their own page.
|
||||||
|
clientData.setSanctionsAlertStatus(this.sanctionsScreeningService.getLatestAlertStatus(clientId));
|
||||||
|
|
||||||
// Get client collaterals
|
// Get client collaterals
|
||||||
final Collection<ClientCollateralManagement> clientCollateralManagements = this.clientCollateralManagementRepositoryWrapper
|
final Collection<ClientCollateralManagement> clientCollateralManagements = this.clientCollateralManagementRepositoryWrapper
|
||||||
.getCollateralsPerClient(clientId);
|
.getCollateralsPerClient(clientId);
|
||||||
@@ -254,6 +275,57 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entity-only block of a client, or null for an individual (or an entity whose details row
|
||||||
|
* was never written). Queried separately because {@link #retrieveOne} maps the Client entity via
|
||||||
|
* ClientMapper rather than the joined SQL the list endpoints use.
|
||||||
|
*/
|
||||||
|
private ClientNonPersonData retrieveClientNonPersonDetails(final Long clientId) {
|
||||||
|
final String sql = "select " + this.clientNonPersonMapper.schema();
|
||||||
|
final List<ClientNonPersonData> details = this.jdbcTemplate.query(sql, this.clientNonPersonMapper, // NOSONAR
|
||||||
|
clientId);
|
||||||
|
return details.isEmpty() ? null : details.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ClientNonPersonDataMapper implements RowMapper<ClientNonPersonData> {
|
||||||
|
|
||||||
|
public String schema() {
|
||||||
|
return " cnp.constitution_cv_id as constitutionId, cvConstitution.code_value as constitutionValue,"
|
||||||
|
+ " cnp.incorp_no as incorpNo, cnp.incorp_validity_till as incorpValidityTill,"
|
||||||
|
+ " cnp.main_business_line_cv_id as mainBusinessLineId,"
|
||||||
|
+ " cvMainBusinessLine.code_value as mainBusinessLineValue, cnp.remarks as remarks,"
|
||||||
|
+ " cnp.authorized_signatory as authorizedSignatory,"
|
||||||
|
+ " cnp.authorized_signatory_client_ids as authorizedSignatoryClientIds,"
|
||||||
|
+ " cnp.additional_info as additionalInfo,"
|
||||||
|
+ " cnp.date_of_registration as dateOfRegistration" + " from m_client_non_person cnp"
|
||||||
|
+ " left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id"
|
||||||
|
+ " left join m_code_value cvMainBusinessLine on cvMainBusinessLine.id = cnp.main_business_line_cv_id"
|
||||||
|
+ " where cnp.client_id = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientNonPersonData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
final Long constitutionId = JdbcSupport.getLong(rs, "constitutionId");
|
||||||
|
final String constitutionValue = rs.getString("constitutionValue");
|
||||||
|
final CodeValueData constitution = CodeValueData.instance(constitutionId, constitutionValue);
|
||||||
|
|
||||||
|
final Long mainBusinessLineId = JdbcSupport.getLong(rs, "mainBusinessLineId");
|
||||||
|
final String mainBusinessLineValue = rs.getString("mainBusinessLineValue");
|
||||||
|
final CodeValueData mainBusinessLine = CodeValueData.instance(mainBusinessLineId, mainBusinessLineValue);
|
||||||
|
|
||||||
|
final String incorpNo = rs.getString("incorpNo");
|
||||||
|
final LocalDate incorpValidityTill = JdbcSupport.getLocalDate(rs, "incorpValidityTill");
|
||||||
|
final String remarks = rs.getString("remarks");
|
||||||
|
final String authorizedSignatory = rs.getString("authorizedSignatory");
|
||||||
|
final String authorizedSignatoryClientIds = rs.getString("authorizedSignatoryClientIds");
|
||||||
|
final String additionalInfo = rs.getString("additionalInfo");
|
||||||
|
final LocalDate dateOfRegistration = JdbcSupport.getLocalDate(rs, "dateOfRegistration");
|
||||||
|
|
||||||
|
return new ClientNonPersonData(constitution, incorpNo, incorpValidityTill, mainBusinessLine, remarks,
|
||||||
|
authorizedSignatory, additionalInfo, dateOfRegistration, authorizedSignatoryClientIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<ClientData> retrieveAllForLookup(final String extraCriteria) {
|
public Collection<ClientData> retrieveAllForLookup(final String extraCriteria) {
|
||||||
|
|
||||||
|
|||||||
+107
-1
@@ -44,6 +44,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -134,6 +135,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
private final ClientRepository clientJpaRepository;
|
private final ClientRepository clientJpaRepository;
|
||||||
private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository;
|
private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository;
|
||||||
private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository;
|
private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository;
|
||||||
|
private final org.apache.fineract.portfolio.sanctions.service.SanctionsScreeningService sanctionsScreeningService;
|
||||||
|
private final org.apache.fineract.portfolio.mobilelookup.service.MobileLookupService mobileLookupService;
|
||||||
private final CifNumberGenerator cifNumberGenerator;
|
private final CifNumberGenerator cifNumberGenerator;
|
||||||
private final ClientContactOtpService clientContactOtpService;
|
private final ClientContactOtpService clientContactOtpService;
|
||||||
private final ClientChangeNotificationService clientChangeNotificationService;
|
private final ClientChangeNotificationService clientChangeNotificationService;
|
||||||
@@ -156,6 +159,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
this.clientNonPersonRepository.delete(clientNonPerson);
|
this.clientNonPersonRepository.delete(clientNonPerson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safely cascade-delete child records that have foreign keys pointing to this client
|
||||||
|
this.jdbcTemplate.update("delete from m_client_identifier where client_id = ?", clientId);
|
||||||
|
this.jdbcTemplate.update("delete from m_family_members where client_id = ?", clientId);
|
||||||
|
this.jdbcTemplate.update("delete from m_client_address where client_id = ?", clientId);
|
||||||
|
this.jdbcTemplate.update("delete from m_address where client_id = ?", clientId);
|
||||||
|
this.jdbcTemplate.update("delete from m_client_kyc where client_id = ?", clientId);
|
||||||
|
|
||||||
this.clientRepository.delete(client);
|
this.clientRepository.delete(client);
|
||||||
this.clientRepository.flush();
|
this.clientRepository.flush();
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -611,6 +621,18 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
newClient.setScreeningLogId(screeningLogId);
|
newClient.setScreeningLogId(screeningLogId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNSC sanctions gate. The UI disables Save & Next on a match, but that is a browser
|
||||||
|
// control only — screen server-side too, so a direct API call cannot onboard a listed
|
||||||
|
// person. The name is cleared only by a false positive the Principal Officer approved.
|
||||||
|
validateNotSanctioned(newClient.getDisplayName());
|
||||||
|
|
||||||
|
// DIP/MNRL gate — same reasoning: dipVerified arrives from the browser and cannot be
|
||||||
|
// trusted on its own. Record the status actually on file alongside it.
|
||||||
|
validateMobileNumberUsable(newClient.getMobileNo(), "mobileNo");
|
||||||
|
validateMobileNumberUsable(newClient.getAlternateMobileNo(), "alternateMobileNo");
|
||||||
|
newClient.setDipStatus(this.mobileLookupService.getLatestStatus(newClient.getMobileNo()));
|
||||||
|
newClient.setAlternateDipStatus(this.mobileLookupService.getLatestStatus(newClient.getAlternateMobileNo()));
|
||||||
|
|
||||||
// Account Number generation
|
// Account Number generation
|
||||||
this.clientRepository.saveAndFlush(newClient);
|
this.clientRepository.saveAndFlush(newClient);
|
||||||
if (StringUtils.isBlank(accountNo)) {
|
if (StringUtils.isBlank(accountNo)) {
|
||||||
@@ -756,6 +778,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
|
|
||||||
final String authorizedSignatory = this.fromApiJsonHelper
|
final String authorizedSignatory = this.fromApiJsonHelper
|
||||||
.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName, clientNonPersonElement);
|
.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName, clientNonPersonElement);
|
||||||
|
final String authorizedSignatoryClientIds = this.fromApiJsonHelper
|
||||||
|
.extractStringNamed(ClientApiConstants.authorizedSignatoryClientIdsParamName, clientNonPersonElement);
|
||||||
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName,
|
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName,
|
||||||
clientNonPersonElement);
|
clientNonPersonElement);
|
||||||
final LocalDate dateOfRegistration = this.fromApiJsonHelper
|
final LocalDate dateOfRegistration = this.fromApiJsonHelper
|
||||||
@@ -763,7 +787,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
|
|
||||||
final ClientNonPerson newClientNonPerson = ClientNonPerson.createNew(client, clientNonPersonConstitution,
|
final ClientNonPerson newClientNonPerson = ClientNonPerson.createNew(client, clientNonPersonConstitution,
|
||||||
clientNonPersonMainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory, additionalInfo,
|
clientNonPersonMainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory, additionalInfo,
|
||||||
dateOfRegistration);
|
dateOfRegistration, authorizedSignatoryClientIds);
|
||||||
|
|
||||||
this.clientNonPersonRepository.save(newClientNonPerson);
|
this.clientNonPersonRepository.save(newClientNonPerson);
|
||||||
}
|
}
|
||||||
@@ -884,12 +908,18 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
oldMobileNoForNotification = oldValue;
|
oldMobileNoForNotification = oldValue;
|
||||||
newMobileNoForNotification = newValue;
|
newMobileNoForNotification = newValue;
|
||||||
mobileNoChanged = true;
|
mobileNoChanged = true;
|
||||||
|
// Replacing the number IS the resolution for a DIP/MNRL hit, so close the open
|
||||||
|
// alert here. Left to the officer as a second manual step it would be forgotten,
|
||||||
|
// and the queue would fill with cases that were in fact already dealt with.
|
||||||
|
closeDipAlertForReplacedNumber(oldValue, newValue, clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.isChangeInStringParameterNamed(ClientApiConstants.alternateMobileNoParamName, clientForUpdate.getAlternateMobileNo())) {
|
if (command.isChangeInStringParameterNamed(ClientApiConstants.alternateMobileNoParamName, clientForUpdate.getAlternateMobileNo())) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.alternateMobileNoParamName);
|
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.alternateMobileNoParamName);
|
||||||
|
final String oldAlternate = clientForUpdate.getAlternateMobileNo();
|
||||||
changes.put(ClientApiConstants.alternateMobileNoParamName, newValue);
|
changes.put(ClientApiConstants.alternateMobileNoParamName, newValue);
|
||||||
clientForUpdate.setAlternateMobileNo(StringUtils.defaultIfEmpty(newValue, null));
|
clientForUpdate.setAlternateMobileNo(StringUtils.defaultIfEmpty(newValue, null));
|
||||||
|
closeDipAlertForReplacedNumber(oldAlternate, newValue, clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.isChangeInStringParameterNamed(ClientApiConstants.emailAddressParamName, clientForUpdate.getEmailAddress())) {
|
if (command.isChangeInStringParameterNamed(ClientApiConstants.emailAddressParamName, clientForUpdate.getEmailAddress())) {
|
||||||
@@ -1700,6 +1730,82 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
|||||||
/*
|
/*
|
||||||
* To become a part of a group, group may have set of criteria to be m et before client can become member of it.
|
* To become a part of a group, group may have set of criteria to be m et before client can become member of it.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Refuses onboarding of a name that hits the UN Security Council consolidated list.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Applies equally to individuals and non-individuals — the display name covers both, and the
|
||||||
|
* list itself holds INDIVIDUAL and ENTITY records. The only way past this is a false positive
|
||||||
|
* on the same name that the Principal Officer has approved, which is deliberately a different
|
||||||
|
* person's decision from the officer who reviewed the hit.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private void validateNotSanctioned(final String displayName) {
|
||||||
|
if (StringUtils.isBlank(displayName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.sanctionsScreeningService.hasApprovedFalsePositive(displayName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Non-logging check on purpose — see SanctionsScreeningService#isNameSanctioned.
|
||||||
|
if (this.sanctionsScreeningService.isNameSanctioned(displayName)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.client.sanctions.match",
|
||||||
|
"'" + displayName + "' matched the UN Security Council Consolidated List. Onboarding is blocked until the "
|
||||||
|
+ "Principal Officer clears the alert.",
|
||||||
|
displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuses a mobile number that the DoT Digital Intelligence Platform flagged, unless an
|
||||||
|
* officer explicitly recorded a decision to proceed with a written reason.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A number with no lookup on record passes: DIP verification is enforced in the onboarding
|
||||||
|
* flow, and failing here would block every legacy update that merely touches an old client.
|
||||||
|
* What this stops is the case that matters — a number the DoT has actively flagged being
|
||||||
|
* saved because the browser said it was fine.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Closes any open DIP/MNRL alert on a number that has just been replaced.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For an existing customer a flagged number is resolved by obtaining a new one — at which
|
||||||
|
* point the old alert describes a number the bank no longer uses. Closing it here keeps the
|
||||||
|
* decision queue a list of work actually outstanding rather than a growing pile of cases that
|
||||||
|
* were already handled. Failure is logged, never propagated: the client update itself must
|
||||||
|
* not fail because a bookkeeping row could not be stamped.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private void closeDipAlertForReplacedNumber(final String oldNumber, final String newNumber, final Long clientId) {
|
||||||
|
if (StringUtils.isBlank(oldNumber) || StringUtils.equals(oldNumber, newNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final String reason = StringUtils.isBlank(newNumber)
|
||||||
|
? "Number removed from the client record on resolution of the DIP/MNRL alert."
|
||||||
|
: "Number replaced with " + newNumber + " on resolution of the DIP/MNRL alert.";
|
||||||
|
this.mobileLookupService.closeOpenAlerts(oldNumber.trim(), clientId, reason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Could not close DIP alert for replaced number {} on client {}: {}", oldNumber, clientId,
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateMobileNumberUsable(final String mobileNo, final String parameter) {
|
||||||
|
if (StringUtils.isBlank(mobileNo)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.mobileLookupService.isMobileNumberUsable(mobileNo.trim())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.client.mobile.dip.flagged",
|
||||||
|
"Mobile number " + mobileNo + " is flagged on the DoT Digital Intelligence Platform. Record a decision "
|
||||||
|
+ "to proceed, with reasons, before saving this client.",
|
||||||
|
parameter);
|
||||||
|
}
|
||||||
|
|
||||||
private void validateParentGroupRulesBeforeClientActivation(Client client) {
|
private void validateParentGroupRulesBeforeClientActivation(Client client) {
|
||||||
Integer minNumberOfClients = configurationDomainService.retrieveMinAllowedClientsInGroup();
|
Integer minNumberOfClients = configurationDomainService.retrieveMinAllowedClientsInGroup();
|
||||||
Integer maxNumberOfClients = configurationDomainService.retrieveMaxAllowedClientsInGroup();
|
Integer maxNumberOfClients = configurationDomainService.retrieveMaxAllowedClientsInGroup();
|
||||||
|
|||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 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 org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface BatchRunRepository extends JpaRepository<BatchRun, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,227 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* 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
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user