From 37c961f713aa6ae2f700a19d7be07c16ba0d26c0 Mon Sep 17 00:00:00 2001 From: tarunmalviya Date: Tue, 25 Aug 2026 10:33:37 +0530 Subject: [PATCH] DDS: Agent settlement (Agent Tran), Close Party account, reports (Supplementary-DDS/Master Data Export/Close Register), Party-Savings link, product category field --- .../service/CommandWrapperBuilder.java | 72 +++ .../organisation/agent/data/AgentData.java | 148 +++++ .../agent/data/AgentGuarantorData.java | 59 ++ .../agent/data/AgentLimitData.java | 48 ++ .../organisation/agent/domain/Agent.java | 440 ++++++++++++++ .../agent/domain/AgentGuarantor.java | 83 +++ .../organisation/agent/domain/AgentLimit.java | 66 ++ .../exception/AgentNotFoundException.java | 40 ++ .../data/CommissionSlabData.java | 39 ++ .../commissionslab/domain/CommissionSlab.java | 61 ++ .../dds/batch/data/BatchRunData.java | 50 ++ .../dds/batch/data/BatchRunRowData.java | 41 ++ .../portfolio/dds/batch/domain/BatchRun.java | 122 ++++ .../dds/batch/domain/BatchRunRow.java | 65 ++ .../BatchRunAlreadyPostedException.java | 32 + .../exception/BatchRunNotFoundException.java | 28 + .../portfolio/dds/party/data/PartyData.java | 107 ++++ .../dds/party/data/PartyNomineeData.java | 59 ++ .../portfolio/dds/party/domain/Party.java | 569 ++++++++++++++++++ .../dds/party/domain/PartyNominee.java | 88 +++ .../PartyAlreadyClosedException.java | 32 + .../exception/PartyHasBalanceException.java | 32 + .../exception/PartyNotFoundException.java | 40 ++ ...SavingsAccountClientMismatchException.java | 33 + .../PartySavingsAccountNotFoundException.java | 32 + .../dds/report/data/DdsReportData.java | 45 ++ .../data/PartyTransactionData.java | 52 ++ .../transaction/domain/PartyTransaction.java | 110 ++++ .../InsufficientAgentBalanceException.java | 33 + .../InsufficientPartyBalanceException.java | 32 + .../PartyTransactionNotFoundException.java | 32 + .../savings/SavingsApiConstants.java | 1 + .../savings/data/SavingsProductData.java | 1 + .../agent/api/AgentsApiResource.java | 104 ++++ .../agent/domain/AgentRepository.java | 36 ++ .../handler/CreateAgentCommandHandler.java | 46 ++ .../handler/UpdateAgentCommandHandler.java | 46 ++ .../AgentCommandFromApiJsonDeserializer.java | 409 +++++++++++++ .../service/AgentReadPlatformService.java | 29 + .../service/AgentReadPlatformServiceImpl.java | 176 ++++++ .../service/AgentWritePlatformService.java | 29 + ...WritePlatformServiceJpaRepositoryImpl.java | 187 ++++++ .../OrganisationAgentConfiguration.java | 48 ++ .../api/CommissionSlabsApiResource.java | 74 +++ .../domain/CommissionSlabRepository.java | 31 + .../CreateCommissionSlabsCommandHandler.java | 46 ++ ...ionSlabCommandFromApiJsonDeserializer.java | 113 ++++ .../CommissionSlabReadPlatformService.java | 27 + ...CommissionSlabReadPlatformServiceImpl.java | 60 ++ .../CommissionSlabWritePlatformService.java | 27 + ...WritePlatformServiceJpaRepositoryImpl.java | 78 +++ ...ganisationCommissionSlabConfiguration.java | 47 ++ .../dds/batch/api/BatchRunsApiResource.java | 102 ++++ .../dds/batch/domain/BatchRunRepository.java | 25 + .../GenerateBatchRunCommandHandler.java | 46 ++ .../handler/PostBatchRunCommandHandler.java | 46 ++ ...atchRunCommandFromApiJsonDeserializer.java | 106 ++++ .../service/BatchRunReadPlatformService.java | 29 + .../BatchRunReadPlatformServiceImpl.java | 137 +++++ .../service/BatchRunWritePlatformService.java | 29 + ...WritePlatformServiceJpaRepositoryImpl.java | 227 +++++++ .../batch/starter/DdsBatchConfiguration.java | 53 ++ .../dds/party/api/PartiesApiResource.java | 128 ++++ .../dds/party/domain/PartyRepository.java | 42 ++ .../handler/ClosePartyCommandHandler.java | 46 ++ .../handler/CreatePartyCommandHandler.java | 46 ++ .../handler/UpdatePartyCommandHandler.java | 46 ++ .../PartyCommandFromApiJsonDeserializer.java | 294 +++++++++ .../service/PartyReadPlatformService.java | 29 + .../service/PartyReadPlatformServiceImpl.java | 199 ++++++ .../service/PartyWritePlatformService.java | 31 + ...WritePlatformServiceJpaRepositoryImpl.java | 235 ++++++++ .../party/starter/DdsPartyConfiguration.java | 52 ++ .../dds/report/api/DdsReportsApiResource.java | 74 +++ .../service/DdsReportReadPlatformService.java | 28 + .../DdsReportReadPlatformServiceImpl.java | 313 ++++++++++ .../starter/DdsReportConfiguration.java | 36 ++ .../api/PartyTransactionsApiResource.java | 90 +++ .../domain/PartyTransactionRepository.java | 33 + .../CreatePartyTransactionCommandHandler.java | 46 ++ ...sactionCommandFromApiJsonDeserializer.java | 137 +++++ .../PartyTransactionReadPlatformService.java | 29 + ...rtyTransactionReadPlatformServiceImpl.java | 98 +++ .../PartyTransactionWritePlatformService.java | 27 + ...WritePlatformServiceJpaRepositoryImpl.java | 177 ++++++ .../DdsPartyTransactionConfiguration.java | 51 ++ ...DepositProductReadPlatformServiceImpl.java | 5 + ...SavingsProductReadPlatformServiceImpl.java | 2 + .../db/changelog/tenant/changelog-tenant.xml | 7 + ...dd_product_category_to_savings_product.xml | 21 + .../tenant/parts/0471_create_dds_agent.xml | 230 +++++++ .../tenant/parts/0473_create_dds_party.xml | 267 ++++++++ .../0474_create_dds_party_transaction.xml | 105 ++++ .../parts/0475_create_dds_commission_slab.xml | 79 +++ .../parts/0476_create_dds_batch_run.xml | 127 ++++ .../parts/0477_add_dds_report_permission.xml | 42 ++ .../savings/data/FixedDepositProductData.java | 3 + .../data/RecurringDepositProductData.java | 3 + .../data/SavingsProductDataValidator.java | 2 +- .../savings/domain/SavingsProduct.java | 18 + .../domain/SavingsProductAssembler.java | 3 + 101 files changed, 8305 insertions(+), 1 deletion(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentGuarantorData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentLimitData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/Agent.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentGuarantor.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentLimit.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/agent/exception/AgentNotFoundException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/data/CommissionSlabData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlab.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunRowData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRun.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRow.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunAlreadyPostedException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunNotFoundException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyNomineeData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/Party.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyNominee.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyAlreadyClosedException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyHasBalanceException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyNotFoundException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountClientMismatchException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountNotFoundException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/report/data/DdsReportData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/data/PartyTransactionData.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransaction.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientAgentBalanceException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientPartyBalanceException.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/PartyTransactionNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/api/AgentsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/domain/AgentRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/CreateAgentCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/UpdateAgentCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/serialization/AgentCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformServiceJpaRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/agent/starter/OrganisationAgentConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/api/CommissionSlabsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlabRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/handler/CreateCommissionSlabsCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/serialization/CommissionSlabCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformServiceJpaRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/starter/OrganisationCommissionSlabConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/api/BatchRunsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/GenerateBatchRunCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/PostBatchRunCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/serialization/BatchRunCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformServiceJpaRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/starter/DdsBatchConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/api/PartiesApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/ClosePartyCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/CreatePartyCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/UpdatePartyCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/serialization/PartyCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformServiceJpaRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/starter/DdsPartyConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/api/DdsReportsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/starter/DdsReportConfiguration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/api/PartyTransactionsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransactionRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/handler/CreatePartyTransactionCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/serialization/PartyTransactionCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformServiceJpaRepositoryImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/starter/DdsPartyTransactionConfiguration.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_add_product_category_to_savings_product.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_create_dds_agent.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0473_create_dds_party.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_create_dds_party_transaction.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_create_dds_commission_slab.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_create_dds_batch_run.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0477_add_dds_report_permission.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 5e2c31402..9fed7072e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -664,6 +664,78 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createAgent() { + this.actionName = "CREATE"; + this.entityName = "AGENT"; + this.entityId = null; + this.href = "/dds/agents/template"; + return this; + } + + public CommandWrapperBuilder updateAgent(final Long agentId) { + this.actionName = "UPDATE"; + this.entityName = "AGENT"; + this.entityId = agentId; + this.href = "/dds/agents/" + agentId; + return this; + } + + public CommandWrapperBuilder createParty() { + this.actionName = "CREATE"; + this.entityName = "PARTY"; + this.entityId = null; + this.href = "/dds/parties/template"; + return this; + } + + public CommandWrapperBuilder updateParty(final Long partyId) { + this.actionName = "UPDATE"; + this.entityName = "PARTY"; + this.entityId = partyId; + this.href = "/dds/parties/" + partyId; + return this; + } + + public CommandWrapperBuilder closeParty(final Long partyId) { + this.actionName = "CLOSE"; + this.entityName = "PARTY"; + this.entityId = partyId; + this.href = "/dds/parties/" + partyId + "?command=close"; + return this; + } + + public CommandWrapperBuilder createPartyTransaction() { + this.actionName = "CREATE"; + this.entityName = "PARTYTRANSACTION"; + this.entityId = null; + this.href = "/dds/partytransactions/template"; + return this; + } + + public CommandWrapperBuilder createCommissionSlabs() { + this.actionName = "CREATE"; + this.entityName = "COMMISSIONSLAB"; + this.entityId = null; + this.href = "/dds/commissionslabs/template"; + return this; + } + + public CommandWrapperBuilder generateDdsBatchRun() { + this.actionName = "GENERATE"; + this.entityName = "DDSBATCH"; + this.entityId = null; + this.href = "/dds/batchruns/template"; + return this; + } + + public CommandWrapperBuilder postDdsBatchRun(final Long batchRunId) { + this.actionName = "POST"; + this.entityName = "DDSBATCH"; + this.entityId = batchRunId; + this.href = "/dds/batchruns/" + batchRunId; + return this; + } + public CommandWrapperBuilder createOfficeTransaction() { this.actionName = "CREATE"; this.entityName = "OFFICETRANSACTION"; diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentData.java new file mode 100644 index 000000000..ef51639e6 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentData.java @@ -0,0 +1,148 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import lombok.Getter; + +/** + * Immutable data object representing a DDS agent, including its collection-limit history and + * guarantors. Carries the collection/commission/security-account fields merged in from SubAgent + * (2026-08-24) - Agent now does the daily collection work directly. + */ +@Getter +public final class AgentData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long clientId; + private final String clientName; + private final String agentCode; + private final String agentNameId; + private final String agentAddress1; + private final String agentAddress2; + private final String agentCity; + private final String agentPinCode; + private final String agentTelephone; + private final String agentSavingAccountType; + private final String agentSavingAccountNo; + private final LocalDate agentOpeningDate; + private final LocalDate agentBirthDate; + private final BigDecimal agentCurrentBalance; + private final BigDecimal agentOpeningBalance; + private final String homeBranch; + private final String savingBranch; + private final Boolean tdsApplicable; + private final BigDecimal tdsPercent; + private final BigDecimal currentTds; + private final BigDecimal tdsAmount; + private final String transferBranch; + private final String transferAmountAccountType; + private final String transferAccountNo; + private final Boolean commissionCalculation; + private final BigDecimal limit; + private final BigDecimal machineRent; + private final String agentSecurityBranch; + private final String agentSecurityAccountType; + private final String agentSecurityAccountNo; + private final BigDecimal commission; + private final BigDecimal otherCommission; + private final BigDecimal agentDeposit; + private final BigDecimal festivalAdvance; + private final BigDecimal otherDeduction; + private final Collection limits; + private final Collection guarantors; + + public static AgentData instance(final Long id, final Long clientId, final String clientName, final String agentCode, + final String agentNameId, final String agentAddress1, final String agentAddress2, final String agentCity, + final String agentPinCode, final String agentTelephone, final String agentSavingAccountType, + final String agentSavingAccountNo, final LocalDate agentOpeningDate, final LocalDate agentBirthDate, + final BigDecimal agentCurrentBalance, final BigDecimal agentOpeningBalance, final String homeBranch, + final String savingBranch, final Boolean tdsApplicable, final BigDecimal tdsPercent, final BigDecimal currentTds, + final BigDecimal tdsAmount, final String transferBranch, final String transferAmountAccountType, + final String transferAccountNo, final Boolean commissionCalculation, final BigDecimal limit, final BigDecimal machineRent, + final String agentSecurityBranch, final String agentSecurityAccountType, final String agentSecurityAccountNo, + final BigDecimal commission, final BigDecimal otherCommission, final BigDecimal agentDeposit, + final BigDecimal festivalAdvance, final BigDecimal otherDeduction, final Collection limits, + final Collection guarantors) { + return new AgentData(id, clientId, clientName, agentCode, agentNameId, agentAddress1, agentAddress2, agentCity, agentPinCode, + agentTelephone, agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentCurrentBalance, + agentOpeningBalance, homeBranch, savingBranch, tdsApplicable, tdsPercent, currentTds, tdsAmount, transferBranch, + transferAmountAccountType, transferAccountNo, commissionCalculation, limit, machineRent, agentSecurityBranch, + agentSecurityAccountType, agentSecurityAccountNo, commission, otherCommission, agentDeposit, festivalAdvance, + otherDeduction, limits, guarantors); + } + + private AgentData(final Long id, final Long clientId, final String clientName, final String agentCode, final String agentNameId, + final String agentAddress1, final String agentAddress2, final String agentCity, final String agentPinCode, + final String agentTelephone, final String agentSavingAccountType, final String agentSavingAccountNo, + final LocalDate agentOpeningDate, final LocalDate agentBirthDate, final BigDecimal agentCurrentBalance, + final BigDecimal agentOpeningBalance, final String homeBranch, final String savingBranch, final Boolean tdsApplicable, + final BigDecimal tdsPercent, final BigDecimal currentTds, final BigDecimal tdsAmount, final String transferBranch, + final String transferAmountAccountType, final String transferAccountNo, final Boolean commissionCalculation, + final BigDecimal limit, final BigDecimal machineRent, final String agentSecurityBranch, + final String agentSecurityAccountType, final String agentSecurityAccountNo, final BigDecimal commission, + final BigDecimal otherCommission, final BigDecimal agentDeposit, final BigDecimal festivalAdvance, + final BigDecimal otherDeduction, final Collection limits, final Collection guarantors) { + this.id = id; + this.clientId = clientId; + this.clientName = clientName; + this.agentCode = agentCode; + this.agentNameId = agentNameId; + this.agentAddress1 = agentAddress1; + this.agentAddress2 = agentAddress2; + this.agentCity = agentCity; + this.agentPinCode = agentPinCode; + this.agentTelephone = agentTelephone; + this.agentSavingAccountType = agentSavingAccountType; + this.agentSavingAccountNo = agentSavingAccountNo; + this.agentOpeningDate = agentOpeningDate; + this.agentBirthDate = agentBirthDate; + this.agentCurrentBalance = agentCurrentBalance; + this.agentOpeningBalance = agentOpeningBalance; + this.homeBranch = homeBranch; + this.savingBranch = savingBranch; + this.tdsApplicable = tdsApplicable; + this.tdsPercent = tdsPercent; + this.currentTds = currentTds; + this.tdsAmount = tdsAmount; + this.transferBranch = transferBranch; + this.transferAmountAccountType = transferAmountAccountType; + this.transferAccountNo = transferAccountNo; + this.commissionCalculation = commissionCalculation; + this.limit = limit; + this.machineRent = machineRent; + this.agentSecurityBranch = agentSecurityBranch; + this.agentSecurityAccountType = agentSecurityAccountType; + this.agentSecurityAccountNo = agentSecurityAccountNo; + this.commission = commission; + this.otherCommission = otherCommission; + this.agentDeposit = agentDeposit; + this.festivalAdvance = festivalAdvance; + this.otherDeduction = otherDeduction; + this.limits = limits; + this.guarantors = guarantors; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentGuarantorData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentGuarantorData.java new file mode 100644 index 000000000..75d990c96 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentGuarantorData.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.data; + +import java.io.Serial; +import java.io.Serializable; +import lombok.Getter; + +/** + * Immutable data object representing a single agent guarantor. + */ +@Getter +public final class AgentGuarantorData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long clientId; + private final String guarantorName; + private final String address1; + private final String address2; + private final String city; + private final String pinCode; + private final String telephone; + + public static AgentGuarantorData instance(final Long id, final Long clientId, final String guarantorName, final String address1, + final String address2, final String city, final String pinCode, final String telephone) { + return new AgentGuarantorData(id, clientId, guarantorName, address1, address2, city, pinCode, telephone); + } + + private AgentGuarantorData(final Long id, final Long clientId, final String guarantorName, final String address1, + final String address2, final String city, final String pinCode, final String telephone) { + this.id = id; + this.clientId = clientId; + this.guarantorName = guarantorName; + this.address1 = address1; + this.address2 = address2; + this.city = city; + this.pinCode = pinCode; + this.telephone = telephone; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentLimitData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentLimitData.java new file mode 100644 index 000000000..60504e1fd --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/data/AgentLimitData.java @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; + +@Getter +public final class AgentLimitData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final LocalDate fromDate; + private final LocalDate toDate; + private final BigDecimal limit; + + public static AgentLimitData instance(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) { + return new AgentLimitData(id, fromDate, toDate, limit); + } + + private AgentLimitData(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) { + this.id = id; + this.fromDate = fromDate; + this.toDate = toDate; + this.limit = limit; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/Agent.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/Agent.java new file mode 100644 index 000000000..8ab0e842e --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/Agent.java @@ -0,0 +1,440 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.portfolio.client.domain.Client; + +/** + * A DDS (Daily Deposit Scheme) collection agent. Can represent either a person or, at the top of the + * hierarchy, the bank itself acting as its own agent. Holds its own settlement savings account and one + * or more guarantors. + */ +@Getter +@Entity +@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") }) +public class Agent extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private Client client; + + @Column(name = "agent_code", length = 20, nullable = false) + private String agentCode; + + @Column(name = "agent_name", length = 100, nullable = false) + private String agentName; + + @Column(name = "agent_address1", length = 200, nullable = false) + private String agentAddress1; + + @Column(name = "agent_address2", length = 200) + private String agentAddress2; + + @Column(name = "agent_city", length = 100, nullable = false) + private String agentCity; + + @Column(name = "agent_pincode", length = 20, nullable = false) + private String agentPinCode; + + @Column(name = "agent_telephone", length = 20) + private String agentTelephone; + + @Column(name = "agent_saving_account_type", length = 100, nullable = false) + private String agentSavingAccountType; + + @Column(name = "agent_saving_account_no", length = 50, nullable = false) + private String agentSavingAccountNo; + + @Column(name = "agent_opening_date", nullable = false) + private LocalDate agentOpeningDate; + + @Column(name = "agent_birth_date", nullable = false) + private LocalDate agentBirthDate; + + @Column(name = "agent_current_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal agentCurrentBalance; + + @Column(name = "agent_opening_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal agentOpeningBalance; + + // Fields below were merged in from SubAgent (2026-08-24) - the Agent itself now does the daily + // collection work directly; SubAgent no longer exists as a separate tier. Ag.Level / Upper Ag.Level + // / Collector (MLM) / CRPB Points / Level Update Date were dropped - they only ever meant anything + // for a multi-level hierarchy, which no longer exists here. + @Column(name = "home_branch", length = 100, nullable = false) + private String homeBranch; + + @Column(name = "saving_branch", length = 100, nullable = false) + private String savingBranch; + + @Column(name = "tds_applicable", nullable = false) + private boolean tdsApplicable; + + @Column(name = "tds_percent", scale = 6, precision = 19) + private BigDecimal tdsPercent; + + @Column(name = "current_tds", scale = 6, precision = 19, nullable = false) + private BigDecimal currentTds; + + @Column(name = "tds_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal tdsAmount; + + @Column(name = "transfer_branch", length = 100) + private String transferBranch; + + @Column(name = "transfer_amount_account_type", length = 100) + private String transferAmountAccountType; + + @Column(name = "transfer_account_no", length = 50) + private String transferAccountNo; + + @Column(name = "collector_commission_calculation", nullable = false) + private boolean commissionCalculation; + + @Column(name = "collector_limit_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal limitAmount; + + @Column(name = "machine_rent", scale = 6, precision = 19, nullable = false) + private BigDecimal machineRent; + + @Column(name = "agent_security_branch", length = 100) + private String agentSecurityBranch; + + @Column(name = "agent_security_account_type", length = 100) + private String agentSecurityAccountType; + + @Column(name = "agent_security_account_no", length = 50) + private String agentSecurityAccountNo; + + @Column(name = "commission", scale = 6, precision = 19, nullable = false) + private BigDecimal commission; + + @Column(name = "other_commission", scale = 6, precision = 19, nullable = false) + private BigDecimal otherCommission; + + @Column(name = "agent_deposit", scale = 6, precision = 19, nullable = false) + private BigDecimal agentDeposit; + + @Column(name = "festival_advance", scale = 6, precision = 19, nullable = false) + private BigDecimal festivalAdvance; + + @Column(name = "other_deduction", scale = 6, precision = 19, nullable = false) + private BigDecimal otherDeduction; + + @Column(name = "last_commission_date") + private LocalDate lastCommissionDate; + + @OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OrderBy("id") + private List guarantors = new ArrayList<>(); + + @OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OrderBy("id") + private List limits = new ArrayList<>(); + + protected Agent() { + // + } + + private Agent(final Client client, final String agentCode, final String agentName, final String agentAddress1, + final String agentAddress2, final String agentCity, final String agentPinCode, final String agentTelephone, + final String agentSavingAccountType, final String agentSavingAccountNo, final LocalDate agentOpeningDate, + final LocalDate agentBirthDate, final BigDecimal agentOpeningBalance, final JsonCommand command) { + this.client = client; + this.agentCode = agentCode; + this.agentName = agentName; + this.agentAddress1 = agentAddress1; + this.agentAddress2 = StringUtils.defaultIfEmpty(agentAddress2, null); + this.agentCity = agentCity; + this.agentPinCode = agentPinCode; + this.agentTelephone = StringUtils.defaultIfEmpty(agentTelephone, null); + this.agentSavingAccountType = agentSavingAccountType; + this.agentSavingAccountNo = agentSavingAccountNo; + this.agentOpeningDate = agentOpeningDate; + this.agentBirthDate = agentBirthDate; + this.agentOpeningBalance = agentOpeningBalance == null ? BigDecimal.ZERO : agentOpeningBalance; + this.agentCurrentBalance = this.agentOpeningBalance; + + this.homeBranch = command.stringValueOfParameterNamed("homeBranch"); + this.savingBranch = command.stringValueOfParameterNamed("savingBranch"); + this.tdsApplicable = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable"); + this.tdsPercent = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("tdsPercent"); + this.currentTds = BigDecimal.ZERO; + this.tdsAmount = BigDecimal.ZERO; + this.transferBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferBranch"), null); + this.transferAmountAccountType = StringUtils + .defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAmountAccountType"), null); + this.transferAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAccountNo"), null); + this.commissionCalculation = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation"); + this.limitAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("limit")); + this.machineRent = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("machineRent")); + this.agentSecurityBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityBranch"), + null); + this.agentSecurityAccountType = StringUtils + .defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountType"), null); + this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountNo"), + null); + this.commission = BigDecimal.ZERO; + this.otherCommission = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherCommission")); + this.agentDeposit = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentDeposit")); + this.festivalAdvance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("festivalAdvance")); + this.otherDeduction = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherDeduction")); + } + + public static Agent fromJson(final Client client, final String agentCode, final JsonCommand command) { + final String agentName = command.stringValueOfParameterNamed("agentNameId"); + final String agentAddress1 = command.stringValueOfParameterNamed("agentAddress1"); + final String agentAddress2 = command.stringValueOfParameterNamedAllowingNull("agentAddress2"); + final String agentCity = command.stringValueOfParameterNamed("agentCity"); + final String agentPinCode = command.stringValueOfParameterNamed("agentPinCode"); + final String agentTelephone = command.stringValueOfParameterNamedAllowingNull("agentTelephone"); + final String agentSavingAccountType = command.stringValueOfParameterNamed("agentSavingAccountType"); + final String agentSavingAccountNo = command.stringValueOfParameterNamed("agentSavingAccountNo"); + final LocalDate agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate"); + final LocalDate agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate"); + final BigDecimal agentOpeningBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentOpeningBalance"); + + return new Agent(client, agentCode, agentName, agentAddress1, agentAddress2, agentCity, agentPinCode, agentTelephone, + agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentOpeningBalance, command); + } + + private static BigDecimal orZero(final BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + public void addGuarantor(final AgentGuarantor guarantor) { + guarantor.setAgent(this); + this.guarantors.add(guarantor); + } + + public void resetGuarantors() { + this.guarantors.clear(); + } + + public void addLimit(final AgentLimit limit) { + limit.setAgent(this); + this.limits.add(limit); + } + + public void resetLimits() { + this.limits.clear(); + } + + /** + * Adds a Commission Posting's amount to this agent's running commission total and stamps the date + * commission was last calculated through, so the next Commission Calculation batch only covers + * collections since then. + */ + public void postCommission(final BigDecimal amount, final LocalDate throughDate) { + this.commission = this.commission.add(amount); + this.lastCommissionDate = throughDate; + } + + /** + * Cash this agent has collected from parties but not yet handed over to a branch (an Agent Tran + * settlement). Raised by a Party Tran collection, lowered by an Agent Tran settlement - this is the + * figure an Agent's collection limit is enforced against. + */ + public BigDecimal credit(final BigDecimal amount) { + this.agentCurrentBalance = this.agentCurrentBalance.add(amount); + return this.agentCurrentBalance; + } + + public BigDecimal settle(final BigDecimal amount) { + this.agentCurrentBalance = this.agentCurrentBalance.subtract(amount); + return this.agentCurrentBalance; + } + + public void changeClient(final Client newClient) { + this.client = newClient; + } + + public Long clientId() { + return this.client.getId(); + } + + public Map update(final JsonCommand command) { + + final Map actualChanges = new LinkedHashMap<>(10); + + if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) { + actualChanges.put("clientId", command.longValueOfParameterNamed("clientId")); + } + + if (command.isChangeInStringParameterNamed("agentNameId", this.agentName)) { + final String newValue = command.stringValueOfParameterNamed("agentNameId"); + actualChanges.put("agentNameId", newValue); + this.agentName = newValue; + } + + if (command.isChangeInStringParameterNamed("agentAddress1", this.agentAddress1)) { + final String newValue = command.stringValueOfParameterNamed("agentAddress1"); + actualChanges.put("agentAddress1", newValue); + this.agentAddress1 = newValue; + } + + if (command.isChangeInStringParameterNamed("agentAddress2", this.agentAddress2)) { + final String newValue = command.stringValueOfParameterNamed("agentAddress2"); + actualChanges.put("agentAddress2", newValue); + this.agentAddress2 = StringUtils.defaultIfEmpty(newValue, null); + } + + if (command.isChangeInStringParameterNamed("agentCity", this.agentCity)) { + final String newValue = command.stringValueOfParameterNamed("agentCity"); + actualChanges.put("agentCity", newValue); + this.agentCity = newValue; + } + + if (command.isChangeInStringParameterNamed("agentPinCode", this.agentPinCode)) { + final String newValue = command.stringValueOfParameterNamed("agentPinCode"); + actualChanges.put("agentPinCode", newValue); + this.agentPinCode = newValue; + } + + if (command.isChangeInStringParameterNamed("agentTelephone", this.agentTelephone)) { + final String newValue = command.stringValueOfParameterNamed("agentTelephone"); + actualChanges.put("agentTelephone", newValue); + this.agentTelephone = StringUtils.defaultIfEmpty(newValue, null); + } + + if (command.isChangeInStringParameterNamed("agentSavingAccountType", this.agentSavingAccountType)) { + final String newValue = command.stringValueOfParameterNamed("agentSavingAccountType"); + actualChanges.put("agentSavingAccountType", newValue); + this.agentSavingAccountType = newValue; + } + + if (command.isChangeInStringParameterNamed("agentSavingAccountNo", this.agentSavingAccountNo)) { + final String newValue = command.stringValueOfParameterNamed("agentSavingAccountNo"); + actualChanges.put("agentSavingAccountNo", newValue); + this.agentSavingAccountNo = newValue; + } + + if (command.isChangeInDateParameterNamed("agentOpeningDate", this.agentOpeningDate)) { + actualChanges.put("agentOpeningDate", command.stringValueOfParameterNamed("agentOpeningDate")); + this.agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate"); + } + + if (command.isChangeInDateParameterNamed("agentBirthDate", this.agentBirthDate)) { + actualChanges.put("agentBirthDate", command.stringValueOfParameterNamed("agentBirthDate")); + this.agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate"); + } + + stringChange(command, actualChanges, "homeBranch", this.homeBranch, v -> this.homeBranch = v); + stringChange(command, actualChanges, "savingBranch", this.savingBranch, v -> this.savingBranch = v); + + if (command.isChangeInBooleanParameterNamed("tdsApplicable", this.tdsApplicable)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable"); + actualChanges.put("tdsApplicable", newValue); + this.tdsApplicable = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("tdsPercent", this.tdsPercent)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("tdsPercent"); + actualChanges.put("tdsPercent", newValue); + this.tdsPercent = newValue; + } + + stringChange(command, actualChanges, "transferBranch", this.transferBranch, + v -> this.transferBranch = StringUtils.defaultIfEmpty(v, null)); + stringChange(command, actualChanges, "transferAmountAccountType", this.transferAmountAccountType, + v -> this.transferAmountAccountType = StringUtils.defaultIfEmpty(v, null)); + stringChange(command, actualChanges, "transferAccountNo", this.transferAccountNo, + v -> this.transferAccountNo = StringUtils.defaultIfEmpty(v, null)); + + if (command.isChangeInBooleanParameterNamed("commissionCalculation", this.commissionCalculation)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation"); + actualChanges.put("commissionCalculation", newValue); + this.commissionCalculation = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("limit", this.limitAmount)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("limit"); + actualChanges.put("limit", newValue); + this.limitAmount = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("machineRent", this.machineRent)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("machineRent"); + actualChanges.put("machineRent", newValue); + this.machineRent = newValue; + } + + stringChange(command, actualChanges, "agentSecurityBranch", this.agentSecurityBranch, + v -> this.agentSecurityBranch = StringUtils.defaultIfEmpty(v, null)); + stringChange(command, actualChanges, "agentSecurityAccountType", this.agentSecurityAccountType, + v -> this.agentSecurityAccountType = StringUtils.defaultIfEmpty(v, null)); + stringChange(command, actualChanges, "agentSecurityAccountNo", this.agentSecurityAccountNo, + v -> this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(v, null)); + + if (command.isChangeInBigDecimalParameterNamed("otherCommission", this.otherCommission)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherCommission"); + actualChanges.put("otherCommission", newValue); + this.otherCommission = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("agentDeposit", this.agentDeposit)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("agentDeposit"); + actualChanges.put("agentDeposit", newValue); + this.agentDeposit = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("festivalAdvance", this.festivalAdvance)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("festivalAdvance"); + actualChanges.put("festivalAdvance", newValue); + this.festivalAdvance = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("otherDeduction", this.otherDeduction)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherDeduction"); + actualChanges.put("otherDeduction", newValue); + this.otherDeduction = newValue; + } + + return actualChanges; + } + + private void stringChange(final JsonCommand command, final Map actualChanges, final String paramName, + final String existingValue, final java.util.function.Consumer setter) { + if (command.isChangeInStringParameterNamed(paramName, existingValue)) { + final String newValue = command.stringValueOfParameterNamed(paramName); + actualChanges.put(paramName, newValue); + setter.accept(newValue); + } + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentGuarantor.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentGuarantor.java new file mode 100644 index 000000000..72b02044d --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentGuarantor.java @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * A single guarantor recorded against an {@link Agent} (or, reused with the same shape, against a + * SubAgent). Always created/replaced as part of its parent agent's own create/update command - it has + * no independent API of its own. + */ +@Getter +@Entity +@Table(name = "m_dds_agent_guarantor") +public class AgentGuarantor extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "agent_id", nullable = false) + private Agent agent; + + @Column(name = "client_id", nullable = false) + private Long clientId; + + @Column(name = "guarantor_name", length = 100, nullable = false) + private String guarantorName; + + @Column(name = "address1", length = 200, nullable = false) + private String address1; + + @Column(name = "address2", length = 200) + private String address2; + + @Column(name = "city", length = 100, nullable = false) + private String city; + + @Column(name = "pin_code", length = 20, nullable = false) + private String pinCode; + + @Column(name = "telephone", length = 20) + private String telephone; + + protected AgentGuarantor() { + // + } + + public AgentGuarantor(final Long clientId, final String guarantorName, final String address1, final String address2, + final String city, final String pinCode, final String telephone) { + this.clientId = clientId; + this.guarantorName = guarantorName; + this.address1 = address1; + this.address2 = StringUtils.defaultIfEmpty(address2, null); + this.city = city; + this.pinCode = pinCode; + this.telephone = StringUtils.defaultIfEmpty(telephone, null); + } + + void setAgent(final Agent agent) { + this.agent = agent; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentLimit.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentLimit.java new file mode 100644 index 000000000..a4c3e14ed --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/domain/AgentLimit.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * One row of an Agent's time-bound collection-limit history (From Date / To Date / Limit) - an + * Agent's authorized collection limit can change over time and every change is kept, not overwritten. + */ +@Getter +@Entity +@Table(name = "m_dds_agent_limit") +public class AgentLimit extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "agent_id", nullable = false) + private Agent agent; + + @Column(name = "from_date", nullable = false) + private LocalDate fromDate; + + @Column(name = "to_date", nullable = false) + private LocalDate toDate; + + @Column(name = "limit_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal limitAmount; + + protected AgentLimit() { + // + } + + public AgentLimit(final LocalDate fromDate, final LocalDate toDate, final BigDecimal limitAmount) { + this.fromDate = fromDate; + this.toDate = toDate; + this.limitAmount = limitAmount; + } + + void setAgent(final Agent agent) { + this.agent = agent; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/agent/exception/AgentNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/exception/AgentNotFoundException.java new file mode 100644 index 000000000..b8de5e56d --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/agent/exception/AgentNotFoundException.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.agent.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; +import org.springframework.dao.EmptyResultDataAccessException; + +/** + * A {@link RuntimeException} thrown when a DDS agent resource is not found. + */ +public class AgentNotFoundException extends AbstractPlatformResourceNotFoundException { + + public AgentNotFoundException(final Long id) { + super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id); + } + + public AgentNotFoundException(final Long id, final EmptyResultDataAccessException e) { + super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id, e); + } + + public AgentNotFoundException(final String agentCode) { + super("error.msg.agent.code.invalid", "Agent with code " + agentCode + " does not exist", agentCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/data/CommissionSlabData.java b/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/data/CommissionSlabData.java new file mode 100644 index 000000000..739e485ab --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/data/CommissionSlabData.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.commissionslab.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class CommissionSlabData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final String slabType; + private final BigDecimal slabFrom; + private final BigDecimal slabTo; + private final BigDecimal commissionPercent; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlab.java b/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlab.java new file mode 100644 index 000000000..d1b46148e --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlab.java @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.commissionslab.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * One row of the agent commission chart: an ACCOUNT slab (commission percentage on collection + * volume) or a COMMISSION slab (the override a senior agent earns on a sub-agent's commission). + * Append-only reference data, like the legacy screen it replaces - there is deliberately no update or + * delete API; a superseded slab range is retired by adding a new row, not editing history. + */ +@Getter +@Entity +@Table(name = "m_dds_commission_slab") +public class CommissionSlab extends AbstractPersistableCustom { + + @Column(name = "slab_type", length = 20, nullable = false) + private String slabType; + + @Column(name = "slab_from", scale = 6, precision = 19, nullable = false) + private BigDecimal slabFrom; + + @Column(name = "slab_to", scale = 6, precision = 19, nullable = false) + private BigDecimal slabTo; + + @Column(name = "commission_percent", scale = 6, precision = 19, nullable = false) + private BigDecimal commissionPercent; + + protected CommissionSlab() { + // + } + + public CommissionSlab(final String slabType, final BigDecimal slabFrom, final BigDecimal slabTo, final BigDecimal commissionPercent) { + this.slabType = slabType; + this.slabFrom = slabFrom; + this.slabTo = slabTo; + this.commissionPercent = commissionPercent; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunData.java new file mode 100644 index 000000000..dcc0344e2 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunData.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(toBuilder = true) +public final class BatchRunData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final String jobType; + private final String branchScope; + private final LocalDate jobDate; + private final Long agentFrom; + private final Long agentTo; + private final Long partyFrom; + private final Long partyTo; + private final String status; + private final Integer totalRows; + private final BigDecimal totalAmount; + private final LocalDate generatedOnDate; + private final LocalDate postedOnDate; + private final Collection rows; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunRowData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunRowData.java new file mode 100644 index 000000000..7d854b0be --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/data/BatchRunRowData.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class BatchRunRowData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long partyId; + private final String partyCode; + private final String partyName; + private final Long agentId; + private final String agentCode; + private final BigDecimal amount; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRun.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRun.java new file mode 100644 index 000000000..253b9af30 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRun.java @@ -0,0 +1,122 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * One Calculate-then-Post batch run - the deliberate two-step control from the legacy Interest Part + * menu (Interest Calculation -> Interest Posting, OD Interest Calculation -> OD Interest Posting, + * Commission Calculation -> Commission Posting): a run is generated (rows computed and saved as a + * preview, nothing else changes), then posted (a separate, explicit step that applies exactly the + * rows that were previewed - never a fresh recalculation, so what gets applied is always what was + * seen). + */ +@Getter +@Entity +@Table(name = "m_dds_batch_run") +public class BatchRun extends AbstractPersistableCustom { + + @Column(name = "job_type", length = 40, nullable = false) + private String jobType; + + @Column(name = "branch_scope", length = 20, nullable = false) + private String branchScope; + + @Column(name = "job_date", nullable = false) + private LocalDate jobDate; + + @Column(name = "agent_from", nullable = false) + private Long agentFrom; + + @Column(name = "agent_to", nullable = false) + private Long agentTo; + + @Column(name = "party_from") + private Long partyFrom; + + @Column(name = "party_to") + private Long partyTo; + + @Column(name = "status", length = 20, nullable = false) + private String status; + + @Column(name = "total_rows", nullable = false) + private Integer totalRows; + + @Column(name = "total_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal totalAmount; + + @Column(name = "generated_on_date", nullable = false) + private LocalDate generatedOnDate; + + @Column(name = "posted_on_date") + private LocalDate postedOnDate; + + @OneToMany(mappedBy = "batchRun", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OrderBy("id") + private List rows = new ArrayList<>(); + + protected BatchRun() { + // + } + + public BatchRun(final String jobType, final String branchScope, final LocalDate jobDate, final Long agentFrom, + final Long agentTo, final Long partyFrom, final Long partyTo, final LocalDate generatedOnDate) { + this.jobType = jobType; + this.branchScope = branchScope; + this.jobDate = jobDate; + this.agentFrom = agentFrom; + this.agentTo = agentTo; + this.partyFrom = partyFrom; + this.partyTo = partyTo; + this.status = "CALCULATED"; + this.totalRows = 0; + this.totalAmount = BigDecimal.ZERO; + this.generatedOnDate = generatedOnDate; + } + + public void addRow(final BatchRunRow row) { + row.setBatchRun(this); + this.rows.add(row); + this.totalRows = this.rows.size(); + this.totalAmount = this.totalAmount.add(row.getAmount()); + } + + public boolean isPosted() { + return "POSTED".equals(this.status); + } + + public void markPosted(final LocalDate postedOnDate) { + this.status = "POSTED"; + this.postedOnDate = postedOnDate; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRow.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRow.java new file mode 100644 index 000000000..5d2e5576f --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRow.java @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * One previewed/posted line of a {@link BatchRun}: for an interest job, one Party's accrued interest; + * for a commission job, one Agent's commission on their collected volume (partyId is null there). + */ +@Getter +@Entity +@Table(name = "m_dds_batch_run_row") +public class BatchRunRow extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "batch_run_id", nullable = false) + private BatchRun batchRun; + + @Column(name = "party_id") + private Long partyId; + + @Column(name = "agent_id", nullable = false) + private Long agentId; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + protected BatchRunRow() { + // + } + + public BatchRunRow(final Long partyId, final Long agentId, final BigDecimal amount) { + this.partyId = partyId; + this.agentId = agentId; + this.amount = amount; + } + + void setBatchRun(final BatchRun batchRun) { + this.batchRun = batchRun; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunAlreadyPostedException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunAlreadyPostedException.java new file mode 100644 index 000000000..c777aff38 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunAlreadyPostedException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when Post is attempted on a batch run that has already been posted - a run applies once, + * matching the calculate-then-post safety control (never re-post the same preview). + */ +public class BatchRunAlreadyPostedException extends AbstractPlatformDomainRuleException { + + public BatchRunAlreadyPostedException(final Long batchRunId) { + super("error.msg.ddsbatchrun.already.posted", "DDS batch run " + batchRunId + " has already been posted", batchRunId); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunNotFoundException.java new file mode 100644 index 000000000..24823a5d1 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/batch/exception/BatchRunNotFoundException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.batch.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class BatchRunNotFoundException extends AbstractPlatformResourceNotFoundException { + + public BatchRunNotFoundException(final Long id) { + super("error.msg.ddsbatchrun.id.invalid", "DDS batch run with identifier " + id + " does not exist", id); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyData.java new file mode 100644 index 000000000..8e0420b54 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyData.java @@ -0,0 +1,107 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import lombok.Builder; +import lombok.Getter; + +/** + * Immutable data object representing a DDS Party (customer scheme account), including its nominees. + * Built via the Lombok builder rather than a long positional constructor - the field count here makes + * positional args unreadable and error-prone to keep in sync. + */ +@Getter +@Builder(toBuilder = true) +public final class PartyData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long clientId; + private final String clientName; + private final Long agentId; + private final String agentCode; + private final String agentName; + private final Long savingsAccountId; + private final String subType; + private final String partyCode; + private final String lastAccount; + private final Boolean nominal; + private final String aadharNo; + private final String partyName; + private final String partyName1; + private final String address1; + private final String address2; + private final String address3; + private final String city; + private final String pincode; + private final String gender; + private final String panCardNo; + private final String telephone; + private final String mobileNoSms; + private final String operation; + private final LocalDate birthDate; + private final String occupation; + private final String risk; + private final String ckycId; + private final String adultMinor; + private final LocalDate minorDate; + private final Long guardianClientId; + private final String minAccountOpt; + private final String relation; + private final LocalDate openingDate; + private final Integer month; + private final LocalDate maturityDate; + private final LocalDate reopenDate; + private final BigDecimal depositAmount; + private final BigDecimal dailyDepositAmount; + private final Integer days; + private final String ledgerNo; + private final BigDecimal maturityAmount; + private final BigDecimal interestRate; + private final String withdrawType; + private final Boolean od1Flag; + private final Boolean od2Flag; + private final Boolean lien; + private final LocalDate payDate; + private final BigDecimal paymentAmount; + private final BigDecimal currentBalance; + private final BigDecimal openingBalance; + private final Boolean isSmsActive; + private final Boolean isEmailActive; + private final BigDecimal interestCalc; + private final LocalDate interestDate; + private final String spDescription; + private final String freezeCode; + private final String advCode; + private final String creditToBranch; + private final String creditToAccountType; + private final String creditToAccount; + private final Boolean nomineeApplicable; + private final String nomineeSplitMode; + private final String status; + private final LocalDate closeDate; + private final Collection nominees; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyNomineeData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyNomineeData.java new file mode 100644 index 000000000..11c4b2276 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/data/PartyNomineeData.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; +import lombok.Getter; + +@Getter +public final class PartyNomineeData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final LocalDate nominationDate; + private final String name; + private final String mobileNo; + private final LocalDate birthDate; + private final Boolean minor; + private final String relation; + private final String gender; + private final String guardianName; + + public static PartyNomineeData instance(final Long id, final LocalDate nominationDate, final String name, final String mobileNo, + final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) { + return new PartyNomineeData(id, nominationDate, name, mobileNo, birthDate, minor, relation, gender, guardianName); + } + + private PartyNomineeData(final Long id, final LocalDate nominationDate, final String name, final String mobileNo, + final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) { + this.id = id; + this.nominationDate = nominationDate; + this.name = name; + this.mobileNo = mobileNo; + this.birthDate = birthDate; + this.minor = minor; + this.relation = relation; + this.gender = gender; + this.guardianName = guardianName; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/Party.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/Party.java new file mode 100644 index 000000000..81a303e0b --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/Party.java @@ -0,0 +1,569 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.organisation.agent.domain.Agent; +import org.apache.fineract.portfolio.client.domain.Client; + +/** + * A DDS Party - the customer's daily-deposit scheme account, linked to the Agent who collects for it. + * Carries the scheme's KYC, terms and two-stage overdue flags, plus a nominee list. + * + * The underlying RD (recurring deposit) account this scheme conceptually reuses is not wired up yet - + * {@code savingsAccountId} is a nullable placeholder for that future linkage (see the DDS architecture + * decision to reuse the RD engine for the deposit half). Until then the scheme/balance fields below + * are this record's own, not mirrored from any m_savings_account row. + */ +@Getter +@Entity +@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") }) +public class Party extends AbstractPersistableCustom { + + /** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */ + private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2"); + + public static final String STATUS_ACTIVE = "ACTIVE"; + public static final String STATUS_CLOSED = "CLOSED"; + + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private Client client; + + @ManyToOne + @JoinColumn(name = "agent_id", nullable = false) + private Agent agent; + + @Column(name = "savings_account_id") + private Long savingsAccountId; + + @Column(name = "sub_type", length = 60, nullable = false) + private String subType; + + @Column(name = "party_code", length = 20, nullable = false) + private String partyCode; + + @Column(name = "last_account", length = 50) + private String lastAccount; + + @Column(name = "nominal", nullable = false) + private boolean nominal; + + @Column(name = "aadhar_no", length = 20) + private String aadharNo; + + @Column(name = "party_name", length = 100, nullable = false) + private String partyName; + + @Column(name = "party_name1", length = 100) + private String partyName1; + + @Column(name = "address1", length = 200, nullable = false) + private String address1; + + @Column(name = "address2", length = 200) + private String address2; + + @Column(name = "address3", length = 200) + private String address3; + + @Column(name = "city", length = 100, nullable = false) + private String city; + + @Column(name = "pincode", length = 20, nullable = false) + private String pincode; + + @Column(name = "gender", length = 20) + private String gender; + + @Column(name = "pan_card_no", length = 20) + private String panCardNo; + + @Column(name = "telephone", length = 20) + private String telephone; + + @Column(name = "mobile_no_sms", length = 20, nullable = false) + private String mobileNoSms; + + @Column(name = "operation", length = 20, nullable = false) + private String operation; + + @Column(name = "birth_date") + private LocalDate birthDate; + + @Column(name = "occupation", length = 40) + private String occupation; + + @Column(name = "risk", length = 20) + private String risk; + + @Column(name = "ckyc_id", length = 40) + private String ckycId; + + @Column(name = "adult_minor", length = 20, nullable = false) + private String adultMinor; + + @Column(name = "minor_date") + private LocalDate minorDate; + + @Column(name = "guardian_client_id") + private Long guardianClientId; + + @Column(name = "min_account_opt", length = 40) + private String minAccountOpt; + + @Column(name = "relation", length = 40) + private String relation; + + @Column(name = "opening_date", nullable = false) + private LocalDate openingDate; + + @Column(name = "months") + private Integer month; + + @Column(name = "maturity_date") + private LocalDate maturityDate; + + @Column(name = "reopen_date") + private LocalDate reopenDate; + + @Column(name = "deposit_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal depositAmount; + + @Column(name = "daily_deposit_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal dailyDepositAmount; + + @Column(name = "days", nullable = false) + private Integer days; + + @Column(name = "ledger_no", length = 40) + private String ledgerNo; + + @Column(name = "maturity_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal maturityAmount; + + @Column(name = "interest_rate", scale = 6, precision = 19, nullable = false) + private BigDecimal interestRate; + + @Column(name = "withdraw_type", length = 20) + private String withdrawType; + + @Column(name = "od1_flag", nullable = false) + private boolean od1Flag; + + @Column(name = "od2_flag", nullable = false) + private boolean od2Flag; + + @Column(name = "lien", nullable = false) + private boolean lien; + + @Column(name = "pay_date") + private LocalDate payDate; + + @Column(name = "payment_amount", scale = 6, precision = 19, nullable = false) + private BigDecimal paymentAmount; + + @Column(name = "current_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal currentBalance; + + @Column(name = "opening_balance", scale = 6, precision = 19, nullable = false) + private BigDecimal openingBalance; + + @Column(name = "is_sms_active", nullable = false) + private boolean isSmsActive; + + @Column(name = "is_email_active", nullable = false) + private boolean isEmailActive; + + @Column(name = "interest_calc", scale = 6, precision = 19, nullable = false) + private BigDecimal interestCalc; + + @Column(name = "interest_date") + private LocalDate interestDate; + + @Column(name = "sp_description", length = 500) + private String spDescription; + + @Column(name = "freeze_code", length = 20, nullable = false) + private String freezeCode; + + @Column(name = "adv_code", length = 40, nullable = false) + private String advCode; + + @Column(name = "credit_to_branch", length = 100) + private String creditToBranch; + + @Column(name = "credit_to_account_type", length = 100) + private String creditToAccountType; + + @Column(name = "credit_to_account", length = 50) + private String creditToAccount; + + @Column(name = "nominee_applicable", nullable = false) + private boolean nomineeApplicable; + + @Column(name = "nominee_split_mode", length = 20) + private String nomineeSplitMode; + + @Column(name = "status", length = 20, nullable = false) + private String status; + + @Column(name = "close_date") + private LocalDate closeDate; + + @OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OrderBy("id") + private List nominees = new ArrayList<>(); + + protected Party() { + // + } + + private Party(final Client client, final Agent agent, final String partyCode, final JsonCommand command) { + this.client = client; + this.agent = agent; + this.partyCode = partyCode; + this.subType = command.stringValueOfParameterNamed("subType"); + this.lastAccount = command.stringValueOfParameterNamedAllowingNull("lastAccount"); + this.nominal = command.booleanPrimitiveValueOfParameterNamed("nominal"); + this.aadharNo = command.stringValueOfParameterNamedAllowingNull("aadharNo"); + this.partyName = command.stringValueOfParameterNamed("partyName"); + this.partyName1 = command.stringValueOfParameterNamedAllowingNull("partyName1"); + this.address1 = command.stringValueOfParameterNamed("address1"); + this.address2 = command.stringValueOfParameterNamedAllowingNull("address2"); + this.address3 = command.stringValueOfParameterNamedAllowingNull("address3"); + this.city = command.stringValueOfParameterNamed("city"); + this.pincode = command.stringValueOfParameterNamed("pincode"); + this.gender = command.stringValueOfParameterNamedAllowingNull("gender"); + this.panCardNo = command.stringValueOfParameterNamedAllowingNull("panCardNo"); + this.telephone = command.stringValueOfParameterNamedAllowingNull("telephone"); + this.mobileNoSms = command.stringValueOfParameterNamed("mobileNoSms"); + this.operation = command.stringValueOfParameterNamed("operation"); + this.birthDate = command.localDateValueOfParameterNamed("birthDate"); + this.occupation = command.stringValueOfParameterNamedAllowingNull("occupation"); + this.risk = command.stringValueOfParameterNamedAllowingNull("risk"); + this.ckycId = command.stringValueOfParameterNamedAllowingNull("ckycId"); + this.adultMinor = command.stringValueOfParameterNamed("adultMinor"); + this.minorDate = command.localDateValueOfParameterNamed("minorDate"); + this.guardianClientId = command.longValueOfParameterNamed("guardianClientId"); + this.minAccountOpt = command.stringValueOfParameterNamedAllowingNull("minAccountOpt"); + this.relation = command.stringValueOfParameterNamedAllowingNull("relation"); + this.openingDate = command.localDateValueOfParameterNamed("openingDate"); + this.month = command.integerValueOfParameterNamedDefaultToNullIfZero("month"); + this.maturityDate = command.localDateValueOfParameterNamed("maturityDate"); + this.reopenDate = command.localDateValueOfParameterNamed("reopenDate"); + this.depositAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("depositAmount")); + this.dailyDepositAmount = command.bigDecimalValueOfParameterNamed("dailyDepositAmount"); + this.days = command.integerValueOfParameterNamed("days"); + this.ledgerNo = command.stringValueOfParameterNamedAllowingNull("ledgerNo"); + this.maturityAmount = BigDecimal.ZERO; + this.interestRate = command.parameterExists("interestRate") + ? command.bigDecimalValueOfParameterNamed("interestRate") + : DEFAULT_INTEREST_RATE; + this.withdrawType = command.stringValueOfParameterNamedAllowingNull("withdrawType"); + this.od1Flag = false; + this.od2Flag = false; + this.lien = command.booleanPrimitiveValueOfParameterNamed("lien"); + this.payDate = command.localDateValueOfParameterNamed("payDate"); + this.paymentAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("paymentAmount")); + this.openingBalance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("openingBalance")); + this.currentBalance = this.openingBalance; + this.isSmsActive = command.booleanPrimitiveValueOfParameterNamed("isSmsActive"); + this.isEmailActive = command.booleanPrimitiveValueOfParameterNamed("isEmailActive"); + this.interestCalc = BigDecimal.ZERO; + this.interestDate = command.localDateValueOfParameterNamed("interestDate"); + this.spDescription = command.stringValueOfParameterNamedAllowingNull("spDescription"); + this.freezeCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("freezeCode"), "Normal"); + this.advCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("advCode"), "None"); + this.creditToBranch = command.stringValueOfParameterNamedAllowingNull("creditToBranch"); + this.creditToAccountType = command.stringValueOfParameterNamedAllowingNull("creditToAccountType"); + this.creditToAccount = command.stringValueOfParameterNamedAllowingNull("creditToAccount"); + this.nomineeApplicable = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable"); + this.nomineeSplitMode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("nomineeSplitMode"), + "Percentage"); + this.status = STATUS_ACTIVE; + } + + public static Party fromJson(final Client client, final Agent agent, final String partyCode, final JsonCommand command) { + return new Party(client, agent, partyCode, command); + } + + private static BigDecimal orZero(final BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + public void addNominee(final PartyNominee nominee) { + nominee.setParty(this); + this.nominees.add(nominee); + } + + public void resetNominees() { + this.nominees.clear(); + } + + public void changeClient(final Client newClient) { + this.client = newClient; + } + + public void changeAgent(final Agent newAgent) { + this.agent = newAgent; + } + + /** + * Links this Party to an existing real Savings Account, resolved by account number in the write + * service. There is no unlink - once a Party's DDS scheme is tied to a real account, it stays + * tied. + */ + public void linkSavingsAccount(final Long savingsAccountId) { + this.savingsAccountId = savingsAccountId; + } + + public BigDecimal credit(final BigDecimal amount) { + this.currentBalance = this.currentBalance.add(amount); + return this.currentBalance; + } + + public BigDecimal debit(final BigDecimal amount) { + this.currentBalance = this.currentBalance.subtract(amount); + return this.currentBalance; + } + + public boolean isClosed() { + return STATUS_CLOSED.equals(this.status); + } + + /** + * Closes this Party's account as of the given date. The balance must already be zero - a Party + * is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money + * still owed to it. + */ + public void close(final LocalDate closeDate) { + this.status = STATUS_CLOSED; + this.closeDate = closeDate; + } + + /** + * Stamps the date interest (regular or OD) was last calculated up to, so the next batch run + * accrues only the days since. Set on POST, not on calculate/preview. + */ + public void markInterestCalculatedThrough(final LocalDate date) { + this.interestDate = date; + } + + public Long clientId() { + return this.client.getId(); + } + + public Long agentId() { + return this.agent.getId(); + } + + public Map update(final JsonCommand command) { + + final Map actualChanges = new LinkedHashMap<>(40); + + if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) { + actualChanges.put("clientId", command.longValueOfParameterNamed("clientId")); + } + if (command.isChangeInLongParameterNamed("agentId", this.agent.getId())) { + actualChanges.put("agentId", command.longValueOfParameterNamed("agentId")); + } + + stringChange(command, actualChanges, "subType", this.subType, v -> this.subType = v); + stringChange(command, actualChanges, "lastAccount", this.lastAccount, v -> this.lastAccount = v); + + if (command.isChangeInBooleanParameterNamed("nominal", this.nominal)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nominal"); + actualChanges.put("nominal", newValue); + this.nominal = newValue; + } + + stringChange(command, actualChanges, "aadharNo", this.aadharNo, v -> this.aadharNo = v); + stringChange(command, actualChanges, "partyName", this.partyName, v -> this.partyName = v); + stringChange(command, actualChanges, "partyName1", this.partyName1, v -> this.partyName1 = v); + stringChange(command, actualChanges, "address1", this.address1, v -> this.address1 = v); + stringChange(command, actualChanges, "address2", this.address2, v -> this.address2 = v); + stringChange(command, actualChanges, "address3", this.address3, v -> this.address3 = v); + stringChange(command, actualChanges, "city", this.city, v -> this.city = v); + stringChange(command, actualChanges, "pincode", this.pincode, v -> this.pincode = v); + stringChange(command, actualChanges, "gender", this.gender, v -> this.gender = v); + stringChange(command, actualChanges, "panCardNo", this.panCardNo, v -> this.panCardNo = v); + stringChange(command, actualChanges, "telephone", this.telephone, v -> this.telephone = v); + stringChange(command, actualChanges, "mobileNoSms", this.mobileNoSms, v -> this.mobileNoSms = v); + stringChange(command, actualChanges, "operation", this.operation, v -> this.operation = v); + + if (command.isChangeInDateParameterNamed("birthDate", this.birthDate)) { + actualChanges.put("birthDate", command.stringValueOfParameterNamed("birthDate")); + this.birthDate = command.localDateValueOfParameterNamed("birthDate"); + } + + stringChange(command, actualChanges, "occupation", this.occupation, v -> this.occupation = v); + stringChange(command, actualChanges, "risk", this.risk, v -> this.risk = v); + stringChange(command, actualChanges, "ckycId", this.ckycId, v -> this.ckycId = v); + stringChange(command, actualChanges, "adultMinor", this.adultMinor, v -> this.adultMinor = v); + + if (command.isChangeInDateParameterNamed("minorDate", this.minorDate)) { + actualChanges.put("minorDate", command.stringValueOfParameterNamed("minorDate")); + this.minorDate = command.localDateValueOfParameterNamed("minorDate"); + } + + if (command.isChangeInLongParameterNamed("guardianClientId", this.guardianClientId)) { + final Long newValue = command.longValueOfParameterNamed("guardianClientId"); + actualChanges.put("guardianClientId", newValue); + this.guardianClientId = newValue; + } + + stringChange(command, actualChanges, "minAccountOpt", this.minAccountOpt, v -> this.minAccountOpt = v); + stringChange(command, actualChanges, "relation", this.relation, v -> this.relation = v); + + if (command.isChangeInDateParameterNamed("openingDate", this.openingDate)) { + actualChanges.put("openingDate", command.stringValueOfParameterNamed("openingDate")); + this.openingDate = command.localDateValueOfParameterNamed("openingDate"); + } + + if (command.isChangeInIntegerParameterNamed("month", this.month)) { + final Integer newValue = command.integerValueOfParameterNamed("month"); + actualChanges.put("month", newValue); + this.month = newValue; + } + + if (command.isChangeInDateParameterNamed("maturityDate", this.maturityDate)) { + actualChanges.put("maturityDate", command.stringValueOfParameterNamed("maturityDate")); + this.maturityDate = command.localDateValueOfParameterNamed("maturityDate"); + } + + if (command.isChangeInDateParameterNamed("reopenDate", this.reopenDate)) { + actualChanges.put("reopenDate", command.stringValueOfParameterNamed("reopenDate")); + this.reopenDate = command.localDateValueOfParameterNamed("reopenDate"); + } + + if (command.isChangeInBigDecimalParameterNamed("depositAmount", this.depositAmount)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("depositAmount"); + actualChanges.put("depositAmount", newValue); + this.depositAmount = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("dailyDepositAmount", this.dailyDepositAmount)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("dailyDepositAmount"); + actualChanges.put("dailyDepositAmount", newValue); + this.dailyDepositAmount = newValue; + } + + if (command.isChangeInIntegerParameterNamed("days", this.days)) { + final Integer newValue = command.integerValueOfParameterNamed("days"); + actualChanges.put("days", newValue); + this.days = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("interestRate", this.interestRate)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("interestRate"); + actualChanges.put("interestRate", newValue); + this.interestRate = newValue; + } + + stringChange(command, actualChanges, "ledgerNo", this.ledgerNo, v -> this.ledgerNo = v); + stringChange(command, actualChanges, "withdrawType", this.withdrawType, v -> this.withdrawType = v); + + if (command.isChangeInBooleanParameterNamed("lien", this.lien)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("lien"); + actualChanges.put("lien", newValue); + this.lien = newValue; + } + + if (command.isChangeInDateParameterNamed("payDate", this.payDate)) { + actualChanges.put("payDate", command.stringValueOfParameterNamed("payDate")); + this.payDate = command.localDateValueOfParameterNamed("payDate"); + } + + if (command.isChangeInBigDecimalParameterNamed("paymentAmount", this.paymentAmount)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("paymentAmount"); + actualChanges.put("paymentAmount", newValue); + this.paymentAmount = newValue; + } + + if (command.isChangeInBigDecimalParameterNamed("openingBalance", this.openingBalance)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("openingBalance"); + actualChanges.put("openingBalance", newValue); + this.openingBalance = newValue; + } + + if (command.isChangeInBooleanParameterNamed("isSmsActive", this.isSmsActive)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isSmsActive"); + actualChanges.put("isSmsActive", newValue); + this.isSmsActive = newValue; + } + + if (command.isChangeInBooleanParameterNamed("isEmailActive", this.isEmailActive)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isEmailActive"); + actualChanges.put("isEmailActive", newValue); + this.isEmailActive = newValue; + } + + if (command.isChangeInDateParameterNamed("interestDate", this.interestDate)) { + actualChanges.put("interestDate", command.stringValueOfParameterNamed("interestDate")); + this.interestDate = command.localDateValueOfParameterNamed("interestDate"); + } + + stringChange(command, actualChanges, "spDescription", this.spDescription, v -> this.spDescription = v); + stringChange(command, actualChanges, "freezeCode", this.freezeCode, v -> this.freezeCode = v); + stringChange(command, actualChanges, "advCode", this.advCode, v -> this.advCode = v); + stringChange(command, actualChanges, "creditToBranch", this.creditToBranch, v -> this.creditToBranch = v); + stringChange(command, actualChanges, "creditToAccountType", this.creditToAccountType, v -> this.creditToAccountType = v); + stringChange(command, actualChanges, "creditToAccount", this.creditToAccount, v -> this.creditToAccount = v); + + if (command.isChangeInBooleanParameterNamed("nomineeApplicable", this.nomineeApplicable)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable"); + actualChanges.put("nomineeApplicable", newValue); + this.nomineeApplicable = newValue; + } + + stringChange(command, actualChanges, "nomineeSplitMode", this.nomineeSplitMode, v -> this.nomineeSplitMode = v); + + return actualChanges; + } + + private void stringChange(final JsonCommand command, final Map actualChanges, final String paramName, + final String existingValue, final Consumer setter) { + if (command.isChangeInStringParameterNamed(paramName, existingValue)) { + final String newValue = command.stringValueOfParameterNamed(paramName); + actualChanges.put(paramName, newValue); + setter.accept(newValue); + } + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyNominee.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyNominee.java new file mode 100644 index 000000000..1021dca53 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyNominee.java @@ -0,0 +1,88 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * A single nominee recorded against a {@link Party}. Maker-checker on nominee edits is enforced at + * the command layer (the whole Party update goes through the standard maker-checker command flow), + * not on this child row individually. + */ +@Getter +@Entity +@Table(name = "m_dds_party_nominee") +public class PartyNominee extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "party_id", nullable = false) + private Party party; + + @Column(name = "nomination_date", nullable = false) + private LocalDate nominationDate; + + @Column(name = "name", length = 100, nullable = false) + private String name; + + @Column(name = "mobile_no", length = 20) + private String mobileNo; + + @Column(name = "birth_date") + private LocalDate birthDate; + + @Column(name = "minor", nullable = false) + private boolean minor; + + @Column(name = "relation", length = 40, nullable = false) + private String relation; + + @Column(name = "gender", length = 20, nullable = false) + private String gender; + + @Column(name = "guardian_name", length = 100) + private String guardianName; + + protected PartyNominee() { + // + } + + public PartyNominee(final LocalDate nominationDate, final String name, final String mobileNo, final LocalDate birthDate, + final boolean minor, final String relation, final String gender, final String guardianName) { + this.nominationDate = nominationDate; + this.name = name; + this.mobileNo = StringUtils.defaultIfEmpty(mobileNo, null); + this.birthDate = birthDate; + this.minor = minor; + this.relation = relation; + this.gender = gender; + this.guardianName = StringUtils.defaultIfEmpty(guardianName, null); + } + + void setParty(final Party party) { + this.party = party; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyAlreadyClosedException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyAlreadyClosedException.java new file mode 100644 index 000000000..19adf89e2 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyAlreadyClosedException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when a Close Party command, or a Party Tran collection, targets a Party that is already + * closed. + */ +public class PartyAlreadyClosedException extends AbstractPlatformDomainRuleException { + + public PartyAlreadyClosedException(final String partyCode) { + super("error.msg.party.already.closed", "Party " + partyCode + " is already closed", partyCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyHasBalanceException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyHasBalanceException.java new file mode 100644 index 000000000..62db2cd0b --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyHasBalanceException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when a Close Party command is attempted while the Party's balance is still non-zero - the + * remaining balance must be paid out (a Withdraw Deposite to zero) before the account can be closed. + */ +public class PartyHasBalanceException extends AbstractPlatformDomainRuleException { + + public PartyHasBalanceException(final String partyCode) { + super("error.msg.party.has.balance", "Party " + partyCode + " still has a non-zero balance and cannot be closed", partyCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyNotFoundException.java new file mode 100644 index 000000000..4c2bf81dd --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartyNotFoundException.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; +import org.springframework.dao.EmptyResultDataAccessException; + +/** + * A {@link RuntimeException} thrown when a DDS party resource is not found. + */ +public class PartyNotFoundException extends AbstractPlatformResourceNotFoundException { + + public PartyNotFoundException(final Long id) { + super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id); + } + + public PartyNotFoundException(final Long id, final EmptyResultDataAccessException e) { + super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id, e); + } + + public PartyNotFoundException(final String partyCode) { + super("error.msg.party.code.invalid", "Party with code " + partyCode + " does not exist", partyCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountClientMismatchException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountClientMismatchException.java new file mode 100644 index 000000000..a20390d11 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountClientMismatchException.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when the savingsAccountNo given for a Party belongs to a different client than the Party's + * own clientId - a Party can only be linked to its own client's savings account. + */ +public class PartySavingsAccountClientMismatchException extends AbstractPlatformDomainRuleException { + + public PartySavingsAccountClientMismatchException(final String savingsAccountNo) { + super("error.msg.party.savings.account.client.mismatch", + "Savings account " + savingsAccountNo + " does not belong to this party's client", savingsAccountNo); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountNotFoundException.java new file mode 100644 index 000000000..6c029c665 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/party/exception/PartySavingsAccountNotFoundException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.party.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +/** + * Thrown when a Party's savingsAccountNo does not match any non-closed Savings Account. + */ +public class PartySavingsAccountNotFoundException extends AbstractPlatformResourceNotFoundException { + + public PartySavingsAccountNotFoundException(final String savingsAccountNo) { + super("error.msg.party.savings.account.not.found", "No open savings account found with account number " + savingsAccountNo, + savingsAccountNo); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/report/data/DdsReportData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/report/data/DdsReportData.java new file mode 100644 index 000000000..0142fee0c --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/report/data/DdsReportData.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.report.data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; + +/** + * A generic tabular result shape shared by every DDS report - one column list plus one row list, both + * keyed the same way, so a single report viewer screen can render any of them without a bespoke + * result type per report. + */ +@Getter +@Builder +public final class DdsReportData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final String reportKey; + private final boolean available; + private final String message; + private final List columns; + private final List> rows; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/data/PartyTransactionData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/data/PartyTransactionData.java new file mode 100644 index 000000000..94866e248 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/data/PartyTransactionData.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.transaction.data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class PartyTransactionData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long partyId; + private final String partyCode; + private final String partyName; + private final Long agentId; + private final String agentCode; + private final LocalDate transactionDate; + private final String transactionType; + private final String direction; + private final String transactionMode; + private final String accountType; + private final String accountNo; + private final BigDecimal amount; + private final String chequeNo; + private final String tokenNo; + private final String description; + private final BigDecimal balanceAfterTransaction; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransaction.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransaction.java new file mode 100644 index 000000000..4caf47fa7 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransaction.java @@ -0,0 +1,110 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.transaction.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.organisation.agent.domain.Agent; +import org.apache.fineract.portfolio.dds.party.domain.Party; + +/** + * A single posted entry on the DDS ledger: a Party's day's collection ("Party Tran", direction + * CREDIT), one leg of a "Withdraw Deposite" transfer between two parties (the creditor leg CREDIT, + * the debtor leg DEBIT), or an Agent's settlement of collected cash to its branch ("Agent Tran", + * direction DEBIT - no {@link Party} involved, so {@code party} is null for this type). + * {@code balanceAfterTransaction} holds the Party's balance for Party-linked rows, or the Agent's + * balance for an Agent Tran row. Immutable once posted - like any transaction, it is reversed with + * an opposing entry, not edited, so there is deliberately no update API for this entity. + */ +@Getter +@Entity +@Table(name = "m_dds_party_transaction") +public class PartyTransaction extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "party_id") + private Party party; + + @ManyToOne + @JoinColumn(name = "agent_id", nullable = false) + private Agent agent; + + @Column(name = "transaction_date", nullable = false) + private LocalDate transactionDate; + + @Column(name = "transaction_type", length = 30, nullable = false) + private String transactionType; + + @Column(name = "direction", length = 10, nullable = false) + private String direction; + + @Column(name = "transaction_mode", length = 20) + private String transactionMode; + + @Column(name = "account_type", length = 100) + private String accountType; + + @Column(name = "account_no", length = 50) + private String accountNo; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + @Column(name = "cheque_no", length = 40) + private String chequeNo; + + @Column(name = "token_no", length = 40) + private String tokenNo; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false) + private BigDecimal balanceAfterTransaction; + + protected PartyTransaction() { + // + } + + public PartyTransaction(final Party party, final Agent agent, final LocalDate transactionDate, final String transactionType, + final String direction, final String transactionMode, final String accountType, final String accountNo, + final BigDecimal amount, final String chequeNo, final String tokenNo, final String description, + final BigDecimal balanceAfterTransaction) { + this.party = party; + this.agent = agent; + this.transactionDate = transactionDate; + this.transactionType = transactionType; + this.direction = direction; + this.transactionMode = transactionMode; + this.accountType = accountType; + this.accountNo = accountNo; + this.amount = amount; + this.chequeNo = chequeNo; + this.tokenNo = tokenNo; + this.description = description; + this.balanceAfterTransaction = balanceAfterTransaction; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientAgentBalanceException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientAgentBalanceException.java new file mode 100644 index 000000000..0686b4480 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientAgentBalanceException.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.transaction.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when an Agent Tran settlement amount exceeds the Agent's current (collected but not yet + * settled) balance. + */ +public class InsufficientAgentBalanceException extends AbstractPlatformDomainRuleException { + + public InsufficientAgentBalanceException(final String agentCode) { + super("error.msg.agent.insufficient.balance", + "Agent " + agentCode + " does not have sufficient collected balance for this settlement", agentCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientPartyBalanceException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientPartyBalanceException.java new file mode 100644 index 000000000..da920dcbd --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/InsufficientPartyBalanceException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.transaction.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown when a Withdraw Deposite transfer would take the debtor Party's balance below zero. + */ +public class InsufficientPartyBalanceException extends AbstractPlatformDomainRuleException { + + public InsufficientPartyBalanceException(final String partyCode) { + super("error.msg.party.insufficient.balance", "Party " + partyCode + " does not have sufficient balance for this transfer", + partyCode); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/PartyTransactionNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/PartyTransactionNotFoundException.java new file mode 100644 index 000000000..2ea13495b --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/dds/transaction/exception/PartyTransactionNotFoundException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.dds.transaction.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; +import org.springframework.dao.EmptyResultDataAccessException; + +/** + * A {@link RuntimeException} thrown when a DDS party transaction resource is not found. + */ +public class PartyTransactionNotFoundException extends AbstractPlatformResourceNotFoundException { + + public PartyTransactionNotFoundException(final Long id, final EmptyResultDataAccessException e) { + super("error.msg.partytransaction.id.invalid", "Party transaction with identifier " + id + " does not exist", id, e); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java index 8b6a61f18..6d7239e52 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/SavingsApiConstants.java @@ -275,6 +275,7 @@ public class SavingsApiConstants { // Additional custom fields public static final String balanceFrequencyParamName = "balanceFrequency"; public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing"; + public static final String productCategoryParamName = "productCategory"; // Current Product — new UI fields public static final String balanceSwapOptionParamName = "balanceSwapOption"; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java index 0c74cde3c..983b2bf1c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java @@ -144,6 +144,7 @@ public final class SavingsProductData implements Serializable { @Getter @Setter private Boolean enableLockinPeriod; @Getter @Setter private String balanceFrequency; @Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing; + @Getter @Setter private String productCategory; // New current product fields @Getter @Setter private Boolean balanceSwapOption; diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/api/AgentsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/api/AgentsApiResource.java new file mode 100644 index 000000000..8a8c4975a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/api/AgentsApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/domain/AgentRepository.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/domain/AgentRepository.java new file mode 100644 index 000000000..b53b236b6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/domain/AgentRepository.java @@ -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 { + + @Query("select coalesce(max(cast(substring(a.agentCode, 3) as long)), 0) from Agent a where a.agentCode like 'AG%'") + Long retrieveMaxAgentCodeSuffix(); + + Optional findByAgentCode(String agentCode); + + @Query("select a from Agent a where a.id between :agentFrom and :agentTo") + List findBetweenIds(@Param("agentFrom") Long agentFrom, @Param("agentTo") Long agentTo); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/CreateAgentCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/CreateAgentCommandHandler.java new file mode 100644 index 000000000..6d1904226 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/CreateAgentCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/UpdateAgentCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/UpdateAgentCommandHandler.java new file mode 100644 index 000000000..21fb56101 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/handler/UpdateAgentCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/serialization/AgentCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/serialization/AgentCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..c6e9b69ff --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/serialization/AgentCommandFromApiJsonDeserializer.java @@ -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 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 SUPPORTED_LIMIT_PARAMETERS = new HashSet<>(Arrays.asList("fromDate", "toDate", "limit")); + + private static final Set 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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 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 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformService.java new file mode 100644 index 000000000..383551587 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformService.java @@ -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 retrieveAll(); + + AgentData retrieveOne(Long agentId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformServiceImpl.java new file mode 100644 index 000000000..d8101760e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentReadPlatformServiceImpl.java @@ -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 { + + 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> { + + 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 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> { + + 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 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 retrieveAll() { + final String sql = "select " + AGENT_MAPPER.schema() + " order by a.id"; + final List 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 attachChildren(final List agents) { + if (agents.isEmpty()) { + return agents; + } + + final String limitSql = "select " + LIMIT_MAPPER.schema() + " order by l.id"; + final List> limitRows = this.jdbcTemplate.query(limitSql, LIMIT_MAPPER); // NOSONAR + final Map> limitsByAgentId = new LinkedHashMap<>(); + for (final Map.Entry 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> guarantorRows = this.jdbcTemplate.query(guarantorSql, GUARANTOR_MAPPER); // NOSONAR + final Map> guarantorsByAgentId = new LinkedHashMap<>(); + for (final Map.Entry row : guarantorRows) { + guarantorsByAgentId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue()); + } + + final List result = new ArrayList<>(agents.size()); + for (final AgentData agent : agents) { + final List limits = limitsByAgentId.getOrDefault(agent.getId(), new ArrayList<>()); + final List 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformService.java new file mode 100644 index 000000000..e731550fa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..c228fa695 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/service/AgentWritePlatformServiceJpaRepositoryImpl.java @@ -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 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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/starter/OrganisationAgentConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/starter/OrganisationAgentConfiguration.java new file mode 100644 index 000000000..3ad81af58 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/agent/starter/OrganisationAgentConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/api/CommissionSlabsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/api/CommissionSlabsApiResource.java new file mode 100644 index 000000000..691285569 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/api/CommissionSlabsApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlabRepository.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlabRepository.java new file mode 100644 index 000000000..f568e8d59 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/domain/CommissionSlabRepository.java @@ -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 { + + @Query("select s from CommissionSlab s where s.slabType = :slabType and :amount between s.slabFrom and s.slabTo") + List findMatchingSlabs(@Param("slabType") String slabType, @Param("amount") BigDecimal amount); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/handler/CreateCommissionSlabsCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/handler/CreateCommissionSlabsCommandHandler.java new file mode 100644 index 000000000..3227aa1a8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/handler/CreateCommissionSlabsCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/serialization/CommissionSlabCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/serialization/CommissionSlabCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..cf16923d6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/serialization/CommissionSlabCommandFromApiJsonDeserializer.java @@ -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 SUPPORTED_SLAB_TYPES = new HashSet<>(Arrays.asList("ACCOUNT", "COMMISSION")); + + private static final Set SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("slabs", "dateFormat", "locale")); + + private static final Set 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformService.java new file mode 100644 index 000000000..4a76d2c84 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformService.java @@ -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 retrieveAll(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformServiceImpl.java new file mode 100644 index 000000000..28dca45a4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabReadPlatformServiceImpl.java @@ -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 { + + 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 retrieveAll() { + final String sql = "select " + MAPPER.schema() + " order by s.slab_type, s.slab_from"; + return this.jdbcTemplate.query(sql, MAPPER); // NOSONAR + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformService.java new file mode 100644 index 000000000..2e0286abd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..725674fa9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/service/CommissionSlabWritePlatformServiceJpaRepositoryImpl.java @@ -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 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)); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/starter/OrganisationCommissionSlabConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/starter/OrganisationCommissionSlabConfiguration.java new file mode 100644 index 000000000..2ef59c5c6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/commissionslab/starter/OrganisationCommissionSlabConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/api/BatchRunsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/api/BatchRunsApiResource.java new file mode 100644 index 000000000..30e4e3142 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/api/BatchRunsApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRepository.java new file mode 100644 index 000000000..a4611d9e3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/domain/BatchRunRepository.java @@ -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 { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/GenerateBatchRunCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/GenerateBatchRunCommandHandler.java new file mode 100644 index 000000000..d85d314b7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/GenerateBatchRunCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/PostBatchRunCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/PostBatchRunCommandHandler.java new file mode 100644 index 000000000..12c0450c5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/handler/PostBatchRunCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/serialization/BatchRunCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/serialization/BatchRunCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..a094447a3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/serialization/BatchRunCommandFromApiJsonDeserializer.java @@ -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 SUPPORTED_JOB_TYPES = new HashSet<>( + Arrays.asList("INTEREST_CALCULATION", "OD_INTEREST_CALCULATION", "COMMISSION_CALCULATION")); + + private static final Set SUPPORTED_BRANCH_SCOPES = new HashSet<>(Arrays.asList("All", "Selected")); + + private static final Set 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformService.java new file mode 100644 index 000000000..79838a36d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformService.java @@ -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 retrieveAll(); + + BatchRunData retrieveOne(Long batchRunId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformServiceImpl.java new file mode 100644 index 000000000..e2808648f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunReadPlatformServiceImpl.java @@ -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 { + + 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> { + + 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 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 retrieveAll() { + final String sql = "select " + BATCH_RUN_MAPPER.schema() + " order by b.id desc"; + final List 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 attachRows(final List batchRuns) { + if (batchRuns.isEmpty()) { + return batchRuns; + } + + final String sql = "select " + ROW_MAPPER.schema() + " order by r.id"; + final List> rows = this.jdbcTemplate.query(sql, ROW_MAPPER); // NOSONAR + + final Map> rowsByBatchRunId = new LinkedHashMap<>(); + for (final Map.Entry row : rows) { + rowsByBatchRunId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue()); + } + + final List result = new ArrayList<>(batchRuns.size()); + for (final BatchRunData b : batchRuns) { + result.add(b.toBuilder().rows(rowsByBatchRunId.getOrDefault(b.getId(), new ArrayList<>())).build()); + } + return result; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformService.java new file mode 100644 index 000000000..827a7ef09 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..0d92dd3e7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/service/BatchRunWritePlatformServiceJpaRepositoryImpl.java @@ -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 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 extractOfficeNames(final JsonCommand command) { + if (!command.parameterExists("officeNames")) { + return null; + } + final JsonArray officeNamesArray = command.arrayOfParameterNamed("officeNames"); + final Set 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 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 officeNames) { + final List 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 officeNames) { + final List 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 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(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/starter/DdsBatchConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/starter/DdsBatchConfiguration.java new file mode 100644 index 000000000..804bf2c9c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/batch/starter/DdsBatchConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/api/PartiesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/api/PartiesApiResource.java new file mode 100644 index 000000000..75770a107 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/api/PartiesApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyRepository.java new file mode 100644 index 000000000..ff1a432b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/domain/PartyRepository.java @@ -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 { + + @Query("select coalesce(max(cast(substring(p.partyCode, 3) as long)), 0) from Party p where p.partyCode like 'PT%'") + Long retrieveMaxPartyCodeSuffix(); + + Optional 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 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 findForOdInterestBatch(@Param("agentFrom") Long agentFrom, @Param("agentTo") Long agentTo, + @Param("partyFrom") Long partyFrom, @Param("partyTo") Long partyTo); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/ClosePartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/ClosePartyCommandHandler.java new file mode 100644 index 000000000..6cec96a61 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/ClosePartyCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/CreatePartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/CreatePartyCommandHandler.java new file mode 100644 index 000000000..43b292e20 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/CreatePartyCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/UpdatePartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/UpdatePartyCommandHandler.java new file mode 100644 index 000000000..37c3c99e5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/handler/UpdatePartyCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/serialization/PartyCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/serialization/PartyCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..c354d5207 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/serialization/PartyCommandFromApiJsonDeserializer.java @@ -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 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 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, Set.of("closeDate", "dateFormat", "locale")); + + final List 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 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformService.java new file mode 100644 index 000000000..10c4e4a5d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformService.java @@ -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 retrieveAll(); + + PartyData retrieveOne(Long partyId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformServiceImpl.java new file mode 100644 index 000000000..32fa6ddcd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyReadPlatformServiceImpl.java @@ -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 { + + 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> { + + 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 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 retrieveAll() { + final String sql = "select " + PARTY_MAPPER.schema() + " order by p.id"; + final List 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 attachNominees(final List parties) { + if (parties.isEmpty()) { + return parties; + } + + final String sql = "select " + NOMINEE_MAPPER.schema() + " order by n.id"; + final List> rows = this.jdbcTemplate.query(sql, NOMINEE_MAPPER); // NOSONAR + + final Map> nomineesByPartyId = new LinkedHashMap<>(); + for (final Map.Entry row : rows) { + nomineesByPartyId.computeIfAbsent(row.getKey(), k -> new ArrayList<>()).add(row.getValue()); + } + + final List result = new ArrayList<>(parties.size()); + for (final PartyData p : parties) { + result.add(p.toBuilder().nominees(nomineesByPartyId.getOrDefault(p.getId(), new ArrayList<>())).build()); + } + return result; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformService.java new file mode 100644 index 000000000..0723477d6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..cb48d5e23 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/service/PartyWritePlatformServiceJpaRepositoryImpl.java @@ -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 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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/starter/DdsPartyConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/starter/DdsPartyConfiguration.java new file mode 100644 index 000000000..ffe71f039 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/party/starter/DdsPartyConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/api/DdsReportsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/api/DdsReportsApiResource.java new file mode 100644 index 000000000..4d5b477d0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/api/DdsReportsApiResource.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformService.java new file mode 100644 index 000000000..bd46f29a8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformServiceImpl.java new file mode 100644 index 000000000..7fb2b08ae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/service/DdsReportReadPlatformServiceImpl.java @@ -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> 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> 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> 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> 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> 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> 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> 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> 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> 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 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> 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> 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> 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> 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> 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> 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> 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 columns, final List> rows) { + return DdsReportData.builder().reportKey(reportKey).available(true).columns(columns).rows(rows).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/starter/DdsReportConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/starter/DdsReportConfiguration.java new file mode 100644 index 000000000..4b190bc76 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/report/starter/DdsReportConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/api/PartyTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/api/PartyTransactionsApiResource.java new file mode 100644 index 000000000..25440dc09 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/api/PartyTransactionsApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransactionRepository.java new file mode 100644 index 000000000..fa084eb6c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/domain/PartyTransactionRepository.java @@ -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 { + + @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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/handler/CreatePartyTransactionCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/handler/CreatePartyTransactionCommandHandler.java new file mode 100644 index 000000000..73a95f95e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/handler/CreatePartyTransactionCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/serialization/PartyTransactionCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/serialization/PartyTransactionCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..551357a13 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/serialization/PartyTransactionCommandFromApiJsonDeserializer.java @@ -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 SUPPORTED_TRANSACTION_TYPES = new HashSet<>( + Arrays.asList("PARTY_TRAN", "WITHDRAW_DEPOSITE", "AGENT_TRAN")); + + private static final Set 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 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>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + + final List 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformService.java new file mode 100644 index 000000000..81736cc39 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformService.java @@ -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 retrieveAll(Long agentId, Long partyId); + + PartyTransactionData retrieveOne(Long transactionId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformServiceImpl.java new file mode 100644 index 000000000..86eb76cb6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionReadPlatformServiceImpl.java @@ -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 { + + 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 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); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformService.java new file mode 100644 index 000000000..6b4389b0c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..3fb002f19 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/service/PartyTransactionWritePlatformServiceJpaRepositoryImpl.java @@ -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 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 postCollections(final Agent agent, final LocalDate transactionDate, final JsonCommand command) { + final List 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 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 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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/starter/DdsPartyTransactionConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/starter/DdsPartyTransactionConfiguration.java new file mode 100644 index 000000000..9d6090e22 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/dds/transaction/starter/DdsPartyTransactionConfiguration.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java index d431acb5b..f0fafc58d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java index 2663310e8..973a77018 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java @@ -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; diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index d2407861e..ed2c29011 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -537,4 +537,11 @@ + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_add_product_category_to_savings_product.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_add_product_category_to_savings_product.xml new file mode 100644 index 000000000..3f887696b --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_add_product_category_to_savings_product.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_create_dds_agent.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_create_dds_agent.xml new file mode 100644 index 000000000..9675856b5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_create_dds_agent.xml @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0473_create_dds_party.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0473_create_dds_party.xml new file mode 100644 index 000000000..680c5dcfb --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0473_create_dds_party.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_create_dds_party_transaction.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_create_dds_party_transaction.xml new file mode 100644 index 000000000..dafb115d4 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_create_dds_party_transaction.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_create_dds_commission_slab.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_create_dds_commission_slab.xml new file mode 100644 index 000000000..faad135a9 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_create_dds_commission_slab.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_create_dds_batch_run.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_create_dds_batch_run.xml new file mode 100644 index 000000000..30fe00e92 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_create_dds_batch_run.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0477_add_dds_report_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0477_add_dds_report_permission.xml new file mode 100644 index 000000000..847ef16fa --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0477_add_dds_report_permission.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java index 53c2e9199..bba7bd200 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java @@ -495,6 +495,7 @@ public final class FixedDepositProductData extends DepositProductData { private Boolean allowDebitTxnAfterDueDate; private Boolean accountClosedZeroBalanceInDayClosing; private String balanceFrequency; + private String productCategory; private Boolean isPendingApproval; // Template option setters @@ -542,6 +543,7 @@ public final class FixedDepositProductData extends DepositProductData { public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; } public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; } public void setBalanceFrequency(String v) { this.balanceFrequency = v; } + public void setProductCategory(String v) { this.productCategory = v; } public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; } // CBS field getters @@ -604,6 +606,7 @@ public final class FixedDepositProductData extends DepositProductData { this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate; this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing; this.balanceFrequency = src.balanceFrequency; + this.productCategory = src.productCategory; this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers; } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java index 3c6537c83..8d30030dc 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java @@ -83,6 +83,7 @@ public final class RecurringDepositProductData extends DepositProductData { private Boolean enableLockinPeriod; private String balanceFrequency; private Boolean accountClosedZeroBalanceInDayClosing; + private String productCategory; private Boolean balanceSwapOption; private Boolean msme; private Boolean autoDebitFacility; @@ -126,6 +127,7 @@ public final class RecurringDepositProductData extends DepositProductData { public void setEnableLockinPeriod(Boolean v) { this.enableLockinPeriod = v; } public void setBalanceFrequency(String v) { this.balanceFrequency = v; } public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; } + public void setProductCategory(String v) { this.productCategory = v; } public void setBalanceSwapOption(Boolean v) { this.balanceSwapOption = v; } public void setMsme(Boolean v) { this.msme = v; } public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } @@ -167,6 +169,7 @@ public final class RecurringDepositProductData extends DepositProductData { this.enableLockinPeriod = src.enableLockinPeriod; this.balanceFrequency = src.balanceFrequency; this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing; + this.productCategory = src.productCategory; this.balanceSwapOption = src.balanceSwapOption; this.msme = src.msme; this.autoDebitFacility = src.autoDebitFacility; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java index c3502162f..512e120c9 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductDataValidator.java @@ -124,7 +124,7 @@ public class SavingsProductDataValidator { SavingsApiConstants.depositLimitKycParamName, // Additional custom fields SavingsApiConstants.balanceFrequencyParamName, SavingsApiConstants.accountClosedZeroBalanceInDayClosingParamName, - SavingsApiConstants.glAccountIdParamName)); + SavingsApiConstants.glAccountIdParamName, SavingsApiConstants.productCategoryParamName)); public void validateForCreate(final String json) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java index 1f21f0a30..08d1d9954 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProduct.java @@ -297,6 +297,9 @@ public class SavingsProduct extends AbstractPersistableCustom { @Column(name = "account_closed_zero_balance_in_day_closing") private Boolean accountClosedZeroBalanceInDayClosing = false; + @Column(name = "product_category", length = 50) + private String productCategory; + // RD-specific flags @Column(name = "balance_swap_option") private boolean balanceSwapOption = false; @@ -894,6 +897,13 @@ public class SavingsProduct extends AbstractPersistableCustom { this.accountClosedZeroBalanceInDayClosing = newValue; } + // Update productCategory + if (command.isChangeInStringParameterNamed("productCategory", this.productCategory)) { + final String newValue = command.stringValueOfParameterNamed("productCategory"); + actualChanges.put("productCategory", newValue); + this.productCategory = newValue; + } + // Update RD-specific flags (0270 migration fields) if (command.isChangeInBooleanParameterNamed("balanceSwapOption", this.balanceSwapOption)) { this.balanceSwapOption = command.booleanPrimitiveValueOfParameterNamed("balanceSwapOption"); @@ -1276,6 +1286,14 @@ public class SavingsProduct extends AbstractPersistableCustom { this.accountClosedZeroBalanceInDayClosing = accountClosedZeroBalanceInDayClosing; } + public String getProductCategory() { + return productCategory; + } + + public void setProductCategory(String productCategory) { + this.productCategory = productCategory; + } + public boolean isBalanceSwapOption() { return balanceSwapOption; } public void setBalanceSwapOption(boolean v) { this.balanceSwapOption = v; } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java index 62387acc1..17235b9ce 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsProductAssembler.java @@ -282,6 +282,9 @@ public class SavingsProductAssembler { if (command.parameterExists("accountClosedZeroBalanceInDayClosing")) { product.setAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("accountClosedZeroBalanceInDayClosing")); } + if (command.parameterExists("productCategory")) { + product.setProductCategory(command.stringValueOfParameterNamed("productCategory")); + } return product; }