DDS: Agent settlement (Agent Tran), Close Party account, reports (Supplementary-DDS/Master Data Export/Close Register), Party-Savings link, product category field
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
tarunmalviya
2026-08-25 10:33:37 +05:30
parent 9eac40bb5c
commit 37c961f713
101 changed files with 8305 additions and 1 deletions
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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();
}
@@ -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
}
}
@@ -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);
}
@@ -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));
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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> {
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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.portfolio.dds.report.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/dds/reports/{reportKey}")
@Component
@Tag(name = "DDS Reports", description = "The DDS reports suite - one shared query engine behind a generic column/row shape, filtered by date range and agent/party id range.")
@RequiredArgsConstructor
public class DdsReportsApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "DDSREPORT";
private final PlatformSecurityContext context;
private final DdsReportReadPlatformService readPlatformService;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a DDS Report", description = "reportKey is one of: AGENT_CHECK_LIST, PARTY_CHECK_LIST, PARTY_BALANCE, "
+ "AGENT_WISE_BALANCE, AGENT_WISE_MONTHLY_COLLECTION, COMMISSION_REGISTER, COLLECTION_SUMMARY_AGENT_WISE, "
+ "AGENT_PARTYWISE_DAILY_COLLECTION, DAILY_COLLECTION_REPORT, PARTY_INTEREST_REGISTER, BALANCE_SUMMARY, "
+ "TRANSACTION_REGISTER, MATURE_DATE_WISE, INTEREST_CERTIFICATE, AGENT_COMMISSION_STATEMENT, SUPPLEMENTARY_DDS, "
+ "CLOSE_PARTY_AC_REGISTER, MASTER_DATA_EXPORT (the last three always return available=false - no matching data exists "
+ "in this system yet). "
+ "fromDate/toDate are ISO (yyyy-MM-dd); agentFrom/To and partyFrom/To default to the full id range.")
public DdsReportData runReport(@PathParam("reportKey") @Parameter(description = "reportKey") final String reportKey,
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate,
@DefaultValue("1") @QueryParam("agentFrom") @Parameter(description = "agentFrom") final Long agentFrom,
@DefaultValue("9223372036854775807") @QueryParam("agentTo") @Parameter(description = "agentTo") final Long agentTo,
@DefaultValue("1") @QueryParam("partyFrom") @Parameter(description = "partyFrom") final Long partyFrom,
@DefaultValue("9223372036854775807") @QueryParam("partyTo") @Parameter(description = "partyTo") final Long partyTo) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final LocalDate from = fromDate != null ? LocalDate.parse(fromDate) : LocalDate.of(1900, 1, 1);
final LocalDate to = toDate != null ? LocalDate.parse(toDate) : LocalDate.of(9999, 12, 31);
return readPlatformService.runReport(reportKey, from, to, agentFrom, agentTo, partyFrom, partyTo);
}
}
@@ -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.report.service;
import java.time.LocalDate;
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
public interface DdsReportReadPlatformService {
DdsReportData runReport(String reportKey, LocalDate fromDate, LocalDate toDate, Long agentFrom, Long agentTo, Long partyFrom,
Long partyTo);
}
@@ -0,0 +1,313 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.report.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.portfolio.dds.report.data.DdsReportData;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* One switch over every DDS report key. Each branch is a real query against data this system
* actually has (Agent/Party/PartyTransaction/CommissionSlab/BatchRun).
*
* Since SubAgent was folded into Agent (2026-08-24, Agent now does the daily collection work
* directly), the queries here dropped a join level wherever they used to go Party/Transaction ->
* SubAgent -> Agent - they now join straight to Agent.
*/
@RequiredArgsConstructor
public class DdsReportReadPlatformServiceImpl implements DdsReportReadPlatformService {
private final JdbcTemplate jdbcTemplate;
@Override
public DdsReportData runReport(final String reportKey, final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo) {
return switch (reportKey) {
case "AGENT_CHECK_LIST" -> agentCheckList();
case "PARTY_CHECK_LIST", "PARTY_BALANCE" -> partyList(agentFrom, agentTo, partyFrom, partyTo);
case "AGENT_WISE_BALANCE" -> agentWiseBalance(agentFrom, agentTo);
case "AGENT_WISE_MONTHLY_COLLECTION" -> agentWiseCollection(fromDate, toDate, agentFrom, agentTo);
case "COMMISSION_REGISTER" -> commissionRegister(fromDate, toDate, agentFrom, agentTo);
case "COLLECTION_SUMMARY_AGENT_WISE" -> collectionSummaryAgentWise(fromDate, toDate, agentFrom, agentTo);
case "AGENT_PARTYWISE_DAILY_COLLECTION" -> agentPartywiseDailyCollection(fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
case "DAILY_COLLECTION_REPORT" -> dailyCollectionReport(fromDate, toDate, agentFrom, agentTo);
case "PARTY_INTEREST_REGISTER" -> partyInterestRegister(fromDate, toDate, partyFrom, partyTo);
case "BALANCE_SUMMARY" -> balanceSummary(agentFrom, agentTo, partyFrom, partyTo);
case "TRANSACTION_REGISTER" -> transactionRegister(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
case "MATURE_DATE_WISE" -> matureDateWise(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
case "INTEREST_CERTIFICATE" -> interestCertificate(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
case "AGENT_COMMISSION_STATEMENT" -> agentCommissionStatement(fromDate, toDate);
case "SUPPLEMENTARY_DDS" -> supplementaryDds(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
case "CLOSE_PARTY_AC_REGISTER" -> closePartyAcRegister(fromDate, toDate, agentFrom, agentTo, partyFrom, partyTo);
case "MASTER_DATA_EXPORT" -> masterDataExport(agentFrom, agentTo, partyFrom, partyTo);
default -> throw new UnrecognizedQueryParamException("reportKey", reportKey);
};
}
private DdsReportData agentCheckList() {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", a.agent_city as \"City\", "
+ "a.agent_current_balance as \"Current Balance\" from m_dds_agent a order by a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql);
return available("AGENT_CHECK_LIST", List.of("Agent Code", "Agent Name", "City", "Current Balance"), rows);
}
private DdsReportData partyList(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
+ "a.agent_code as \"Agent Code\", p.current_balance as \"Current Balance\", "
+ "p.opening_date as \"Opening Date\" from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
+ "where a.id between ? and ? and p.id between ? and ? order by p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo, partyFrom, partyTo);
return available("PARTY_CHECK_LIST", List.of("Party Code", "Party Name", "Agent Code", "Current Balance", "Opening Date"), rows);
}
private DdsReportData agentWiseBalance(final Long agentFrom, final Long agentTo) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", "
+ "a.agent_current_balance as \"Current Balance\" from m_dds_agent a where a.id between ? and ? "
+ "order by a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo);
return available("AGENT_WISE_BALANCE", List.of("Agent Code", "Name", "Current Balance"), rows);
}
private DdsReportData agentWiseCollection(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", "
+ "coalesce(sum(t.amount), 0) as \"Total Collection\" from m_dds_agent a "
+ "left join m_dds_party_transaction t on t.agent_id = a.id and t.transaction_type = 'PARTY_TRAN' "
+ "and t.direction = 'CREDIT' and t.transaction_date between ? and ? " + "where a.id between ? and ? "
+ "group by a.id, a.agent_code, a.agent_name order by a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
return available("AGENT_WISE_MONTHLY_COLLECTION", List.of("Agent Code", "Name", "Total Collection"), rows);
}
private DdsReportData commissionRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Name\", b.job_date as \"Job Date\", "
+ "b.posted_on_date as \"Posted On\", r.amount as \"Commission Amount\" from m_dds_batch_run_row r "
+ "join m_dds_batch_run b on b.id = r.batch_run_id join m_dds_agent a on a.id = r.agent_id "
+ "where b.job_type = 'COMMISSION_CALCULATION' and b.status = 'POSTED' and b.posted_on_date between ? and ? "
+ "and a.id between ? and ? order by b.posted_on_date, a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
return available("COMMISSION_REGISTER", List.of("Agent Code", "Name", "Job Date", "Posted On", "Commission Amount"), rows);
}
private DdsReportData collectionSummaryAgentWise(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
+ "coalesce(sum(t.amount), 0) as \"Total Collection\" from m_dds_agent a "
+ "left join m_dds_party_transaction t on t.agent_id = a.id and t.transaction_type = 'PARTY_TRAN' "
+ "and t.direction = 'CREDIT' and t.transaction_date between ? and ? " + "where a.id between ? and ? "
+ "group by a.id, a.agent_code, a.agent_name order by a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
return available("COLLECTION_SUMMARY_AGENT_WISE", List.of("Agent Code", "Agent Name", "Total Collection"), rows);
}
private DdsReportData agentPartywiseDailyCollection(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select a.agent_code as \"Agent Code\", p.party_code as \"Party Code\", "
+ "t.transaction_date as \"Transaction Date\", t.amount as \"Amount\", "
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
+ "join m_dds_party p on p.id = t.party_id join m_dds_agent a on a.id = t.agent_id "
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
+ "and a.id between ? and ? and p.id between ? and ? order by t.transaction_date, a.agent_code, p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("AGENT_PARTYWISE_DAILY_COLLECTION",
List.of("Agent Code", "Party Code", "Transaction Date", "Amount", "Balance After"), rows);
}
private DdsReportData dailyCollectionReport(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo) {
final String sql = "select t.transaction_date as \"Transaction Date\", a.agent_code as \"Agent Code\", "
+ "coalesce(sum(t.amount), 0) as \"Total Collection\", count(*) as \"Transaction Count\" "
+ "from m_dds_party_transaction t join m_dds_agent a on a.id = t.agent_id "
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
+ "and a.id between ? and ? group by t.transaction_date, a.id, a.agent_code "
+ "order by t.transaction_date, a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo);
return available("DAILY_COLLECTION_REPORT",
List.of("Transaction Date", "Agent Code", "Total Collection", "Transaction Count"), rows);
}
private DdsReportData partyInterestRegister(final LocalDate fromDate, final LocalDate toDate, final Long partyFrom,
final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", t.transaction_date as \"Transaction Date\", "
+ "t.transaction_type as \"Transaction Type\", t.amount as \"Amount\", "
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
+ "join m_dds_party p on p.id = t.party_id "
+ "where t.transaction_type in ('INTEREST_POST', 'OD_INTEREST_POST') and t.transaction_date between ? and ? "
+ "and p.id between ? and ? order by t.transaction_date, p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, partyFrom, partyTo);
return available("PARTY_INTEREST_REGISTER",
List.of("Party Code", "Transaction Date", "Transaction Type", "Amount", "Balance After"), rows);
}
private DdsReportData balanceSummary(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
final Integer totalAgents = this.jdbcTemplate.queryForObject("select count(*) from m_dds_agent where id between ? and ?",
Integer.class, agentFrom, agentTo);
final Integer totalParties = this.jdbcTemplate.queryForObject(
"select count(*) from m_dds_party where agent_id between ? and ? and id between ? and ?", Integer.class, agentFrom,
agentTo, partyFrom, partyTo);
final BigDecimal totalBalance = this.jdbcTemplate.queryForObject(
"select coalesce(sum(current_balance), 0) from m_dds_party where agent_id between ? and ? and id between ? and ?",
BigDecimal.class, agentFrom, agentTo, partyFrom, partyTo);
final Map<String, Object> row = Map.of("Total Agents", totalAgents, "Total Parties", totalParties, "Total Party Balance",
totalBalance);
return available("BALANCE_SUMMARY", List.of("Total Agents", "Total Parties", "Total Party Balance"), List.of(row));
}
private DdsReportData transactionRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", a.agent_code as \"Agent Code\", "
+ "t.transaction_date as \"Transaction Date\", t.transaction_type as \"Transaction Type\", "
+ "t.direction as \"Direction\", t.amount as \"Amount\", t.description as \"Description\" "
+ "from m_dds_party_transaction t join m_dds_party p on p.id = t.party_id "
+ "join m_dds_agent a on a.id = t.agent_id "
+ "where t.transaction_date between ? and ? and a.id between ? and ? and p.id between ? and ? "
+ "order by t.transaction_date, t.id";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("TRANSACTION_REGISTER",
List.of("Party Code", "Agent Code", "Transaction Date", "Transaction Type", "Direction", "Amount", "Description"), rows);
}
private DdsReportData matureDateWise(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom, final Long agentTo,
final Long partyFrom, final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", p.maturity_date as \"Maturity Date\", "
+ "p.current_balance as \"Current Balance\" from m_dds_party p "
+ "where p.maturity_date is not null and p.maturity_date between ? and ? and p.agent_id between ? and ? "
+ "and p.id between ? and ? order by p.maturity_date";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("MATURE_DATE_WISE", List.of("Party Code", "Maturity Date", "Current Balance"), rows);
}
private DdsReportData interestCertificate(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
+ "p.opening_date as \"Opening Date\", p.daily_deposit_amount as \"Daily Deposit Amount\", "
+ "p.current_balance as \"Current Balance\", coalesce((select sum(t.amount) from m_dds_party_transaction t "
+ "where t.party_id = p.id and t.transaction_type in ('INTEREST_POST', 'OD_INTEREST_POST') "
+ "and t.transaction_date between ? and ?), 0) as \"Interest Earned\" from m_dds_party p "
+ "where p.agent_id between ? and ? and p.id between ? and ? order by p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("INTEREST_CERTIFICATE",
List.of("Party Code", "Party Name", "Opening Date", "Daily Deposit Amount", "Current Balance", "Interest Earned"), rows);
}
/**
* Per-agent settlement statement for a date range: collection volume, commission actually
* posted in that window (via the same BatchRunRow join as COMMISSION_REGISTER), and the
* deposit/advance/deduction fields tracked directly on the agent. "Deduct Commission" and
* "PC Diff" have no corresponding data anywhere in this schema yet, so they report as 0 rather
* than a guessed figure - matching the legacy sample where most of these columns were 0.00 too.
* No agent id range filter is applied here: the legacy screenshot for this report only ever
* showed a date range, no agent-range inputs.
*/
private DdsReportData agentCommissionStatement(final LocalDate fromDate, final LocalDate toDate) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
+ "a.agent_saving_account_no as \"A/c No\", coalesce(collection.total, 0) as \"Total Collection\", "
+ "coalesce(commission.total, 0) as \"Commission\", 0 as \"Deduct Commission\", "
+ "a.agent_deposit as \"Agent Deposit\", a.festival_advance as \"Festival Advance\", "
+ "a.other_deduction as \"Other Deduction\", "
+ "(coalesce(commission.total, 0) - a.agent_deposit - a.festival_advance - a.other_deduction) as \"Total\", "
+ "0 as \"PC Diff\" " + "from m_dds_agent a "
+ "left join (select t.agent_id as agent_id, sum(t.amount) as total from m_dds_party_transaction t "
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
+ "group by t.agent_id) collection on collection.agent_id = a.id "
+ "left join (select r.agent_id as agent_id, sum(r.amount) as total from m_dds_batch_run_row r "
+ "join m_dds_batch_run b on b.id = r.batch_run_id "
+ "where b.job_type = 'COMMISSION_CALCULATION' and b.status = 'POSTED' and b.posted_on_date between ? and ? "
+ "group by r.agent_id) commission on commission.agent_id = a.id " + "order by a.agent_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, fromDate, toDate);
return available("AGENT_COMMISSION_STATEMENT",
List.of("Agent Code", "Agent Name", "A/c No", "Total Collection", "Commission", "Deduct Commission", "Agent Deposit",
"Festival Advance", "Other Deduction", "Total", "PC Diff"),
rows);
}
/**
* The legacy Supplementary-DDS (As On) report: every regular collection (PARTY_TRAN, CREDIT)
* posted in the given date range - clarified with the business as "the regular transactions
* going on" (2026-08-24), same shape as the other collection-listing reports.
*/
private DdsReportData supplementaryDds(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom, final Long agentTo,
final Long partyFrom, final Long partyTo) {
final String sql = "select a.agent_code as \"Agent Code\", p.party_code as \"Party Code\", p.party_name as \"Party Name\", "
+ "t.transaction_date as \"Transaction Date\", t.transaction_mode as \"Mode\", t.amount as \"Amount\", "
+ "t.balance_after_transaction as \"Balance After\" from m_dds_party_transaction t "
+ "join m_dds_party p on p.id = t.party_id join m_dds_agent a on a.id = t.agent_id "
+ "where t.transaction_type = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transaction_date between ? and ? "
+ "and a.id between ? and ? and p.id between ? and ? order by t.transaction_date, a.agent_code, p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("SUPPLEMENTARY_DDS",
List.of("Agent Code", "Party Code", "Party Name", "Transaction Date", "Mode", "Amount", "Balance After"), rows);
}
/**
* The legacy Close Party Ac Register report: Parties closed (via the Close Party command) in the
* given date range, with their close date and final (zero) balance.
*/
private DdsReportData closePartyAcRegister(final LocalDate fromDate, final LocalDate toDate, final Long agentFrom,
final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select p.party_code as \"Party Code\", p.party_name as \"Party Name\", a.agent_code as \"Agent Code\", "
+ "p.opening_date as \"Opening Date\", p.close_date as \"Close Date\", p.current_balance as \"Final Balance\" "
+ "from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
+ "where p.status = 'CLOSED' and p.close_date between ? and ? and a.id between ? and ? and p.id between ? and ? "
+ "order by p.close_date, p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, fromDate, toDate, agentFrom, agentTo, partyFrom,
partyTo);
return available("CLOSE_PARTY_AC_REGISTER",
List.of("Party Code", "Party Name", "Agent Code", "Opening Date", "Close Date", "Final Balance"), rows);
}
/**
* The legacy Master Data Export report: clarified with the business (2026-08-24) as "select an
* agent, show the full detail of every party under that agent" - filtered by the same agent
* range as every other report here (a single agent is just agentFrom == agentTo).
*/
private DdsReportData masterDataExport(final Long agentFrom, final Long agentTo, final Long partyFrom, final Long partyTo) {
final String sql = "select a.agent_code as \"Agent Code\", a.agent_name as \"Agent Name\", "
+ "p.party_code as \"Party Code\", p.party_name as \"Party Name\", p.address1 as \"Address\", "
+ "p.city as \"City\", p.pincode as \"Pincode\", p.mobile_no_sms as \"Mobile No\", "
+ "p.aadhar_no as \"Aadhar No\", p.pan_card_no as \"PAN No\", p.opening_date as \"Opening Date\", "
+ "p.maturity_date as \"Maturity Date\", p.daily_deposit_amount as \"Daily Deposit Amount\", "
+ "p.interest_rate as \"Interest Rate\", p.current_balance as \"Current Balance\" "
+ "from m_dds_party p join m_dds_agent a on a.id = p.agent_id "
+ "where a.id between ? and ? and p.id between ? and ? order by a.agent_code, p.party_code";
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(sql, agentFrom, agentTo, partyFrom, partyTo);
return available("MASTER_DATA_EXPORT",
List.of("Agent Code", "Agent Name", "Party Code", "Party Name", "Address", "City", "Pincode", "Mobile No", "Aadhar No",
"PAN No", "Opening Date", "Maturity Date", "Daily Deposit Amount", "Interest Rate", "Current Balance"),
rows);
}
private DdsReportData available(final String reportKey, final List<String> columns, final List<Map<String, Object>> rows) {
return DdsReportData.builder().reportKey(reportKey).available(true).columns(columns).rows(rows).build();
}
}
@@ -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.portfolio.dds.report.starter;
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformService;
import org.apache.fineract.portfolio.dds.report.service.DdsReportReadPlatformServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class DdsReportConfiguration {
@Bean
@ConditionalOnMissingBean(DdsReportReadPlatformService.class)
public DdsReportReadPlatformService ddsReportReadPlatformService(JdbcTemplate jdbcTemplate) {
return new DdsReportReadPlatformServiceImpl(jdbcTemplate);
}
}
@@ -0,0 +1,90 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/dds/partytransactions")
@Component
@Tag(name = "DDS Party Transactions", description = "The Daily Transaction Entry ledger: a day's PARTY_TRAN collection rows, the two legs of a WITHDRAW_DEPOSITE transfer between two parties, or an AGENT_TRAN settlement of an Agent's collected cash to its branch. Immutable once posted - reversed with an opposing entry, never edited.")
@RequiredArgsConstructor
public class PartyTransactionsApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "PARTYTRANSACTION";
private final PlatformSecurityContext context;
private final PartyTransactionReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve DDS Party Transactions", description = "Returns the list of posted DDS party transactions, most recent first. Optionally filter by agentId and/or partyId.")
public Collection<PartyTransactionData> retrieveAll(@QueryParam("agentId") @Parameter(description = "agentId") final Long agentId,
@QueryParam("partyId") @Parameter(description = "partyId") final Long partyId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return readPlatformService.retrieveAll(agentId, partyId);
}
@GET
@Path("{transactionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a DDS Party Transaction", description = "Returns the details of a single posted DDS party transaction.")
public PartyTransactionData retrieveOne(@PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return readPlatformService.retrieveOne(transactionId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Post DDS Party Transactions", description = "Mandatory fields: agentId, transactionType (PARTY_TRAN, "
+ "WITHDRAW_DEPOSITE or AGENT_TRAN), transactionDate\n" + "\n"
+ "For PARTY_TRAN: collections (array of {partyCode, transactionMode, accountType, accountNo, amount, chequeNo, tokenNo, "
+ "description})\n" + "\n" + "For WITHDRAW_DEPOSITE: creditorPartyCode, debitPartyCode, withdrawAmount, withdrawDescription\n"
+ "\n" + "For AGENT_TRAN: amount, transactionMode, accountType, accountNo, description")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createPartyTransaction() //
.withJson(apiRequestBodyAsJson) //
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
}
@@ -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.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PartyTransactionRepository extends JpaRepository<PartyTransaction, Long> {
@Query("select coalesce(sum(t.amount), 0) from PartyTransaction t where t.agent.id = :agentId "
+ "and t.transactionType = 'PARTY_TRAN' and t.direction = 'CREDIT' and t.transactionDate between :fromDate and :toDate")
BigDecimal sumPartyTranCredits(@Param("agentId") Long agentId, @Param("fromDate") LocalDate fromDate,
@Param("toDate") LocalDate toDate);
}
@@ -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.transaction.handler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "PARTYTRANSACTION", action = "CREATE")
public class CreatePartyTransactionCommandHandler implements NewCommandSourceHandler {
private final PartyTransactionWritePlatformService writePlatformService;
@Autowired
public CreatePartyTransactionCommandHandler(final PartyTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createPartyTransaction(command);
}
}
@@ -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.transaction.serialization;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* PARTY_TRAN (batch collection rows), WITHDRAW_DEPOSITE (a transfer between two parties) and
* AGENT_TRAN (an Agent settling its collected cash to a branch, no party involved) are
* validated/processed - the three transaction types the Daily Transaction Entry screen actually
* collects fields for today. PARTY_PAYMENT / OD_ENTRY / OD_PAYMENT are listed on the frontend as
* not-yet-implemented placeholders, so they are rejected here rather than silently accepted with no
* real effect.
*/
@Component
public final class PartyTransactionCommandFromApiJsonDeserializer {
private static final Set<String> SUPPORTED_TRANSACTION_TYPES = new HashSet<>(
Arrays.asList("PARTY_TRAN", "WITHDRAW_DEPOSITE", "AGENT_TRAN"));
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("agentId", "transactionType",
"transactionDate", "collections", "creditorPartyCode", "debitPartyCode", "withdrawAmount", "withdrawDescription",
"amount", "transactionMode", "accountType", "accountNo", "description", "dateFormat", "locale"));
private static final Set<String> SUPPORTED_COLLECTION_PARAMETERS = new HashSet<>(Arrays.asList("partyCode", "transactionMode",
"accountType", "accountNo", "amount", "chequeNo", "tokenNo", "description"));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public PartyTransactionCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(dataValidationErrors).resource("partytransaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
v.reset().parameter("agentId").value(this.fromApiJsonHelper.extractLongNamed("agentId", element)).notNull()
.integerGreaterThanZero();
final String transactionType = this.fromApiJsonHelper.extractStringNamed("transactionType", element);
v.reset().parameter("transactionType").value(transactionType).notBlank().isOneOfTheseStringValues(
SUPPORTED_TRANSACTION_TYPES.toArray());
v.reset().parameter("transactionDate").value(this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element)).notNull();
if ("PARTY_TRAN".equals(transactionType)) {
if (!this.fromApiJsonHelper.parameterExists("collections", element)) {
v.reset().parameter("collections").value(null).notNull();
} else {
final JsonObject topLevel = element.getAsJsonObject();
final var collectionsArray = topLevel.getAsJsonArray("collections");
v.reset().parameter("collections").value(collectionsArray).jsonArrayNotEmpty();
for (int i = 0; i < collectionsArray.size(); i++) {
final JsonObject row = collectionsArray.get(i).getAsJsonObject();
this.fromApiJsonHelper.checkForUnsupportedParameters(row, SUPPORTED_COLLECTION_PARAMETERS);
v.reset().parameterAtIndexArray("partyCode", i).value(extractString(row, "partyCode")).notBlank();
v.reset().parameterAtIndexArray("accountNo", i).value(extractString(row, "accountNo")).notBlank();
v.reset().parameterAtIndexArray("amount", i).value(extractBigDecimal(row, "amount")).notNull().positiveAmount();
}
}
} else if ("WITHDRAW_DEPOSITE".equals(transactionType)) {
v.reset().parameter("creditorPartyCode").value(this.fromApiJsonHelper.extractStringNamed("creditorPartyCode", element))
.notBlank();
v.reset().parameter("debitPartyCode").value(this.fromApiJsonHelper.extractStringNamed("debitPartyCode", element))
.notBlank();
v.reset().parameter("withdrawAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element))
.notNull().positiveAmount();
} else if ("AGENT_TRAN".equals(transactionType)) {
v.reset().parameter("amount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element)).notNull()
.positiveAmount();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private String extractString(final JsonObject json, final String paramName) {
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
}
private java.math.BigDecimal extractBigDecimal(final JsonObject json, final String paramName) {
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsBigDecimal() : null;
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -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.transaction.service;
import java.util.Collection;
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
public interface PartyTransactionReadPlatformService {
Collection<PartyTransactionData> retrieveAll(Long agentId, Long partyId);
PartyTransactionData retrieveOne(Long transactionId);
}
@@ -0,0 +1,98 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.security.utils.SQLBuilder;
import org.apache.fineract.portfolio.dds.transaction.data.PartyTransactionData;
import org.apache.fineract.portfolio.dds.transaction.exception.PartyTransactionNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
@RequiredArgsConstructor
public class PartyTransactionReadPlatformServiceImpl implements PartyTransactionReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private static final PartyTransactionMapper MAPPER = new PartyTransactionMapper();
private static final class PartyTransactionMapper implements RowMapper<PartyTransactionData> {
public String schema() {
return " t.id as id, t.party_id as partyId, p.party_code as partyCode, p.party_name as partyName, "
+ " t.agent_id as agentId, a.agent_code as agentCode, t.transaction_date as transactionDate, "
+ " t.transaction_type as transactionType, t.direction as direction, t.transaction_mode as transactionMode, "
+ " t.account_type as accountType, t.account_no as accountNo, t.amount as amount, t.cheque_no as chequeNo, "
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction "
+ " from m_dds_party_transaction t left join m_dds_party p on p.id = t.party_id "
+ " join m_dds_agent a on a.id = t.agent_id";
}
@Override
public PartyTransactionData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return PartyTransactionData.builder() //
.id(rs.getLong("id")) //
.partyId(JdbcSupport.getLongDefaultToNullIfZero(rs, "partyId")) //
.partyCode(rs.getString("partyCode")) //
.partyName(rs.getString("partyName")) //
.agentId(rs.getLong("agentId")) //
.agentCode(rs.getString("agentCode")) //
.transactionDate(JdbcSupport.getLocalDate(rs, "transactionDate")) //
.transactionType(rs.getString("transactionType")) //
.direction(rs.getString("direction")) //
.transactionMode(rs.getString("transactionMode")) //
.accountType(rs.getString("accountType")) //
.accountNo(rs.getString("accountNo")) //
.amount(rs.getBigDecimal("amount")) //
.chequeNo(rs.getString("chequeNo")) //
.tokenNo(rs.getString("tokenNo")) //
.description(rs.getString("description")) //
.balanceAfterTransaction(rs.getBigDecimal("balanceAfterTransaction")) //
.build();
}
}
@Override
public Collection<PartyTransactionData> retrieveAll(final Long agentId, final Long partyId) {
final SQLBuilder extraCriteria = new SQLBuilder();
extraCriteria.addNonNullCriteria(" t.agent_id = ", agentId);
extraCriteria.addNonNullCriteria(" t.party_id = ", partyId);
String sql = "select " + MAPPER.schema() + " " + extraCriteria.getSQLTemplate();
sql = sql + " order by t.id desc";
return this.jdbcTemplate.query(sql, MAPPER, extraCriteria.getArguments()); // NOSONAR
}
@Override
public PartyTransactionData retrieveOne(final Long transactionId) {
try {
final String sql = "select " + MAPPER.schema() + " where t.id = ?";
return this.jdbcTemplate.queryForObject(sql, MAPPER, transactionId); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new PartyTransactionNotFoundException(transactionId, e);
}
}
}
@@ -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.portfolio.dds.transaction.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface PartyTransactionWritePlatformService {
CommandProcessingResult createPartyTransaction(JsonCommand command);
}
@@ -0,0 +1,177 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jakarta.persistence.PersistenceException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.organisation.agent.domain.Agent;
import org.apache.fineract.organisation.agent.domain.AgentRepository;
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
import org.apache.fineract.portfolio.dds.party.domain.Party;
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
import org.apache.fineract.portfolio.dds.party.exception.PartyAlreadyClosedException;
import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientAgentBalanceException;
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientPartyBalanceException;
import org.apache.fineract.portfolio.dds.transaction.serialization.PartyTransactionCommandFromApiJsonDeserializer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class PartyTransactionWritePlatformServiceJpaRepositoryImpl implements PartyTransactionWritePlatformService {
private final PartyTransactionCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final PartyTransactionRepository partyTransactionRepository;
private final PartyRepository partyRepository;
private final AgentRepository agentRepository;
@Transactional
@Override
public CommandProcessingResult createPartyTransaction(final JsonCommand command) {
try {
this.fromApiJsonDeserializer.validateForCreate(command.json());
final Long agentId = command.longValueOfParameterNamed("agentId");
final Agent agent = this.agentRepository.findById(agentId)
.orElseThrow(() -> new AgentNotFoundException(agentId));
final String transactionType = command.stringValueOfParameterNamed("transactionType");
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final List<PartyTransaction> posted = new ArrayList<>();
if ("PARTY_TRAN".equals(transactionType)) {
posted.addAll(postCollections(agent, transactionDate, command));
} else if ("AGENT_TRAN".equals(transactionType)) {
posted.add(postAgentSettlement(agent, transactionDate, command));
} else {
posted.addAll(postWithdrawDeposite(agent, transactionDate, command));
}
this.partyTransactionRepository.saveAllAndFlush(posted);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(posted.isEmpty() ? null : posted.get(0).getId()) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleDataIntegrityIssues(dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(throwable, dve);
return CommandProcessingResult.empty();
}
}
private List<PartyTransaction> postCollections(final Agent agent, final LocalDate transactionDate, final JsonCommand command) {
final List<PartyTransaction> transactions = new ArrayList<>();
final JsonArray collectionsArray = command.arrayOfParameterNamed("collections");
for (int i = 0; i < collectionsArray.size(); i++) {
final JsonObject row = collectionsArray.get(i).getAsJsonObject();
final String partyCode = row.get("partyCode").getAsString();
final Party party = this.partyRepository.findByPartyCode(partyCode)
.orElseThrow(() -> new PartyNotFoundException(partyCode));
if (party.isClosed()) {
throw new PartyAlreadyClosedException(partyCode);
}
final BigDecimal amount = row.get("amount").getAsBigDecimal();
final BigDecimal newBalance = party.credit(amount);
agent.credit(amount);
transactions.add(new PartyTransaction(party, agent, transactionDate, "PARTY_TRAN", "CREDIT",
stringOrNull(row, "transactionMode"), stringOrNull(row, "accountType"), stringOrNull(row, "accountNo"), amount,
stringOrNull(row, "chequeNo"), stringOrNull(row, "tokenNo"), stringOrNull(row, "description"), newBalance));
}
return transactions;
}
private PartyTransaction postAgentSettlement(final Agent agent, final LocalDate transactionDate, final JsonCommand command) {
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount");
if (agent.getAgentCurrentBalance().compareTo(amount) < 0) {
throw new InsufficientAgentBalanceException(agent.getAgentCode());
}
final BigDecimal newBalance = agent.settle(amount);
return new PartyTransaction(null, agent, transactionDate, "AGENT_TRAN", "DEBIT",
command.stringValueOfParameterNamedAllowingNull("transactionMode"),
command.stringValueOfParameterNamedAllowingNull("accountType"),
command.stringValueOfParameterNamedAllowingNull("accountNo"), amount, null, null,
command.stringValueOfParameterNamedAllowingNull("description"), newBalance);
}
private List<PartyTransaction> postWithdrawDeposite(final Agent agent, final LocalDate transactionDate,
final JsonCommand command) {
final String creditorPartyCode = command.stringValueOfParameterNamed("creditorPartyCode");
final String debitPartyCode = command.stringValueOfParameterNamed("debitPartyCode");
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("withdrawAmount");
final String description = command.stringValueOfParameterNamedAllowingNull("withdrawDescription");
final Party creditorParty = this.partyRepository.findByPartyCode(creditorPartyCode)
.orElseThrow(() -> new PartyNotFoundException(creditorPartyCode));
final Party debitParty = this.partyRepository.findByPartyCode(debitPartyCode)
.orElseThrow(() -> new PartyNotFoundException(debitPartyCode));
if (debitParty.getCurrentBalance().compareTo(amount) < 0) {
throw new InsufficientPartyBalanceException(debitPartyCode);
}
final BigDecimal debtorNewBalance = debitParty.debit(amount);
final BigDecimal creditorNewBalance = creditorParty.credit(amount);
final List<PartyTransaction> transactions = new ArrayList<>();
transactions.add(new PartyTransaction(debitParty, agent, transactionDate, "WITHDRAW_DEPOSITE", "DEBIT", null, null, null,
amount, null, null, description, debtorNewBalance));
transactions.add(new PartyTransaction(creditorParty, agent, transactionDate, "WITHDRAW_DEPOSITE", "CREDIT", null, null, null,
amount, null, null, description, creditorNewBalance));
return transactions;
}
private String stringOrNull(final JsonObject json, final String paramName) {
return json.has(paramName) && !json.get(paramName).isJsonNull() ? json.get(paramName).getAsString() : null;
}
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
if (realCause.getMessage() != null && realCause.getMessage().contains("party_code")) {
throw new PlatformDataIntegrityException("error.msg.partytransaction.invalid.party",
"One of the party codes in this transaction does not exist");
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.partytransaction.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
}
@@ -0,0 +1,51 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.dds.transaction.starter;
import org.apache.fineract.organisation.agent.domain.AgentRepository;
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
import org.apache.fineract.portfolio.dds.transaction.serialization.PartyTransactionCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformService;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformService;
import org.apache.fineract.portfolio.dds.transaction.service.PartyTransactionWritePlatformServiceJpaRepositoryImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class DdsPartyTransactionConfiguration {
@Bean
@ConditionalOnMissingBean(PartyTransactionReadPlatformService.class)
public PartyTransactionReadPlatformService partyTransactionReadPlatformService(JdbcTemplate jdbcTemplate) {
return new PartyTransactionReadPlatformServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(PartyTransactionWritePlatformService.class)
public PartyTransactionWritePlatformService partyTransactionWritePlatformService(
PartyTransactionCommandFromApiJsonDeserializer fromApiJsonDeserializer, PartyTransactionRepository partyTransactionRepository,
PartyRepository partyRepository, AgentRepository agentRepository) {
return new PartyTransactionWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, partyTransactionRepository,
partyRepository, agentRepository);
}
}
@@ -324,6 +324,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.product_category as productCategory, ");
sqlBuilder.append("sp.auto_debit_facility as autoDebitFacility, sp.loan_facility as loanFacility, ");
sqlBuilder.append("sp.premature_withdrawal_option as prematureWithdrawalOption, sp.tenure_flexibility as tenureFlexibility, ");
sqlBuilder.append("sp.lien_marked as lienMarked, sp.maturity_instructions as maturityInstructions, ");
@@ -412,6 +413,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
result.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod"));
result.setBalanceFrequency(rs.getString("balanceFrequency"));
result.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing"));
result.setProductCategory(rs.getString("productCategory"));
result.setAutoDebitFacility(rs.getBoolean("autoDebitFacility"));
result.setLoanFacility(rs.getBoolean("loanFacility"));
result.setPrematureWithdrawalOption(rs.getBoolean("prematureWithdrawalOption"));
@@ -463,6 +465,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.product_category as productCategory, ");
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, ");
sqlBuilder.append("sp.auto_debit_facility as autoDebitFacility, sp.loan_facility as loanFacility, ");
sqlBuilder.append("sp.premature_withdrawal_option as prematureWithdrawalOption, sp.tenure_flexibility as tenureFlexibility, ");
@@ -539,6 +542,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
final boolean enableLockinPeriod = rs.getBoolean("enableLockinPeriod");
final String balanceFrequency = rs.getString("balanceFrequency");
final boolean accountClosedZeroBalanceInDayClosing = rs.getBoolean("accountClosedZeroBalanceInDayClosing");
final String productCategory = rs.getString("productCategory");
final boolean balanceSwapOption = rs.getBoolean("balanceSwapOption");
final boolean msme = rs.getBoolean("msme");
final boolean autoDebitFacility = rs.getBoolean("autoDebitFacility");
@@ -581,6 +585,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
result.setEnableLockinPeriod(enableLockinPeriod);
result.setBalanceFrequency(balanceFrequency);
result.setAccountClosedZeroBalanceInDayClosing(accountClosedZeroBalanceInDayClosing);
result.setProductCategory(productCategory);
result.setBalanceSwapOption(balanceSwapOption);
result.setMsme(msme);
result.setAutoDebitFacility(autoDebitFacility);
@@ -288,6 +288,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
sqlBuilder.append("sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.product_category as productCategory, ");
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id "
+ "and spt.product_type = 'SAVINGS' and spt.status = 'PENDING') as isPendingApproval ");
@@ -412,6 +413,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
productData.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod"));
productData.setBalanceFrequency(rs.getString("balanceFrequency"));
productData.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing"));
productData.setProductCategory(rs.getString("productCategory"));
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
return productData;
@@ -537,4 +537,11 @@
<include file="parts/0467_add_welcome_kit_return_tracking.xml" relativeToChangelogFile="true"/>
<include file="parts/0468_add_address_id_to_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
<include file="parts/0469_create_database_export_request.xml" relativeToChangelogFile="true"/>
<include file="parts/0470_add_product_category_to_savings_product.xml" relativeToChangelogFile="true"/>
<include file="parts/0471_create_dds_agent.xml" relativeToChangelogFile="true"/>
<include file="parts/0473_create_dds_party.xml" relativeToChangelogFile="true"/>
<include file="parts/0474_create_dds_party_transaction.xml" relativeToChangelogFile="true"/>
<include file="parts/0475_create_dds_commission_slab.xml" relativeToChangelogFile="true"/>
<include file="parts/0476_create_dds_batch_run.xml" relativeToChangelogFile="true"/>
<include file="parts/0477_add_dds_report_permission.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Tags a savings product with a business category (e.g. "DDS") so product types built as
Recurring Deposit variants can be distinguished from plain Recurring Deposit products in
listings, without duplicating the underlying product/account engine. Nullable and unused
by existing products. -->
<changeSet id="0470_add_product_category_to_savings_product" author="CBS" runOnChange="false">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_savings_product" columnName="product_category"/>
</not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="product_category" type="VARCHAR(50)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS (Daily Deposit Scheme) Phase 2 - Agent Master. An Agent is a DDS collection agent - either
a person or, at the top of the hierarchy, the bank itself - with its own settlement savings
account and one or more guarantors. This is new feature work (not a reused RD/savings table),
per the DDS architecture decision to keep the agent/commission/reporting half of DDS separate.
-->
<changeSet id="0471_create_dds_agent" author="CBS">
<createTable tableName="m_dds_agent">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="agent_code" type="VARCHAR(20)">
<constraints nullable="false" unique="true" uniqueConstraintName="uq_dds_agent_code"/>
</column>
<column name="agent_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="agent_address1" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="agent_address2" type="VARCHAR(200)"/>
<column name="agent_city" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="agent_pincode" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="agent_telephone" type="VARCHAR(20)"/>
<column name="agent_saving_account_type" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="agent_saving_account_no" type="VARCHAR(50)">
<constraints nullable="false"/>
</column>
<column name="agent_opening_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="agent_birth_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="agent_current_balance" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="agent_opening_balance" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<!-- Merged in from SubAgent (2026-08-24) - Agent now does the daily collection work
directly, SubAgent no longer exists as a separate tier. -->
<column name="home_branch" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="saving_branch" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="tds_applicable" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="tds_percent" type="DECIMAL(19,6)"/>
<column name="current_tds" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="tds_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="transfer_branch" type="VARCHAR(100)"/>
<column name="transfer_amount_account_type" type="VARCHAR(100)"/>
<column name="transfer_account_no" type="VARCHAR(50)"/>
<column name="collector_commission_calculation" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="collector_limit_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="machine_rent" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="agent_security_branch" type="VARCHAR(100)"/>
<column name="agent_security_account_type" type="VARCHAR(100)"/>
<column name="agent_security_account_no" type="VARCHAR(50)"/>
<column name="commission" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="other_commission" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="agent_deposit" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="festival_advance" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="other_deduction" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="last_commission_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_agent" baseColumnNames="client_id" constraintName="fk_dds_agent_client"
referencedTableName="m_client" referencedColumnNames="id"/>
<createIndex tableName="m_dds_agent" indexName="idx_dds_agent_client_id">
<column name="client_id"/>
</createIndex>
<createTable tableName="m_dds_agent_guarantor">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="agent_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="guarantor_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="address1" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="address2" type="VARCHAR(200)"/>
<column name="city" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="pin_code" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="telephone" type="VARCHAR(20)"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_agent_guarantor" baseColumnNames="agent_id"
constraintName="fk_dds_agent_guarantor_agent" referencedTableName="m_dds_agent"
referencedColumnNames="id" onDelete="CASCADE"/>
<createIndex tableName="m_dds_agent_guarantor" indexName="idx_dds_agent_guarantor_agent_id">
<column name="agent_id"/>
</createIndex>
<createTable tableName="m_dds_agent_limit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="agent_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="to_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="limit_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_agent_limit" baseColumnNames="agent_id"
constraintName="fk_dds_agent_limit_agent" referencedTableName="m_dds_agent"
referencedColumnNames="id" onDelete="CASCADE"/>
<createIndex tableName="m_dds_agent_limit" indexName="idx_dds_agent_limit_agent_id">
<column name="agent_id"/>
</createIndex>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="READ_AGENT"/>
<column name="entity_name" value="AGENT"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="CREATE_AGENT"/>
<column name="entity_name" value="AGENT"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="CREATE_AGENT_CHECKER"/>
<column name="entity_name" value="AGENT"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="UPDATE_AGENT"/>
<column name="entity_name" value="AGENT"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="UPDATE_AGENT_CHECKER"/>
<column name="entity_name" value="AGENT"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,267 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS Phase 4 - Party Master. A Party is the customer's daily-deposit scheme account, linked to
the Agent who collects for it (m_dds_agent). Carries the scheme's KYC, terms, two-stage
overdue flags and a nominee list.
savings_account_id is a nullable placeholder, not yet wired up: the DDS architecture decision
is to eventually reuse the existing Recurring Deposit account engine for the deposit half
(interest, maturity, posting), but the Party Master screen this table backs collects several
mandatory RD-account fields (interest/compounding/posting periods, chart selection, etc.) that
this screen does not capture. Rather than guess defaults for those, the scheme/balance fields
below (depositAmount, maturityDate, interestRate, currentBalance, ...) are this record's own
for now; wiring a real m_savings_account row is a follow-up, not attempted here.
-->
<changeSet id="0473_create_dds_party" author="CBS">
<createTable tableName="m_dds_party">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="agent_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="savings_account_id" type="BIGINT"/>
<column name="sub_type" type="VARCHAR(60)">
<constraints nullable="false"/>
</column>
<column name="party_code" type="VARCHAR(20)">
<constraints nullable="false" unique="true" uniqueConstraintName="uq_dds_party_code"/>
</column>
<column name="last_account" type="VARCHAR(50)"/>
<column name="nominal" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="aadhar_no" type="VARCHAR(20)"/>
<column name="party_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="party_name1" type="VARCHAR(100)"/>
<column name="address1" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="address2" type="VARCHAR(200)"/>
<column name="address3" type="VARCHAR(200)"/>
<column name="city" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="pincode" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="gender" type="VARCHAR(20)"/>
<column name="pan_card_no" type="VARCHAR(20)"/>
<column name="telephone" type="VARCHAR(20)"/>
<column name="mobile_no_sms" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="operation" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="birth_date" type="DATE"/>
<column name="occupation" type="VARCHAR(40)"/>
<column name="risk" type="VARCHAR(20)"/>
<column name="ckyc_id" type="VARCHAR(40)"/>
<column name="adult_minor" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="minor_date" type="DATE"/>
<column name="guardian_client_id" type="BIGINT"/>
<column name="min_account_opt" type="VARCHAR(40)"/>
<column name="relation" type="VARCHAR(40)"/>
<column name="opening_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="months" type="INT"/>
<column name="maturity_date" type="DATE"/>
<column name="reopen_date" type="DATE"/>
<column name="deposit_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="daily_deposit_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="days" type="INT">
<constraints nullable="false"/>
</column>
<column name="ledger_no" type="VARCHAR(40)"/>
<column name="maturity_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="interest_rate" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="withdraw_type" type="VARCHAR(20)"/>
<column name="od1_flag" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="od2_flag" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="lien" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="pay_date" type="DATE"/>
<column name="payment_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="current_balance" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="opening_balance" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="is_sms_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="is_email_active" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="interest_calc" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="interest_date" type="DATE"/>
<column name="sp_description" type="VARCHAR(500)"/>
<column name="freeze_code" type="VARCHAR(20)" defaultValue="Normal">
<constraints nullable="false"/>
</column>
<column name="adv_code" type="VARCHAR(40)" defaultValue="None">
<constraints nullable="false"/>
</column>
<column name="credit_to_branch" type="VARCHAR(100)"/>
<column name="credit_to_account_type" type="VARCHAR(100)"/>
<column name="credit_to_account" type="VARCHAR(50)"/>
<column name="nominee_applicable" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="nominee_split_mode" type="VARCHAR(20)" defaultValue="Percentage"/>
<column name="status" type="VARCHAR(20)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="close_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_party" baseColumnNames="client_id" constraintName="fk_dds_party_client"
referencedTableName="m_client" referencedColumnNames="id"/>
<createIndex tableName="m_dds_party" indexName="idx_dds_party_client_id">
<column name="client_id"/>
</createIndex>
<addForeignKeyConstraint baseTableName="m_dds_party" baseColumnNames="agent_id" constraintName="fk_dds_party_agent"
referencedTableName="m_dds_agent" referencedColumnNames="id"/>
<createIndex tableName="m_dds_party" indexName="idx_dds_party_agent_id">
<column name="agent_id"/>
</createIndex>
<createTable tableName="m_dds_party_nominee">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="party_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="nomination_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="mobile_no" type="VARCHAR(20)"/>
<column name="birth_date" type="DATE"/>
<column name="minor" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="relation" type="VARCHAR(40)">
<constraints nullable="false"/>
</column>
<column name="gender" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="guardian_name" type="VARCHAR(100)"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_party_nominee" baseColumnNames="party_id" constraintName="fk_dds_party_nominee_party"
referencedTableName="m_dds_party" referencedColumnNames="id" onDelete="CASCADE"/>
<createIndex tableName="m_dds_party_nominee" indexName="idx_dds_party_nominee_party_id">
<column name="party_id"/>
</createIndex>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_PARTY"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_PARTY"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_PARTY_CHECKER"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_PARTY"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_PARTY_CHECKER"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CLOSE_PARTY"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="CLOSE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CLOSE_PARTY_CHECKER"/>
<column name="entity_name" value="PARTY"/>
<column name="action_name" value="CLOSE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS Phase 5 - Daily Transaction Entry. Posted ledger entries: a Party's day's collection rows
(PARTY_TRAN, direction CREDIT), the two legs of a Withdraw Deposite transfer between two
parties (creditor leg CREDIT, debtor leg DEBIT), or an Agent settling collected cash to its
branch (AGENT_TRAN, direction DEBIT - party_id is null for this type, there is no party
involved). No update permission is seeded - transactions are reversed with an opposing entry,
not edited, matching normal accounting practice.
-->
<changeSet id="0474_create_dds_party_transaction" author="CBS">
<createTable tableName="m_dds_party_transaction">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="party_id" type="BIGINT"/>
<column name="agent_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="transaction_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="transaction_type" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="transaction_mode" type="VARCHAR(20)"/>
<column name="account_type" type="VARCHAR(100)"/>
<column name="account_no" type="VARCHAR(50)"/>
<column name="amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="cheque_no" type="VARCHAR(40)"/>
<column name="token_no" type="VARCHAR(40)"/>
<column name="description" type="VARCHAR(500)"/>
<column name="balance_after_transaction" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_party_transaction" baseColumnNames="party_id"
constraintName="fk_dds_party_transaction_party" referencedTableName="m_dds_party"
referencedColumnNames="id"/>
<createIndex tableName="m_dds_party_transaction" indexName="idx_dds_party_transaction_party_id">
<column name="party_id"/>
</createIndex>
<addForeignKeyConstraint baseTableName="m_dds_party_transaction" baseColumnNames="agent_id"
constraintName="fk_dds_party_transaction_agent" referencedTableName="m_dds_agent"
referencedColumnNames="id"/>
<createIndex tableName="m_dds_party_transaction" indexName="idx_dds_party_transaction_agent_id">
<column name="agent_id"/>
</createIndex>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_PARTYTRANSACTION"/>
<column name="entity_name" value="PARTYTRANSACTION"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_PARTYTRANSACTION"/>
<column name="entity_name" value="PARTYTRANSACTION"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_PARTYTRANSACTION_CHECKER"/>
<column name="entity_name" value="PARTYTRANSACTION"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS Phase 6 - Commission Slab Master: the agent commission chart (ACCOUNT slabs and
COMMISSION override slabs). Append-only reference data - no update/delete permission is
seeded, matching the legacy screen's behaviour.
-->
<changeSet id="0475_create_dds_commission_slab" author="CBS">
<createTable tableName="m_dds_commission_slab">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="slab_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="slab_from" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="slab_to" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="commission_percent" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_dds_commission_slab" indexName="idx_dds_commission_slab_type">
<column name="slab_type"/>
</createIndex>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="READ_COMMISSIONSLAB"/>
<column name="entity_name" value="COMMISSIONSLAB"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="CREATE_COMMISSIONSLAB"/>
<column name="entity_name" value="COMMISSIONSLAB"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="CREATE_COMMISSIONSLAB_CHECKER"/>
<column name="entity_name" value="COMMISSIONSLAB"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS Phase 7 - Interest/Commission Batch. The Calculate-then-Post control from the legacy
Interest Part menu: Generate previews a run's rows (persisted, but nothing else changes);
Post applies exactly those previewed rows and can only happen once per run.
last_commission_date on m_dds_agent (added directly in Phase 2's create table, 2026-08-24)
tracks how far an agent's commission has already been calculated through, so each Commission
Calculation run only covers collections since the last one.
-->
<changeSet id="0476_create_dds_batch_run" author="CBS">
<createTable tableName="m_dds_batch_run">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="job_type" type="VARCHAR(40)">
<constraints nullable="false"/>
</column>
<column name="branch_scope" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="job_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="agent_from" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="agent_to" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="party_from" type="BIGINT"/>
<column name="party_to" type="BIGINT"/>
<column name="status" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="total_rows" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="generated_on_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="posted_on_date" type="DATE"/>
</createTable>
<createTable tableName="m_dds_batch_run_row">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="batch_run_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="party_id" type="BIGINT"/>
<column name="agent_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_dds_batch_run_row" baseColumnNames="batch_run_id"
constraintName="fk_dds_batch_run_row_batch_run" referencedTableName="m_dds_batch_run"
referencedColumnNames="id" onDelete="CASCADE"/>
<createIndex tableName="m_dds_batch_run_row" indexName="idx_dds_batch_run_row_batch_run_id">
<column name="batch_run_id"/>
</createIndex>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_DDSBATCH"/>
<column name="entity_name" value="DDSBATCH"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="GENERATE_DDSBATCH"/>
<column name="entity_name" value="DDSBATCH"/>
<column name="action_name" value="GENERATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="POST_DDSBATCH"/>
<column name="entity_name" value="DDSBATCH"/>
<column name="action_name" value="POST"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="POST_DDSBATCH_CHECKER"/>
<column name="entity_name" value="DDSBATCH"/>
<column name="action_name" value="POST_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
DDS Phase 8 - Reports. Pure read-only queries over the existing DDS tables, so no new tables
are needed here - just the permission gating access to the report endpoint.
-->
<changeSet id="0477_add_dds_report_permission" author="CBS">
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_DDSREPORT"/>
<column name="entity_name" value="DDSREPORT"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>