Merge self-hosted/feature/account-charges into feature/Backend-DSC-MFA

This commit is contained in:
Aditya
2026-06-01 12:14:49 +05:30
85 changed files with 16973 additions and 11837 deletions
@@ -1,71 +1,79 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), //
;
private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) {
switch (type) {
case 1:
enumType = LOAN;
break;
case 2:
enumType = SAVINGS;
break;
default:
enumType = INVALID;
}
}
return enumType;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isSavingsAccount() {
return this.equals(SAVINGS);
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isLoanAccount() {
return this.equals(LOAN);
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account;
public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), //
CURRENT(3, "accountType.current"), //
;
private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) {
switch (type) {
case 1:
enumType = LOAN;
break;
case 2:
enumType = SAVINGS;
break;
case 3:
enumType = CURRENT;
break;
default:
enumType = INVALID;
}
}
return enumType;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isSavingsAccount() {
return this.equals(SAVINGS);
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
public boolean isLoanAccount() {
return this.equals(LOAN);
}
public boolean isCurrentAccount() {
return this.equals(CURRENT);
}
}
@@ -600,6 +600,27 @@ public class AccountingProcessorHelper {
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount);
}
/**
* Creates a direct transfer journal entry between two savings-type accounts (savings or current),
* each with their own product GL mapping and entity ID so the correct account number appears per entry.
*
* @param debitProductId product ID whose SAVINGS_CONTROL GL is debited
* @param debitEntityId entity (account) ID shown on the debit entry
* @param creditProductId product ID whose SAVINGS_CONTROL GL is credited
* @param creditEntityId entity (account) ID shown on the credit entry
*/
public void createDirectTransferJournalEntriesForSavings(final Office office, final String currencyCode,
final Long debitProductId, final Long debitEntityId,
final Long creditProductId, final Long creditEntityId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(debitProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(creditProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount);
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
}
/**
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
*
@@ -106,8 +106,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.append(" eao.external_id as externalAssetOwner, ")
.append(" CASE ")
.append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' THEN ca.account_no ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
.append(" ELSE NULL ")
.append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" join m_currency curr on curr.code = journalEntry.currency_code ")
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id NOT LIKE 'CA%' ")
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' ");
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
if (associationParametersData.isTransactionDetailsRequired()) {
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
@@ -1,220 +1,220 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam;
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.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry;
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.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions")
@Component
@Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n"
+ "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor
public class StandingInstructionApiResource {
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. "
+ "It can be useful when building maintenance user interface screens for client applications. "
+ "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n"
+ "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n"
+ "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType());
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@PUT
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n"
+ "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n"
+ "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n"
+ "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null;
final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
}
@GET
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class)))
public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template"));
}
if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
}
if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(),
standingInstructionData.getTransferType().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
}
}
return standingInstructionData;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam;
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.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry;
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.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionCreationRequest;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions")
@Component
@Tag(name = "Standing Instructions", description = "Standing instructions (or standing orders) refer to instructions a bank account holder (\"the payer\") gives to his or her bank to pay a set amount at regular intervals to another's (\"the payee's\") account.\n"
+ "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor
public class StandingInstructionApiResource {
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. "
+ "It can be useful when building maintenance user interface screens for client applications. "
+ "The template data returned consists of any or all of:\n" + "\n" + "Field Defaults\n" + "Allowed Value Lists\n"
+ "Example Requests:\n" + "\n" + "standinginstructions/template?fromAccountType=2&fromOfficeId=1\n" + "\n"
+ "standinginstructions/template?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType());
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create new Standing Instruction", description = "Ability to create new instruction for transfer of monetary funds from one account to another")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@PUT
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Standing Instruction | Delete Standing Instruction", description = "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n"
+ "\n" + "PUT https://DomainName/api/v1/standinginstructions/1?command=update\n" + "\n\n"
+ "Ability to modify existing instruction for transfer of monetary funds from one account to another.\n" + "\n"
+ "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null;
final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
}
@GET
@Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class)))
public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template"));
}
if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
}
if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
}
}
return standingInstructionData;
}
}
@@ -1,79 +1,79 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2));
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2), Integer.valueOf(3));
}
}
@@ -1,329 +1,359 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.ChronoField;
import java.util.Collection;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/**
* Immutable data object representing a savings account.
*/
@SuppressWarnings("unused")
public final class StandingInstructionData {
@Getter
private final Long id;
@Getter
private final Long accountDetailId;
@Getter
private final String name;
private final OfficeData fromOffice;
@Getter
private final ClientData fromClient;
private final EnumOptionData fromAccountType;
@Getter
private final PortfolioAccountData fromAccount;
private final OfficeData toOffice;
@Getter
private final ClientData toClient;
private final EnumOptionData toAccountType;
@Getter
private final PortfolioAccountData toAccount;
private final EnumOptionData transferType;
private final EnumOptionData priority;
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@Getter
private final BigDecimal amount;
@Getter
private final LocalDate validFrom;
private final LocalDate validTill;
private final EnumOptionData recurrenceType;
private final EnumOptionData recurrenceFrequency;
@Getter
private final Integer recurrenceInterval;
private final MonthDay recurrenceOnMonthDay;
private final Page<AccountTransferData> transactions;
private final Collection<OfficeData> fromOfficeOptions;
private final Collection<ClientData> fromClientOptions;
private final Collection<EnumOptionData> fromAccountTypeOptions;
private final Collection<PortfolioAccountData> fromAccountOptions;
private final Collection<OfficeData> toOfficeOptions;
private final Collection<ClientData> toClientOptions;
private final Collection<EnumOptionData> toAccountTypeOptions;
private final Collection<PortfolioAccountData> toAccountOptions;
private final Collection<EnumOptionData> transferTypeOptions;
private final Collection<EnumOptionData> statusOptions;
private final Collection<EnumOptionData> instructionTypeOptions;
private final Collection<EnumOptionData> priorityOptions;
private final Collection<EnumOptionData> recurrenceTypeOptions;
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final OfficeData fromOffice = null;
final OfficeData toOffice = null;
final ClientData fromClient = null;
final ClientData toClient = null;
final EnumOptionData fromAccountType = null;
final PortfolioAccountData fromAccount = null;
final EnumOptionData toAccountType = null;
final PortfolioAccountData toAccount = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = null;
final Page<AccountTransferData> transactions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType,
final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority,
final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
final Page<AccountTransferData> transactions = null;
final Collection<OfficeData> fromOfficeOptions = null;
final Collection<ClientData> fromClientOptions = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null;
final Collection<PortfolioAccountData> fromAccountOptions = null;
final Collection<OfficeData> toOfficeOptions = null;
final Collection<ClientData> toClientOptions = null;
final Collection<EnumOptionData> toAccountTypeOptions = null;
final Collection<PortfolioAccountData> toAccountOptions = null;
final Collection<EnumOptionData> transferTypeOptions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
final Collection<EnumOptionData> priorityOptions = null;
final Collection<EnumOptionData> recurrenceTypeOptions = null;
final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
}
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.id = id;
this.accountDetailId = accountDetailId;
this.name = name;
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromAccountType = fromAccountType;
this.fromAccount = fromAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toAccountType = toAccountType;
this.toAccount = toAccount;
this.transferType = transferType;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnMonthDay = recurrenceOnMonthDay;
this.fromOfficeOptions = fromOfficeOptions;
this.fromClientOptions = fromClientOptions;
this.fromAccountTypeOptions = fromAccountTypeOptions;
this.fromAccountOptions = fromAccountOptions;
this.toOfficeOptions = toOfficeOptions;
this.toClientOptions = toClientOptions;
this.toAccountTypeOptions = toAccountTypeOptions;
this.toAccountOptions = toAccountOptions;
this.transferTypeOptions = transferTypeOptions;
this.statusOptions = statusOptions;
this.instructionTypeOptions = instructionTypeOptions;
this.priorityOptions = priorityOptions;
this.recurrenceTypeOptions = recurrenceTypeOptions;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
this.transactions = transactions;
}
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = null;
final Page<AccountTransferData> transactions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
public StandingInstructionType getInstructionType() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferRecurrenceType getRecurrenceType() {
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
}
public PeriodFrequencyType getRecurrenceFrequency() {
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getFromAccountType() {
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType getToAccountType() {
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferType getTransferType() {
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
}
public Integer getRecurrenceOnDay() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0);
}
public Integer getRecurrenceOnMonth() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0);
}
public Integer toTransferType() {
Integer transferType = null;
AccountTransferType accountTransferType = getTransferType();
if (accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue();
}
return transferType;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.ChronoField;
import java.util.Collection;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/**
* Immutable data object representing a savings account.
*/
@SuppressWarnings("unused")
public final class StandingInstructionData {
@Getter
private final Long id;
@Getter
private final Long accountDetailId;
@Getter
private final String name;
@Getter
private final OfficeData fromOffice;
@Getter
private final ClientData fromClient;
@Getter
private final EnumOptionData fromAccountType;
@Getter
private final PortfolioAccountData fromAccount;
@Getter
private final OfficeData toOffice;
@Getter
private final ClientData toClient;
@Getter
private final EnumOptionData toAccountType;
@Getter
private final PortfolioAccountData toAccount;
@Getter
private final EnumOptionData transferType;
@Getter
private final EnumOptionData priority;
@Getter
private final EnumOptionData instructionType;
@Getter
private final EnumOptionData status;
@Getter
private final BigDecimal amount;
@Getter
private final LocalDate validFrom;
@Getter
private final LocalDate validTill;
@Getter
private final EnumOptionData recurrenceType;
@Getter
private final EnumOptionData recurrenceFrequency;
@Getter
private final Integer recurrenceInterval;
@Getter
private final MonthDay recurrenceOnMonthDay;
@Getter
private final Page<AccountTransferData> transactions;
@Getter
private final Collection<OfficeData> fromOfficeOptions;
@Getter
private final Collection<ClientData> fromClientOptions;
@Getter
private final Collection<EnumOptionData> fromAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> fromAccountOptions;
@Getter
private final Collection<OfficeData> toOfficeOptions;
@Getter
private final Collection<ClientData> toClientOptions;
@Getter
private final Collection<EnumOptionData> toAccountTypeOptions;
@Getter
private final Collection<PortfolioAccountData> toAccountOptions;
@Getter
private final Collection<EnumOptionData> transferTypeOptions;
@Getter
private final Collection<EnumOptionData> statusOptions;
@Getter
private final Collection<EnumOptionData> instructionTypeOptions;
@Getter
private final Collection<EnumOptionData> priorityOptions;
@Getter
private final Collection<EnumOptionData> recurrenceTypeOptions;
@Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions;
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final OfficeData fromOffice = null;
final OfficeData toOffice = null;
final ClientData fromClient = null;
final ClientData toClient = null;
final EnumOptionData fromAccountType = null;
final PortfolioAccountData fromAccount = null;
final EnumOptionData toAccountType = null;
final PortfolioAccountData toAccount = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = null;
final Page<AccountTransferData> transactions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType,
final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority,
final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
final Page<AccountTransferData> transactions = null;
final Collection<OfficeData> fromOfficeOptions = null;
final Collection<ClientData> fromClientOptions = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null;
final Collection<PortfolioAccountData> fromAccountOptions = null;
final Collection<OfficeData> toOfficeOptions = null;
final Collection<ClientData> toClientOptions = null;
final Collection<EnumOptionData> toAccountTypeOptions = null;
final Collection<PortfolioAccountData> toAccountOptions = null;
final Collection<EnumOptionData> transferTypeOptions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
final Collection<EnumOptionData> priorityOptions = null;
final Collection<EnumOptionData> recurrenceTypeOptions = null;
final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
}
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.id = id;
this.accountDetailId = accountDetailId;
this.name = name;
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromAccountType = fromAccountType;
this.fromAccount = fromAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toAccountType = toAccountType;
this.toAccount = toAccount;
this.transferType = transferType;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnMonthDay = recurrenceOnMonthDay;
this.fromOfficeOptions = fromOfficeOptions;
this.fromClientOptions = fromClientOptions;
this.fromAccountTypeOptions = fromAccountTypeOptions;
this.fromAccountOptions = fromAccountOptions;
this.toOfficeOptions = toOfficeOptions;
this.toClientOptions = toClientOptions;
this.toAccountTypeOptions = toAccountTypeOptions;
this.toAccountOptions = toAccountOptions;
this.transferTypeOptions = transferTypeOptions;
this.statusOptions = statusOptions;
this.instructionTypeOptions = instructionTypeOptions;
this.priorityOptions = priorityOptions;
this.recurrenceTypeOptions = recurrenceTypeOptions;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
this.transactions = transactions;
}
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) {
final Long id = null;
final Long accountDetailId = null;
final String name = null;
final EnumOptionData transferType = null;
final EnumOptionData priority = null;
final EnumOptionData instructionType = null;
final EnumOptionData status = null;
final BigDecimal amount = null;
final LocalDate validFrom = null;
final LocalDate validTill = null;
final EnumOptionData recurrenceType = null;
final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
final MonthDay recurrenceOnMonthDay = null;
final Page<AccountTransferData> transactions = null;
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
}
public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
/**
* Returns the domain enum for instruction type — used internally by business logic.
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
* return EnumOptionData so Jackson serializes it as an object, not a plain string).
*/
public StandingInstructionType instructionTypeEnum() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferRecurrenceType recurrenceTypeEnum() {
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
}
public PeriodFrequencyType recurrenceFrequencyEnum() {
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType fromAccountTypeEnum() {
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public PortfolioAccountType toAccountTypeEnum() {
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null);
}
public AccountTransferType transferTypeEnum() {
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null);
}
public Integer getRecurrenceOnDay() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0);
}
public Integer getRecurrenceOnMonth() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0);
}
public Integer toTransferType() {
Integer transferType = null;
AccountTransferType accountTransferType = transferTypeEnum();
if (accountTransferType != null && accountTransferType.isChargePayment()) {
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue();
} else if (accountTransferType != null && accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue();
}
return transferType;
}
}
@@ -1,294 +1,296 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
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.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
}
public void validateForCreate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4);
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
boolean isPeriodic = false;
if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
}
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull();
}
}
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull();
}
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 1);
}
if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
if (accountTransferType.isAccountTransfer()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) {
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) {
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1,
2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) {
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull()
.inMinMaxRange(1, 4);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) {
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) {
final Integer recurrenceType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) {
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) {
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) {
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
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.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
}
public void validateForCreate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4);
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
boolean isPeriodic = false;
if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
}
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull();
}
}
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull();
}
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
|| PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 1);
}
if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
if (accountTransferType.isAccountTransfer()
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
errorCode = "not.account.transfer";
} else if (accountTransferType.isLoanRepayment()
&& (fromPortfolioAccountType.isLoanAccount()
|| (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) {
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) {
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1,
2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) {
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull()
.inMinMaxRange(1, 4);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) {
final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) {
final Integer recurrenceType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) {
final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3);
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) {
final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.nameParamName, element)) {
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
@@ -1,214 +1,314 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountTransferDetailAssembler {
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler;
@Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) {
this.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Loan toLoanAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transfertype);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final SavingsAccount toSavingsAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transferType);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transferType);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transferType);
}
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
transferType);
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountTransferDetailAssembler {
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
private final LoanAssembler loanAccountAssembler;
private final CurrentAccountRepository currentAccountRepository;
@Autowired
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
final CurrentAccountRepository currentAccountRepository) {
this.clientRepository = clientRepository;
this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
this.currentAccountRepository = currentAccountRepository;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Loan toLoanAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transfertype);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final SavingsAccount toSavingsAccount) {
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
transferType);
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toSavingsAccount, transferType);
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transferType);
}
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice();
final Client fromClient = fromLoanAccount.client();
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client();
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount,
transferType);
}
// --- Current account assembly methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToCurrentTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, false);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToSavingsTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toSavingsAccount, transfertype);
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
final Long toCurrentAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final CurrentAccount toCurrentAccount = currentAccountRepository.findById(toCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("To current account not found: " + toCurrentAccountId));
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.savingsToCurrentTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
toCurrentAccount, transfertype);
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final Long fromCurrentAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final CurrentAccount fromCurrentAccount = currentAccountRepository.findById(fromCurrentAccountId)
.orElseThrow(() -> new IllegalArgumentException("From current account not found: " + fromCurrentAccountId));
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
return AccountTransferDetails.currentToLoanTransfer(fromOffice, fromClient, fromCurrentAccount, toOffice, toClient,
toLoanAccount, transfertype);
}
}
@@ -1,165 +1,216 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.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.OneToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@Column(name = "transfer_type")
private Integer transferType;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null,
transferType, null);
}
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount,
transferType, null);
}
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, toSavingsAccount, null,
transferType, null);
}
protected AccountTransferDetails() {
//
}
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public SavingsAccount toSavingsAccount() {
return this.toSavingsAccount;
}
public SavingsAccount fromSavingsAccount() {
return this.fromSavingsAccount;
}
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
this.accountTransferTransactions.add(accountTransferTransaction);
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public Loan toLoanAccount() {
return this.toLoanAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount;
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction;
}
public AccountTransferType transferType() {
return AccountTransferType.fromInt(this.transferType);
}
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount,
transferType, null);
}
public List<AccountTransferTransaction> getAccountTransferTransactions() {
return accountTransferTransactions;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.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.OneToOne;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@ManyToOne
@JoinColumn(name = "from_current_account_id", nullable = true)
private CurrentAccount fromCurrentAccount;
@ManyToOne
@JoinColumn(name = "to_current_account_id", nullable = true)
private CurrentAccount toCurrentAccount;
@Column(name = "transfer_type")
private Integer transferType;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount,
Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
null, transferType, null);
}
protected AccountTransferDetails() {
//
}
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
final Integer transferType,
final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
this.fromClient = fromClient;
this.fromSavingsAccount = fromSavingsAccount;
this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
this.toOffice = toOffice;
this.toClient = toClient;
this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
this.toCurrentAccount = toCurrentAccount;
this.transferType = transferType;
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public SavingsAccount toSavingsAccount() {
return this.toSavingsAccount;
}
public SavingsAccount fromSavingsAccount() {
return this.fromSavingsAccount;
}
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
this.accountTransferTransactions.add(accountTransferTransaction);
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public Loan toLoanAccount() {
return this.toLoanAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount;
}
public CurrentAccount fromCurrentAccount() {
return this.fromCurrentAccount;
}
public CurrentAccount toCurrentAccount() {
return this.toCurrentAccount;
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction;
}
public AccountTransferType transferType() {
return AccountTransferType.fromInt(this.transferType);
}
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
// --- Current account factory methods ---
public static AccountTransferDetails currentToCurrentTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToSavingsTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final SavingsAccount toSavingsAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, toSavingsAccount,
null, null, transferType, null);
}
public static AccountTransferDetails savingsToCurrentTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient,
final CurrentAccount toCurrentAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, null, null,
toCurrentAccount, transferType, null);
}
public static AccountTransferDetails currentToLoanTransfer(final Office fromOffice, final Client fromClient,
final CurrentAccount fromCurrentAccount, final Office toOffice, final Client toClient,
final Loan toLoanAccount, Integer transferType) {
return new AccountTransferDetails(fromOffice, fromClient, null, null, fromCurrentAccount, toOffice, toClient, null, toLoanAccount,
null, transferType, null);
}
public List<AccountTransferTransaction> getAccountTransferTransactions() {
return accountTransferTransactions;
}
}
@@ -1,290 +1,302 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails;
@Column(name = "name")
private String name;
@Column(name = "priority")
private Integer priority;
@Column(name = "instruction_type")
private Integer instructionType;
@Column(name = "status")
private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount;
@Column(name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_till")
private LocalDate validTill;
@Column(name = "recurrence_type")
private Integer recurrenceType;
@Column(name = "recurrence_frequency")
private Integer recurrenceFrequency;
@Column(name = "recurrence_interval")
private Integer recurrenceInterval;
@Column(name = "recurrence_on_day")
private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth;
@Column(name = "last_run_date")
private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() {
}
public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
}
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
}
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails;
this.name = name;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
}
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom);
}
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill);
}
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue);
this.amount = newValue;
}
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue);
this.status = newValue;
}
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue);
this.priority = newValue;
}
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue;
}
if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue;
}
final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear;
}
}
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue;
}
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
return actualChanges;
}
private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
}
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
}
}
}
if (this.accountTransferDetails.toSavingsAccount() != null) {
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
}
if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) {
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
}
String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toSavingsAccount() == null)) {
errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
public void updateLatsRunDate(LocalDate latsRunDate) {
this.latsRunDate = latsRunDate;
}
public void updateStatus(Integer status) {
this.status = status;
}
/**
* delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name
**/
public void delete() {
this.status = StandingInstructionStatus.DELETED.getValue();
this.name = this.name + "_deleted_" + this.getId();
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails;
@Column(name = "name")
private String name;
@Column(name = "priority")
private Integer priority;
@Column(name = "instruction_type")
private Integer instructionType;
@Column(name = "status")
private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount;
@Column(name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_till")
private LocalDate validTill;
@Column(name = "recurrence_type")
private Integer recurrenceType;
@Column(name = "recurrence_frequency")
private Integer recurrenceFrequency;
@Column(name = "recurrence_interval")
private Integer recurrenceInterval;
@Column(name = "recurrence_on_day")
private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth;
@Column(name = "last_run_date")
private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() {
}
public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
}
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
}
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom,
final LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails;
this.name = name;
this.priority = priority;
this.instructionType = instructionType;
this.status = status;
this.amount = amount;
this.validFrom = validFrom;
this.validTill = validTill;
this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
}
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom);
}
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill);
}
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue);
this.amount = newValue;
}
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue);
this.status = newValue;
}
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue);
this.priority = newValue;
}
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue;
}
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue;
}
if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue;
}
final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear;
}
}
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue;
}
validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
return actualChanges;
}
private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
}
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
}
}
}
// For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
if (this.accountTransferDetails.toSavingsAccount() != null
|| this.accountTransferDetails.toCurrentAccount() != null) {
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1);
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1);
}
if (StandingInstructionType.fromInt(this.instructionType).isFixedAmoutTransfer()) {
baseDataValidator.reset().parameter(amountParamName).value(this.amount).notNull();
}
// Account Transfer: from must be savings OR current; to must be savings OR current
boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
|| this.accountTransferDetails.toCurrentAccount() != null;
// Loan Repayment: from must be savings OR current; to must be loan
boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
|| this.accountTransferDetails.fromCurrentAccount() != null;
String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
&& !(fromAccountValid && toAccountValid)) {
errorCode = "not.account.transfer";
} else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
errorCode = "not.loan.repayment";
}
if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
}
public void updateLatsRunDate(LocalDate latsRunDate) {
this.latsRunDate = latsRunDate;
}
public void updateStatus(Integer status) {
this.status = status;
}
/**
* delete the standing instruction by setting the status to 3 and appending "_deleted_" and the id to the name
**/
public void delete() {
this.status = StandingInstructionStatus.DELETED.getValue();
this.name = this.name + "_deleted_" + this.getId();
}
}
@@ -1,93 +1,143 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StandingInstructionAssembler {
private final AccountTransferDetailAssembler accountTransferDetailAssembler;
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
this.accountTransferDetailAssembler = accountTransferDetailAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) {
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount);
amount = monetaryAmount.getAmount();
}
final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final String name = command.stringValueOfParameterNamed(nameParamName);
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.domain;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.nameParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StandingInstructionAssembler {
private final AccountTransferDetailAssembler accountTransferDetailAssembler;
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
this.accountTransferDetailAssembler = accountTransferDetailAssembler;
}
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null;
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
if (transferAmount != null) {
final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
if (currency != null) {
final Money monetaryAmount = Money.of(currency, transferAmount);
amount = monetaryAmount.getAmount();
} else {
amount = transferAmount;
}
}
final Integer status = command.integerValueOfParameterNamed(statusParamName);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final String name = command.stringValueOfParameterNamed(nameParamName);
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
}
/**
* Resolves the currency from the "from" account — savings, loan (null), or current account.
*/
private MonetaryCurrency resolveCurrency(final AccountTransferDetails details) {
final SavingsAccount fromSavings = details.fromSavingsAccount();
if (fromSavings != null) {
return fromSavings.getCurrency();
}
final CurrentAccount fromCurrent = details.fromCurrentAccount();
if (fromCurrent != null) {
return new MonetaryCurrency(fromCurrent.getCurrencyCode(), fromCurrent.getCurrencyDigits(),
fromCurrent.getCurrencyMultiplesOf());
}
return null; // loan accounts don't have a direct currency on AccountTransferDetails
}
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
// --- Current account methods ---
public AccountTransferDetails assembleCurrentToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToSavingsTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleSavingsToCurrentTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToCurrentTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
public AccountTransferDetails assembleCurrentToLoanTransfer(final JsonCommand command) {
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleCurrentToLoanTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails;
}
}
@@ -1,70 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService);
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Autowired
private AccountTransferDetailRepository accountTransferDetailRepository;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService, accountTransferDetailRepository);
}
}
@@ -1,172 +1,180 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.jobs.executestandinginstructions;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType();
StandingInstructionType instructionType = data.getInstructionType();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency();
LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.getAmount();
if (data.getToAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId());
if (data.getInstructionType().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount();
}
if (recurrenceType.isDuesRecurrence()) {
isDueForTransfer = isDueForTransfer(standingInstructionDuesData);
}
}
if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount,execution_time, error_log) VALUES (");
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
updateQuery.append(", now(),");
updateQuery.append("'").append(errorLog).append("')");
jdbcTemplate.update(updateQuery.toString());
return transferCompleted;
}
public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) {
return standingInstructionDuesData.dueDate() != null
&& !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.jobs.executestandinginstructions;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final AccountTransferDetailRepository accountTransferDetailRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
StandingInstructionType instructionType = data.instructionTypeEnum();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
LocalDate startDate = data.getValidFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.getAmount();
if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.getToAccount().getId());
if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount();
}
if (recurrenceType.isDuesRecurrence()) {
isDueForTransfer = isDueForTransfer(standingInstructionDuesData);
}
}
if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
// Load existing AccountTransferDetails so transactions are linked to
// this standing instruction and appear in its transaction history
final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
: null;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.transferTypeEnum().getValue(), existingDetails, null, ExternalId.empty(), null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount,execution_time, error_log) VALUES (");
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
updateQuery.append(", now(),");
updateQuery.append("'").append(errorLog).append("')");
jdbcTemplate.update(updateQuery.toString());
return transferCompleted;
}
public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) {
return standingInstructionDuesData.dueDate() != null
&& !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
}
}
@@ -1,202 +1,206 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations {
private AccountTransferEnumerations() {
}
public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type));
}
public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account");
break;
case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break;
}
}
return optionData;
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
}
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break;
case AS_PER_DUES:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
break;
}
}
return optionData;
}
public static EnumOptionData transferType(final Integer type) {
return transferType(AccountTransferType.fromInt(type));
}
public static EnumOptionData transferType(final AccountTransferType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break;
case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break;
case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break;
case LOAN_DOWN_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionPriority(final Integer type) {
return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
}
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.HIGH.getCode(), "High Priority");
break;
case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority");
break;
case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break;
case URGENT:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionStatus(final Integer type) {
return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
}
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.ACTIVE.getCode(), "Active");
break;
case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted");
break;
case DISABLED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionType(final Integer type) {
return standingInstructionType(StandingInstructionType.fromInt(type));
}
public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case DUES:
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.DUES.getCode(), "Dues");
break;
case FIXED:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed");
break;
}
}
return optionData;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations {
private AccountTransferEnumerations() {
}
public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type));
}
public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account");
break;
case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break;
case CURRENT:
optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
PortfolioAccountType.CURRENT.getCode(), "Current Account");
break;
}
}
return optionData;
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
}
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break;
case AS_PER_DUES:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
break;
}
}
return optionData;
}
public static EnumOptionData transferType(final Integer type) {
return transferType(AccountTransferType.fromInt(type));
}
public static EnumOptionData transferType(final AccountTransferType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break;
case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break;
case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break;
case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break;
case LOAN_DOWN_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionPriority(final Integer type) {
return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
}
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.HIGH.getCode(), "High Priority");
break;
case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority");
break;
case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break;
case URGENT:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionStatus(final Integer type) {
return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
}
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.ACTIVE.getCode(), "Active");
break;
case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted");
break;
case DISABLED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled");
break;
}
}
return optionData;
}
public static EnumOptionData standingInstructionType(final Integer type) {
return standingInstructionType(StandingInstructionType.fromInt(type));
}
public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
EnumOptionData optionData = null;
if (type != null) {
switch (type) {
case INVALID:
break;
case DUES:
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.DUES.getCode(), "Dues");
break;
case FIXED:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed");
break;
}
}
return optionData;
}
}
@@ -1,396 +1,482 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate;
// mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
}
@Override
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) {
return retrieveOne(accountId, accountTypeId, null);
}
@Override
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
Object[] sqlParams = new Object[] { accountId };
PortfolioAccountData accountData = null;
try {
String sql = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
if (currencyCode != null) {
sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
if (currencyCode != null) {
sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break;
}
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
final List<Object> sqlParams = new ArrayList<>();
// sqlParams.add(portfolioAccountDTO.getClientId());
Collection<PortfolioAccountData> accounts = null;
String sql = null;
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) {
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " la.loan_status_id in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and la.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
if (portfolioAccountDTO.getDepositType() != null) {
sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
}
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
}
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
}
return accounts;
}
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioSavingsAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("sp.id as productId, sp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_savings_account sa ");
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = sa.client_id ");
sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioLoanAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
private String schemaSql;
private final DatabaseSpecificSQLGenerator sqlGenerator;
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
this.sqlGenerator = sqlGenerator;
}
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400);
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id");
amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300");
amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having");
amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + ");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append(amountQueryString.toString());
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
@Override
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub
Object[] sqlParams = new Object[] { accountId, accountId };
PortfolioAccountData accountData = null;
// String currencyCode = null;
try {
String sql = null;
// final PortfolioAccountType accountType =
// PortfolioAccountType.fromInt(accountTypeId);
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?";
/*
* if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId ,
* accountId,currencyCode }; }
*/
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate;
// mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate;
this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.loanAccountMapper = new PortfolioLoanAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator);
this.currentAccountMapper = new PortfolioCurrentAccountMapper();
}
@Override
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId) {
return retrieveOne(accountId, accountTypeId, null);
}
@Override
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
Object[] sqlParams = new Object[] { accountId };
PortfolioAccountData accountData = null;
try {
String sql = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
if (currencyCode != null) {
sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
if (currencyCode != null) {
sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
if (currencyCode != null) {
sql += " and ca.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
}
accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
break;
}
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
final List<Object> sqlParams = new ArrayList<>();
// sqlParams.add(portfolioAccountDTO.getClientId());
Collection<PortfolioAccountData> accounts = null;
String sql = null;
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) {
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
switch (accountType) {
case INVALID:
break;
case LOAN:
sql = "select " + this.loanAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " la.loan_status_id in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and la.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case SAVINGS:
sql = "select " + this.savingsAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
if (portfolioAccountDTO.getDepositType() != null) {
sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
}
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
}
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
case CURRENT:
sql = "select " + this.currentAccountMapper.schema() + " where ";
if (portfolioAccountDTO.getClientId() != null) {
sql += " ca.client_id = ? and ca.status_enum in (?) ";
sqlParams.add(portfolioAccountDTO.getClientId());
sqlParams.add(defaultAccountStatus);
} else {
sql += " ca.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
}
if (portfolioAccountDTO.getCurrencyCode() != null) {
sql += " and ca.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
}
accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
break;
}
return accounts;
}
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioSavingsAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("sp.id as productId, sp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_savings_account sa ");
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = sa.client_id ");
sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioLoanAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
private String schemaSql;
private final DatabaseSpecificSQLGenerator sqlGenerator;
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
this.sqlGenerator = sqlGenerator;
}
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400);
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id");
amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300");
amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having");
amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + ");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append(amountQueryString.toString());
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
this.schemaSql = sqlBuilder.toString();
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
}
}
@Override
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub
Object[] sqlParams = new Object[] { accountId, accountId };
PortfolioAccountData accountData = null;
// String currencyCode = null;
try {
String sql = null;
// final PortfolioAccountType accountType =
// PortfolioAccountType.fromInt(accountTypeId);
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?";
/*
* if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId ,
* accountId,currencyCode }; }
*/
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) {
throw new AccountTransferNotFoundException(accountId, e);
}
return accountData;
}
private static final class PortfolioCurrentAccountMapper implements RowMapper<PortfolioAccountData> {
private final String schemaSql;
PortfolioCurrentAccountMapper() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("ca.id as id, ca.account_no as accountNo, ca.external_id as externalId, ");
sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("null as groupId, null as groupName, ");
sqlBuilder.append("cp.id as productId, cp.name as productName, ");
sqlBuilder.append("null as fieldOfficerId, null as fieldOfficerName, ");
sqlBuilder.append("ca.currency_code as currencyCode, ca.currency_digits as currencyDigits, ");
sqlBuilder.append("ca.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_current_account ca ");
sqlBuilder.append("join m_current_product cp ON ca.product_id = cp.id ");
sqlBuilder.append("join m_currency curr on curr.code = ca.currency_code ");
sqlBuilder.append("left join m_client c ON c.id = ca.client_id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
final Long groupId = JdbcSupport.getLong(rs, "groupId");
final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final String clientName = rs.getString("clientName");
final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
final Long fieldOfficerId = JdbcSupport.getLong(rs, "fieldOfficerId");
final String fieldOfficerName = rs.getString("fieldOfficerName");
final String currencyCode = rs.getString("currencyCode");
final String currencyName = rs.getString("currencyName");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
currencyDisplaySymbol, currencyNameCode);
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency);
}
}
}
@@ -1,145 +1,179 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository;
@Transactional
@Override
public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null;
try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
}
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
return builder.build();
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("name")) {
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
}
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) {
this.standingInstructionDataValidator.validateForUpdate(command);
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id)
.orElseThrow(() -> new StandingInstructionNotFoundException(id));
final Map<String, Object> actualChanges = standingInstructionsForUpdate.update(command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build();
}
@Override
public CommandProcessingResult delete(final Long id) {
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow();
// update the "deleted" and "name" properties of the standing
// instruction
standingInstructionsForUpdate.delete();
final Map<String, Object> actualChanges = new HashMap<>();
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue());
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.service;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository;
@Transactional
@Override
public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null;
try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
}
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
return builder.build();
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("name")) {
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
}
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
}
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
}
private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isSavingsAccount();
}
private boolean isSavingsToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isSavingsAccount() && toAccountType.isCurrentAccount();
}
private boolean isCurrentToLoanAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
return fromAccountType.isCurrentAccount() && toAccountType.isLoanAccount();
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) {
this.standingInstructionDataValidator.validateForUpdate(command);
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id)
.orElseThrow(() -> new StandingInstructionNotFoundException(id));
final Map<String, Object> actualChanges = standingInstructionsForUpdate.update(command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build();
}
@Override
public CommandProcessingResult delete(final Long id) {
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow();
// update the "deleted" and "name" properties of the standing
// instruction
standingInstructionsForUpdate.delete();
final Map<String, Object> actualChanges = new HashMap<>();
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue());
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
}
}
@@ -1,134 +1,140 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.starter;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
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 AccountConfiguration {
@Bean
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties);
}
@Bean
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator) {
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionWritePlatformService.class)
public StandingInstructionWritePlatformService standingInstructionWritePlatformService(
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) {
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository);
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.account.starter;
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
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 AccountConfiguration {
@Bean
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
}
@Bean
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository,
AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
}
@Bean
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator) {
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
}
@Bean
@ConditionalOnMissingBean(StandingInstructionWritePlatformService.class)
public StandingInstructionWritePlatformService standingInstructionWritePlatformService(
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) {
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository);
}
}
@@ -0,0 +1,276 @@
package org.apache.fineract.portfolio.cashexchange.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchange;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Path("/v1/cashexchange")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashExchangeApiResource {
private final CashExchangeRepository repository;
private final CashExchangeDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
private final DenominationBreakdownWriter denominationBreakdownWriter;
private final Gson gson = new Gson();
@POST
@Transactional
public String create(String body) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long accountId = json.has("accountId") && !json.get("accountId").isJsonNull() ? json.get("accountId").getAsLong() : null;
final String accountNo = json.has("accountNo") && !json.get("accountNo").isJsonNull() ? json.get("accountNo").getAsString()
: null;
final Long clientId = json.has("clientId") && !json.get("clientId").isJsonNull() ? json.get("clientId").getAsLong() : null;
final LocalDate exchangeDate = json.has("exchangeDate") && !json.get("exchangeDate").isJsonNull()
? LocalDate.parse(json.get("exchangeDate").getAsString())
: null;
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
final String customerIdProof = json.has("customerIdProof") && !json.get("customerIdProof").isJsonNull()
? json.get("customerIdProof").getAsString() : null;
final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received")
: new JsonArray();
final JsonArray given = json.has("given") && json.get("given").isJsonArray() ? json.getAsJsonArray("given") : new JsonArray();
final BigDecimal receivedTotal = sumTotal(received);
final BigDecimal givenTotal = sumTotal(given);
if (!hasPositiveCount(received) || !hasPositiveCount(given)) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
if (receivedTotal.compareTo(givenTotal) != 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.mismatch",
"Received total (" + receivedTotal + ") must equal given total (" + givenTotal + ").");
}
if (receivedTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
CashExchange header = new CashExchange();
header.setAccountId(accountId);
header.setAccountNo(accountNo);
header.setClientId(clientId);
header.setExchangeAmount(receivedTotal);
header.setExchangeDate(exchangeDate);
header.setNote(note);
header.setCustomerIdProof(customerIdProof);
header.setIsReversed(false);
header.setCreatedAt(LocalDateTime.now());
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
// Auto-detect cashier linked to this user
try {
Long cashierId = jdbcTemplate.queryForObject(
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
Long.class, user.getId());
header.setCashierId(cashierId);
} catch (Exception ignored) {}
}
} catch (Exception e) {
// resolving the current user must never block recording the exchange
}
// Validate that cashier has enough of each denomination being GIVEN to the customer
if (header.getCashierId() != null) {
final java.util.Map<Long, Long> stock = denominationBreakdownWriter.getCashierNetPosition(header.getCashierId());
final java.util.List<String> shortages = new java.util.ArrayList<>();
for (JsonElement el : given) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) continue;
if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue;
long denomId = item.get("denominationId").getAsLong();
long available = stock.getOrDefault(denomId, 0L);
if (count > available) {
int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt() : 0;
shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")");
}
}
if (!shortages.isEmpty()) {
throw new PlatformDataIntegrityException(
"error.msg.cashier.denomination.insufficient",
"Insufficient denomination stock for exchange — cannot give: " + String.join("; ", shortages));
}
}
CashExchange saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), "RECEIVED", received);
saveDetails(saved.getId(), "GIVEN", given);
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
return gson.toJson(result);
}
@POST
@Path("{id}/reverse")
public String reverse(@PathParam("id") Long id, String body) {
CashExchange exchange = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
if (Boolean.TRUE.equals(exchange.getIsReversed())) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.already.reversed",
"Cash exchange " + id + " is already reversed.");
}
String reason = null;
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {}
exchange.setIsReversed(true);
exchange.setReversedAt(LocalDateTime.now());
exchange.setReverseReason(reason);
try {
AppUser user = this.context.authenticatedUser();
if (user != null) exchange.setReversedBy(user.getId());
} catch (Exception ignored) {}
repository.save(exchange);
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("reversed", true);
return gson.toJson(result);
}
@GET
public String getAll(@QueryParam("accountId") Long accountId) {
List<CashExchange> list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId)
: repository.findAllByOrderByCreatedAtDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashExchange header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
Map<String, Object> map = toMap(header);
List<Map<String, Object>> details = detailRepository.findByExchangeId(id).stream().map(this::detailToMap)
.collect(Collectors.toList());
map.put("details", details);
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
total = total.add(BigDecimal.valueOf((long) value * count));
}
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
return true;
}
}
return false;
}
private void saveDetails(Long exchangeId, String direction, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) {
continue;
}
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong()
: null;
CashExchangeDetail detail = new CashExchangeDetail();
detail.setExchangeId(exchangeId);
detail.setDirection(direction);
detail.setDenominationId(denominationId);
detail.setDenominationValue(value);
detail.setCount(count);
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(detail);
}
}
private Map<String, Object> toMap(CashExchange e) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", e.getId());
map.put("cashierId", e.getCashierId());
map.put("accountId", e.getAccountId());
map.put("accountNo", e.getAccountNo());
map.put("clientId", e.getClientId());
map.put("customerIdProof", e.getCustomerIdProof());
map.put("exchangeAmount", e.getExchangeAmount());
map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null);
map.put("note", e.getNote());
map.put("performedBy", e.getPerformedBy());
map.put("performedByUsername", e.getPerformedByUsername());
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
map.put("reversedBy", e.getReversedBy());
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
map.put("reverseReason", e.getReverseReason());
return map;
}
private Map<String, Object> detailToMap(CashExchangeDetail d) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", d.getId());
map.put("direction", d.getDirection());
map.put("denominationId", d.getDenominationId());
map.put("denominationValue", d.getDenominationValue());
map.put("count", d.getCount());
map.put("totalAmount", d.getTotalAmount());
return map;
}
}
@@ -0,0 +1,75 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange")
@Getter
@Setter
@NoArgsConstructor
public class CashExchange {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "account_id")
private Long accountId;
@Column(name = "account_no", length = 50)
private String accountNo;
@Column(name = "client_id")
private Long clientId;
@Column(name = "exchange_amount", nullable = false)
private BigDecimal exchangeAmount;
@Column(name = "exchange_date")
private LocalDate exchangeDate;
@Column(name = "note", length = 500)
private String note;
@Column(name = "cashier_id")
private Long cashierId;
@Column(name = "customer_id_proof", length = 50)
private String customerIdProof;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "is_reversed")
private Boolean isReversed;
@Column(name = "reversed_by")
private Long reversedBy;
@Column(name = "reversed_at")
private LocalDateTime reversedAt;
@Column(name = "reverse_reason", length = 500)
private String reverseReason;
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashExchangeDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "exchange_id", nullable = false)
private Long exchangeId;
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashexchange.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashExchangeDetailRepository extends JpaRepository<CashExchangeDetail, Long> {
List<CashExchangeDetail> findByExchangeId(Long exchangeId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.cashexchange.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashExchangeRepository extends JpaRepository<CashExchange, Long> {
List<CashExchange> findAllByOrderByCreatedAtDesc();
List<CashExchange> findByAccountIdOrderByCreatedAtDesc(Long accountId);
}
@@ -0,0 +1,224 @@
package org.apache.fineract.portfolio.cashintercashier.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashier;
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetail;
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetailRepository;
import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Path("/v1/cashintercashier")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashInterCashierApiResource {
private final CashInterCashierRepository repository;
private final CashInterCashierDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final Gson gson = new Gson();
@POST
@Transactional
public String create(String body) {
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHINTERCASHIER");
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
? json.get("tellerId").getAsLong() : null;
final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull()
? json.get("fromCashierId").getAsLong() : null;
final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull()
? json.get("toCashierId").getAsLong() : null;
if (tellerId == null || fromCashierId == null || toCashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashintercashier.parties.required",
"tellerId, fromCashierId and toCashierId are required.");
}
if (fromCashierId.equals(toCashierId)) {
throw new PlatformDataIntegrityException("error.msg.cashintercashier.same.cashier",
"From-cashier and To-cashier cannot be the same.");
}
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now();
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
? json.get("fromParty").getAsString() : null;
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
? json.get("toParty").getAsString() : null;
final String note = json.has("note") && !json.get("note").isJsonNull()
? json.get("note").getAsString() : null;
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
? json.getAsJsonArray("denominations") : new JsonArray();
if (!hasPositiveCount(denominations)) {
throw new PlatformDataIntegrityException("error.msg.cashintercashier.empty",
"At least one denomination with a positive count is required.");
}
final BigDecimal total = sumTotal(denominations);
CashInterCashier header = new CashInterCashier();
header.setTransferDate(transferDate);
header.setTellerId(tellerId);
header.setFromCashierId(fromCashierId);
header.setToCashierId(toCashierId);
header.setFromParty(fromParty);
header.setToParty(toParty);
header.setTransferAmount(total);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now());
header.setIsReversed(false);
try {
AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {}
CashInterCashier saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), denominations);
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
return gson.toJson(result);
}
@POST
@Path("{id}/reverse")
@Transactional
public String reverse(@PathParam("id") Long id, String body) {
context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHINTERCASHIER");
CashInterCashier record = repository.findById(id)
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashintercashier.not.found",
"Inter-cashier transfer not found: " + id));
if (Boolean.TRUE.equals(record.getIsReversed())) {
throw new PlatformDataIntegrityException("error.msg.cashintercashier.already.reversed",
"Inter-cashier transfer " + id + " is already reversed.");
}
String reason = null;
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {}
AppUser user = null;
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
record.setIsReversed(true);
record.setReversedAt(LocalDateTime.now());
record.setReverseReason(reason);
if (user != null) record.setReversedBy(user.getId());
repository.save(record);
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("reversed", true);
return gson.toJson(result);
}
@GET
public String getAll() {
return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream()
.map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashInterCashier header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Inter-cashier transfer not found: " + id));
Map<String, Object> map = toMap(header);
map.put("details", detailRepository.findByInterCashierId(id).stream()
.map(this::detailToMap).collect(Collectors.toList()));
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
int count = item.has("count") ? item.get("count").getAsInt() : 0;
if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count));
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
if (el.getAsJsonObject().get("count").getAsInt() > 0) return true;
}
return false;
}
private void saveDetails(Long interCashierId, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") ? item.get("count").getAsInt() : 0;
if (count <= 0) continue;
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong() : null;
CashInterCashierDetail d = new CashInterCashierDetail();
d.setInterCashierId(interCashierId);
d.setDenominationId(denomId);
d.setDenominationValue(value);
d.setCount(count);
d.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(d);
}
}
private Map<String, Object> toMap(CashInterCashier e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", e.getId());
m.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null);
m.put("tellerId", e.getTellerId());
m.put("fromCashierId", e.getFromCashierId());
m.put("toCashierId", e.getToCashierId());
m.put("fromParty", e.getFromParty());
m.put("toParty", e.getToParty());
m.put("transferAmount", e.getTransferAmount());
m.put("note", e.getNote());
m.put("performedByUsername", e.getPerformedByUsername());
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
m.put("isReversed", e.getIsReversed());
return m;
}
private Map<String, Object> detailToMap(CashInterCashierDetail d) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", d.getId());
m.put("denominationId", d.getDenominationId());
m.put("denominationValue", d.getDenominationValue());
m.put("count", d.getCount());
m.put("totalAmount", d.getTotalAmount());
return m;
}
}
@@ -0,0 +1,71 @@
package org.apache.fineract.portfolio.cashintercashier.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_inter_cashier")
@Getter
@Setter
@NoArgsConstructor
public class CashInterCashier {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "transfer_date")
private LocalDate transferDate;
@Column(name = "teller_id")
private Long tellerId;
@Column(name = "from_cashier_id")
private Long fromCashierId;
@Column(name = "to_cashier_id")
private Long toCashierId;
@Column(name = "from_party", length = 100)
private String fromParty;
@Column(name = "to_party", length = 100)
private String toParty;
@Column(name = "transfer_amount", nullable = false)
private BigDecimal transferAmount;
@Column(name = "note", length = 500)
private String note;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "is_reversed")
private Boolean isReversed = false;
@Column(name = "reversed_by")
private Long reversedBy;
@Column(name = "reversed_at")
private LocalDateTime reversedAt;
@Column(name = "reverse_reason", length = 500)
private String reverseReason;
}
@@ -0,0 +1,39 @@
package org.apache.fineract.portfolio.cashintercashier.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_inter_cashier_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashInterCashierDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "inter_cashier_id", nullable = false)
private Long interCashierId;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashintercashier.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashInterCashierDetailRepository extends JpaRepository<CashInterCashierDetail, Long> {
List<CashInterCashierDetail> findByInterCashierId(Long interCashierId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.cashintercashier.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashInterCashierRepository extends JpaRepository<CashInterCashier, Long> {
List<CashInterCashier> findAllByOrderByCreatedAtDesc();
List<CashInterCashier> findByTellerIdOrderByCreatedAtDesc(Long tellerId);
}
@@ -0,0 +1,387 @@
package org.apache.fineract.portfolio.cashreconciliation.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliation;
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetail;
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationDetailRepository;
import org.apache.fineract.portfolio.cashreconciliation.domain.CashReconciliationRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Path("/v1/cashreconciliation")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashReconciliationApiResource {
private final CashReconciliationRepository repository;
private final CashReconciliationDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new Gson();
/**
* GET /cashreconciliation/expected?cashierId=X&asOfDate=yyyy-MM-dd
* Returns denomination-wise expected balance for a cashier.
* Computation: allocations (m_cash_transfer_detail where to_cashier_id=X)
* + inter-cashier-in (m_cash_inter_cashier_detail where to_cashier_id=X)
* - settlements (m_cash_settlement_detail where from_cashier_id=X)
* - inter-cashier-out (m_cash_inter_cashier_detail where from_cashier_id=X)
*/
@GET
@Path("expected")
public String getExpected(@QueryParam("cashierId") Long cashierId, @QueryParam("asOfDate") String asOfDate) {
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
if (cashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
"cashierId query param is required.");
}
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
// In: vault → cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
+ "FROM m_cash_transfer_detail ctd "
+ "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
+ "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
+ "AND ct.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind";
// In: inter-cashier in
final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
+ "FROM m_cash_inter_cashier_detail icd "
+ "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
+ "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
+ "AND ic.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
// In: customer deposits received by this cashier
final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
+ "FROM m_savings_txn_denomination std "
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'DEPOSIT' "
+ "AND std.txn_date <= CAST(? AS DATE) "
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
// Out: settlements to vault
final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count "
+ "FROM m_cash_settlement_detail csd "
+ "JOIN m_cash_settlement cs ON cs.id = csd.settlement_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id "
+ "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) "
+ "AND cs.settlement_date <= CAST(? AS DATE) "
+ "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind";
// Out: inter-cashier out
final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
+ "FROM m_cash_inter_cashier_detail icd "
+ "JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
+ "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
+ "AND ic.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
// Out: customer withdrawals paid by this cashier
final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
+ "FROM m_savings_txn_denomination std "
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'WITHDRAWAL' "
+ "AND std.txn_date <= CAST(? AS DATE) "
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
// In: cash exchange — customer gave denomination TO cashier (RECEIVED direction)
final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
+ "FROM m_cash_exchange_detail ced "
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
+ "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' "
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
+ "AND ce.exchange_date <= CAST(? AS DATE) "
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
// Out: cash exchange — cashier gave denomination TO customer (GIVEN direction)
final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
+ "FROM m_cash_exchange_detail ced "
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
+ "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' "
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
+ "AND ce.exchange_date <= CAST(? AS DATE) "
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
// Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount}
Map<Long, long[]> balance = new LinkedHashMap<>();
addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1);
List<Map<String, Object>> rows = new ArrayList<>();
balance.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0]))
.forEach(e -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", e.getKey());
row.put("denominationValue", (int) e.getValue()[0]);
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
row.put("expectedCount", e.getValue()[2]);
row.put("expectedAmount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
rows.add(row);
});
BigDecimal grandTotal = rows.stream()
.map(r -> (BigDecimal) r.get("expectedAmount"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
JsonObject result = new JsonObject();
result.addProperty("cashierId", cashierId);
result.addProperty("asOfDate", dateFilter);
result.addProperty("expectedTotal", grandTotal);
result.add("denominations", gson.toJsonTree(rows));
return gson.toJson(result);
}
/**
* GET /cashreconciliation/expected/me
* Same as /expected but auto-detects cashierId from the authenticated user.
*/
@GET
@Path("expected/me")
public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) {
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
Long appUserId;
try {
appUserId = context.authenticatedUser().getId();
} catch (Exception e) {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.not.authenticated", "Not authenticated.");
}
Long cashierId;
try {
cashierId = jdbcTemplate.queryForObject(
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
Long.class, appUserId);
} catch (Exception e) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("cashierId", null);
empty.put("asOfDate", dateFilter);
empty.put("expectedTotal", java.math.BigDecimal.ZERO);
empty.put("denominations", new ArrayList<>());
return gson.toJson(empty);
}
// Delegate to the existing /expected logic
return getExpected(cashierId, dateFilter);
}
/**
* GET /cashreconciliation/check?cashierId=X&date=yyyy-MM-dd
* Returns whether reconciliation has been done today for a cashier.
*/
@GET
@Path("check")
public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) {
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
if (cashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
"cashierId query param is required.");
}
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
Integer.class, cashierId, reconDate);
Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId);
result.put("date", reconDate);
result.put("reconciled", count != null && count > 0);
return gson.toJson(result);
}
@POST
@Transactional
public String create(String body) {
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHRECONCILIATION");
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long cashierId = json.has("cashierId") ? json.get("cashierId").getAsLong() : null;
if (cashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
"cashierId is required.");
}
final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull()
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now();
final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull()
? json.get("cashierName").getAsString() : null;
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
? json.get("tellerId").getAsLong() : null;
final String note = json.has("note") && !json.get("note").isJsonNull()
? json.get("note").getAsString() : null;
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
? json.getAsJsonArray("denominations") : new JsonArray();
BigDecimal expectedTotal = BigDecimal.ZERO;
BigDecimal physicalTotal = BigDecimal.ZERO;
List<CashReconciliationDetail> details = new ArrayList<>();
for (JsonElement el : denominations) {
JsonObject item = el.getAsJsonObject();
int denomValue = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
int expectedCount = item.has("expectedCount") ? item.get("expectedCount").getAsInt() : 0;
int physicalCount = item.has("physicalCount") ? item.get("physicalCount").getAsInt() : 0;
int varianceCount = physicalCount - expectedCount;
BigDecimal varianceAmount = BigDecimal.valueOf((long) denomValue * varianceCount);
expectedTotal = expectedTotal.add(BigDecimal.valueOf((long) denomValue * expectedCount));
physicalTotal = physicalTotal.add(BigDecimal.valueOf((long) denomValue * physicalCount));
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong() : null;
CashReconciliationDetail d = new CashReconciliationDetail();
d.setDenominationId(denomId);
d.setDenominationValue(denomValue);
d.setExpectedCount(expectedCount);
d.setPhysicalCount(physicalCount);
d.setVarianceCount(varianceCount);
d.setVarianceAmount(varianceAmount);
details.add(d);
}
BigDecimal variance = physicalTotal.subtract(expectedTotal);
String status = variance.compareTo(BigDecimal.ZERO) == 0 ? "OK"
: variance.compareTo(BigDecimal.ZERO) > 0 ? "EXCESS" : "SHORT";
CashReconciliation header = new CashReconciliation();
header.setReconDate(reconDate);
header.setCashierId(cashierId);
header.setCashierName(cashierName);
header.setTellerId(tellerId);
header.setExpectedAmount(expectedTotal);
header.setPhysicalAmount(physicalTotal);
header.setVariance(variance);
header.setStatus(status);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now());
try {
AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {}
CashReconciliation saved = repository.saveAndFlush(header);
for (CashReconciliationDetail d : details) {
d.setReconciliationId(saved.getId());
detailRepository.save(d);
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
result.addProperty("status", status);
result.addProperty("variance", variance);
return gson.toJson(result);
}
@GET
public String getAll() {
return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream()
.map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashReconciliation header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash reconciliation not found: " + id));
Map<String, Object> map = toMap(header);
map.put("details", detailRepository.findByReconciliationId(id).stream()
.map(this::detailToMap).collect(Collectors.toList()));
return gson.toJson(map);
}
// Row mapper: Object[]{Long denomId, Integer denomValue, String kind, Long count}
private static final org.springframework.jdbc.core.RowMapper<Object[]> DENOM_ROW =
(rs, r) -> new Object[] {
rs.getLong("denomination_id"),
rs.getInt("denomination_value"),
rs.getString("kind"),
rs.getLong("total_count")
};
private void addToBalance(Map<Long, long[]> balance, List<Object[]> rows, int sign) {
for (Object[] row : rows) {
Long denomId = (Long) row[0];
int denomValue = (Integer) row[1];
long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L;
long count = (Long) row[3];
long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L });
entry[2] += count * sign;
}
}
private Map<String, Object> toMap(CashReconciliation e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", e.getId());
m.put("reconDate", e.getReconDate() != null ? e.getReconDate().toString() : null);
m.put("cashierId", e.getCashierId());
m.put("cashierName", e.getCashierName());
m.put("tellerId", e.getTellerId());
m.put("expectedAmount", e.getExpectedAmount());
m.put("physicalAmount", e.getPhysicalAmount());
m.put("variance", e.getVariance());
m.put("status", e.getStatus());
m.put("note", e.getNote());
m.put("performedByUsername", e.getPerformedByUsername());
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
return m;
}
private Map<String, Object> detailToMap(CashReconciliationDetail d) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", d.getId());
m.put("denominationId", d.getDenominationId());
m.put("denominationValue", d.getDenominationValue());
m.put("expectedCount", d.getExpectedCount());
m.put("physicalCount", d.getPhysicalCount());
m.put("varianceCount", d.getVarianceCount());
m.put("varianceAmount", d.getVarianceAmount());
return m;
}
}
@@ -0,0 +1,62 @@
package org.apache.fineract.portfolio.cashreconciliation.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_reconciliation")
@Getter
@Setter
@NoArgsConstructor
public class CashReconciliation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "recon_date", nullable = false)
private LocalDate reconDate;
@Column(name = "cashier_id", nullable = false)
private Long cashierId;
@Column(name = "cashier_name", length = 150)
private String cashierName;
@Column(name = "teller_id")
private Long tellerId;
@Column(name = "expected_amount")
private BigDecimal expectedAmount = BigDecimal.ZERO;
@Column(name = "physical_amount")
private BigDecimal physicalAmount = BigDecimal.ZERO;
@Column(name = "variance")
private BigDecimal variance = BigDecimal.ZERO;
@Column(name = "status", length = 10)
private String status = "OK";
@Column(name = "note", length = 500)
private String note;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@@ -0,0 +1,45 @@
package org.apache.fineract.portfolio.cashreconciliation.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_reconciliation_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashReconciliationDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "reconciliation_id", nullable = false)
private Long reconciliationId;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "expected_count")
private Integer expectedCount = 0;
@Column(name = "physical_count")
private Integer physicalCount = 0;
@Column(name = "variance_count")
private Integer varianceCount = 0;
@Column(name = "variance_amount")
private BigDecimal varianceAmount = BigDecimal.ZERO;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashreconciliation.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashReconciliationDetailRepository extends JpaRepository<CashReconciliationDetail, Long> {
List<CashReconciliationDetail> findByReconciliationId(Long reconciliationId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.cashreconciliation.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashReconciliationRepository extends JpaRepository<CashReconciliation, Long> {
List<CashReconciliation> findAllByOrderByCreatedAtDesc();
List<CashReconciliation> findByCashierIdOrderByReconDateDesc(Long cashierId);
}
@@ -0,0 +1,437 @@
package org.apache.fineract.portfolio.cashreport.api;
import com.google.gson.Gson;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
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.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Path("/v1/cashreports")
@Component
@RequiredArgsConstructor
@Produces({ MediaType.APPLICATION_JSON })
public class CashReportApiResource {
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
private final Gson gson = new Gson();
@GET
@Path("closing")
public String closing(@QueryParam("date") String date) {
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
// Single query — conditional aggregation for deposit, withdrawal, net per denomination
final String sql = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
+ "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.count ELSE 0 END) AS deposit_count, "
+ "SUM(CASE WHEN std.txn_type = 'DEPOSIT' THEN std.total_amount ELSE 0 END) AS deposit_amount, "
+ "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.count ELSE 0 END) AS withdrawal_count, "
+ "SUM(CASE WHEN std.txn_type = 'WITHDRAWAL' THEN std.total_amount ELSE 0 END) AS withdrawal_amount "
+ "FROM m_savings_txn_denomination std "
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_date = CAST(? AS DATE) "
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind "
+ "ORDER BY std.denomination_value DESC, dm.kind";
final List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
long depositCount = rs.getLong("deposit_count");
long withdrawalCount = rs.getLong("withdrawal_count");
BigDecimal depositAmount = rs.getBigDecimal("deposit_amount");
BigDecimal withdrawalAmount = rs.getBigDecimal("withdrawal_amount");
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", rs.getLong("denomination_id"));
row.put("denominationValue", rs.getInt("denomination_value"));
row.put("kind", rs.getString("kind"));
row.put("depositCount", depositCount);
row.put("depositAmount", depositAmount);
row.put("withdrawalCount", withdrawalCount);
row.put("withdrawalAmount", withdrawalAmount);
row.put("netCount", depositCount - withdrawalCount);
row.put("netAmount", depositAmount.subtract(withdrawalAmount));
return row;
}, txnDate);
BigDecimal depositTotal = BigDecimal.ZERO;
BigDecimal withdrawalTotal = BigDecimal.ZERO;
for (Map<String, Object> row : rows) {
depositTotal = depositTotal.add((BigDecimal) row.get("depositAmount"));
withdrawalTotal = withdrawalTotal.add((BigDecimal) row.get("withdrawalAmount"));
}
// Cash exchange rows — separate section (denomination swap, net monetary = 0)
final String exchangeSql = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
+ "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.count ELSE 0 END) AS received_count, "
+ "SUM(CASE WHEN ced.direction = 'RECEIVED' THEN ced.total_amount ELSE 0 END) AS received_amount, "
+ "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.count ELSE 0 END) AS given_count, "
+ "SUM(CASE WHEN ced.direction = 'GIVEN' THEN ced.total_amount ELSE 0 END) AS given_amount "
+ "FROM m_cash_exchange_detail ced "
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
+ "WHERE ce.exchange_date = CAST(? AS DATE) "
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind "
+ "ORDER BY ced.denomination_value DESC, dm.kind";
final List<Map<String, Object>> exchangeRows = jdbcTemplate.query(exchangeSql, (rs, rowNum) -> {
long receivedCount = rs.getLong("received_count");
long givenCount = rs.getLong("given_count");
BigDecimal receivedAmount = rs.getBigDecimal("received_amount");
BigDecimal givenAmount = rs.getBigDecimal("given_amount");
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", rs.getLong("denomination_id"));
row.put("denominationValue", rs.getInt("denomination_value"));
row.put("kind", rs.getString("kind"));
row.put("receivedCount", receivedCount);
row.put("receivedAmount", receivedAmount);
row.put("givenCount", givenCount);
row.put("givenAmount", givenAmount);
row.put("netCount", receivedCount - givenCount);
row.put("netAmount", receivedAmount.subtract(givenAmount));
return row;
}, txnDate);
Map<String, Object> result = new LinkedHashMap<>();
result.put("date", txnDate);
result.put("rows", rows);
result.put("depositTotal", depositTotal);
result.put("withdrawalTotal", withdrawalTotal);
result.put("netTotal", depositTotal.subtract(withdrawalTotal));
result.put("exchangeRows", exchangeRows);
return gson.toJson(result);
}
@GET
@Path("excess")
public String excess(@QueryParam("threshold") BigDecimal threshold, @QueryParam("fromDate") String fromDate,
@QueryParam("toDate") String toDate) {
final BigDecimal effectiveThreshold = threshold != null ? threshold : BigDecimal.valueOf(50000);
final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank();
// Savings / Current / FD / RD transactions
final StringBuilder sql = new StringBuilder();
sql.append("SELECT txn_id, MAX(account_no) account_no, MAX(client_id) client_id, MAX(cif_no) cif_no, ");
sql.append("MAX(txn_date) txn_date, SUM(total_amount) total_amount FROM m_savings_txn_denomination ");
final List<Object> args = new ArrayList<>();
if (hasDateRange) {
sql.append("WHERE txn_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) ");
args.add(fromDate);
args.add(toDate);
}
sql.append("GROUP BY txn_id HAVING SUM(total_amount) >= ? ORDER BY total_amount DESC");
args.add(effectiveThreshold);
final List<Map<String, Object>> savingsRows = jdbcTemplate.query(sql.toString(), (rs, rowNum) -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("type", "SAVINGS_TXN");
row.put("txnId", rs.getLong("txn_id"));
row.put("accountNo", rs.getString("account_no"));
row.put("clientId", rs.getObject("client_id"));
row.put("cifNo", rs.getString("cif_no"));
final java.sql.Date d = rs.getDate("txn_date");
row.put("txnDate", d != null ? d.toString() : null);
row.put("totalAmount", rs.getBigDecimal("total_amount"));
row.put("idProof", null);
return row;
}, args.toArray());
// Cash exchange transactions
final StringBuilder exSql = new StringBuilder();
exSql.append("SELECT id, account_no, client_id, exchange_date, exchange_amount, customer_id_proof, performed_by_username ");
exSql.append("FROM m_cash_exchange ");
exSql.append("WHERE (is_reversed IS NULL OR is_reversed = false) AND exchange_amount >= ? ");
final List<Object> exArgs = new ArrayList<>();
exArgs.add(effectiveThreshold);
if (hasDateRange) {
exSql.append("AND exchange_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) ");
exArgs.add(fromDate);
exArgs.add(toDate);
}
exSql.append("ORDER BY exchange_amount DESC");
final List<Map<String, Object>> exchangeRows = jdbcTemplate.query(exSql.toString(), (rs, rowNum) -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("type", "EXCHANGE");
row.put("txnId", rs.getLong("id"));
row.put("accountNo", rs.getString("account_no"));
row.put("clientId", rs.getObject("client_id"));
row.put("cifNo", null);
final java.sql.Date d = rs.getDate("exchange_date");
row.put("txnDate", d != null ? d.toString() : null);
row.put("totalAmount", rs.getBigDecimal("exchange_amount"));
row.put("idProof", rs.getString("customer_id_proof"));
return row;
}, exArgs.toArray());
// Merge and sort by amount DESC
final List<Map<String, Object>> combined = new ArrayList<>();
combined.addAll(savingsRows);
combined.addAll(exchangeRows);
combined.sort((a, b) -> ((BigDecimal) b.get("totalAmount")).compareTo((BigDecimal) a.get("totalAmount")));
return gson.toJson(combined);
}
/**
* GET /cashreports/cashier-position?cashierId=X&asOfDate=yyyy-MM-dd
* Returns denomination-wise net position per cashier grouped by denomination_id+kind.
*/
@GET
@Path("cashier-position")
public String cashierPosition(@QueryParam("cashierId") Long cashierId,
@QueryParam("asOfDate") String asOfDate) {
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
if (cashierId != null) {
return gson.toJson(buildPositionForCashier(cashierId, dateFilter));
}
final String idSql = "SELECT DISTINCT cashier_id FROM ("
+ " SELECT to_cashier_id AS cashier_id FROM m_cash_transfer WHERE to_cashier_id IS NOT NULL "
+ " UNION SELECT from_cashier_id FROM m_cash_settlement WHERE from_cashier_id IS NOT NULL "
+ " UNION SELECT from_cashier_id FROM m_cash_inter_cashier WHERE from_cashier_id IS NOT NULL "
+ " UNION SELECT to_cashier_id FROM m_cash_inter_cashier WHERE to_cashier_id IS NOT NULL "
+ " UNION SELECT cashier_id FROM m_cash_exchange WHERE cashier_id IS NOT NULL "
+ ") ids";
List<Long> cashierIds = jdbcTemplate.queryForList(idSql, Long.class);
List<Map<String, Object>> allPositions = new ArrayList<>();
for (Long cid : cashierIds) {
allPositions.add(buildPositionForCashier(cid, dateFilter));
}
return gson.toJson(allPositions);
}
@GET
@Path("cashier-position/me")
public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) {
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
Long appUserId;
try {
appUserId = context.authenticatedUser().getId();
} catch (Exception e) {
Map<String, Object> err = new LinkedHashMap<>();
err.put("error", "Not authenticated");
return gson.toJson(err);
}
Long cashierId;
try {
cashierId = jdbcTemplate.queryForObject(
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
Long.class, appUserId);
} catch (Exception e) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("cashierId", null);
empty.put("asOfDate", dateFilter);
empty.put("totalAmount", BigDecimal.ZERO);
empty.put("denominations", new ArrayList<>());
return gson.toJson(empty);
}
return gson.toJson(buildPositionForCashier(cashierId, dateFilter));
}
/**
* GET /cashreports/cashier-morning?cashierId=X&date=YYYY-MM-DD
* Returns denomination-wise cash received by the cashier from vault on the given date
* (today's morning opening allocation from m_cash_transfer only, non-reversed).
*/
@GET
@Path("cashier-morning")
public String cashierMorning(@QueryParam("cashierId") Long cashierId,
@QueryParam("date") String date) {
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
if (cashierId == null) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("cashierId", null);
empty.put("date", txnDate);
empty.put("totalMorning", BigDecimal.ZERO);
empty.put("denominations", new ArrayList<>());
return gson.toJson(empty);
}
final String sql = "SELECT ctd.denomination_id, ctd.denomination_value, "
+ "COALESCE(dm.kind, 'note') AS kind, "
+ "SUM(ctd.count) AS total_count, "
+ "SUM(ctd.total_amount) AS total_amount "
+ "FROM m_cash_transfer_detail ctd "
+ "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
+ "WHERE ct.to_cashier_id = ? "
+ "AND ct.transfer_date = CAST(? AS DATE) "
+ "AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind "
+ "ORDER BY ctd.denomination_value DESC";
List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", rs.getLong("denomination_id"));
row.put("denominationValue", rs.getInt("denomination_value"));
row.put("kind", rs.getString("kind"));
row.put("morningCount", rs.getLong("total_count"));
row.put("morningAmount", rs.getBigDecimal("total_amount"));
return row;
}, cashierId, txnDate);
BigDecimal total = rows.stream()
.map(r -> (BigDecimal) r.get("morningAmount"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId);
result.put("date", txnDate);
result.put("totalMorning", total);
result.put("denominations", rows);
return gson.toJson(result);
}
private Map<String, Object> buildPositionForCashier(Long cashierId, String dateFilter) {
// Vault → Cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
+ "FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
+ "WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
+ "AND ct.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind";
// Inter-cashier IN
final String sqlIn2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
+ "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
+ "WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
+ "AND ic.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
// Customer deposits
final String sqlIn3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
+ "FROM m_savings_txn_denomination std "
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'DEPOSIT' AND std.txn_date <= CAST(? AS DATE) "
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
// Settlements back to vault
final String sqlOut1 = "SELECT csd.denomination_id, csd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(csd.count) AS total_count "
+ "FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = csd.denomination_id "
+ "WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false) "
+ "AND cs.settlement_date <= CAST(? AS DATE) "
+ "GROUP BY csd.denomination_id, csd.denomination_value, dm.kind";
// Inter-cashier OUT
final String sqlOut2 = "SELECT icd.denomination_id, icd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(icd.count) AS total_count "
+ "FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = icd.denomination_id "
+ "WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false) "
+ "AND ic.transfer_date <= CAST(? AS DATE) "
+ "GROUP BY icd.denomination_id, icd.denomination_value, dm.kind";
// Customer withdrawals
final String sqlOut3 = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(std.count) AS total_count "
+ "FROM m_savings_txn_denomination std "
+ "LEFT JOIN m_denomination_master dm ON dm.id = std.denomination_id "
+ "WHERE std.txn_type = 'WITHDRAWAL' AND std.txn_date <= CAST(? AS DATE) "
+ "AND std.officer_id IN ("
+ " SELECT au.id FROM m_appuser au "
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?) "
+ "GROUP BY std.denomination_id, std.denomination_value, dm.kind";
// Exchange IN: customer gave denomination to cashier (RECEIVED direction)
final String sqlIn4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
+ "FROM m_cash_exchange_detail ced "
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
+ "WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED' "
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
+ "AND ce.exchange_date <= CAST(? AS DATE) "
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
// Exchange OUT: cashier gave denomination to customer (GIVEN direction)
final String sqlOut4 = "SELECT ced.denomination_id, ced.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ced.count) AS total_count "
+ "FROM m_cash_exchange_detail ced "
+ "JOIN m_cash_exchange ce ON ce.id = ced.exchange_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ced.denomination_id "
+ "WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN' "
+ "AND (ce.is_reversed IS NULL OR ce.is_reversed = false) "
+ "AND ce.exchange_date <= CAST(? AS DATE) "
+ "GROUP BY ced.denomination_id, ced.denomination_value, dm.kind";
// Balance map: denomination_id → long[3] {denomValue, kindFlag (0=note/1=coin), netCount}
Map<Long, long[]> balance = new LinkedHashMap<>();
addToBalance(balance, jdbcTemplate.query(sqlIn1, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn2, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn3, DENOM_ROW, dateFilter, cashierId), 1);
addToBalance(balance, jdbcTemplate.query(sqlIn4, DENOM_ROW, cashierId, dateFilter), 1);
addToBalance(balance, jdbcTemplate.query(sqlOut1, DENOM_ROW, cashierId, dateFilter), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut2, DENOM_ROW, cashierId, dateFilter), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut3, DENOM_ROW, dateFilter, cashierId), -1);
addToBalance(balance, jdbcTemplate.query(sqlOut4, DENOM_ROW, cashierId, dateFilter), -1);
List<Map<String, Object>> rows = new ArrayList<>();
balance.entrySet().stream()
.filter(e -> e.getValue()[2] != 0)
.sorted((a, b) -> Long.compare(b.getValue()[0], a.getValue()[0]))
.forEach(e -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", e.getKey());
row.put("denominationValue", (int) e.getValue()[0]);
row.put("kind", e.getValue()[1] == 1L ? "coin" : "note");
row.put("count", e.getValue()[2]);
row.put("amount", BigDecimal.valueOf(e.getValue()[0] * e.getValue()[2]));
rows.add(row);
});
BigDecimal totalAmount = rows.stream()
.map(r -> (BigDecimal) r.get("amount"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
String cashierName = null;
try {
cashierName = jdbcTemplate.queryForObject(
"SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?",
String.class, cashierId);
} catch (Exception ignored) {}
Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId);
result.put("cashierName", cashierName);
result.put("asOfDate", dateFilter);
result.put("totalAmount", totalAmount);
result.put("denominations", rows);
return result;
}
// Row mapper: returns Object[]{Long denomId, Integer denomValue, String kind, Long count}
private static final org.springframework.jdbc.core.RowMapper<Object[]> DENOM_ROW =
(rs, r) -> new Object[] {
rs.getLong("denomination_id"),
rs.getInt("denomination_value"),
rs.getString("kind"),
rs.getLong("total_count")
};
private void addToBalance(Map<Long, long[]> balance, List<Object[]> rows, int sign) {
for (Object[] row : rows) {
Long denomId = (Long) row[0];
int denomValue = (Integer) row[1];
long kindFlag = "coin".equalsIgnoreCase((String) row[2]) ? 1L : 0L;
long count = (Long) row[3];
long[] entry = balance.computeIfAbsent(denomId, k -> new long[] { denomValue, kindFlag, 0L });
entry[2] += count * sign;
}
}
}
@@ -0,0 +1,309 @@
package org.apache.fineract.portfolio.cashsettlement.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement;
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetail;
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetailRepository;
import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementRepository;
import org.apache.fineract.portfolio.teller.service.GlDispatchService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
@Path("/v1/cashsettlement")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashSettlementApiResource {
private final CashSettlementRepository repository;
private final CashSettlementDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final GlDispatchService glDispatchService;
private final PlatformTransactionManager transactionManager;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new Gson();
@POST
public String create(String body) {
context.authenticatedUser().validateHasPermissionTo("CREATE_CASHSETTLEMENT");
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long fromCashierId = json.has("fromCashierId") && !json.get("fromCashierId").isJsonNull()
? json.get("fromCashierId").getAsLong() : null;
final Long toTellerId = json.has("toTellerId") && !json.get("toTellerId").isJsonNull()
? json.get("toTellerId").getAsLong() : null;
if (fromCashierId == null || toTellerId == null) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.parties.required",
"fromCashierId and toTellerId are required.");
}
final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull()
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now();
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
? json.get("fromParty").getAsString() : null;
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
? json.get("toParty").getAsString() : null;
final String note = json.has("note") && !json.get("note").isJsonNull()
? json.get("note").getAsString() : null;
final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull()
? json.get("currencyCode").getAsString() : "INR";
// Require reconciliation before settlement
Integer reconCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
Integer.class, fromCashierId, settlementDate.toString());
if (reconCount == null || reconCount == 0) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.reconciliation.required",
"Reconciliation must be completed before settlement for cashier " + fromCashierId + " on " + settlementDate + ". Please complete today's reconciliation first.");
}
// Prevent duplicate settlement for same cashier+date
Integer existingCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
Integer.class, fromCashierId, settlementDate.toString());
if (existingCount != null && existingCount > 0) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.duplicate",
"A settlement for cashier " + fromCashierId + " on " + settlementDate + " already exists. Reverse the existing settlement first.");
}
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
? json.getAsJsonArray("denominations") : new JsonArray();
if (!hasPositiveCount(denominations)) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.empty",
"At least one denomination with a positive count is required.");
}
final BigDecimal total = sumTotal(denominations);
CashSettlement header = new CashSettlement();
header.setSettlementDate(settlementDate);
header.setFromCashierId(fromCashierId);
header.setToTellerId(toTellerId);
header.setFromParty(fromParty);
header.setToParty(toParty);
header.setSettlementAmount(total);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now());
header.setIsReversed(false);
try {
AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {}
// Commit header + details first, then GL dispatch separately to avoid
// EclipseLink flushing pending detail rows inside Fineract's GL transaction.
final Long savedId = new TransactionTemplate(transactionManager).execute(status -> {
CashSettlement saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), denominations);
return saved.getId();
});
String glWarning = null;
try {
// Use cashier's own teller_id from m_cashiers for GL dispatch so the
// Fineract cashier-teller link is respected regardless of which vault teller
// the user submitted cash to.
Long glTellerId = toTellerId;
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : "");
} catch (Exception e) {
glWarning = "Settlement saved but GL entry failed: " + e.getMessage();
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", savedId);
if (glWarning != null) result.addProperty("glWarning", glWarning);
return gson.toJson(result);
}
@POST
@Path("{id}/reverse")
@Transactional
public String reverse(@PathParam("id") Long id, String body) {
context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHSETTLEMENT");
CashSettlement settlement = repository.findById(id)
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashsettlement.not.found",
"Settlement not found: " + id));
if (Boolean.TRUE.equals(settlement.getIsReversed())) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.already.reversed",
"Settlement " + id + " is already reversed.");
}
String reason = null;
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {}
AppUser user = null;
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
settlement.setIsReversed(true);
settlement.setReversedAt(LocalDateTime.now());
settlement.setReverseReason(reason);
if (user != null) settlement.setReversedBy(user.getId());
repository.save(settlement);
if (settlement.getToTellerId() != null && settlement.getFromCashierId() != null) {
try {
Long glTellerId = settlement.getToTellerId();
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.allocateCashToCashier(glTellerId,
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR",
LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
"Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
} catch (Exception ignored) {}
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("reversed", true);
return gson.toJson(result);
}
@GET
@Path("check")
public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) {
context.authenticatedUser().validateHasPermissionTo("READ_CASHSETTLEMENT");
if (cashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required",
"cashierId query param is required.");
}
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
Integer.class, cashierId, settlementDate);
Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId);
result.put("date", settlementDate);
result.put("alreadySettled", count != null && count > 0);
return gson.toJson(result);
}
@GET
public String getAll() {
List<CashSettlement> list = repository.findAllByOrderByCreatedAtDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashSettlement header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash settlement not found: " + id));
Map<String, Object> map = toMap(header);
List<Map<String, Object>> details = detailRepository.findBySettlementId(id).stream()
.map(this::detailToMap).collect(Collectors.toList());
map.put("details", details);
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
int count = item.has("count") ? item.get("count").getAsInt() : 0;
if (count > 0) total = total.add(BigDecimal.valueOf((long) value * count));
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
if (el.getAsJsonObject().get("count").getAsInt() > 0) return true;
}
return false;
}
private void saveDetails(Long settlementId, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") ? item.get("count").getAsInt() : 0;
if (count <= 0) continue;
int value = item.has("denominationValue") ? item.get("denominationValue").getAsInt() : 0;
Long denomId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong() : null;
CashSettlementDetail d = new CashSettlementDetail();
d.setSettlementId(settlementId);
d.setDenominationId(denomId);
d.setDenominationValue(value);
d.setCount(count);
d.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(d);
}
}
private Map<String, Object> toMap(CashSettlement e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", e.getId());
m.put("settlementDate", e.getSettlementDate() != null ? e.getSettlementDate().toString() : null);
m.put("fromCashierId", e.getFromCashierId());
m.put("toTellerId", e.getToTellerId());
m.put("fromParty", e.getFromParty());
m.put("toParty", e.getToParty());
m.put("settlementAmount", e.getSettlementAmount());
m.put("note", e.getNote());
m.put("performedBy", e.getPerformedBy());
m.put("performedByUsername", e.getPerformedByUsername());
m.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
m.put("isReversed", e.getIsReversed());
m.put("reversedBy", e.getReversedBy());
m.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
m.put("reverseReason", e.getReverseReason());
return m;
}
private Map<String, Object> detailToMap(CashSettlementDetail d) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", d.getId());
m.put("denominationId", d.getDenominationId());
m.put("denominationValue", d.getDenominationValue());
m.put("count", d.getCount());
m.put("totalAmount", d.getTotalAmount());
return m;
}
}
@@ -0,0 +1,68 @@
package org.apache.fineract.portfolio.cashsettlement.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_settlement")
@Getter
@Setter
@NoArgsConstructor
public class CashSettlement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "settlement_date")
private LocalDate settlementDate;
@Column(name = "from_cashier_id")
private Long fromCashierId;
@Column(name = "to_teller_id")
private Long toTellerId;
@Column(name = "from_party", length = 100)
private String fromParty;
@Column(name = "to_party", length = 100)
private String toParty;
@Column(name = "settlement_amount", nullable = false)
private BigDecimal settlementAmount;
@Column(name = "note", length = 500)
private String note;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "is_reversed")
private Boolean isReversed = false;
@Column(name = "reversed_by")
private Long reversedBy;
@Column(name = "reversed_at")
private LocalDateTime reversedAt;
@Column(name = "reverse_reason", length = 500)
private String reverseReason;
}
@@ -0,0 +1,39 @@
package org.apache.fineract.portfolio.cashsettlement.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_settlement_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashSettlementDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "settlement_id", nullable = false)
private Long settlementId;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashsettlement.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashSettlementDetailRepository extends JpaRepository<CashSettlementDetail, Long> {
List<CashSettlementDetail> findBySettlementId(Long settlementId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.cashsettlement.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashSettlementRepository extends JpaRepository<CashSettlement, Long> {
List<CashSettlement> findAllByOrderByCreatedAtDesc();
List<CashSettlement> findByFromCashierIdOrderByCreatedAtDesc(Long cashierId);
}
@@ -0,0 +1,298 @@
package org.apache.fineract.portfolio.cashtransfer.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.teller.service.GlDispatchService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransfer;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetail;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetailRepository;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
@Path("/v1/cashtransfer")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashTransferApiResource {
private final CashTransferRepository repository;
private final CashTransferDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final GlDispatchService glDispatchService;
private final PlatformTransactionManager transactionManager;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new Gson();
@POST
public String create(String body) {
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER");
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
? LocalDate.parse(json.get("transferDate").getAsString())
: null;
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull() ? json.get("fromParty").getAsString()
: null;
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() ? json.get("toParty").getAsString() : null;
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
final Long fromTellerId = json.has("fromTellerId") && !json.get("fromTellerId").isJsonNull()
? json.get("fromTellerId").getAsLong()
: null;
final Long toCashierId = json.has("toCashierId") && !json.get("toCashierId").isJsonNull()
? json.get("toCashierId").getAsLong()
: null;
if (fromTellerId == null || toCashierId == null) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.parties.required",
"fromTellerId and toCashierId are required.");
}
final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull()
? json.get("currencyCode").getAsString()
: "INR";
final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray()
? json.getAsJsonArray("denominations")
: new JsonArray();
if (!hasPositiveCount(denominations)) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty",
"At least one denomination with a positive count is required.");
}
final BigDecimal total = sumTotal(denominations);
if (total.compareTo(BigDecimal.ZERO) <= 0) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.empty",
"At least one denomination with a positive count is required.");
}
CashTransfer header = new CashTransfer();
header.setTransferDate(transferDate);
header.setFromParty(fromParty);
header.setToParty(toParty);
header.setFromTellerId(fromTellerId);
header.setToCashierId(toCashierId);
header.setTransferAmount(total);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now());
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
}
} catch (Exception e) {
// resolving the current user must never block recording the transfer
}
// Save header + details in one committed transaction, then GL dispatch separately.
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
// ALLOCATECASHTOCASHIER transaction, which caused null transfer_id constraint errors.
final Long savedId = new TransactionTemplate(transactionManager).execute(status -> {
CashTransfer saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), denominations);
return saved.getId();
});
String glWarning = null;
try {
// Use the cashier's own teller_id from m_cashiers for GL dispatch.
// Fineract's allocateCashToCashier requires cashier and teller to be linked —
// the user-selected fromTellerId is the business vault but may differ from the
// cashier's registered teller_id. Always resolve from DB to avoid GL failures.
Long glTellerId = fromTellerId;
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now();
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : "");
} catch (Exception e) {
glWarning = "Transfer saved but GL entry failed: " + e.getMessage();
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", savedId);
if (glWarning != null) result.addProperty("glWarning", glWarning);
return gson.toJson(result);
}
@POST
@Path("{id}/reverse")
@Transactional
public String reverse(@PathParam("id") Long id, String body) {
this.context.authenticatedUser().validateHasPermissionTo("REVERSE_CASHTRANSFER");
CashTransfer transfer = repository.findById(id)
.orElseThrow(() -> new PlatformDataIntegrityException("error.msg.cashtransfer.not.found",
"Cash transfer not found: " + id));
if (Boolean.TRUE.equals(transfer.getIsReversed())) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.already.reversed",
"Cash transfer " + id + " is already reversed.");
}
String reason = null;
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {}
try {
AppUser user = this.context.authenticatedUser();
transfer.setReversedBy(user.getId());
} catch (Exception ignored) {}
transfer.setIsReversed(true);
transfer.setReversedAt(java.time.LocalDateTime.now());
transfer.setReverseReason(reason);
repository.save(transfer);
if (transfer.getFromTellerId() != null && transfer.getToCashierId() != null) {
try {
Long glTellerId = transfer.getFromTellerId();
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.settleCashFromCashier(glTellerId,
transfer.getToCashierId(), transfer.getTransferAmount(), "INR",
java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
"Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
} catch (Exception ignored) {}
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("reversed", true);
return gson.toJson(result);
}
@GET
public String getAll() {
List<CashTransfer> list = repository.findAllByOrderByCreatedAtDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashTransfer header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash transfer not found: " + id));
Map<String, Object> map = toMap(header);
List<Map<String, Object>> details = detailRepository.findByTransferId(id).stream().map(this::detailToMap)
.collect(Collectors.toList());
map.put("details", details);
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
total = total.add(BigDecimal.valueOf((long) value * count));
}
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
return true;
}
}
return false;
}
private void saveDetails(Long transferId, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) {
continue;
}
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong()
: null;
CashTransferDetail detail = new CashTransferDetail();
detail.setTransferId(transferId);
detail.setDenominationId(denominationId);
detail.setDenominationValue(value);
detail.setCount(count);
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(detail);
}
}
private Map<String, Object> toMap(CashTransfer e) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", e.getId());
map.put("transferDate", e.getTransferDate() != null ? e.getTransferDate().toString() : null);
map.put("fromParty", e.getFromParty());
map.put("toParty", e.getToParty());
map.put("fromTellerId", e.getFromTellerId());
map.put("toCashierId", e.getToCashierId());
map.put("transferAmount", e.getTransferAmount());
map.put("note", e.getNote());
map.put("performedBy", e.getPerformedBy());
map.put("performedByUsername", e.getPerformedByUsername());
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
map.put("reversedBy", e.getReversedBy());
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
map.put("reverseReason", e.getReverseReason());
return map;
}
private Map<String, Object> detailToMap(CashTransferDetail d) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", d.getId());
map.put("denominationId", d.getDenominationId());
map.put("denominationValue", d.getDenominationValue());
map.put("count", d.getCount());
map.put("totalAmount", d.getTotalAmount());
return map;
}
}
@@ -0,0 +1,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashtransfer.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_transfer")
@Getter
@Setter
@NoArgsConstructor
public class CashTransfer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "transfer_date")
private LocalDate transferDate;
@Column(name = "from_party", length = 100)
private String fromParty;
@Column(name = "to_party", length = 100)
private String toParty;
@Column(name = "transfer_amount", nullable = false)
private BigDecimal transferAmount;
@Column(name = "note", length = 500)
private String note;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "from_teller_id")
private Long fromTellerId;
@Column(name = "to_cashier_id")
private Long toCashierId;
@Column(name = "is_reversed")
private Boolean isReversed = false;
@Column(name = "reversed_by")
private Long reversedBy;
@Column(name = "reversed_at")
private java.time.LocalDateTime reversedAt;
@Column(name = "reverse_reason", length = 500)
private String reverseReason;
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashtransfer.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_transfer_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashTransferDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "transfer_id", nullable = false)
private Long transferId;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashtransfer.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashTransferDetailRepository extends JpaRepository<CashTransferDetail, Long> {
List<CashTransferDetail> findByTransferId(Long transferId);
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashtransfer.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashTransferRepository extends JpaRepository<CashTransfer, Long> {
List<CashTransfer> findAllByOrderByCreatedAtDesc();
}
@@ -0,0 +1,232 @@
package org.apache.fineract.portfolio.denominationmaster.api;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
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.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMaster;
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAudit;
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAuditRepository;
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
@Path("/v1/denominationmaster")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class DenominationMasterApiResource {
private final DenominationMasterRepository repository;
private final DenominationMasterAuditRepository auditRepository;
private final PlatformSecurityContext context;
private final Gson gson = new Gson();
@GET
public String getAll() {
List<DenominationMaster> list = repository.findByDeletedFalseOrderByKindDescDenominationDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("active")
public String getActive() {
List<DenominationMaster> list = repository.findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("deleted")
public String getDeleted() {
List<DenominationMaster> list = repository.findByDeletedTrueOrderByKindDescDenominationDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("audit")
public String getAllAudit() {
List<DenominationMasterAudit> list = auditRepository.findAllByOrderByPerformedOnDesc();
return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList()));
}
@GET
@Path("{id}/audit")
public String getAuditForDenomination(@PathParam("id") Long id) {
List<DenominationMasterAudit> list = auditRepository.findByDenominationIdOrderByPerformedOnDesc(id);
return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList()));
}
@POST
public String create(String body) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final int denomination = json.get("denomination").getAsInt();
final String kind = json.get("kind").getAsString();
if (repository.existsByDenominationAndKindAndDeletedFalse(denomination, kind)) {
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate",
"A " + kind + " of denomination " + denomination + " already exists.", "denomination", denomination);
}
DenominationMaster dm = DenominationMaster.create(denomination, kind,
json.has("description") ? json.get("description").getAsString() : null,
json.has("isActive") ? json.get("isActive").getAsBoolean() : true,
json.has("introducedOn") ? LocalDate.parse(json.get("introducedOn").getAsString()) : null, null);
DenominationMaster saved = repository.save(dm);
writeAudit("CREATE", saved, null);
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
return gson.toJson(result);
}
@PUT
@Path("{id}")
public String update(@PathParam("id") Long id, @QueryParam("command") String command, String body) {
DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id));
if ("activate".equals(command)) {
dm.setIsActive(true);
dm.setDiscontinuedOn(null);
repository.save(dm);
writeAudit("ACTIVATE", dm, null);
} else if ("deactivate".equals(command)) {
dm.setIsActive(false);
dm.setDiscontinuedOn(LocalDate.now());
repository.save(dm);
writeAudit("DEACTIVATE", dm, null);
} else if ("restore".equals(command)) {
if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(dm.getDenomination(), dm.getKind(), id)) {
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate.restore",
"Cannot restore: a " + dm.getKind() + " of denomination " + dm.getDenomination() + " already exists.",
"denomination", dm.getDenomination());
}
dm.setDeleted(false);
repository.save(dm);
writeAudit("RESTORE", dm, null);
} else {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final int effectiveDenomination = json.has("denomination") ? json.get("denomination").getAsInt() : dm.getDenomination();
final String effectiveKind = json.has("kind") ? json.get("kind").getAsString() : dm.getKind();
if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(effectiveDenomination, effectiveKind, id)) {
throw new PlatformDataIntegrityException("error.msg.denomination.duplicate",
"A " + effectiveKind + " of denomination " + effectiveDenomination + " already exists.", "denomination",
effectiveDenomination);
}
List<String> changes = new ArrayList<>();
if (json.has("denomination")) {
int newVal = json.get("denomination").getAsInt();
if (!java.util.Objects.equals(dm.getDenomination(), newVal)) {
changes.add("denomination: " + dm.getDenomination() + " -> " + newVal);
}
dm.setDenomination(newVal);
}
if (json.has("kind")) {
String newVal = json.get("kind").getAsString();
if (!java.util.Objects.equals(dm.getKind(), newVal)) {
changes.add("kind: " + dm.getKind() + " -> " + newVal);
}
dm.setKind(newVal);
}
if (json.has("description")) {
String newVal = json.get("description").getAsString();
if (!java.util.Objects.equals(dm.getDescription(), newVal)) {
changes.add("description: " + dm.getDescription() + " -> " + newVal);
}
dm.setDescription(newVal);
}
if (json.has("isActive")) {
boolean newVal = json.get("isActive").getAsBoolean();
if (!java.util.Objects.equals(dm.getIsActive(), newVal)) {
changes.add("isActive: " + dm.getIsActive() + " -> " + newVal);
}
dm.setIsActive(newVal);
}
if (json.has("introducedOn")) {
LocalDate newVal = LocalDate.parse(json.get("introducedOn").getAsString());
if (!java.util.Objects.equals(dm.getIntroducedOn(), newVal)) {
changes.add("introducedOn: " + dm.getIntroducedOn() + " -> " + newVal);
}
dm.setIntroducedOn(newVal);
}
repository.save(dm);
writeAudit("UPDATE", dm, changes.isEmpty() ? null : String.join("; ", changes));
}
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
return gson.toJson(result);
}
@DELETE
@Path("{id}")
public String delete(@PathParam("id") Long id) {
DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id));
// soft delete: flag the row instead of removing it, so it can be restored and history is preserved
dm.setDeleted(true);
dm.setIsActive(false);
repository.save(dm);
writeAudit("DELETE", dm, null);
JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
return gson.toJson(result);
}
private void writeAudit(final String action, final DenominationMaster dm, final String changes) {
Long userId = null;
String username = null;
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
userId = user.getId();
username = user.getUsername();
}
} catch (Exception e) {
// audit should never block the main operation if the user cannot be resolved
}
auditRepository.save(DenominationMasterAudit.of(action, dm, changes, userId, username));
}
private java.util.Map<String, Object> toMap(DenominationMaster dm) {
java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
map.put("id", dm.getId());
map.put("denomination", dm.getDenomination());
map.put("kind", dm.getKind());
map.put("description", dm.getDescription());
map.put("isActive", dm.getIsActive());
map.put("introducedOn", dm.getIntroducedOn() != null ? dm.getIntroducedOn().toString() : null);
map.put("discontinuedOn", dm.getDiscontinuedOn() != null ? dm.getDiscontinuedOn().toString() : null);
map.put("deleted", dm.getDeleted());
return map;
}
private java.util.Map<String, Object> auditToMap(DenominationMasterAudit a) {
java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
map.put("id", a.getId());
map.put("denominationId", a.getDenominationId());
map.put("action", a.getAction());
map.put("denomination", a.getDenomination());
map.put("kind", a.getKind());
map.put("description", a.getDescription());
map.put("isActive", a.getIsActive());
map.put("introducedOn", a.getIntroducedOn() != null ? a.getIntroducedOn().toString() : null);
map.put("discontinuedOn", a.getDiscontinuedOn() != null ? a.getDiscontinuedOn().toString() : null);
map.put("changes", a.getChanges());
map.put("performedBy", a.getPerformedBy());
map.put("performedByUsername", a.getPerformedByUsername());
map.put("performedOn", a.getPerformedOn() != null ? a.getPerformedOn().toString() : null);
return map;
}
}
@@ -0,0 +1,62 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_denomination_master")
@Getter
@Setter
@NoArgsConstructor
public class DenominationMaster {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "denomination", nullable = false)
private Integer denomination;
@Column(name = "kind", nullable = false, length = 10)
private String kind;
@Column(name = "description", length = 100)
private String description;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "introduced_on")
private LocalDate introducedOn;
@Column(name = "discontinued_on")
private LocalDate discontinuedOn;
@Column(name = "deleted", nullable = false)
private Boolean deleted = false;
public static DenominationMaster create(Integer denomination, String kind, String description,
Boolean isActive, LocalDate introducedOn, LocalDate discontinuedOn) {
DenominationMaster dm = new DenominationMaster();
dm.denomination = denomination;
dm.kind = kind;
dm.description = description;
dm.isActive = isActive != null ? isActive : true;
dm.introducedOn = introducedOn;
dm.discontinuedOn = discontinuedOn;
dm.deleted = false;
return dm;
}
}
@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_denomination_master_audit")
@Getter
@Setter
@NoArgsConstructor
public class DenominationMasterAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "action", nullable = false, length = 20)
private String action;
@Column(name = "denomination")
private Integer denomination;
@Column(name = "kind", length = 10)
private String kind;
@Column(name = "description", length = 100)
private String description;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "introduced_on")
private LocalDate introducedOn;
@Column(name = "discontinued_on")
private LocalDate discontinuedOn;
@Column(name = "changes", length = 1000)
private String changes;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "performed_on", nullable = false)
private LocalDateTime performedOn;
public static DenominationMasterAudit of(final String action, final DenominationMaster dm, final String changes,
final Long performedBy, final String performedByUsername) {
final DenominationMasterAudit a = new DenominationMasterAudit();
a.action = action;
a.denominationId = dm.getId();
a.denomination = dm.getDenomination();
a.kind = dm.getKind();
a.description = dm.getDescription();
a.isActive = dm.getIsActive();
a.introducedOn = dm.getIntroducedOn();
a.discontinuedOn = dm.getDiscontinuedOn();
a.changes = changes;
a.performedBy = performedBy;
a.performedByUsername = performedByUsername;
a.performedOn = LocalDateTime.now();
return a;
}
}
@@ -0,0 +1,17 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface DenominationMasterAuditRepository extends JpaRepository<DenominationMasterAudit, Long> {
List<DenominationMasterAudit> findByDenominationIdOrderByPerformedOnDesc(Long denominationId);
List<DenominationMasterAudit> findAllByOrderByPerformedOnDesc();
}
@@ -0,0 +1,15 @@
package org.apache.fineract.portfolio.denominationmaster.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface DenominationMasterRepository extends JpaRepository<DenominationMaster, Long> {
List<DenominationMaster> findByDeletedFalseOrderByKindDescDenominationDesc();
List<DenominationMaster> findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc();
List<DenominationMaster> findByDeletedTrueOrderByKindDescDenominationDesc();
boolean existsByDenominationAndKindAndDeletedFalse(Integer denomination, String kind);
boolean existsByDenominationAndKindAndDeletedFalseAndIdNot(Integer denomination, String kind, Long id);
}
@@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.denominationmaster.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_savings_txn_denomination")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SavingsTxnDenomination {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "txn_id", nullable = false)
private Long txnId;
@Column(name = "savings_account_id", nullable = false)
private Long savingsAccountId;
@Column(name = "account_no", length = 50)
private String accountNo;
@Column(name = "client_id")
private Long clientId;
@Column(name = "cif_no", length = 50)
private String cifNo;
@Column(name = "denomination_id", nullable = false)
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal totalAmount;
@Column(name = "txn_date")
private LocalDate txnDate;
@Column(name = "created_at")
private LocalDateTime createdAt;
/** "DEPOSIT" or "WITHDRAWAL" — identifies the direction of the cash flow. */
@Column(name = "txn_type", nullable = false, length = 20)
private String txnType;
/** ID of the logged-in teller / officer who performed the transaction. */
@Column(name = "officer_id")
private Long officerId;
/** Username of the teller / officer who performed the transaction. */
@Column(name = "officer_username", length = 100)
private String officerUsername;
}
@@ -0,0 +1,15 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SavingsTxnDenominationRepository extends JpaRepository<SavingsTxnDenomination, Long> {
List<SavingsTxnDenomination> findByTxnId(Long txnId);
List<SavingsTxnDenomination> findBySavingsAccountId(Long savingsAccountId);
}
@@ -0,0 +1,349 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* Validates and persists the per-transaction cash denomination breakdown sent by the UI
* on both deposits and withdrawals.
*/
@Service
@RequiredArgsConstructor
public class DenominationBreakdownWriter {
private final SavingsTxnDenominationRepository repository;
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
// ── Public API ────────────────────────────────────────────────────────────
/**
* Full validation of the denomination breakdown:
* <ol>
* <li>Each item must have a non-null {@code denominationId}.</li>
* <li>Each item's {@code count} must be > 0.</li>
* <li>Each item's {@code denominationValue} (or {@code denomination}) must be > 0.</li>
* <li>The grand total (sum of value × count) must equal {@code expectedAmount}.</li>
* </ol>
* No-op when no breakdown is provided.
*
* @param command the JSON command containing the denominationBreakdown array
* @param expectedAmount the transaction amount that the breakdown must sum to
* @param txnType "DEPOSIT" or "WITHDRAWAL" — used in error messages
*/
public void validateBreakdown(final JsonCommand command, final BigDecimal expectedAmount, final String txnType) {
if (command == null || !command.parameterExists("denominationBreakdown")) {
return;
}
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (breakdown == null || breakdown.size() == 0) {
return;
}
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("denominationBreakdown");
BigDecimal computedTotal = BigDecimal.ZERO;
final String label = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "TRANSACTION";
for (int i = 0; i < breakdown.size(); i++) {
final JsonObject obj = breakdown.get(i).getAsJsonObject();
final String itemPrefix = "denominationBreakdown[" + i + "]";
// 1. denominationId is required
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull()) {
validator.reset().parameter(itemPrefix + ".denominationId")
.value(null)
.failWithCode("required", "denominationId is required for each denomination item.");
continue; // cannot compute further without ID
}
final long denominationId = obj.get("denominationId").getAsLong();
if (denominationId <= 0) {
validator.reset().parameter(itemPrefix + ".denominationId")
.value(denominationId)
.failWithCode("invalid", "denominationId must be a positive number.");
}
// 2. count is required and must be > 0
if (!obj.has("count") || obj.get("count").isJsonNull()) {
validator.reset().parameter(itemPrefix + ".count")
.value(null)
.failWithCode("required", "count is required for each denomination item.");
continue;
}
final int count = obj.get("count").getAsInt();
if (count <= 0) {
// count = 0 is silently skipped (UI may send zero for unselected rows)
continue;
}
// 3. denominationValue (or denomination) must be present and > 0
int denominationValue = 0;
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
denominationValue = obj.get("denominationValue").getAsInt();
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
denominationValue = obj.get("denomination").getAsInt();
}
if (denominationValue <= 0) {
validator.reset().parameter(itemPrefix + ".denominationValue")
.value(denominationValue)
.failWithCode("invalid",
"denominationValue must be greater than zero for denomination item " + i + ".");
continue;
}
// 4. Accumulate total: value × count
computedTotal = computedTotal.add(BigDecimal.valueOf((long) denominationValue * count));
}
// Throw item-level validation errors first
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Denomination breakdown validation errors exist.", errors);
}
// 5. Grand total must match the transaction amount
if (expectedAmount != null && computedTotal.compareTo(expectedAmount) != 0) {
throw new PlatformDataIntegrityException(
"error.msg.denomination.total.mismatch",
label + " denomination breakdown total (" + computedTotal
+ ") does not match the transaction amount (" + expectedAmount
+ "). Please re-check the note/coin counts.");
}
}
/**
* Convenience wrapper kept for backward-compatibility with any callers that do not
* yet pass a {@code txnType}. Delegates to {@link #validateBreakdown}.
*/
public void validateTotal(final JsonCommand command, final BigDecimal expectedAmount) {
validateBreakdown(command, expectedAmount, "TRANSACTION");
}
/**
* Validates that the cashier currently holds enough of each denomination
* in the withdrawal breakdown. Auto-detects the cashier from the authenticated
* user. If the user is not linked to a cashier (e.g. admin/back-office), the
* check is skipped silently.
*
* @param command the JSON command containing the denominationBreakdown array
*/
public void validateWithdrawalStock(final JsonCommand command) {
if (command == null || !command.parameterExists("denominationBreakdown")) return;
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (breakdown == null || breakdown.size() == 0) return;
Long cashierId = null;
try {
final AppUser user = context.authenticatedUser();
cashierId = jdbcTemplate.queryForObject(
"SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id "
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
Long.class, user.getId());
} catch (Exception e) {
return; // not a cashier user — skip stock check
}
if (cashierId == null) return;
final Map<Long, Long> stock = getCashierNetPosition(cashierId);
final List<String> shortages = new ArrayList<>();
for (final JsonElement el : breakdown) {
final JsonObject item = el.getAsJsonObject();
final int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) continue;
if (!item.has("denominationId") || item.get("denominationId").isJsonNull()) continue;
final long denomId = item.get("denominationId").getAsLong();
final long available = stock.getOrDefault(denomId, 0L);
if (count > available) {
final int denomValue = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt() : 0;
shortages.add("₹" + denomValue + " (need " + count + ", available " + available + ")");
}
}
if (!shortages.isEmpty()) {
throw new PlatformDataIntegrityException(
"error.msg.cashier.denomination.insufficient",
"Insufficient denomination stock for withdrawal: " + String.join("; ", shortages));
}
}
/**
* Computes the cashier's current net denomination-wise stock by summing all IN
* transactions and subtracting all OUT transactions for that cashier.
*
* @param cashierId the cashier whose position to compute
* @return map of denominationId → net count available
*/
public Map<Long, Long> getCashierNetPosition(final Long cashierId) {
final String sql = "SELECT denomination_id, SUM(net_count) AS available FROM ("
+ " SELECT ctd.denomination_id, CAST(SUM(ctd.count) AS BIGINT) AS net_count"
+ " FROM m_cash_transfer_detail ctd JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id"
+ " WHERE ct.to_cashier_id = ? AND (ct.is_reversed IS NULL OR ct.is_reversed = false)"
+ " GROUP BY ctd.denomination_id"
+ " UNION ALL"
+ " SELECT icd.denomination_id, CAST(SUM(icd.count) AS BIGINT)"
+ " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
+ " WHERE ic.to_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)"
+ " GROUP BY icd.denomination_id"
+ " UNION ALL"
+ " SELECT std.denomination_id, CAST(SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'DEPOSIT'"
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
+ " UNION ALL"
+ " SELECT ced.denomination_id, CAST(SUM(ced.count) AS BIGINT)"
+ " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id"
+ " WHERE ce.cashier_id = ? AND ced.direction = 'RECEIVED'"
+ " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)"
+ " GROUP BY ced.denomination_id"
+ " UNION ALL"
+ " SELECT csd.denomination_id, CAST(-SUM(csd.count) AS BIGINT)"
+ " FROM m_cash_settlement_detail csd JOIN m_cash_settlement cs ON cs.id = csd.settlement_id"
+ " WHERE cs.from_cashier_id = ? AND (cs.is_reversed IS NULL OR cs.is_reversed = false)"
+ " GROUP BY csd.denomination_id"
+ " UNION ALL"
+ " SELECT icd.denomination_id, CAST(-SUM(icd.count) AS BIGINT)"
+ " FROM m_cash_inter_cashier_detail icd JOIN m_cash_inter_cashier ic ON ic.id = icd.inter_cashier_id"
+ " WHERE ic.from_cashier_id = ? AND (ic.is_reversed IS NULL OR ic.is_reversed = false)"
+ " GROUP BY icd.denomination_id"
+ " UNION ALL"
+ " SELECT std.denomination_id, CAST(-SUM(std.count) AS BIGINT)"
+ " FROM m_savings_txn_denomination std"
+ " WHERE std.txn_type = 'WITHDRAWAL'"
+ " AND std.officer_id IN (SELECT au.id FROM m_appuser au"
+ " JOIN m_cashiers mc ON mc.staff_id = au.staff_id WHERE mc.id = ?)"
+ " GROUP BY std.denomination_id"
+ " UNION ALL"
+ " SELECT ced.denomination_id, CAST(-SUM(ced.count) AS BIGINT)"
+ " FROM m_cash_exchange_detail ced JOIN m_cash_exchange ce ON ce.id = ced.exchange_id"
+ " WHERE ce.cashier_id = ? AND ced.direction = 'GIVEN'"
+ " AND (ce.is_reversed IS NULL OR ce.is_reversed = false)"
+ " GROUP BY ced.denomination_id"
+ ") t GROUP BY denomination_id";
final Map<Long, Long> stock = new HashMap<>();
jdbcTemplate.query(sql, rs -> {
stock.put(rs.getLong("denomination_id"), rs.getLong("available"));
}, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId, cashierId);
return stock;
}
/**
* Persist each valid denomination row for the given transaction.
* <p>
* Items with {@code count <= 0} are silently skipped (UI sends zero for
* unselected rows). Items missing {@code denominationId} are also skipped
* (they should have already been rejected by {@link #validateBreakdown}).
* <p>
* The logged-in officer (teller) is automatically resolved from the
* security context; if resolution fails (e.g. batch job) the officer fields
* are left {@code null}.
*
* @param command the JSON command containing the denominationBreakdown array
* @param txnId ID of the saved savings / current / RD / FD transaction
* @param savingsAccountId savings / current / RD / FD account ID
* @param accountNo account number string
* @param clientId client ID (may be null for group accounts)
* @param cifNo CIF number (may be null)
* @param txnDate transaction date
* @param txnType "DEPOSIT" or "WITHDRAWAL"
*/
public void save(final JsonCommand command, final Long txnId, final Long savingsAccountId, final String accountNo,
final Long clientId, final String cifNo, final LocalDate txnDate, final String txnType) {
if (command == null || txnId == null || !command.parameterExists("denominationBreakdown")) {
return;
}
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (breakdown == null || breakdown.size() == 0) {
return;
}
// Resolve the officer / teller from the security context
Long officerId = null;
String officerUsername = null;
try {
final AppUser user = this.context.authenticatedUser();
if (user != null) {
officerId = user.getId();
officerUsername = user.getUsername();
}
} catch (final Exception ignored) {
// If user cannot be resolved (e.g. batch / async job), leave officer fields null
}
final String resolvedTxnType = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "DEPOSIT";
for (final JsonElement entry : breakdown) {
final JsonObject obj = entry.getAsJsonObject();
// denominationId is mandatory (NOT NULL in DB)
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull() || !obj.has("count")) {
continue;
}
final int count = obj.get("count").getAsInt();
if (count <= 0) {
continue; // zero-count rows sent by UI are silently ignored
}
final long denominationId = obj.get("denominationId").getAsLong();
// Resolve denomination face value
int denominationValue = 0;
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
denominationValue = obj.get("denominationValue").getAsInt();
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
denominationValue = obj.get("denomination").getAsInt();
}
final BigDecimal lineTotal = BigDecimal.valueOf((long) denominationValue * count);
final SavingsTxnDenomination row = SavingsTxnDenomination.builder()
.txnId(txnId)
.savingsAccountId(savingsAccountId)
.accountNo(accountNo)
.clientId(clientId)
.cifNo(cifNo)
.denominationId(denominationId)
.denominationValue(denominationValue)
.count(count)
.totalAmount(lineTotal)
.txnDate(txnDate)
.createdAt(LocalDateTime.now())
.txnType(resolvedTxnType)
.officerId(officerId)
.officerUsername(officerUsername)
.build();
this.repository.save(row);
}
}
}
@@ -1,257 +1,257 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
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.api.JsonCommand;
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.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DepositAccountTransactionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName));
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
@Autowired
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateDepositAmountUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateActivation(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
if (onAccountClosureId != null) {
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
if (accountOnClosureType.isTransferToSavings()) {
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
}
}
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
//
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
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.api.JsonCommand;
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.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DepositAccountTransactionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown"));
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
@Autowired
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateDepositAmountUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateActivation(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
if (onAccountClosureId != null) {
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
if (accountOnClosureType.isTransferToSavings()) {
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
}
}
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
//
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -31,6 +31,8 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.deposit
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.expectedFirstDepositOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.mandatoryRecommendedDepositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
@@ -230,6 +232,16 @@ public class DepositAccountAssembler {
interestRate = product.nominalAnnualInterestRate();
}
// Senior citizen interest rate override: use the higher rate when applicable
if (command.parameterExists(isSeniorCitizenParamName) && command.booleanPrimitiveValueOfParameterNamed(isSeniorCitizenParamName)) {
if (command.parameterExists(seniorCitizenInterestRateParamName)) {
final BigDecimal scRate = command.bigDecimalValueOfParameterNamed(seniorCitizenInterestRateParamName);
if (scRate != null && scRate.compareTo(BigDecimal.ZERO) > 0) {
interestRate = scRate;
}
}
}
SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null;
final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName);
if (interestPeriodTypeValue != null) {
@@ -1,75 +1,75 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_fd_base_interest_rate_effective")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "fd_type", length = 20)
private String fdType;
@Column(name = "version_label")
private String versionLabel;
@Column(name = "effective_from_date")
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "remarks")
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks,
Long createdById) {
FdBaseInterestRateEffective e = new FdBaseInterestRateEffective();
e.fdType = fdType;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_cbs_base_interest_rate_effective")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "fd_type", length = 20)
private String fdType;
@Column(name = "version_label")
private String versionLabel;
@Column(name = "effective_from_date")
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "remarks")
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks,
Long createdById) {
FdBaseInterestRateEffective e = new FdBaseInterestRateEffective();
e.fdType = fdType;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
@@ -1,76 +1,76 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_fd_base_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id")
private Long effectiveId;
@Column(name = "sr_no")
private Integer srNo;
@Column(name = "description")
private String description;
@Column(name = "period_type_enum")
private Integer periodTypeEnum;
@Column(name = "from_period")
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate;
@Column(name = "senior_citizen_rate")
private BigDecimal seniorCitizenRate;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) {
FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
s.seniorCitizenRate = seniorCitizenRate;
s.createdDate = LocalDate.now();
return s;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_cbs_base_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id")
private Long effectiveId;
@Column(name = "sr_no")
private Integer srNo;
@Column(name = "description")
private String description;
@Column(name = "period_type_enum")
private Integer periodTypeEnum;
@Column(name = "from_period")
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate;
@Column(name = "senior_citizen_rate")
private BigDecimal seniorCitizenRate;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) {
FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
s.seniorCitizenRate = seniorCitizenRate;
s.createdDate = LocalDate.now();
return s;
}
}
@@ -1,333 +1,356 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService;
this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService;
}
@Transactional
@Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
}
final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.save(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
}
@Transactional
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForCreditBlock();
// Global configurations
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
}
boolean isInterestTransfer = false;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
saveTransactionToGenerateTransactionId(deposit);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit;
}
@Transactional
@Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
Boolean lienAllowed) {
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
Money.of(account.getCurrency(), amount), lienAllowed);
}
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
final PaymentDetail paymentDetail = null;
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
return transaction.getId();
}
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
}
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
}
@Transactional
@Override
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
final boolean isAccountTransfer = false;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
@Override
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
SavingsAccountTransaction reversal = null;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.undoTransaction(savingsAccountTransaction);
if (postReversals) {
newTransactions.add(reversal);
}
}
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
if (savingsAccountTransaction.isPostInterestCalculationRequired()
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, "savingsaccount");
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
account.activateAccountBasedOnBalance();
}
this.savingsAccountRepository.save(account);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
this.savingsAccountTransactionRepository.saveAll(newTransactions);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
return reversal;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService;
this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService;
}
@Transactional
@Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
}
final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.save(account);
if (!transactionBooleanValues.isSkipAccountingPostings()) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
}
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
}
@Transactional
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
}
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
final boolean skipAccountingPostings) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
final boolean skipAccountingPostings) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForCreditBlock();
// Global configurations
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
}
boolean isInterestTransfer = false;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
saveTransactionToGenerateTransactionId(deposit);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.saveAndFlush(account);
if (!skipAccountingPostings) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit;
}
@Transactional
@Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
Boolean lienAllowed) {
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
Money.of(account.getCurrency(), amount), lienAllowed);
}
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
final PaymentDetail paymentDetail = null;
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
return transaction.getId();
}
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
}
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
}
@Transactional
@Override
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
final boolean isAccountTransfer = false;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
@Override
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
SavingsAccountTransaction reversal = null;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.undoTransaction(savingsAccountTransaction);
if (postReversals) {
newTransactions.add(reversal);
}
}
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
if (savingsAccountTransaction.isPostInterestCalculationRequired()
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, "savingsaccount");
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
account.activateAccountBasedOnBalance();
}
this.savingsAccountRepository.save(account);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
this.savingsAccountTransactionRepository.saveAll(newTransactions);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
return reversal;
}
}
@@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotF
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
private final CurrentProductRepository currentProductRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final JournalEntryRepository journalEntryRepository;
private final DenominationBreakdownWriter denominationBreakdownWriter;
@Transactional
@Override
@@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "deposit");
this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT");
final String refNo = command.stringValueOfParameterNamed("accountNumber");
@@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
currentAccountTransactionRepository.saveAndFlush(txn);
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
account.getClient() != null ? account.getClient().getId() : null,
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
"DEPOSIT");
account.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
currentAccountRepository.save(account);
@@ -135,6 +143,14 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
currentAccountRepository.save(account);
// Validate denomination stock availability then validate totals before saving breakdown
this.denominationBreakdownWriter.validateWithdrawalStock(command);
this.denominationBreakdownWriter.validateBreakdown(command, amount, "WITHDRAWAL");
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
account.getClient() != null ? account.getClient().getId() : null,
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
"WITHDRAWAL");
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl;
@@ -107,6 +108,7 @@ import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalc
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
@@ -267,7 +269,8 @@ public class SavingsConfiguration {
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
WorkingDaysRepositoryWrapper workingDaysRepository,
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
DenominationBreakdownWriter denominationBreakdownWriter
) {
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
@@ -276,7 +279,8 @@ public class SavingsConfiguration {
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository);
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
denominationBreakdownWriter);
}
@Bean
@@ -424,7 +428,8 @@ public class SavingsConfiguration {
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) {
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
DenominationBreakdownWriter denominationBreakdownWriter) {
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
@@ -432,7 +437,7 @@ public class SavingsConfiguration {
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
errorHandler, facilityChargeAutoApplyService);
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
}
@Bean
@@ -0,0 +1,57 @@
package org.apache.fineract.portfolio.teller.service;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class GlDispatchService {
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final Gson gson = new Gson();
/**
* Vault → Cashier GL entry. Runs in its own transaction so a failure
* never rolls back the caller's (already-committed) transfer record.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void allocateCashToCashier(Long tellerId, Long cashierId,
BigDecimal amount, String currencyCode, String txnDate, String notes) {
JsonObject body = new JsonObject();
body.addProperty("locale", "en");
body.addProperty("dateFormat", "yyyy-MM-dd");
body.addProperty("txnDate", txnDate);
body.addProperty("txnAmount", amount);
body.addProperty("currencyCode", currencyCode);
body.addProperty("txnNote", notes != null ? notes : "");
CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body))
.allocateCashToCashier(tellerId, cashierId).build();
commandsSourceWritePlatformService.logCommandSource(wrapper);
}
/**
* Cashier → Vault GL entry (settlement / transfer reversal).
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void settleCashFromCashier(Long tellerId, Long cashierId,
BigDecimal amount, String currencyCode, String txnDate, String notes) {
JsonObject body = new JsonObject();
body.addProperty("locale", "en");
body.addProperty("dateFormat", "yyyy-MM-dd");
body.addProperty("txnDate", txnDate);
body.addProperty("txnAmount", amount);
body.addProperty("currencyCode", currencyCode);
body.addProperty("txnNote", notes != null ? notes : "");
CommandWrapper wrapper = new CommandWrapperBuilder().withJson(gson.toJson(body))
.settleCashFromCashier(tellerId, cashierId).build();
commandsSourceWritePlatformService.logCommandSource(wrapper);
}
}
@@ -316,4 +316,15 @@
<include file="parts/0297_fix_unique_nominee_per_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0299_seed_indian_states.xml" relativeToChangelogFile="true"/>
<include file="parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml" relativeToChangelogFile="true"/>
<include file="parts/0301_add_current_account_to_transfer_details.xml" relativeToChangelogFile="true"/>
<include file="parts/0012_denomination_master.xml" relativeToChangelogFile="true"/>
<include file="parts/0013_cash_exchange.xml" relativeToChangelogFile="true"/>
<include file="parts/0014_cash_transfer.xml" relativeToChangelogFile="true"/>
<include file="parts/0015_cash_transfer_add_ids.xml" relativeToChangelogFile="true"/>
<include file="parts/0016_cash_settlement.xml" relativeToChangelogFile="true"/>
<include file="parts/0017_cash_inter_cashier.xml" relativeToChangelogFile="true"/>
<include file="parts/0018_cash_reconciliation.xml" relativeToChangelogFile="true"/>
<include file="parts/0019_cash_reversal_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0020_cash_permissions.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="denomination_master_table_001" author="CBS">
<createTable tableName="m_denomination_master">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="denomination" type="INT">
<constraints nullable="false"/>
</column>
<column name="kind" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(100)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="introduced_on" type="DATE"/>
<column name="discontinued_on" type="DATE"/>
</createTable>
</changeSet>
<changeSet id="denomination_master_initial_data_001" author="CBS">
<insert tableName="m_denomination_master">
<column name="denomination" value="2000"/>
<column name="kind" value="note"/>
<column name="description" value="2000 Rupee Note"/>
<column name="is_active" valueBoolean="false"/>
<column name="introduced_on" value="2016-11-08"/>
<column name="discontinued_on" value="2023-09-30"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="500"/>
<column name="kind" value="note"/>
<column name="description" value="500 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2016-11-08"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="200"/>
<column name="kind" value="note"/>
<column name="description" value="200 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2017-08-25"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="100"/>
<column name="kind" value="note"/>
<column name="description" value="100 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2018-07-19"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="50"/>
<column name="kind" value="note"/>
<column name="description" value="50 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2017-08-25"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="20"/>
<column name="kind" value="note"/>
<column name="description" value="20 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2019-08-26"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="10"/>
<column name="kind" value="note"/>
<column name="description" value="10 Rupee Note"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2018-01-01"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="20"/>
<column name="kind" value="coin"/>
<column name="description" value="20 Rupee Coin"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2019-01-01"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="10"/>
<column name="kind" value="coin"/>
<column name="description" value="10 Rupee Coin"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2009-01-01"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="5"/>
<column name="kind" value="coin"/>
<column name="description" value="5 Rupee Coin"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2009-01-01"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="2"/>
<column name="kind" value="coin"/>
<column name="description" value="2 Rupee Coin"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2009-01-01"/>
</insert>
<insert tableName="m_denomination_master">
<column name="denomination" value="1"/>
<column name="kind" value="coin"/>
<column name="description" value="1 Rupee Coin"/>
<column name="is_active" valueBoolean="true"/>
<column name="introduced_on" value="2009-01-01"/>
</insert>
</changeSet>
<changeSet id="savings_txn_denomination_table_001" author="CBS">
<createTable tableName="m_savings_txn_denomination">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="txn_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="savings_account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="account_no" type="VARCHAR(50)"/>
<column name="client_id" type="BIGINT"/>
<column name="cif_no" type="VARCHAR(50)"/>
<column name="denomination_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="txn_date" type="DATE"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
</changeSet>
<changeSet id="denomination_master_audit_table_001" author="CBS">
<createTable tableName="m_denomination_master_audit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="action" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="denomination" type="INT"/>
<column name="kind" type="VARCHAR(10)"/>
<column name="description" type="VARCHAR(100)"/>
<column name="is_active" type="BOOLEAN"/>
<column name="introduced_on" type="DATE"/>
<column name="discontinued_on" type="DATE"/>
<column name="changes" type="VARCHAR(1000)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="performed_on" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_denomination_master_audit" indexName="idx_denom_audit_denom_id">
<column name="denomination_id"/>
</createIndex>
</changeSet>
<changeSet id="denomination_master_soft_delete_001" author="CBS">
<addColumn tableName="m_denomination_master">
<column name="deleted" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- =====================================================================
Migration: Add txn_type, officer_id, officer_username to
m_savings_txn_denomination.
txn_type distinguishes DEPOSIT from WITHDRAWAL.
officer_id / officer_username record who performed the transaction.
===================================================================== -->
<changeSet id="savings_txn_denomination_add_txn_type_officer_001" author="CBS">
<addColumn tableName="m_savings_txn_denomination">
<column name="txn_type" type="VARCHAR(20)" defaultValue="DEPOSIT">
<constraints nullable="false"/>
</column>
<column name="officer_id" type="BIGINT"/>
<column name="officer_username" type="VARCHAR(100)"/>
</addColumn>
<createIndex tableName="m_savings_txn_denomination"
indexName="idx_savings_txn_denom_txntype">
<column name="txn_type"/>
</createIndex>
<createIndex tableName="m_savings_txn_denomination"
indexName="idx_savings_txn_denom_officer">
<column name="officer_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_exchange_table_001" author="CBS">
<createTable tableName="m_cash_exchange">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="account_id" type="BIGINT"/>
<column name="account_no" type="VARCHAR(50)"/>
<column name="client_id" type="BIGINT"/>
<column name="exchange_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="exchange_date" type="DATE"/>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
</changeSet>
<changeSet id="cash_exchange_detail_table_001" author="CBS">
<createTable tableName="m_cash_exchange_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="exchange_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_cash_exchange_detail" indexName="idx_cash_exchange_detail_eid">
<column name="exchange_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_transfer_table_001" author="CBS">
<createTable tableName="m_cash_transfer">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="transfer_date" type="DATE"/>
<column name="from_party" type="VARCHAR(100)"/>
<column name="to_party" type="VARCHAR(100)"/>
<column name="transfer_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
</changeSet>
<changeSet id="cash_transfer_detail_table_001" author="CBS">
<createTable tableName="m_cash_transfer_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="transfer_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_cash_transfer_detail" indexName="idx_cash_transfer_detail_tid">
<column name="transfer_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_transfer_add_party_ids_001" author="CBS">
<addColumn tableName="m_cash_transfer">
<column name="from_teller_id" type="BIGINT"/>
<column name="to_cashier_id" type="BIGINT"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_settlement_table_001" author="CBS">
<createTable tableName="m_cash_settlement">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="settlement_date" type="DATE"/>
<column name="from_cashier_id" type="BIGINT"/>
<column name="to_teller_id" type="BIGINT"/>
<column name="from_party" type="VARCHAR(100)"/>
<column name="to_party" type="VARCHAR(100)"/>
<column name="settlement_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="reversed_by" type="BIGINT"/>
<column name="reversed_at" type="TIMESTAMP"/>
<column name="reverse_reason" type="VARCHAR(500)"/>
</createTable>
</changeSet>
<changeSet id="cash_settlement_detail_table_001" author="CBS">
<createTable tableName="m_cash_settlement_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="settlement_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_cash_settlement_detail" indexName="idx_cash_settlement_detail_sid">
<column name="settlement_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_inter_cashier_table_001" author="CBS">
<createTable tableName="m_cash_inter_cashier">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="transfer_date" type="DATE"/>
<column name="teller_id" type="BIGINT"/>
<column name="from_cashier_id" type="BIGINT"/>
<column name="to_cashier_id" type="BIGINT"/>
<column name="from_party" type="VARCHAR(100)"/>
<column name="to_party" type="VARCHAR(100)"/>
<column name="transfer_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="reversed_by" type="BIGINT"/>
<column name="reversed_at" type="TIMESTAMP"/>
<column name="reverse_reason" type="VARCHAR(500)"/>
</createTable>
</changeSet>
<changeSet id="cash_inter_cashier_detail_table_001" author="CBS">
<createTable tableName="m_cash_inter_cashier_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="inter_cashier_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_cash_inter_cashier_detail" indexName="idx_cash_inter_cashier_detail_pid">
<column name="inter_cashier_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_reconciliation_table_001" author="CBS">
<createTable tableName="m_cash_reconciliation">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="recon_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="cashier_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="cashier_name" type="VARCHAR(150)"/>
<column name="teller_id" type="BIGINT"/>
<column name="expected_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
<column name="physical_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
<column name="variance" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
<column name="status" type="VARCHAR(10)" defaultValue="OK"/>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
<createIndex tableName="m_cash_reconciliation" indexName="idx_cash_recon_cashier_date">
<column name="cashier_id"/>
<column name="recon_date"/>
</createIndex>
</changeSet>
<changeSet id="cash_reconciliation_detail_table_001" author="CBS">
<createTable tableName="m_cash_reconciliation_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="reconciliation_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="expected_count" type="INT" defaultValueNumeric="0"/>
<column name="physical_count" type="INT" defaultValueNumeric="0"/>
<column name="variance_count" type="INT" defaultValueNumeric="0"/>
<column name="variance_amount" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
</createTable>
<createIndex tableName="m_cash_reconciliation_detail" indexName="idx_cash_recon_detail_rid">
<column name="reconciliation_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_transfer_reversal_cols_001" author="CBS">
<addColumn tableName="m_cash_transfer">
<column name="is_reversed" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="reversed_by" type="BIGINT"/>
<column name="reversed_at" type="TIMESTAMP"/>
<column name="reverse_reason" type="VARCHAR(500)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_module_permissions_001" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'CREATE_CASHTRANSFER', 'CASHTRANSFER', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'READ_CASHTRANSFER', 'CASHTRANSFER', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'REVERSE_CASHTRANSFER', 'CASHTRANSFER', 'REVERSE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'CREATE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHSETTLEMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'READ_CASHSETTLEMENT', 'CASHSETTLEMENT', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHSETTLEMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'REVERSE_CASHSETTLEMENT', 'CASHSETTLEMENT', 'REVERSE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHSETTLEMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'CREATE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHINTERCASHIER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'READ_CASHINTERCASHIER', 'CASHINTERCASHIER', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHINTERCASHIER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'REVERSE_CASHINTERCASHIER', 'CASHINTERCASHIER', 'REVERSE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVERSE_CASHINTERCASHIER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'CREATE_CASHRECONCILIATION', 'CASHRECONCILIATION', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CASHRECONCILIATION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'cashier', 'READ_CASHRECONCILIATION', 'CASHRECONCILIATION', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CASHRECONCILIATION');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0300-1" author="cbs">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_fd_base_interest_rate_effective"/>
</preConditions>
<renameTable oldTableName="m_fd_base_interest_rate_effective"
newTableName="m_cbs_base_interest_rate_effective"/>
</changeSet>
<changeSet id="0300-2" author="cbs">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_fd_base_interest_rate_slab_history"/>
</preConditions>
<renameTable oldTableName="m_fd_base_interest_rate_slab_history"
newTableName="m_cbs_base_interest_rate_slab_history"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Add from_current_account_id column to m_account_transfer_details -->
<changeSet id="0301-1" author="cbs">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_account_transfer_details" columnName="from_current_account_id"/>
</not>
</preConditions>
<addColumn tableName="m_account_transfer_details">
<column name="from_current_account_id" type="BIGINT">
<constraints nullable="true"
foreignKeyName="FK_atd_from_current_account"
references="m_current_account(id)"/>
</column>
</addColumn>
</changeSet>
<!-- Add to_current_account_id column to m_account_transfer_details -->
<changeSet id="0301-2" author="cbs">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_account_transfer_details" columnName="to_current_account_id"/>
</not>
</preConditions>
<addColumn tableName="m_account_transfer_details">
<column name="to_current_account_id" type="BIGINT">
<constraints nullable="true"
foreignKeyName="FK_atd_to_current_account"
references="m_current_account(id)"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -1,59 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings;
public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer;
private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck;
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this.isAccountTransfer = isAccountTransfer;
this.isRegularTransaction = isRegularTransaction;
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
}
public boolean isAccountTransfer() {
return this.isAccountTransfer;
}
public boolean isRegularTransaction() {
return this.isRegularTransaction;
}
public boolean isApplyWithdrawFee() {
return this.isApplyWithdrawFee;
}
public boolean isInterestTransfer() {
return this.isInterestTransfer;
}
public boolean isExceptionForBalanceCheck() {
return this.isExceptionForBalanceCheck;
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings;
public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer;
private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck;
private final boolean skipAccountingPostings;
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false);
}
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck,
final boolean skipAccountingPostings) {
this.isAccountTransfer = isAccountTransfer;
this.isRegularTransaction = isRegularTransaction;
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
this.skipAccountingPostings = skipAccountingPostings;
}
public boolean isAccountTransfer() {
return this.isAccountTransfer;
}
public boolean isRegularTransaction() {
return this.isRegularTransaction;
}
public boolean isApplyWithdrawFee() {
return this.isApplyWithdrawFee;
}
public boolean isInterestTransfer() {
return this.isInterestTransfer;
}
public boolean isExceptionForBalanceCheck() {
return this.isExceptionForBalanceCheck;
}
public boolean isSkipAccountingPostings() {
return this.skipAccountingPostings;
}
}
@@ -1,82 +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.portfolio.savings.data;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
public class SavingsAccountConstant extends SavingsApiConstants {
/**
* These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to
* get response parameters to match those of request parameters.
*/
protected static final Set<String> SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName,
externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName,
nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName,
interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName,
// withdrawalFeeAmountParamName, withdrawalFeeTypeParamName,
withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName,
overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName,
maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName,
withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication,
// CBS Account fields
"accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName,
specialCategoryParamName, weakerSectionParamName,
guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName,
guardianClientIdParamName, minorDateOfBirthParamName,
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName,
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName));
/**
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to
* get response parameters to match those of request parameters.
*/
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName,
retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction,
lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName));
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(idParamName, accountNoParamName));
protected static final Set<String> SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance"));
protected static final Set<String> SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName,
withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName,
receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure));
protected static final Set<String> SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName,
feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName));
protected static final Set<String> SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName));
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.data;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
public class SavingsAccountConstant extends SavingsApiConstants {
/**
* These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to
* get response parameters to match those of request parameters.
*/
protected static final Set<String> SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName,
externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName,
nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName,
interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName,
// withdrawalFeeAmountParamName, withdrawalFeeTypeParamName,
withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName,
overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName,
maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName,
withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication,
// CBS Account fields
"accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName,
specialCategoryParamName, weakerSectionParamName,
guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName,
guardianClientIdParamName, minorDateOfBirthParamName,
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName,
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName));
/**
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to
* get response parameters to match those of request parameters.
*/
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName,
retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction,
lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName,
"denominationBreakdown"));
protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(idParamName, accountNoParamName));
protected static final Set<String> SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance"));
protected static final Set<String> SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName,
withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName,
receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure));
protected static final Set<String> SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName,
feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName));
protected static final Set<String> SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName));
}
@@ -3969,6 +3969,12 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
@Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
private Boolean autoDebitFacility = false;
@Column(name = "is_senior_citizen", nullable = false, columnDefinition = "boolean default false")
private boolean isSeniorCitizen = false;
@Column(name = "senior_citizen_interest_rate", scale = 6, precision = 19, nullable = true)
private BigDecimal seniorCitizenInterestRate;
@jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY)
private SavingsAccountFacilityConfig facilityConfig;
@@ -4041,4 +4047,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
public Boolean getAutoDebitFacility() { return autoDebitFacility; }
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
public boolean isSeniorCitizen() { return isSeniorCitizen; }
public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; }
public BigDecimal getSeniorCitizenInterestRate() { return seniorCitizenInterestRate; }
public void setSeniorCitizenInterestRate(BigDecimal v) { this.seniorCitizenInterestRate = v; }
public void updateNominalAnnualInterestRate(BigDecimal rate) {
if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) {
this.nominalAnnualInterestRate = rate;
}
}
}
@@ -1,51 +1,55 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 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.savings.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
}
@@ -1,40 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Sequence is starting from 2000 to make it easier to move existing liquibase changesets here -->
<include file="parts/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
<include file="parts/2002_add_savings_accrual_permission.xml" relativeToChangelogFile="true" />
<include file="parts/2003_add_accrued_till_date_to_savings_account.xml" relativeToChangelogFile="true" />
<include file="parts/2004_add_savings_account_cob_infrastructure.xml" relativeToChangelogFile="true" />
<include file="parts/2005_add_savings_product_classification_code_values.xml" relativeToChangelogFile="true" />
<include file="parts/2006_add_period_frequency_options.xml" relativeToChangelogFile="true" />
<include file="parts/2007_add_interest_calculation_type_option.xml" relativeToChangelogFile="true" />
<include file="parts/2008_add_other_gl_mappings_to_savings_product.xml" relativeToChangelogFile="true" />
<include file="parts/2009_add_gl_account_id_to_savings_product.xml" relativeToChangelogFile="true" />
<include file="parts/2010_add_savings_account_cbs_fields.xml" relativeToChangelogFile="true" />
<include file="parts/2011_add_facility_charge_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2013_add_missing_current_account_columns.xml" relativeToChangelogFile="true" />
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Sequence is starting from 2000 to make it easier to move existing liquibase changesets here -->
<include file="parts/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
<include file="parts/2002_add_savings_accrual_permission.xml" relativeToChangelogFile="true" />
<include file="parts/2003_add_accrued_till_date_to_savings_account.xml" relativeToChangelogFile="true" />
<include file="parts/2004_add_savings_account_cob_infrastructure.xml" relativeToChangelogFile="true" />
<include file="parts/2005_add_savings_product_classification_code_values.xml" relativeToChangelogFile="true" />
<include file="parts/2006_add_period_frequency_options.xml" relativeToChangelogFile="true" />
<include file="parts/2007_add_interest_calculation_type_option.xml" relativeToChangelogFile="true" />
<include file="parts/2008_add_other_gl_mappings_to_savings_product.xml" relativeToChangelogFile="true" />
<include file="parts/2009_add_gl_account_id_to_savings_product.xml" relativeToChangelogFile="true" />
<include file="parts/2010_add_savings_account_cbs_fields.xml" relativeToChangelogFile="true" />
<include file="parts/2011_add_facility_charge_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2013_add_missing_current_account_columns.xml" relativeToChangelogFile="true" />
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
<include file="parts/2015_add_senior_citizen_to_savings_account.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="2015_add_senior_citizen_to_savings_account" author="CBS">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_savings_account"/>
<not><columnExists tableName="m_savings_account" columnName="is_senior_citizen"/></not>
</preConditions>
<addColumn tableName="m_savings_account">
<column name="is_senior_citizen" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="senior_citizen_interest_rate" type="DECIMAL(19,6)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>