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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account; package org.apache.fineract.portfolio.account;
public enum PortfolioAccountType { public enum PortfolioAccountType {
INVALID(0, "accountType.invalid"), // INVALID(0, "accountType.invalid"), //
LOAN(1, "accountType.loan"), // LOAN(1, "accountType.loan"), //
SAVINGS(2, "accountType.savings"), // SAVINGS(2, "accountType.savings"), //
; CURRENT(3, "accountType.current"), //
;
private final Integer value;
private final String code; private final Integer value;
private final String code;
PortfolioAccountType(final Integer value, final String code) {
this.value = value; PortfolioAccountType(final Integer value, final String code) {
this.code = code; this.value = value;
} this.code = code;
}
public Integer getValue() {
return this.value; public Integer getValue() {
} return this.value;
}
public String getCode() {
return this.code; public String getCode() {
} return this.code;
}
public static PortfolioAccountType fromInt(final Integer type) {
public static PortfolioAccountType fromInt(final Integer type) {
PortfolioAccountType enumType = INVALID;
if (type != null) { PortfolioAccountType enumType = INVALID;
switch (type) { if (type != null) {
case 1: switch (type) {
enumType = LOAN; case 1:
break; enumType = LOAN;
case 2: break;
enumType = SAVINGS; case 2:
break; enumType = SAVINGS;
default: break;
enumType = INVALID; case 3:
} enumType = CURRENT;
} break;
return enumType; default:
} enumType = INVALID;
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! }
public boolean isSavingsAccount() { return enumType;
return this.equals(SAVINGS); }
}
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?!
// TODO: bad practice and unnecessary code! why not just use the enum values themselves!?! public boolean isSavingsAccount() {
public boolean isLoanAccount() { return this.equals(SAVINGS);
return this.equals(LOAN); }
}
} // 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); 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. * 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(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.append(" eao.external_id as externalAssetOwner, ") .append(" eao.external_id as externalAssetOwner, ")
.append(" CASE ") .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 COALESCE(ca.account_no, sa.account_no) ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
.append(" ELSE NULL ") .append(" ELSE NULL ")
.append(" END as savingsAccountNo "); .append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) { if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" join m_currency curr on curr.code = journalEntry.currency_code ") .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_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_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_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 AND journalEntry.transaction_id LIKE 'CA%' "); .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
if (associationParametersData.isTransactionDetailsRequired()) { if (associationParametersData.isTransactionDetailsRequired()) {
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ") 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 ") .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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.api; package org.apache.fineract.portfolio.account.api;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandHandlerRegistry; import org.apache.fineract.batch.command.CommandHandlerRegistry;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.account.data.AccountTransferData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDTO; import org.apache.fineract.portfolio.account.data.StandingInstructionDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData; 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.StandingInstructionCreationRequest;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam; import org.apache.fineract.portfolio.account.data.request.StandingInstructionSearchParam;
import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest; import org.apache.fineract.portfolio.account.data.request.StandingInstructionUpdatesRequest;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Path("/v1/standinginstructions") @Path("/v1/standinginstructions")
@Component @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" @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.") + "\n" + "Note: At present only savings account to savings account and savings account to Loan account transfers are permitted.")
@RequiredArgsConstructor @RequiredArgsConstructor
public class StandingInstructionApiResource { public class StandingInstructionApiResource {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer; private final DefaultToApiJsonSerializer<StandingInstructionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final SqlValidator sqlValidator; private final SqlValidator sqlValidator;
private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>( private static final CommandHandlerRegistry<String, Long, String, CommandWrapper> COMMAND_HANDLER_REGISTRY = new CommandHandlerRegistry<>(
Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE, Map.of(CommandParameterUtil.UPDATE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(), (id, json) -> new CommandWrapperBuilder().updateStandingInstruction(id).withJson(json).build(),
CommandParameterUtil.DELETE_COMMAND_VALUE, CommandParameterUtil.DELETE_COMMAND_VALUE,
(id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build())); (id, json) -> new CommandWrapperBuilder().deleteStandingInstruction(id).withJson(json).build()));
@GET @GET
@Path("template") @Path("template")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction Template", description = "This is a convenience resource. " @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. " + "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" + "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" + "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?fromAccountType=2&fromOfficeId=1&fromClientId=1&transferType=1\n" + "\n"
+ "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1") + "standinginstructions/template?fromClientId=1&fromAccountType=2&fromAccountId=1&transferType=1")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsTemplateResponse.class)))
public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) { public StandingInstructionData template(@BeanParam StandingInstructionSearchParam instructionParam) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(), return standingInstructionReadPlatformService.retrieveTemplate(instructionParam.getFromOfficeId(),
instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(), instructionParam.getFromClientId(), instructionParam.getFromAccountId(), instructionParam.getFromAccountType(),
instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(), instructionParam.getToOfficeId(), instructionParam.getToClientId(), instructionParam.getToAccountId(),
instructionParam.getToAccountType(), instructionParam.getTransferType()); instructionParam.getToAccountType(), instructionParam.getTransferType());
} }
@POST @POST
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ 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") @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))) @RequestBody(required = true, content = @Content(schema = @Schema(implementation = StandingInstructionCreationRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PostStandingInstructionsResponse.class)))
public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) { public CommandProcessingResult create(@Parameter(hidden = true) StandingInstructionCreationRequest creationRequest) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction() final CommandWrapper commandRequest = new CommandWrapperBuilder().createStandingInstruction()
.withJson(toApiJsonSerializer.serialize(creationRequest)).build(); .withJson(toApiJsonSerializer.serialize(creationRequest)).build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest); return commandsSourceWritePlatformService.logCommandSource(commandRequest);
} }
@PUT @PUT
@Path("{standingInstructionId}") @Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ 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" @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" + "\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" + "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") + "PUT https://DomainName/api/v1/standinginstructions/1?command=delete")
@RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class))) @RequestBody(content = @Content(schema = @Schema(implementation = StandingInstructionUpdatesRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.PutStandingInstructionsStandingInstructionIdResponse.class)))
public CommandProcessingResult update( public CommandProcessingResult update(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest, @Parameter(hidden = true) StandingInstructionUpdatesRequest updatesRequest,
@QueryParam("command") @Parameter(description = "command") final String commandParam) { @QueryParam("command") @Parameter(description = "command") final String commandParam) {
final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest); final String serializedUpdatesRequest = toApiJsonSerializer.serialize(updatesRequest);
final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId, final CommandWrapper commandRequest = COMMAND_HANDLER_REGISTRY.execute(commandParam, standingInstructionId,
serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam)); serializedUpdatesRequest, new UnrecognizedQueryParamException("command", commandParam));
return commandsSourceWritePlatformService.logCommandSource(commandRequest); return commandsSourceWritePlatformService.logCommandSource(commandRequest);
} }
@GET @GET
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions") @Operation(summary = "List Standing Instructions", description = "Example Requests:\n" + "\n" + "standinginstructions")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsResponse.class)))
public Page<StandingInstructionData> retrieveAll( public Page<StandingInstructionData> retrieveAll(
@QueryParam("externalId") @Parameter(description = "externalId") final String externalId, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType, @QueryParam("transferType") @Parameter(description = "transferType") final Integer transferType,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName, @QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId, @QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount, @QueryParam("fromAccountId") @Parameter(description = "fromAccountId") final Long fromAccount,
@QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) { @QueryParam("fromAccountType") @Parameter(description = "fromAccountType") final Integer fromAccountType) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId); sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build(); .orderBy(orderBy).sortOrder(sortOrder).build();
final LocalDate startDateRange = null; final LocalDate startDateRange = null;
final LocalDate endDateRange = null; final LocalDate endDateRange = null;
StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId, StandingInstructionDTO standingInstructionDTO = new StandingInstructionDTO(searchParameters, transferType, clientName, clientId,
fromAccount, fromAccountType, startDateRange, endDateRange); fromAccount, fromAccountType, startDateRange, endDateRange);
return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO); return standingInstructionReadPlatformService.retrieveAll(standingInstructionDTO);
} }
@GET @GET
@Path("{standingInstructionId}") @Path("{standingInstructionId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Standing Instruction", description = "Example Requests :\n" + "\n" + "standinginstructions/1") @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))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StandingInstructionApiResourceSwagger.GetStandingInstructionsStandingInstructionIdResponse.class)))
public StandingInstructionData retrieveOne( public StandingInstructionData retrieveOne(
@PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId, @PathParam("standingInstructionId") @Parameter(description = "standingInstructionId") final Long standingInstructionId,
@Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId, @Context final UriInfo uriInfo, @QueryParam("externalId") @Parameter(description = "externalId") final String externalId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId); StandingInstructionData standingInstructionData = standingInstructionReadPlatformService.retrieveOne(standingInstructionId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build(); .orderBy(orderBy).sortOrder(sortOrder).build();
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) { if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) { if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList("transactions", "template")); associationParameters.addAll(Arrays.asList("transactions", "template"));
} }
if (associationParameters.contains("transactions")) { if (associationParameters.contains("transactions")) {
Page<AccountTransferData> transfers = accountTransfersReadPlatformService Page<AccountTransferData> transfers = accountTransfersReadPlatformService
.retrieveByStandingInstruction(standingInstructionId, searchParameters); .retrieveByStandingInstruction(standingInstructionId, searchParameters);
standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers); standingInstructionData = StandingInstructionData.withTransferData(standingInstructionData, transfers);
} }
if (associationParameters.contains("template")) { if (associationParameters.contains("template")) {
final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate( final StandingInstructionData templateData = standingInstructionReadPlatformService.retrieveTemplate(
standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(), standingInstructionData.getFromClient().getOfficeId(), standingInstructionData.getFromClient().getId(),
standingInstructionData.getFromAccount().getId(), standingInstructionData.getFromAccountType().getValue(), standingInstructionData.getFromAccount().getId(), standingInstructionData.fromAccountTypeEnum().getValue(),
standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(), standingInstructionData.getToClient().getOfficeId(), standingInstructionData.getToClient().getId(),
standingInstructionData.getToAccount().getId(), standingInstructionData.getToAccountType().getValue(), standingInstructionData.getToAccount().getId(), standingInstructionData.toAccountTypeEnum().getValue(),
standingInstructionData.getTransferType().getValue()); standingInstructionData.transferTypeEnum().getValue());
standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData); standingInstructionData = StandingInstructionData.withTemplateData(standingInstructionData, templateData);
} }
} }
return standingInstructionData; return standingInstructionData;
} }
} }
@@ -1,79 +1,79 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; 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.fromAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromClientIdParamName; 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.fromOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountIdParamName; 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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; 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.toOfficeIdParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class AccountTransfersDetailDataValidator { public class AccountTransfersDetailDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@Autowired @Autowired
public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) { public AccountTransfersDetailDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
} }
public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) { public void validate(final JsonCommand command, final DataValidatorBuilder baseDataValidator) {
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromOfficeIdParamName).value(fromOfficeId).notNull().integerGreaterThanZero();
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromClientIdParamName).value(fromClientId).notNull().integerGreaterThanZero();
final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element); final Long fromAccountId = this.fromApiJsonHelper.extractLongNamed(fromAccountIdParamName, element);
baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(fromAccountIdParamName).value(fromAccountId).notNull().integerGreaterThanZero();
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull() baseDataValidator.reset().parameter(fromAccountTypeParamName).value(fromAccountType).notNull()
.isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2)); .isOneOfTheseValues(Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3));
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toOfficeIdParamName).value(toOfficeId).notNull().integerGreaterThanZero();
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toClientIdParamName).value(toClientId).notNull().integerGreaterThanZero();
final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element); final Long toAccountId = this.fromApiJsonHelper.extractLongNamed(toAccountIdParamName, element);
baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(toAccountIdParamName).value(toAccountId).notNull().integerGreaterThanZero();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1), baseDataValidator.reset().parameter(toAccountTypeParamName).value(toAccountType).notNull().isOneOfTheseValues(Integer.valueOf(1),
Integer.valueOf(2)); Integer.valueOf(2), Integer.valueOf(3));
} }
} }
@@ -1,329 +1,359 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.organisation.office.data.OfficeData; import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.client.data.ClientData; import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
/** /**
* Immutable data object representing a savings account. * Immutable data object representing a savings account.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class StandingInstructionData { public final class StandingInstructionData {
@Getter @Getter
private final Long id; private final Long id;
@Getter @Getter
private final Long accountDetailId; private final Long accountDetailId;
@Getter @Getter
private final String name; private final String name;
private final OfficeData fromOffice; @Getter
@Getter private final OfficeData fromOffice;
private final ClientData fromClient; @Getter
private final EnumOptionData fromAccountType; private final ClientData fromClient;
@Getter @Getter
private final PortfolioAccountData fromAccount; private final EnumOptionData fromAccountType;
private final OfficeData toOffice; @Getter
@Getter private final PortfolioAccountData fromAccount;
private final ClientData toClient; @Getter
private final EnumOptionData toAccountType; private final OfficeData toOffice;
@Getter @Getter
private final PortfolioAccountData toAccount; private final ClientData toClient;
private final EnumOptionData transferType; @Getter
private final EnumOptionData priority; private final EnumOptionData toAccountType;
private final EnumOptionData instructionType; @Getter
@Getter private final PortfolioAccountData toAccount;
private final EnumOptionData status; @Getter
@Getter private final EnumOptionData transferType;
private final BigDecimal amount; @Getter
@Getter private final EnumOptionData priority;
private final LocalDate validFrom; @Getter
private final LocalDate validTill; private final EnumOptionData instructionType;
private final EnumOptionData recurrenceType; @Getter
private final EnumOptionData recurrenceFrequency; private final EnumOptionData status;
@Getter @Getter
private final Integer recurrenceInterval; private final BigDecimal amount;
private final MonthDay recurrenceOnMonthDay; @Getter
private final Page<AccountTransferData> transactions; private final LocalDate validFrom;
@Getter
private final Collection<OfficeData> fromOfficeOptions; private final LocalDate validTill;
private final Collection<ClientData> fromClientOptions; @Getter
private final Collection<EnumOptionData> fromAccountTypeOptions; private final EnumOptionData recurrenceType;
private final Collection<PortfolioAccountData> fromAccountOptions; @Getter
private final Collection<OfficeData> toOfficeOptions; private final EnumOptionData recurrenceFrequency;
private final Collection<ClientData> toClientOptions; @Getter
private final Collection<EnumOptionData> toAccountTypeOptions; private final Integer recurrenceInterval;
private final Collection<PortfolioAccountData> toAccountOptions; @Getter
private final Collection<EnumOptionData> transferTypeOptions; private final MonthDay recurrenceOnMonthDay;
private final Collection<EnumOptionData> statusOptions; @Getter
private final Collection<EnumOptionData> instructionTypeOptions; private final Page<AccountTransferData> transactions;
private final Collection<EnumOptionData> priorityOptions;
private final Collection<EnumOptionData> recurrenceTypeOptions; @Getter
private final Collection<EnumOptionData> recurrenceFrequencyOptions; private final Collection<OfficeData> fromOfficeOptions;
@Getter
public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions, private final Collection<ClientData> fromClientOptions;
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions, @Getter
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions, private final Collection<EnumOptionData> fromAccountTypeOptions;
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions, @Getter
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions, private final Collection<PortfolioAccountData> fromAccountOptions;
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions, @Getter
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions, private final Collection<OfficeData> toOfficeOptions;
final Collection<EnumOptionData> recurrenceFrequencyOptions) { @Getter
private final Collection<ClientData> toClientOptions;
final Long id = null; @Getter
final Long accountDetailId = null; private final Collection<EnumOptionData> toAccountTypeOptions;
final String name = null; @Getter
final OfficeData fromOffice = null; private final Collection<PortfolioAccountData> toAccountOptions;
final OfficeData toOffice = null; @Getter
final ClientData fromClient = null; private final Collection<EnumOptionData> transferTypeOptions;
final ClientData toClient = null; @Getter
final EnumOptionData fromAccountType = null; private final Collection<EnumOptionData> statusOptions;
final PortfolioAccountData fromAccount = null; @Getter
final EnumOptionData toAccountType = null; private final Collection<EnumOptionData> instructionTypeOptions;
final PortfolioAccountData toAccount = null; @Getter
final EnumOptionData transferType = null; private final Collection<EnumOptionData> priorityOptions;
final EnumOptionData priority = null; @Getter
final EnumOptionData instructionType = null; private final Collection<EnumOptionData> recurrenceTypeOptions;
final EnumOptionData status = null; @Getter
final BigDecimal amount = null; private final Collection<EnumOptionData> recurrenceFrequencyOptions;
final LocalDate validFrom = null;
final LocalDate validTill = null; public static StandingInstructionData template(final Collection<OfficeData> fromOfficeOptions,
final EnumOptionData recurrenceType = null; final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
final EnumOptionData recurrenceFrequency = null; final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
final Integer recurrenceInterval = null; final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
final MonthDay recurrenceOnMonthDay = null; final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Page<AccountTransferData> transactions = null; final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, final Long id = null;
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, final Long accountDetailId = null;
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final String name = null;
recurrenceFrequencyOptions); final OfficeData fromOffice = null;
} final OfficeData toOffice = null;
final ClientData fromClient = null;
public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name, final ClientData toClient = null;
final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient, final EnumOptionData fromAccountType = null;
final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final EnumOptionData toAccountType, final PortfolioAccountData fromAccount = null;
final PortfolioAccountData toAccount, final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData toAccountType = null;
final EnumOptionData instructionType, final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, final PortfolioAccountData toAccount = null;
final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final EnumOptionData transferType = null;
final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) { final EnumOptionData priority = null;
final Page<AccountTransferData> transactions = null; final EnumOptionData instructionType = null;
final Collection<OfficeData> fromOfficeOptions = null; final EnumOptionData status = null;
final Collection<ClientData> fromClientOptions = null; final BigDecimal amount = null;
final Collection<EnumOptionData> fromAccountTypeOptions = null; final LocalDate validFrom = null;
final Collection<PortfolioAccountData> fromAccountOptions = null; final LocalDate validTill = null;
final Collection<OfficeData> toOfficeOptions = null; final EnumOptionData recurrenceType = null;
final Collection<ClientData> toClientOptions = null; final EnumOptionData recurrenceFrequency = null;
final Collection<EnumOptionData> toAccountTypeOptions = null; final Integer recurrenceInterval = null;
final Collection<PortfolioAccountData> toAccountOptions = null; final MonthDay recurrenceOnMonthDay = null;
final Collection<EnumOptionData> transferTypeOptions = null; final Page<AccountTransferData> transactions = null;
final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null; return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
final Collection<EnumOptionData> priorityOptions = null; toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
final Collection<EnumOptionData> recurrenceTypeOptions = null; recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions = null; fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, recurrenceFrequencyOptions);
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, }
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, public static StandingInstructionData instance(final Long id, final Long accountDetailId, final String name,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final OfficeData fromOffice, final OfficeData toOffice, final ClientData fromClient, final ClientData toClient,
recurrenceFrequencyOptions); 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,
public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) { final LocalDate validTill, final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency,
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, final Integer recurrenceInterval, final MonthDay recurrenceOnMonthDay) {
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, final Page<AccountTransferData> transactions = null;
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, final Collection<OfficeData> fromOfficeOptions = null;
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, final Collection<ClientData> fromClientOptions = null;
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, final Collection<EnumOptionData> fromAccountTypeOptions = null;
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, final Collection<PortfolioAccountData> fromAccountOptions = null;
instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions, final Collection<OfficeData> toOfficeOptions = null;
templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions, final Collection<ClientData> toClientOptions = null;
templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions, final Collection<EnumOptionData> toAccountTypeOptions = null;
templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions, final Collection<PortfolioAccountData> toAccountOptions = null;
templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions); final Collection<EnumOptionData> transferTypeOptions = null;
} final Collection<EnumOptionData> statusOptions = null;
final Collection<EnumOptionData> instructionTypeOptions = null;
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice, final Collection<EnumOptionData> priorityOptions = null;
final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount, final Collection<EnumOptionData> recurrenceTypeOptions = null;
final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount, final Collection<EnumOptionData> recurrenceFrequencyOptions = null;
final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill, return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice,
final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval, toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill,
final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions, fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions, toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions, recurrenceFrequencyOptions);
final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions, }
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions, public static StandingInstructionData withTemplateData(StandingInstructionData instructionData, StandingInstructionData templateData) {
final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) { return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
this.id = id; instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
this.accountDetailId = accountDetailId; instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
this.name = name; instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
this.fromOffice = fromOffice; instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
this.fromClient = fromClient; instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay,
this.fromAccountType = fromAccountType; instructionData.transactions, templateData.fromOfficeOptions, templateData.fromClientOptions,
this.fromAccount = fromAccount; templateData.fromAccountTypeOptions, templateData.fromAccountOptions, templateData.toOfficeOptions,
this.toOffice = toOffice; templateData.toClientOptions, templateData.toAccountTypeOptions, templateData.toAccountOptions,
this.toClient = toClient; templateData.transferTypeOptions, templateData.statusOptions, templateData.instructionTypeOptions,
this.toAccountType = toAccountType; templateData.priorityOptions, templateData.recurrenceTypeOptions, templateData.recurrenceFrequencyOptions);
this.toAccount = toAccount; }
this.transferType = transferType;
private StandingInstructionData(final Long id, final Long accountDetailId, final String name, final OfficeData fromOffice,
this.priority = priority; final ClientData fromClient, final EnumOptionData fromAccountType, final PortfolioAccountData fromAccount,
this.instructionType = instructionType; final OfficeData toOffice, final ClientData toClient, final EnumOptionData toAccountType, final PortfolioAccountData toAccount,
this.status = status; final EnumOptionData transferType, final EnumOptionData priority, final EnumOptionData instructionType,
this.amount = amount; final EnumOptionData status, final BigDecimal amount, final LocalDate validFrom, LocalDate validTill,
this.validFrom = validFrom; final EnumOptionData recurrenceType, final EnumOptionData recurrenceFrequency, final Integer recurrenceInterval,
this.validTill = validTill; final MonthDay recurrenceOnMonthDay, final Page<AccountTransferData> transactions,
this.recurrenceType = recurrenceType; final Collection<OfficeData> fromOfficeOptions, final Collection<ClientData> fromClientOptions,
this.recurrenceFrequency = recurrenceFrequency; final Collection<EnumOptionData> fromAccountTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions,
this.recurrenceInterval = recurrenceInterval; final Collection<OfficeData> toOfficeOptions, final Collection<ClientData> toClientOptions,
this.recurrenceOnMonthDay = recurrenceOnMonthDay; final Collection<EnumOptionData> toAccountTypeOptions, final Collection<PortfolioAccountData> toAccountOptions,
final Collection<EnumOptionData> transferTypeOptions, final Collection<EnumOptionData> statusOptions,
this.fromOfficeOptions = fromOfficeOptions; final Collection<EnumOptionData> instructionTypeOptions, final Collection<EnumOptionData> priorityOptions,
this.fromClientOptions = fromClientOptions; final Collection<EnumOptionData> recurrenceTypeOptions, final Collection<EnumOptionData> recurrenceFrequencyOptions) {
this.fromAccountTypeOptions = fromAccountTypeOptions; this.id = id;
this.fromAccountOptions = fromAccountOptions; this.accountDetailId = accountDetailId;
this.toOfficeOptions = toOfficeOptions; this.name = name;
this.toClientOptions = toClientOptions; this.fromOffice = fromOffice;
this.toAccountTypeOptions = toAccountTypeOptions; this.fromClient = fromClient;
this.toAccountOptions = toAccountOptions; this.fromAccountType = fromAccountType;
this.transferTypeOptions = transferTypeOptions; this.fromAccount = fromAccount;
this.statusOptions = statusOptions; this.toOffice = toOffice;
this.instructionTypeOptions = instructionTypeOptions; this.toClient = toClient;
this.priorityOptions = priorityOptions; this.toAccountType = toAccountType;
this.recurrenceTypeOptions = recurrenceTypeOptions; this.toAccount = toAccount;
this.recurrenceFrequencyOptions = recurrenceFrequencyOptions; this.transferType = transferType;
this.transactions = transactions;
} this.priority = priority;
this.instructionType = instructionType;
public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType, this.status = status;
PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient, this.amount = amount;
EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions, this.validFrom = validFrom;
final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions, this.validTill = validTill;
final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions, this.recurrenceType = recurrenceType;
final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions, this.recurrenceFrequency = recurrenceFrequency;
final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions, this.recurrenceInterval = recurrenceInterval;
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions, this.recurrenceOnMonthDay = recurrenceOnMonthDay;
final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Collection<EnumOptionData> recurrenceFrequencyOptions) { this.fromOfficeOptions = fromOfficeOptions;
final Long id = null; this.fromClientOptions = fromClientOptions;
final Long accountDetailId = null; this.fromAccountTypeOptions = fromAccountTypeOptions;
final String name = null; this.fromAccountOptions = fromAccountOptions;
final EnumOptionData transferType = null; this.toOfficeOptions = toOfficeOptions;
final EnumOptionData priority = null; this.toClientOptions = toClientOptions;
final EnumOptionData instructionType = null; this.toAccountTypeOptions = toAccountTypeOptions;
final EnumOptionData status = null; this.toAccountOptions = toAccountOptions;
final BigDecimal amount = null; this.transferTypeOptions = transferTypeOptions;
final LocalDate validFrom = null; this.statusOptions = statusOptions;
final LocalDate validTill = null; this.instructionTypeOptions = instructionTypeOptions;
final EnumOptionData recurrenceType = null; this.priorityOptions = priorityOptions;
final EnumOptionData recurrenceFrequency = null; this.recurrenceTypeOptions = recurrenceTypeOptions;
final Integer recurrenceInterval = null; this.recurrenceFrequencyOptions = recurrenceFrequencyOptions;
final MonthDay recurrenceOnMonthDay = null; this.transactions = transactions;
final Page<AccountTransferData> transactions = null; }
return new StandingInstructionData(id, accountDetailId, name, fromOffice, fromClient, fromAccountType, fromAccount, toOffice, public static StandingInstructionData template(OfficeData fromOffice, ClientData fromClient, EnumOptionData fromAccountType,
toClient, toAccountType, toAccount, transferType, priority, instructionType, status, amount, validFrom, validTill, PortfolioAccountData fromAccount, LocalDate transferDate, OfficeData toOffice, ClientData toClient,
recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions, EnumOptionData toAccountType, PortfolioAccountData toAccount, final Collection<OfficeData> fromOfficeOptions,
fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions, final Collection<ClientData> fromClientOptions, final Collection<EnumOptionData> fromAccountTypeOptions,
toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions, final Collection<PortfolioAccountData> fromAccountOptions, final Collection<OfficeData> toOfficeOptions,
recurrenceFrequencyOptions); final Collection<ClientData> toClientOptions, final Collection<EnumOptionData> toAccountTypeOptions,
} final Collection<PortfolioAccountData> toAccountOptions, final Collection<EnumOptionData> transferTypeOptions,
final Collection<EnumOptionData> statusOptions, final Collection<EnumOptionData> instructionTypeOptions,
public static StandingInstructionData withTransferData(StandingInstructionData instructionData, final Collection<EnumOptionData> priorityOptions, final Collection<EnumOptionData> recurrenceTypeOptions,
final Page<AccountTransferData> transactions) { final Collection<EnumOptionData> recurrenceFrequencyOptions) {
return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name, final Long id = null;
instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount, final Long accountDetailId = null;
instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount, final String name = null;
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status, final EnumOptionData transferType = null;
instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType, final EnumOptionData priority = null;
instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions, final EnumOptionData instructionType = null;
instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions, final EnumOptionData status = null;
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions, final BigDecimal amount = null;
instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions, final LocalDate validFrom = null;
instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions, final LocalDate validTill = null;
instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions); final EnumOptionData recurrenceType = null;
} final EnumOptionData recurrenceFrequency = null;
final Integer recurrenceInterval = null;
public StandingInstructionType getInstructionType() { final MonthDay recurrenceOnMonthDay = null;
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(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,
public AccountTransferRecurrenceType getRecurrenceType() { recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay, transactions, fromOfficeOptions,
return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null); fromClientOptions, fromAccountTypeOptions, fromAccountOptions, toOfficeOptions, toClientOptions, toAccountTypeOptions,
} toAccountOptions, transferTypeOptions, statusOptions, instructionTypeOptions, priorityOptions, recurrenceTypeOptions,
recurrenceFrequencyOptions);
public PeriodFrequencyType getRecurrenceFrequency() { }
return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
} public static StandingInstructionData withTransferData(StandingInstructionData instructionData,
final Page<AccountTransferData> transactions) {
public PortfolioAccountType getFromAccountType() { return new StandingInstructionData(instructionData.id, instructionData.accountDetailId, instructionData.name,
return Optional.ofNullable(this.fromAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); instructionData.fromOffice, instructionData.fromClient, instructionData.fromAccountType, instructionData.fromAccount,
} instructionData.toOffice, instructionData.toClient, instructionData.toAccountType, instructionData.toAccount,
instructionData.transferType, instructionData.priority, instructionData.instructionType, instructionData.status,
public PortfolioAccountType getToAccountType() { instructionData.amount, instructionData.validFrom, instructionData.validTill, instructionData.recurrenceType,
return Optional.ofNullable(this.toAccountType).map(e -> PortfolioAccountType.fromInt(e.getId().intValue())).orElse(null); instructionData.recurrenceFrequency, instructionData.recurrenceInterval, instructionData.recurrenceOnMonthDay, transactions,
} instructionData.fromOfficeOptions, instructionData.fromClientOptions, instructionData.fromAccountTypeOptions,
instructionData.fromAccountOptions, instructionData.toOfficeOptions, instructionData.toClientOptions,
public AccountTransferType getTransferType() { instructionData.toAccountTypeOptions, instructionData.toAccountOptions, instructionData.transferTypeOptions,
return Optional.ofNullable(this.transferType).map(e -> AccountTransferType.fromInt(e.getId().intValue())).orElse(null); instructionData.statusOptions, instructionData.instructionTypeOptions, instructionData.priorityOptions,
} instructionData.recurrenceTypeOptions, instructionData.recurrenceFrequencyOptions);
}
public Integer getRecurrenceOnDay() {
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.DAY_OF_MONTH)).orElse(0); /**
} * Returns the domain enum for instruction type — used internally by business logic.
* NOTE: renamed from getInstructionType() to avoid overriding Lombok @Getter (which must
public Integer getRecurrenceOnMonth() { * return EnumOptionData so Jackson serializes it as an object, not a plain string).
return Optional.ofNullable(this.recurrenceOnMonthDay).map(monthDay -> monthDay.get(ChronoField.MONTH_OF_YEAR)).orElse(0); */
} public StandingInstructionType instructionTypeEnum() {
return Optional.ofNullable(this.instructionType).map(e -> StandingInstructionType.fromInt(e.getId().intValue())).orElse(null);
public Integer toTransferType() { }
Integer transferType = null;
AccountTransferType accountTransferType = getTransferType(); public AccountTransferRecurrenceType recurrenceTypeEnum() {
if (accountTransferType.isChargePayment()) { return Optional.ofNullable(this.recurrenceType).map(e -> AccountTransferRecurrenceType.fromInt(e.getId().intValue())).orElse(null);
transferType = LoanTransactionType.CHARGE_PAYMENT.getValue(); }
} else if (accountTransferType.isLoanRepayment()) {
transferType = LoanTransactionType.REPAYMENT.getValue(); public PeriodFrequencyType recurrenceFrequencyEnum() {
} return Optional.ofNullable(this.recurrenceFrequency).map(e -> PeriodFrequencyType.fromInt(e.getId().intValue())).orElse(null);
return transferType; }
}
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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.data; package org.apache.fineract.portfolio.account.data;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; 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.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.account.AccountDetailConstants; import org.apache.fineract.portfolio.account.AccountDetailConstants;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class StandingInstructionDataValidator { public class StandingInstructionDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator; private final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator;
private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, private static final Set<String> CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName, AccountDetailConstants.dateFormatParamName, AccountDetailConstants.fromOfficeIdParamName,
AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName, AccountDetailConstants.fromClientIdParamName, AccountDetailConstants.fromAccountTypeParamName,
AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName, AccountDetailConstants.fromAccountIdParamName, AccountDetailConstants.toOfficeIdParamName,
AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName, AccountDetailConstants.toClientIdParamName, AccountDetailConstants.toAccountTypeParamName,
AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName, AccountDetailConstants.toAccountIdParamName, AccountDetailConstants.transferTypeParamName,
StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.priorityParamName, StandingInstructionApiConstants.instructionTypeParamName,
StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.amountParamName,
StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.validTillParamName,
StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName,
StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceOnMonthDayParamName,
StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName)); StandingInstructionApiConstants.nameParamName, StandingInstructionApiConstants.monthDayFormatParamName));
private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName, private static final Set<String> UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(AccountDetailConstants.localeParamName,
AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName, AccountDetailConstants.dateFormatParamName, StandingInstructionApiConstants.priorityParamName,
StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName, StandingInstructionApiConstants.instructionTypeParamName, StandingInstructionApiConstants.statusParamName,
StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName, StandingInstructionApiConstants.amountParamName, StandingInstructionApiConstants.validFromParamName,
StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName, StandingInstructionApiConstants.validTillParamName, StandingInstructionApiConstants.recurrenceTypeParamName,
StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName, StandingInstructionApiConstants.recurrenceFrequencyParamName, StandingInstructionApiConstants.recurrenceIntervalParamName,
StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName)); StandingInstructionApiConstants.recurrenceOnMonthDayParamName, StandingInstructionApiConstants.monthDayFormatParamName));
@Autowired @Autowired
public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper, public StandingInstructionDataValidator(final FromJsonHelper fromApiJsonHelper,
final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) { final AccountTransfersDetailDataValidator accountTransfersDetailDataValidator) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator; this.accountTransfersDetailDataValidator = accountTransfersDetailDataValidator;
} }
public void validateForCreate(final JsonCommand command) { public void validateForCreate(final JsonCommand command) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
this.accountTransfersDetailDataValidator.validate(command, baseDataValidator); this.accountTransfersDetailDataValidator.validate(command, baseDataValidator);
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element, final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
Locale.getDefault()); Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2); baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, 2);
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName,
element); element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull();
final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName, final LocalDate validTill = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validTillParamName,
element); element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill) baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill)
.validateDateAfter(validFrom); .validateDateAfter(validFrom);
final BigDecimal transferAmount = this.fromApiJsonHelper final BigDecimal transferAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element); .extractBigDecimalWithLocaleNamed(StandingInstructionApiConstants.amountParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount();
final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Integer transferType = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3); baseDataValidator.reset().parameter(transferTypeParamName).value(transferType).notNull().inMinMaxRange(1, 3);
final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element, final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
Locale.getDefault()); Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1, baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull().inMinMaxRange(1,
4); 4);
final Integer standingInstructionType = this.fromApiJsonHelper final Integer standingInstructionType = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.instructionTypeParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
.notNull().inMinMaxRange(1, 2); .notNull().inMinMaxRange(1, 2);
final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName, final Integer recurrenceType = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.recurrenceTypeParamName,
element, Locale.getDefault()); element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull()
.inMinMaxRange(1, 2); .inMinMaxRange(1, 2);
boolean isPeriodic = false; boolean isPeriodic = false;
if (recurrenceType != null) { if (recurrenceType != null) {
isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence(); isPeriodic = AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence();
} }
final Integer recurrenceFrequency = this.fromApiJsonHelper final Integer recurrenceFrequency = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceFrequencyParamName, element, Locale.getDefault());
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.inMinMaxRange(0, 3); .inMinMaxRange(0, 3);
if (recurrenceFrequency != null) { if (recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency); PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(recurrenceFrequency);
if (frequencyType.isMonthly() || frequencyType.isYearly()) { if (frequencyType.isMonthly() || frequencyType.isYearly()) {
final MonthDay monthDay = this.fromApiJsonHelper final MonthDay monthDay = this.fromApiJsonHelper
.extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element); .extractMonthDayNamed(StandingInstructionApiConstants.recurrenceOnMonthDayParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceOnMonthDayParamName).value(monthDay)
.notNull(); .notNull();
} }
} }
final Integer recurrenceInterval = this.fromApiJsonHelper final Integer recurrenceInterval = this.fromApiJsonHelper
.extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault()); .extractIntegerNamed(StandingInstructionApiConstants.recurrenceIntervalParamName, element, Locale.getDefault());
if (isPeriodic) { if (isPeriodic) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.notNull(); .notNull();
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency)
.notNull(); .notNull();
} }
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval)
.integerGreaterThanZero(); .integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element); final String name = this.fromApiJsonHelper.extractStringNamed(StandingInstructionApiConstants.nameParamName, element);
baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull(); baseDataValidator.reset().parameter(StandingInstructionApiConstants.nameParamName).value(name).notNull();
final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element); final Integer toAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(toAccountTypeParamName, element);
if (toAccountType != null && PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()) { if (toAccountType != null && (PortfolioAccountType.fromInt(toAccountType).isSavingsAccount()
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) || PortfolioAccountType.fromInt(toAccountType).isCurrentAccount())) {
.notNull().inMinMaxRange(1, 1); baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType)
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() .notNull().inMinMaxRange(1, 1);
.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(); if (standingInstructionType != null && StandingInstructionType.fromInt(standingInstructionType).isFixedAmoutTransfer()) {
} baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).notNull();
}
String errorCode = null;
AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType); String errorCode = null;
final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element); AccountTransferType accountTransferType = AccountTransferType.fromInt(transferType);
if (fromAccountType != null && toAccountType != null) { final Integer fromAccountType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(fromAccountTypeParamName, element);
PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType); if (fromAccountType != null && toAccountType != null) {
PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType); PortfolioAccountType fromPortfolioAccountType = PortfolioAccountType.fromInt(fromAccountType);
if (accountTransferType.isAccountTransfer() PortfolioAccountType toPortfolioAccountType = PortfolioAccountType.fromInt(toAccountType);
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) { if (accountTransferType.isAccountTransfer()
errorCode = "not.account.transfer"; && (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isLoanAccount())) {
} else if (accountTransferType.isLoanRepayment() errorCode = "not.account.transfer";
&& (fromPortfolioAccountType.isLoanAccount() || toPortfolioAccountType.isSavingsAccount())) { } else if (accountTransferType.isLoanRepayment()
errorCode = "not.loan.repayment"; && (fromPortfolioAccountType.isLoanAccount()
} || (toPortfolioAccountType.isSavingsAccount() || toPortfolioAccountType.isCurrentAccount()))) {
if (errorCode != null) { errorCode = "not.loan.repayment";
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); }
} if (errorCode != null) {
} baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors); }
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
public void validateForUpdate(final JsonCommand command) { }
final String json = command.json();
public void validateForUpdate(final JsonCommand command) {
if (StringUtils.isBlank(json)) { final String json = command.json();
throw new InvalidJsonException();
} 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 Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_DATA_PARAMETERS);
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
final JsonElement element = command.parsedJson(); .resource(StandingInstructionApiConstants.STANDING_INSTRUCTION_RESOURCE_NAME);
if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
final LocalDate validFrom = this.fromApiJsonHelper.extractLocalDateNamed(StandingInstructionApiConstants.validFromParamName, final JsonElement element = command.parsedJson();
element); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validFromParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validFromParamName).value(validFrom).notNull(); 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); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.validTillParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.validTillParamName).value(validTill).notNull(); 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); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.amountParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.amountParamName).value(transferAmount).positiveAmount(); 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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.statusParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.statusParamName).value(status).notNull().inMinMaxRange(1, final Integer status = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.statusParamName, element,
2); 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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.priorityParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.priorityParamName).value(priority).notNull() final Integer priority = this.fromApiJsonHelper.extractIntegerNamed(StandingInstructionApiConstants.priorityParamName, element,
.inMinMaxRange(1, 4); 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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.instructionTypeParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.instructionTypeParamName).value(standingInstructionType) final Integer standingInstructionType = this.fromApiJsonHelper
.notNull().inMinMaxRange(1, 2); .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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceTypeParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceTypeParamName).value(recurrenceType).notNull() final Integer recurrenceType = this.fromApiJsonHelper
.inMinMaxRange(1, 2); .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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceFrequencyParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceFrequencyParamName).value(recurrenceFrequency) final Integer recurrenceFrequency = this.fromApiJsonHelper
.inMinMaxRange(0, 3); .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()); if (this.fromApiJsonHelper.parameterExists(StandingInstructionApiConstants.recurrenceIntervalParamName, element)) {
baseDataValidator.reset().parameter(StandingInstructionApiConstants.recurrenceIntervalParamName).value(recurrenceInterval) final Integer recurrenceInterval = this.fromApiJsonHelper
.integerGreaterThanZero(); .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(); 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); }
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) { }
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountIdParamName; 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.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromOfficeIdParamName; 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.toAccountIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toClientIdParamName; 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.toOfficeIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import java.util.Locale; import java.util.Locale;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.springframework.beans.factory.annotation.Autowired; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.stereotype.Service; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.beans.factory.annotation.Autowired;
@Service import org.springframework.stereotype.Service;
public class AccountTransferDetailAssembler {
@Service
private final ClientRepositoryWrapper clientRepository; public class AccountTransferDetailAssembler {
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountAssembler savingsAccountAssembler; private final ClientRepositoryWrapper clientRepository;
private final FromJsonHelper fromApiJsonHelper; private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final LoanAssembler loanAccountAssembler; private final SavingsAccountAssembler savingsAccountAssembler;
private final FromJsonHelper fromApiJsonHelper;
@Autowired private final LoanAssembler loanAccountAssembler;
public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository, private final CurrentAccountRepository currentAccountRepository;
final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler) { @Autowired
this.clientRepository = clientRepository; public AccountTransferDetailAssembler(final ClientRepositoryWrapper clientRepository,
this.officeRepositoryWrapper = officeRepositoryWrapper; final OfficeRepositoryWrapper officeRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
this.savingsAccountAssembler = savingsAccountAssembler; final FromJsonHelper fromApiJsonHelper, final LoanAssembler loanAccountAssembler,
this.fromApiJsonHelper = fromApiJsonHelper; final CurrentAccountRepository currentAccountRepository) {
this.loanAccountAssembler = loanAccountAssembler; this.clientRepository = clientRepository;
} this.officeRepositoryWrapper = officeRepositoryWrapper;
this.savingsAccountAssembler = savingsAccountAssembler;
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { this.fromApiJsonHelper = fromApiJsonHelper;
this.loanAccountAssembler = loanAccountAssembler;
final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName); this.currentAccountRepository = currentAccountRepository;
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false); }
final boolean backdatedTxnsAllowedTill = false; public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill); final Long fromSavingsId = command.longValueOfParameterNamed(fromAccountIdParamName);
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsId, false);
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
final boolean backdatedTxnsAllowedTill = false;
} final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
return assembleSavingsToSavingsTransfer(command, fromSavingsAccount, toSavingsAccount);
final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false; }
final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) {
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId); final Long fromSavingsAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final boolean backdatedTxnsAllowedTill = false;
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount); final SavingsAccount fromSavingsAccount = this.savingsAccountAssembler.assembleFrom(fromSavingsAccountId, backdatedTxnsAllowedTill);
} final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
return assembleSavingsToLoanTransfer(command, fromSavingsAccount, toLoanAccount);
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId); }
final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName); public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) {
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
final Long fromLoanAccountId = command.longValueOfParameterNamed(fromAccountIdParamName);
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount); final Loan fromLoanAccount = this.loanAccountAssembler.assembleFrom(fromLoanAccountId);
} final boolean backdatedTxnsAllowedTill = false;
final Long toSavingsAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsAccountId, backdatedTxnsAllowedTill);
final SavingsAccount toSavingsAccount) {
return assembleLoanToSavingsTransfer(command, fromLoanAccount, toSavingsAccount);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final SavingsAccount toSavingsAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
toSavingsAccount, transfertype); final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
} final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount, return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
final Loan toLoanAccount) { toSavingsAccount, transfertype);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command, final SavingsAccount fromSavingsAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final Loan toLoanAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element);
transfertype); final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
} final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault());
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount, return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
final SavingsAccount toSavingsAccount) { transfertype);
final JsonElement element = command.parsedJson(); }
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element); public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command, final Loan fromLoanAccount,
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId); final SavingsAccount toSavingsAccount) {
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element); final JsonElement element = command.parsedJson();
final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(fromOfficeIdParamName, element);
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element); final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final Long fromClientId = this.fromApiJsonHelper.extractLongNamed(fromClientIdParamName, element);
final Long toClientId = this.fromApiJsonHelper.extractLongNamed(toClientIdParamName, element); final Client fromClient = this.clientRepository.findOneWithNotFoundDetection(fromClientId);
final Client toClient = this.clientRepository.findOneWithNotFoundDetection(toClientId);
final Integer transfertype = this.fromApiJsonHelper.extractIntegerNamed(transferTypeParamName, element, Locale.getDefault()); final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(toOfficeIdParamName, element);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
transfertype); 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());
public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) { return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
final Office fromOffice = fromSavingsAccount.office(); transfertype);
final Client fromClient = fromSavingsAccount.getClient(); }
final Office toOffice = toLoanAccount.getOffice();
final Client toClient = toLoanAccount.client(); public AccountTransferDetails assembleSavingsToLoanTransfer(final SavingsAccount fromSavingsAccount, final Loan toLoanAccount,
Integer transferType) {
return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount, final Office fromOffice = fromSavingsAccount.office();
transferType); final Client fromClient = fromSavingsAccount.getClient();
final Office toOffice = toLoanAccount.getOffice();
} final Client toClient = toLoanAccount.client();
public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount, return AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, toLoanAccount,
final SavingsAccount toSavingsAccount, Integer transferType) { transferType);
final Office fromOffice = fromSavingsAccount.office();
final Client fromClient = fromSavingsAccount.getClient(); }
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient(); public AccountTransferDetails assembleSavingsToSavingsTransfer(final SavingsAccount fromSavingsAccount,
final SavingsAccount toSavingsAccount, Integer transferType) {
return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient, final Office fromOffice = fromSavingsAccount.office();
toSavingsAccount, transferType); final Client fromClient = fromSavingsAccount.getClient();
} final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) { return AccountTransferDetails.savingsToSavingsTransfer(fromOffice, fromClient, fromSavingsAccount, toOffice, toClient,
final Office fromOffice = fromLoanAccount.getOffice(); toSavingsAccount, transferType);
final Client fromClient = fromLoanAccount.client(); }
final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient(); public AccountTransferDetails assembleLoanToSavingsTransfer(final Loan fromLoanAccount, final SavingsAccount toSavingsAccount,
Integer transferType) {
return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount, final Office fromOffice = fromLoanAccount.getOffice();
transferType); final Client fromClient = fromLoanAccount.client();
} final Office toOffice = toSavingsAccount.office();
final Client toClient = toSavingsAccount.getClient();
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
final Office fromOffice = fromLoanAccount.getOffice(); return AccountTransferDetails.loanTosavingsTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toSavingsAccount,
final Client fromClient = fromLoanAccount.client(); transferType);
final Office toOffice = toLoanAccount.getOffice(); }
final Client toClient = toLoanAccount.client();
public AccountTransferDetails assembleLoanToLoanTransfer(Loan fromLoanAccount, Loan toLoanAccount, Integer transferType) {
return AccountTransferDetails.loanToLoanTransfer(fromOffice, fromClient, fromLoanAccount, toOffice, toClient, toLoanAccount, final Office fromOffice = fromLoanAccount.getOffice();
transferType); 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import jakarta.persistence.CascadeType; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Entity
@Table(name = "m_account_transfer_details") @Entity
public class AccountTransferDetails extends AbstractPersistableCustom<Long> { @Table(name = "m_account_transfer_details")
public class AccountTransferDetails extends AbstractPersistableCustom<Long> {
@ManyToOne
@JoinColumn(name = "from_office_id", nullable = false) @ManyToOne
private Office fromOffice; @JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne
@JoinColumn(name = "from_client_id", nullable = false) @ManyToOne
private Client fromClient; @JoinColumn(name = "from_client_id", nullable = false)
private Client fromClient;
@ManyToOne
@JoinColumn(name = "from_savings_account_id", nullable = true) @ManyToOne
private SavingsAccount fromSavingsAccount; @JoinColumn(name = "from_savings_account_id", nullable = true)
private SavingsAccount fromSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_office_id", nullable = false) @ManyToOne
private Office toOffice; @JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne
@JoinColumn(name = "to_client_id", nullable = false) @ManyToOne
private Client toClient; @JoinColumn(name = "to_client_id", nullable = false)
private Client toClient;
@ManyToOne
@JoinColumn(name = "to_savings_account_id", nullable = true) @ManyToOne
private SavingsAccount toSavingsAccount; @JoinColumn(name = "to_savings_account_id", nullable = true)
private SavingsAccount toSavingsAccount;
@ManyToOne
@JoinColumn(name = "to_loan_account_id", nullable = true) @ManyToOne
private Loan toLoanAccount; @JoinColumn(name = "to_loan_account_id", nullable = true)
private Loan toLoanAccount;
@ManyToOne
@JoinColumn(name = "from_loan_account_id", nullable = true) @ManyToOne
private Loan fromLoanAccount; @JoinColumn(name = "from_loan_account_id", nullable = true)
private Loan fromLoanAccount;
@Column(name = "transfer_type")
private Integer transferType; @ManyToOne
@JoinColumn(name = "from_current_account_id", nullable = true)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER) private CurrentAccount fromCurrentAccount;
private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
@ManyToOne
@OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "to_current_account_id", nullable = true)
private AccountTransferStandingInstruction accountTransferStandingInstruction; private CurrentAccount toCurrentAccount;
public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient, @Column(name = "transfer_type")
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, private Integer transferType;
Integer transferType) {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountTransferDetails", orphanRemoval = true, fetch = FetchType.EAGER)
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, toSavingsAccount, null, private List<AccountTransferTransaction> accountTransferTransactions = new ArrayList<>();
transferType, null);
} @OneToOne(mappedBy = "accountTransferDetails", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER)
private AccountTransferStandingInstruction accountTransferStandingInstruction;
public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final Loan toLoanAccount, public static AccountTransferDetails savingsToSavingsTransfer(final Office fromOffice, final Client fromClient,
Integer transferType) { final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, toOffice, toClient, null, toLoanAccount, Integer transferType) {
transferType, null);
} return new AccountTransferDetails(fromOffice, fromClient, fromSavingsAccount, null, null, toOffice, toClient, toSavingsAccount,
null, 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, toOffice, toClient, toSavingsAccount, null, public static AccountTransferDetails savingsToLoanTransfer(final Office fromOffice, final Client fromClient,
transferType, null); 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,
protected AccountTransferDetails() { null, transferType, null);
// }
}
public static AccountTransferDetails loanTosavingsTransfer(final Office fromOffice, final Client fromClient, final Loan fromLoanAccount,
private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, Integer transferType) {
final Loan fromLoanAccount, final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount, return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, null, toOffice, toClient, toSavingsAccount, null,
final Loan toLoanAccount, final Integer transferType, null, transferType, null);
final AccountTransferStandingInstruction accountTransferStandingInstruction) { }
this.fromOffice = fromOffice;
this.fromClient = fromClient; protected AccountTransferDetails() {
this.fromSavingsAccount = fromSavingsAccount; //
this.fromLoanAccount = fromLoanAccount; }
this.toOffice = toOffice;
this.toClient = toClient; private AccountTransferDetails(final Office fromOffice, final Client fromClient, final SavingsAccount fromSavingsAccount,
this.toSavingsAccount = toSavingsAccount; final Loan fromLoanAccount, final CurrentAccount fromCurrentAccount,
this.toLoanAccount = toLoanAccount; final Office toOffice, final Client toClient, final SavingsAccount toSavingsAccount,
this.transferType = transferType; final Loan toLoanAccount, final CurrentAccount toCurrentAccount,
this.accountTransferStandingInstruction = accountTransferStandingInstruction; final Integer transferType,
} final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.fromOffice = fromOffice;
public SavingsAccount toSavingsAccount() { this.fromClient = fromClient;
return this.toSavingsAccount; this.fromSavingsAccount = fromSavingsAccount;
} this.fromLoanAccount = fromLoanAccount;
this.fromCurrentAccount = fromCurrentAccount;
public SavingsAccount fromSavingsAccount() { this.toOffice = toOffice;
return this.fromSavingsAccount; this.toClient = toClient;
} this.toSavingsAccount = toSavingsAccount;
this.toLoanAccount = toLoanAccount;
public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) { this.toCurrentAccount = toCurrentAccount;
this.accountTransferTransactions.add(accountTransferTransaction); this.transferType = transferType;
} this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
this.accountTransferStandingInstruction = accountTransferStandingInstruction; public SavingsAccount toSavingsAccount() {
} return this.toSavingsAccount;
}
public Loan toLoanAccount() {
return this.toLoanAccount; public SavingsAccount fromSavingsAccount() {
} return this.fromSavingsAccount;
}
public Loan fromLoanAccount() {
return this.fromLoanAccount; public void addAccountTransferTransaction(AccountTransferTransaction accountTransferTransaction) {
} this.accountTransferTransactions.add(accountTransferTransaction);
}
public AccountTransferStandingInstruction accountTransferStandingInstruction() {
return this.accountTransferStandingInstruction; public void updateAccountTransferStandingInstruction(final AccountTransferStandingInstruction accountTransferStandingInstruction) {
} this.accountTransferStandingInstruction = accountTransferStandingInstruction;
}
public AccountTransferType transferType() {
return AccountTransferType.fromInt(this.transferType); public Loan toLoanAccount() {
} return this.toLoanAccount;
}
public static AccountTransferDetails loanToLoanTransfer(Office fromOffice, Client fromClient, Loan fromLoanAccount, Office toOffice,
Client toClient, Loan toLoanAccount, Integer transferType) { public Loan fromLoanAccount() {
return new AccountTransferDetails(fromOffice, fromClient, null, fromLoanAccount, toOffice, toClient, null, toLoanAccount, return this.fromLoanAccount;
transferType, null); }
}
public CurrentAccount fromCurrentAccount() {
public List<AccountTransferTransaction> getAccountTransferTransactions() { return this.fromCurrentAccount;
return accountTransferTransactions; }
}
} 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; package org.apache.fineract.portfolio.account.domain;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.transferTypeParamName; 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.STANDING_INSTRUCTION_RESOURCE_NAME;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.amountParamName; 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.instructionTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; 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.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; 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.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; 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.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Entity @Entity
@Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = { @Table(name = "m_account_transfer_standing_instructions", uniqueConstraints = {
@UniqueConstraint(columnNames = { "name" }, name = "name") }) @UniqueConstraint(columnNames = { "name" }, name = "name") })
public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> { public class AccountTransferStandingInstruction extends AbstractPersistableCustom<Long> {
@ManyToOne @ManyToOne
@JoinColumn(name = "account_transfer_details_id", nullable = true) @JoinColumn(name = "account_transfer_details_id", nullable = true)
private AccountTransferDetails accountTransferDetails; private AccountTransferDetails accountTransferDetails;
@Column(name = "name") @Column(name = "name")
private String name; private String name;
@Column(name = "priority") @Column(name = "priority")
private Integer priority; private Integer priority;
@Column(name = "instruction_type") @Column(name = "instruction_type")
private Integer instructionType; private Integer instructionType;
@Column(name = "status") @Column(name = "status")
private Integer status; private Integer status;
@Column(name = "amount", scale = 6, precision = 19, nullable = true) @Column(name = "amount", scale = 6, precision = 19, nullable = true)
private BigDecimal amount; private BigDecimal amount;
@Column(name = "valid_from") @Column(name = "valid_from")
private LocalDate validFrom; private LocalDate validFrom;
@Column(name = "valid_till") @Column(name = "valid_till")
private LocalDate validTill; private LocalDate validTill;
@Column(name = "recurrence_type") @Column(name = "recurrence_type")
private Integer recurrenceType; private Integer recurrenceType;
@Column(name = "recurrence_frequency") @Column(name = "recurrence_frequency")
private Integer recurrenceFrequency; private Integer recurrenceFrequency;
@Column(name = "recurrence_interval") @Column(name = "recurrence_interval")
private Integer recurrenceInterval; private Integer recurrenceInterval;
@Column(name = "recurrence_on_day") @Column(name = "recurrence_on_day")
private Integer recurrenceOnDay; private Integer recurrenceOnDay;
@Column(name = "recurrence_on_month") @Column(name = "recurrence_on_month")
private Integer recurrenceOnMonth; private Integer recurrenceOnMonth;
@Column(name = "last_run_date") @Column(name = "last_run_date")
private LocalDate latsRunDate; private LocalDate latsRunDate;
protected AccountTransferStandingInstruction() { protected AccountTransferStandingInstruction() {
} }
public static AccountTransferStandingInstruction create(final AccountTransferDetails accountTransferDetails, final String name, 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 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 LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final MonthDay recurrenceOnMonthDay) { final MonthDay recurrenceOnMonthDay) {
Integer recurrenceOnDay = null; Integer recurrenceOnDay = null;
Integer recurrenceOnMonth = null; Integer recurrenceOnMonth = null;
if (recurrenceOnMonthDay != null) { if (recurrenceOnMonthDay != null) {
recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth(); recurrenceOnDay = recurrenceOnMonthDay.getDayOfMonth();
recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue(); recurrenceOnMonth = recurrenceOnMonthDay.getMonthValue();
} }
return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom, return new AccountTransferStandingInstruction(accountTransferDetails, name, priority, instructionType, status, amount, validFrom,
validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth); validTill, recurrenceType, recurrenceFrequency, recurrenceInterval, recurrenceOnDay, recurrenceOnMonth);
} }
private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name, private AccountTransferStandingInstruction(final AccountTransferDetails accountTransferDetails, final String name,
final Integer priority, final Integer instructionType, final Integer status, final BigDecimal amount, final LocalDate validFrom, 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 LocalDate validTill, final Integer recurrenceType, final Integer recurrenceFrequency, final Integer recurrenceInterval,
final Integer recurrenceOnDay, final Integer recurrenceOnMonth) { final Integer recurrenceOnDay, final Integer recurrenceOnMonth) {
this.accountTransferDetails = accountTransferDetails; this.accountTransferDetails = accountTransferDetails;
this.name = name; this.name = name;
this.priority = priority; this.priority = priority;
this.instructionType = instructionType; this.instructionType = instructionType;
this.status = status; this.status = status;
this.amount = amount; this.amount = amount;
this.validFrom = validFrom; this.validFrom = validFrom;
this.validTill = validTill; this.validTill = validTill;
this.recurrenceType = recurrenceType; this.recurrenceType = recurrenceType;
this.recurrenceFrequency = recurrenceFrequency; this.recurrenceFrequency = recurrenceFrequency;
this.recurrenceInterval = recurrenceInterval; this.recurrenceInterval = recurrenceInterval;
this.recurrenceOnDay = recurrenceOnDay; this.recurrenceOnDay = recurrenceOnDay;
this.recurrenceOnMonth = recurrenceOnMonth; this.recurrenceOnMonth = recurrenceOnMonth;
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME); .resource(STANDING_INSTRUCTION_RESOURCE_NAME);
validateDependencies(baseDataValidator); validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
} }
public Map<String, Object> update(JsonCommand command) { public Map<String, Object> update(JsonCommand command) {
final Map<String, Object> actualChanges = new HashMap<>(); final Map<String, Object> actualChanges = new HashMap<>();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(STANDING_INSTRUCTION_RESOURCE_NAME); .resource(STANDING_INSTRUCTION_RESOURCE_NAME);
if (StandingInstructionStatus.fromInt(this.status).isDeleted()) { if (StandingInstructionStatus.fromInt(this.status).isDeleted()) {
baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted"); baseDataValidator.reset().parameter(statusParamName).failWithCode("can.not.modify.once.deleted");
} }
if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) { if (command.isChangeInDateParameterNamed(validFromParamName, this.validFrom)) {
this.validFrom = command.localDateValueOfParameterNamed(validFromParamName); this.validFrom = command.localDateValueOfParameterNamed(validFromParamName);
actualChanges.put(validFromParamName, this.validFrom); actualChanges.put(validFromParamName, this.validFrom);
} }
if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) { if (command.isChangeInDateParameterNamed(validTillParamName, this.validTill)) {
this.validTill = command.localDateValueOfParameterNamed(validTillParamName); this.validTill = command.localDateValueOfParameterNamed(validTillParamName);
actualChanges.put(validTillParamName, this.validTill); actualChanges.put(validTillParamName, this.validTill);
} }
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) { if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName); final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
actualChanges.put(amountParamName, newValue); actualChanges.put(amountParamName, newValue);
this.amount = newValue; this.amount = newValue;
} }
if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) { if (command.isChangeInIntegerParameterNamed(statusParamName, this.status)) {
final Integer newValue = command.integerValueOfParameterNamed(statusParamName); final Integer newValue = command.integerValueOfParameterNamed(statusParamName);
actualChanges.put(statusParamName, newValue); actualChanges.put(statusParamName, newValue);
this.status = newValue; this.status = newValue;
} }
if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) { if (command.isChangeInIntegerParameterNamed(priorityParamName, this.priority)) {
final Integer newValue = command.integerValueOfParameterNamed(priorityParamName); final Integer newValue = command.integerValueOfParameterNamed(priorityParamName);
actualChanges.put(priorityParamName, newValue); actualChanges.put(priorityParamName, newValue);
this.priority = newValue; this.priority = newValue;
} }
if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) { if (command.isChangeInIntegerParameterNamed(instructionTypeParamName, this.instructionType)) {
final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName); final Integer newValue = command.integerValueOfParameterNamed(instructionTypeParamName);
actualChanges.put(instructionTypeParamName, newValue); actualChanges.put(instructionTypeParamName, newValue);
this.instructionType = newValue; this.instructionType = newValue;
} }
if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) { if (command.isChangeInIntegerParameterNamed(recurrenceTypeParamName, this.recurrenceType)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceTypeParamName);
actualChanges.put(recurrenceTypeParamName, newValue); actualChanges.put(recurrenceTypeParamName, newValue);
this.recurrenceType = newValue; this.recurrenceType = newValue;
} }
if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) { if (command.isChangeInIntegerParameterNamed(recurrenceFrequencyParamName, this.recurrenceFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
actualChanges.put(recurrenceFrequencyParamName, newValue); actualChanges.put(recurrenceFrequencyParamName, newValue);
this.recurrenceFrequency = newValue; this.recurrenceFrequency = newValue;
} }
if (command.hasParameter(recurrenceOnMonthDayParamName)) { if (command.hasParameter(recurrenceOnMonthDayParamName)) {
final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); final MonthDay monthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName); final String actualValueEntered = command.stringValueOfParameterNamed(recurrenceOnMonthDayParamName);
final Integer dayOfMonthValue = monthDay.getDayOfMonth(); final Integer dayOfMonthValue = monthDay.getDayOfMonth();
if (!this.recurrenceOnDay.equals(dayOfMonthValue)) { if (!this.recurrenceOnDay.equals(dayOfMonthValue)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnDay = dayOfMonthValue; this.recurrenceOnDay = dayOfMonthValue;
} }
final Integer monthOfYear = monthDay.getMonthValue(); final Integer monthOfYear = monthDay.getMonthValue();
if (!this.recurrenceOnMonth.equals(monthOfYear)) { if (!this.recurrenceOnMonth.equals(monthOfYear)) {
actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered); actualChanges.put(recurrenceOnMonthDayParamName, actualValueEntered);
this.recurrenceOnMonth = monthOfYear; this.recurrenceOnMonth = monthOfYear;
} }
} }
if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) { if (command.isChangeInIntegerParameterNamed(recurrenceIntervalParamName, this.recurrenceInterval)) {
final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName); final Integer newValue = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
actualChanges.put(recurrenceIntervalParamName, newValue); actualChanges.put(recurrenceIntervalParamName, newValue);
this.recurrenceInterval = newValue; this.recurrenceInterval = newValue;
} }
validateDependencies(baseDataValidator); validateDependencies(baseDataValidator);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
} }
return actualChanges; return actualChanges;
} }
private void validateDependencies(final DataValidatorBuilder baseDataValidator) { private void validateDependencies(final DataValidatorBuilder baseDataValidator) {
if (this.validTill != null && this.validFrom != null) { if (this.validTill != null && this.validFrom != null) {
baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom); baseDataValidator.reset().parameter(validTillParamName).value(this.validTill).validateDateAfter(this.validFrom);
} }
if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) { if (AccountTransferRecurrenceType.fromInt(recurrenceType).isPeriodicRecurrence()) {
baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull(); baseDataValidator.reset().parameter(recurrenceFrequencyParamName).value(this.recurrenceFrequency).notNull();
baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull(); baseDataValidator.reset().parameter(recurrenceIntervalParamName).value(this.recurrenceInterval).notNull();
if (this.recurrenceFrequency != null) { if (this.recurrenceFrequency != null) {
PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency); PeriodFrequencyType frequencyType = PeriodFrequencyType.fromInt(this.recurrenceFrequency);
if (frequencyType.isMonthly()) { if (frequencyType.isMonthly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
} else if (frequencyType.isYearly()) { } else if (frequencyType.isYearly()) {
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnDay).notNull();
baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull(); baseDataValidator.reset().parameter(recurrenceOnMonthDayParamName).value(this.recurrenceOnMonth).notNull();
} }
} }
} }
if (this.accountTransferDetails.toSavingsAccount() != null) { // For savings-to-savings or current-to-savings: enforce Fixed instruction + Periodic recurrence
baseDataValidator.reset().parameter(instructionTypeParamName).value(this.instructionType).notNull().inMinMaxRange(1, 1); if (this.accountTransferDetails.toSavingsAccount() != null
baseDataValidator.reset().parameter(recurrenceTypeParamName).value(this.recurrenceType).notNull().inMinMaxRange(1, 1); || 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();
} 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)) { // Account Transfer: from must be savings OR current; to must be savings OR current
errorCode = "not.account.transfer"; boolean fromAccountValid = this.accountTransferDetails.fromSavingsAccount() != null
} else if (this.accountTransferDetails.transferType().isLoanRepayment() || this.accountTransferDetails.fromCurrentAccount() != null;
&& (this.accountTransferDetails.fromSavingsAccount() == null || this.accountTransferDetails.toLoanAccount() == null)) { boolean toAccountValid = this.accountTransferDetails.toSavingsAccount() != null
errorCode = "not.loan.repayment"; || this.accountTransferDetails.toCurrentAccount() != null;
}
if (errorCode != null) { // Loan Repayment: from must be savings OR current; to must be loan
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode); boolean fromAccountValidForLoan = this.accountTransferDetails.fromSavingsAccount() != null
} || this.accountTransferDetails.fromCurrentAccount() != null;
} String errorCode = null;
if (this.accountTransferDetails.transferType().isAccountTransfer()
public void updateLatsRunDate(LocalDate latsRunDate) { && !(fromAccountValid && toAccountValid)) {
this.latsRunDate = latsRunDate; errorCode = "not.account.transfer";
} } else if (this.accountTransferDetails.transferType().isLoanRepayment()
&& !(fromAccountValidForLoan && this.accountTransferDetails.toLoanAccount() != null)) {
public void updateStatus(Integer status) { errorCode = "not.loan.repayment";
this.status = status; }
} if (errorCode != null) {
baseDataValidator.reset().parameter(transferTypeParamName).failWithCode(errorCode);
/** }
* 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(); public void updateLatsRunDate(LocalDate latsRunDate) {
this.name = this.name + "_deleted_" + this.getId(); 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.domain; 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.amountParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.instructionTypeParamName; 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.nameParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.priorityParamName; 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.recurrenceFrequencyParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceIntervalParamName; 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.recurrenceOnMonthDayParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.recurrenceTypeParamName; 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.statusParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validFromParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.validTillParamName;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.MonthDay; import java.time.MonthDay;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.springframework.beans.factory.annotation.Autowired; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.springframework.stereotype.Service; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@Service import org.springframework.beans.factory.annotation.Autowired;
public class StandingInstructionAssembler { import org.springframework.stereotype.Service;
private final AccountTransferDetailAssembler accountTransferDetailAssembler; @Service
public class StandingInstructionAssembler {
@Autowired
public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) { private final AccountTransferDetailAssembler accountTransferDetailAssembler;
this.accountTransferDetailAssembler = accountTransferDetailAssembler; @Autowired
} public StandingInstructionAssembler(final AccountTransferDetailAssembler accountTransferDetailAssembler) {
public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) { this.accountTransferDetailAssembler = accountTransferDetailAssembler;
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command); }
assembleStandingInstruction(command, accountTransferDetails);
return accountTransferDetails; public AccountTransferDetails assembleSavingsToSavingsTransfer(final JsonCommand command) {
} final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails);
public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) { return accountTransferDetails;
final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName); }
final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
BigDecimal amount = null; public void assembleStandingInstruction(final JsonCommand command, final AccountTransferDetails accountTransferDetails) {
final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName); final LocalDate validFrom = command.localDateValueOfParameterNamed(validFromParamName);
if (transferAmount != null) { final LocalDate validTill = command.localDateValueOfParameterNamed(validTillParamName);
final Money monetaryAmount = Money.of(accountTransferDetails.fromSavingsAccount().getCurrency(), transferAmount); BigDecimal amount = null;
amount = monetaryAmount.getAmount(); final BigDecimal transferAmount = command.bigDecimalValueOfParameterNamed(amountParamName);
} if (transferAmount != null) {
final Integer status = command.integerValueOfParameterNamed(statusParamName); final MonetaryCurrency currency = resolveCurrency(accountTransferDetails);
final Integer priority = command.integerValueOfParameterNamed(priorityParamName); if (currency != null) {
final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName); final Money monetaryAmount = Money.of(currency, transferAmount);
final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName); amount = monetaryAmount.getAmount();
final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName); } else {
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName); amount = transferAmount;
final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName); }
final String name = command.stringValueOfParameterNamed(nameParamName); }
AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create( final Integer status = command.integerValueOfParameterNamed(statusParamName);
accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType, final Integer priority = command.integerValueOfParameterNamed(priorityParamName);
recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay); final Integer standingInstructionType = command.integerValueOfParameterNamed(instructionTypeParamName);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); final Integer recurrenceType = command.integerValueOfParameterNamed(recurrenceTypeParamName);
} final Integer recurrenceFrequency = command.integerValueOfParameterNamed(recurrenceFrequencyParamName);
final MonthDay recurrenceOnMonthDay = command.extractMonthDayNamed(recurrenceOnMonthDayParamName);
public AccountTransferDetails assembleSavingsToLoanTransfer(final JsonCommand command) { final Integer recurrenceInterval = command.integerValueOfParameterNamed(recurrenceIntervalParamName);
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleSavingsToLoanTransfer(command); final String name = command.stringValueOfParameterNamed(nameParamName);
assembleStandingInstruction(command, accountTransferDetails); AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create(
return accountTransferDetails; accountTransferDetails, name, priority, standingInstructionType, status, amount, validFrom, validTill, recurrenceType,
} recurrenceFrequency, recurrenceInterval, recurrenceOnMonthDay);
accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction);
public AccountTransferDetails assembleLoanToSavingsTransfer(final JsonCommand command) { }
final AccountTransferDetails accountTransferDetails = this.accountTransferDetailAssembler.assembleLoanToSavingsTransfer(command);
assembleStandingInstruction(command, accountTransferDetails); /**
return accountTransferDetails; * 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.springframework.batch.core.Job; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.Step;
import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig { @Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository; @Autowired
@Autowired private JobRepository jobRepository;
private PlatformTransactionManager transactionManager; @Autowired
@Autowired private PlatformTransactionManager transactionManager;
private StandingInstructionReadPlatformService standingInstructionReadPlatformService; @Autowired
@Autowired private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private JdbcTemplate jdbcTemplate; @Autowired
@Autowired private JdbcTemplate jdbcTemplate;
private DatabaseSpecificSQLGenerator sqlGenerator; @Autowired
@Autowired private DatabaseSpecificSQLGenerator sqlGenerator;
private AccountTransfersWritePlatformService accountTransfersWritePlatformService; @Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean @Autowired
protected Step executeStandingInstructionsStep() { private AccountTransferDetailRepository accountTransferDetailRepository;
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build(); @Bean
} protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
@Bean .tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
public Job executeStandingInstructionsJob() { }
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build(); @Bean
} public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
@Bean .incrementer(new RunIdIncrementer()).build();
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() { }
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService); @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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO; import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData; import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.springframework.batch.core.StepContribution; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.batch.core.scope.context.ChunkContext; import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.StepContribution;
import org.springframework.batch.repeat.RepeatStatus; import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j import org.springframework.jdbc.core.JdbcTemplate;
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet { @Slf4j
@RequiredArgsConstructor
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService; public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator; private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
@Override private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { private final AccountTransferDetailRepository accountTransferDetailRepository;
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue()); @Override
List<Throwable> errors = new ArrayList<>(); public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
for (StandingInstructionData data : instructionData) { Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
boolean isDueForTransfer = false; .retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
AccountTransferRecurrenceType recurrenceType = data.getRecurrenceType(); List<Throwable> errors = new ArrayList<>();
StandingInstructionType instructionType = data.getInstructionType(); for (StandingInstructionData data : instructionData) {
LocalDate transactionDate = DateUtils.getBusinessLocalDate(); boolean isDueForTransfer = false;
if (recurrenceType.isPeriodicRecurrence()) { AccountTransferRecurrenceType recurrenceType = data.recurrenceTypeEnum();
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); StandingInstructionType instructionType = data.instructionTypeEnum();
PeriodFrequencyType frequencyType = data.getRecurrenceFrequency(); LocalDate transactionDate = DateUtils.getBusinessLocalDate();
LocalDate startDate = data.getValidFrom(); if (recurrenceType.isPeriodicRecurrence()) {
if (frequencyType.isMonthly()) { final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()); PeriodFrequencyType frequencyType = data.recurrenceFrequencyEnum();
if (DateUtils.isBefore(startDate, data.getValidFrom())) { LocalDate startDate = data.getValidFrom();
startDate = startDate.plusMonths(1); if (frequencyType.isMonthly()) {
} startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay());
} else if (frequencyType.isYearly()) { if (DateUtils.isBefore(startDate, data.getValidFrom())) {
startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth()); startDate = startDate.plusMonths(1);
if (DateUtils.isBefore(startDate, data.getValidFrom())) { }
startDate = startDate.plusYears(1); } else if (frequencyType.isYearly()) {
} startDate = startDate.withDayOfMonth(data.getRecurrenceOnDay()).withMonth(data.getRecurrenceOnMonth());
} if (DateUtils.isBefore(startDate, data.getValidFrom())) {
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate, startDate = startDate.plusYears(1);
transactionDate); }
}
} isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.getRecurrenceInterval(), startDate,
BigDecimal transactionAmount = data.getAmount(); transactionDate);
if (data.getToAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) { }
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService BigDecimal transactionAmount = data.getAmount();
.retriveLoanDuesData(data.getToAccount().getId()); if (data.toAccountTypeEnum() != null && data.toAccountTypeEnum().isLoanAccount()
if (data.getInstructionType().isDuesAmoutTransfer()) { && (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
transactionAmount = standingInstructionDuesData.totalDueAmount(); StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
} .retriveLoanDuesData(data.getToAccount().getId());
if (recurrenceType.isDuesRecurrence()) { if (data.instructionTypeEnum().isDuesAmoutTransfer()) {
isDueForTransfer = isDueForTransfer(standingInstructionDuesData); 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; if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, final SavingsAccount fromSavingsAccount = null;
data.getFromAccountType(), data.getToAccountType(), data.getFromAccount().getId(), data.getToAccount().getId(), final boolean isRegularTransaction = true;
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, final boolean isExceptionForBalanceCheck = false;
data.getTransferType().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, // Load existing AccountTransferDetails so transactions are linked to
isRegularTransaction, isExceptionForBalanceCheck); // this standing instruction and appear in its transaction history
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
if (transferCompleted) { : null;
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?"; AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
jdbcTemplate.update(updateQuery, transactionDate, data.getId()); 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 (!errors.isEmpty()) {
throw new JobExecutionException(errors); if (transferCompleted) {
} final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
return RepeatStatus.FINISHED; jdbcTemplate.update(updateQuery, transactionDate, data.getId());
} }
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) { }
boolean transferCompleted = true; }
StringBuilder errorLog = new StringBuilder(); if (!errors.isEmpty()) {
StringBuilder updateQuery = new StringBuilder( throw new JobExecutionException(errors);
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status") }
+ ", amount,execution_time, error_log) VALUES ("); return RepeatStatus.FINISHED;
try { }
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) { private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from " boolean transferCompleted = true;
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); StringBuilder errorLog = new StringBuilder();
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage()); StringBuilder updateQuery = new StringBuilder(
} catch (final InsufficientAccountBalanceException e) { "INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id" + ", amount,execution_time, error_log) VALUES (");
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); try {
errorLog.append("InsufficientAccountBalance Exception "); accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final AbstractPlatformServiceUnavailableException e) { } catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from " errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage()); errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) { } catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e)); + instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage()); errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
} errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
updateQuery.append(instructionId).append(","); + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
if (errorLog.length() > 0) { errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
transferCompleted = false; } catch (Exception e) {
updateQuery.append("'failed'").append(","); errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
} else { + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
updateQuery.append("'success'").append(","); errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue()); }
updateQuery.append(", now(),"); updateQuery.append(instructionId).append(",");
updateQuery.append("'").append(errorLog).append("')"); if (errorLog.length() > 0) {
jdbcTemplate.update(updateQuery.toString()); transferCompleted = false;
return transferCompleted; updateQuery.append("'failed'").append(",");
} } else {
updateQuery.append("'success'").append(",");
public boolean isDueForTransfer(StandingInstructionDuesData standingInstructionDuesData) { }
return standingInstructionDuesData.dueDate() != null updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
&& !standingInstructionDuesData.dueDate().isAfter(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
public final class AccountTransferEnumerations { public final class AccountTransferEnumerations {
private AccountTransferEnumerations() { private AccountTransferEnumerations() {
} }
public static EnumOptionData accountType(final Integer type) { public static EnumOptionData accountType(final Integer type) {
return accountType(PortfolioAccountType.fromInt(type)); return accountType(PortfolioAccountType.fromInt(type));
} }
public static EnumOptionData accountType(final PortfolioAccountType type) { public static EnumOptionData accountType(final PortfolioAccountType type) {
EnumOptionData optionData = null; EnumOptionData optionData = null;
if (type != null) { if (type != null) {
switch (type) { switch (type) {
case INVALID: case INVALID:
break; break;
case LOAN: case LOAN:
optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(), optionData = new EnumOptionData(PortfolioAccountType.LOAN.getValue().longValue(), PortfolioAccountType.LOAN.getCode(),
"Loan Account"); "Loan Account");
break; break;
case SAVINGS: case SAVINGS:
optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(), optionData = new EnumOptionData(PortfolioAccountType.SAVINGS.getValue().longValue(),
PortfolioAccountType.SAVINGS.getCode(), "Savings Account"); PortfolioAccountType.SAVINGS.getCode(), "Savings Account");
break; break;
} case CURRENT:
} optionData = new EnumOptionData(PortfolioAccountType.CURRENT.getValue().longValue(),
PortfolioAccountType.CURRENT.getCode(), "Current Account");
return optionData; break;
} }
}
public static EnumOptionData recurrenceType(final Integer type) {
return recurrenceType(AccountTransferRecurrenceType.fromInt(type)); return optionData;
} }
public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) { public static EnumOptionData recurrenceType(final Integer type) {
EnumOptionData optionData = null; return recurrenceType(AccountTransferRecurrenceType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData recurrenceType(final AccountTransferRecurrenceType type) {
break; EnumOptionData optionData = null;
case PERIODIC: if (type != null) {
optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(), switch (type) {
AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence"); case INVALID:
break; break;
case AS_PER_DUES: case PERIODIC:
optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(), optionData = new EnumOptionData(AccountTransferRecurrenceType.PERIODIC.getValue().longValue(),
AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence"); AccountTransferRecurrenceType.PERIODIC.getCode(), "Periodic Recurrence");
break; break;
} case AS_PER_DUES:
} optionData = new EnumOptionData(AccountTransferRecurrenceType.AS_PER_DUES.getValue().longValue(),
return optionData; AccountTransferRecurrenceType.AS_PER_DUES.getCode(), "As Per Dues Recurrence");
} break;
}
public static EnumOptionData transferType(final Integer type) { }
return transferType(AccountTransferType.fromInt(type)); return optionData;
} }
public static EnumOptionData transferType(final AccountTransferType type) { public static EnumOptionData transferType(final Integer type) {
EnumOptionData optionData = null; return transferType(AccountTransferType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData transferType(final AccountTransferType type) {
break; EnumOptionData optionData = null;
case ACCOUNT_TRANSFER: if (type != null) {
optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(), switch (type) {
AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer"); case INVALID:
break; break;
case LOAN_REPAYMENT: case ACCOUNT_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.ACCOUNT_TRANSFER.getValue().longValue(),
AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment"); AccountTransferType.ACCOUNT_TRANSFER.getCode(), "Account Transfer");
break; break;
case CHARGE_PAYMENT: case LOAN_REPAYMENT:
optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.LOAN_REPAYMENT.getValue().longValue(),
AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment"); AccountTransferType.LOAN_REPAYMENT.getCode(), "Loan Repayment");
break; break;
case INTEREST_TRANSFER: case CHARGE_PAYMENT:
optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.CHARGE_PAYMENT.getValue().longValue(),
AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer"); AccountTransferType.CHARGE_PAYMENT.getCode(), "Charge Payment");
break; break;
case LOAN_DOWN_PAYMENT: case INTEREST_TRANSFER:
optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(), optionData = new EnumOptionData(AccountTransferType.INTEREST_TRANSFER.getValue().longValue(),
AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment"); AccountTransferType.INTEREST_TRANSFER.getCode(), "Interest Transfer");
break; break;
case LOAN_DOWN_PAYMENT:
} optionData = new EnumOptionData(AccountTransferType.LOAN_DOWN_PAYMENT.getValue().longValue(),
} AccountTransferType.LOAN_DOWN_PAYMENT.getCode(), "Loan Down Payment");
return optionData; break;
}
}
public static EnumOptionData standingInstructionPriority(final Integer type) { }
return standingInstructionPriority(StandingInstructionPriority.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) { public static EnumOptionData standingInstructionPriority(final Integer type) {
EnumOptionData optionData = null; return standingInstructionPriority(StandingInstructionPriority.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionPriority(final StandingInstructionPriority type) {
break; EnumOptionData optionData = null;
case HIGH: if (type != null) {
optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(), switch (type) {
StandingInstructionPriority.HIGH.getCode(), "High Priority"); case INVALID:
break; break;
case LOW: case HIGH:
optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.HIGH.getValue().longValue(),
StandingInstructionPriority.LOW.getCode(), "Low Priority"); StandingInstructionPriority.HIGH.getCode(), "High Priority");
break; break;
case MEDIUM: case LOW:
optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.LOW.getValue().longValue(),
StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority"); StandingInstructionPriority.LOW.getCode(), "Low Priority");
break; break;
case URGENT: case MEDIUM:
optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionPriority.MEDIUM.getValue().longValue(),
StandingInstructionPriority.URGENT.getCode(), "Urgent Priority"); StandingInstructionPriority.MEDIUM.getCode(), "Medium Priority");
break; break;
} case URGENT:
} optionData = new EnumOptionData(StandingInstructionPriority.URGENT.getValue().longValue(),
return optionData; StandingInstructionPriority.URGENT.getCode(), "Urgent Priority");
} break;
}
public static EnumOptionData standingInstructionStatus(final Integer type) { }
return standingInstructionStatus(StandingInstructionStatus.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) { public static EnumOptionData standingInstructionStatus(final Integer type) {
EnumOptionData optionData = null; return standingInstructionStatus(StandingInstructionStatus.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionStatus(final StandingInstructionStatus type) {
break; EnumOptionData optionData = null;
case ACTIVE: if (type != null) {
optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(), switch (type) {
StandingInstructionStatus.ACTIVE.getCode(), "Active"); case INVALID:
break; break;
case DELETED: case ACTIVE:
optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionStatus.ACTIVE.getValue().longValue(),
StandingInstructionStatus.DELETED.getCode(), "Deleted"); StandingInstructionStatus.ACTIVE.getCode(), "Active");
break; break;
case DISABLED: case DELETED:
optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionStatus.DELETED.getValue().longValue(),
StandingInstructionStatus.DISABLED.getCode(), "Disabled"); StandingInstructionStatus.DELETED.getCode(), "Deleted");
break; break;
} case DISABLED:
} optionData = new EnumOptionData(StandingInstructionStatus.DISABLED.getValue().longValue(),
return optionData; StandingInstructionStatus.DISABLED.getCode(), "Disabled");
} break;
}
public static EnumOptionData standingInstructionType(final Integer type) { }
return standingInstructionType(StandingInstructionType.fromInt(type)); return optionData;
} }
public static EnumOptionData standingInstructionType(final StandingInstructionType type) { public static EnumOptionData standingInstructionType(final Integer type) {
EnumOptionData optionData = null; return standingInstructionType(StandingInstructionType.fromInt(type));
if (type != null) { }
switch (type) {
case INVALID: public static EnumOptionData standingInstructionType(final StandingInstructionType type) {
break; EnumOptionData optionData = null;
case DUES: if (type != null) {
optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(), switch (type) {
StandingInstructionType.DUES.getCode(), "Dues"); case INVALID:
break; break;
case FIXED: case DUES:
optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(), optionData = new EnumOptionData(StandingInstructionType.DUES.getValue().longValue(),
StandingInstructionType.FIXED.getCode(), "Fixed"); StandingInstructionType.DUES.getCode(), "Dues");
break; break;
case FIXED:
} optionData = new EnumOptionData(StandingInstructionType.FIXED.getValue().longValue(),
} StandingInstructionType.FIXED.getCode(), "Fixed");
return optionData; break;
}
}
} }
return optionData;
}
}
@@ -1,396 +1,482 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO; import org.apache.fineract.portfolio.account.data.PortfolioAccountDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData; import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService { public class PortfolioAccountReadPlatformServiceImpl implements PortfolioAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
// mapper // mapper
private final PortfolioSavingsAccountMapper savingsAccountMapper; private final PortfolioSavingsAccountMapper savingsAccountMapper;
private final PortfolioLoanAccountMapper loanAccountMapper; private final PortfolioLoanAccountMapper loanAccountMapper;
private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper; private final PortfolioLoanAccountRefundByTransferMapper accountRefundByTransferMapper;
private final PortfolioCurrentAccountMapper currentAccountMapper;
public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.jdbcTemplate = jdbcTemplate; public PortfolioAccountReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
this.savingsAccountMapper = new PortfolioSavingsAccountMapper(); this.jdbcTemplate = jdbcTemplate;
this.loanAccountMapper = new PortfolioLoanAccountMapper(); this.savingsAccountMapper = new PortfolioSavingsAccountMapper();
this.accountRefundByTransferMapper = new PortfolioLoanAccountRefundByTransferMapper(sqlGenerator); 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) {
return retrieveOne(accountId, accountTypeId, null);
@Override }
public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
@Override
Object[] sqlParams = new Object[] { accountId }; public PortfolioAccountData retrieveOne(final Long accountId, final Integer accountTypeId, final String currencyCode) {
PortfolioAccountData accountData = null;
try { Object[] sqlParams = new Object[] { accountId };
String sql = null; PortfolioAccountData accountData = null;
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId); try {
switch (accountType) { String sql = null;
case INVALID: final PortfolioAccountType accountType = PortfolioAccountType.fromInt(accountTypeId);
break; switch (accountType) {
case LOAN: case INVALID:
break;
sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?"; case LOAN:
if (currencyCode != null) {
sql += " and la.currency_code = ?"; sql = "select " + this.loanAccountMapper.schema() + " where la.id = ?";
sqlParams = new Object[] { accountId, currencyCode }; if (currencyCode != null) {
} sql += " and la.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams); }
break;
case SAVINGS: accountData = this.jdbcTemplate.queryForObject(sql, this.loanAccountMapper, sqlParams);
sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?"; break;
if (currencyCode != null) { case SAVINGS:
sql += " and sa.currency_code = ?"; sql = "select " + this.savingsAccountMapper.schema() + " where sa.id = ?";
sqlParams = new Object[] { accountId, currencyCode }; if (currencyCode != null) {
} sql += " and sa.currency_code = ?";
sqlParams = new Object[] { accountId, currencyCode };
accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams); }
break;
} accountData = this.jdbcTemplate.queryForObject(sql, this.savingsAccountMapper, sqlParams);
} catch (final EmptyResultDataAccessException e) { break;
throw new AccountTransferNotFoundException(accountId, e); case CURRENT:
} sql = "select " + this.currentAccountMapper.schema() + " where ca.id = ?";
if (currencyCode != null) {
return accountData; sql += " and ca.currency_code = ?";
} sqlParams = new Object[] { accountId, currencyCode };
}
@Override
public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) { accountData = this.jdbcTemplate.queryForObject(sql, this.currentAccountMapper, sqlParams);
break;
final List<Object> sqlParams = new ArrayList<>(); }
// sqlParams.add(portfolioAccountDTO.getClientId()); } catch (final EmptyResultDataAccessException e) {
Collection<PortfolioAccountData> accounts = null; throw new AccountTransferNotFoundException(accountId, e);
String sql = null; }
long defaultAccountStatus = 300; // Active Status
if (portfolioAccountDTO.getAccountStatus() != null) { return accountData;
defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus(); }
}
final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId()); @Override
switch (accountType) { public Collection<PortfolioAccountData> retrieveAllForLookup(final PortfolioAccountDTO portfolioAccountDTO) {
case INVALID:
break; final List<Object> sqlParams = new ArrayList<>();
case LOAN: // sqlParams.add(portfolioAccountDTO.getClientId());
sql = "select " + this.loanAccountMapper.schema() + " where "; Collection<PortfolioAccountData> accounts = null;
if (portfolioAccountDTO.getClientId() != null) { String sql = null;
sql += " la.client_id = ? and la.loan_status_id in (?) "; long defaultAccountStatus = 300; // Active Status
sqlParams.add(portfolioAccountDTO.getClientId()); if (portfolioAccountDTO.getAccountStatus() != null) {
sqlParams.add(defaultAccountStatus); defaultAccountStatus = portfolioAccountDTO.getFirstAccountStatus();
} else { }
sql += " la.loan_status_id in (?) "; final PortfolioAccountType accountType = PortfolioAccountType.fromInt(portfolioAccountDTO.getAccountTypeId());
sqlParams.add(defaultAccountStatus); switch (accountType) {
} case INVALID:
if (portfolioAccountDTO.getCurrencyCode() != null) { break;
sql += " and la.currency_code = ?"; case LOAN:
sqlParams.add(portfolioAccountDTO.getCurrencyCode()); sql = "select " + this.loanAccountMapper.schema() + " where ";
} if (portfolioAccountDTO.getClientId() != null) {
sql += " la.client_id = ? and la.loan_status_id in (?) ";
accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR sqlParams.add(portfolioAccountDTO.getClientId());
break; sqlParams.add(defaultAccountStatus);
case SAVINGS: } else {
sql = "select " + this.savingsAccountMapper.schema() + " where "; sql += " la.loan_status_id in (?) ";
if (portfolioAccountDTO.getClientId() != null) { sqlParams.add(defaultAccountStatus);
sql += " sa.client_id = ? and sa.status_enum in (?) "; }
sqlParams.add(portfolioAccountDTO.getClientId()); if (portfolioAccountDTO.getCurrencyCode() != null) {
sqlParams.add(defaultAccountStatus); sql += " and la.currency_code = ?";
} else { sqlParams.add(portfolioAccountDTO.getCurrencyCode());
sql += " sa.status_enum in (?) "; }
sqlParams.add(defaultAccountStatus);
} accounts = this.jdbcTemplate.query(sql, this.loanAccountMapper, sqlParams.toArray()); // NOSONAR
if (portfolioAccountDTO.getCurrencyCode() != null) { break;
sql += " and sa.currency_code = ?"; case SAVINGS:
sqlParams.add(portfolioAccountDTO.getCurrencyCode()); sql = "select " + this.savingsAccountMapper.schema() + " where ";
} if (portfolioAccountDTO.getClientId() != null) {
sql += " sa.client_id = ? and sa.status_enum in (?) ";
if (portfolioAccountDTO.getDepositType() != null) { sqlParams.add(portfolioAccountDTO.getClientId());
sql += " and sa.deposit_type_enum = ?"; sqlParams.add(defaultAccountStatus);
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue()); } else {
} sql += " sa.status_enum in (?) ";
sqlParams.add(defaultAccountStatus);
if (portfolioAccountDTO.isExcludeOverDraftAccounts()) { }
sql += " and sa.allow_overdraft = false"; if (portfolioAccountDTO.getCurrencyCode() != null) {
} sql += " and sa.currency_code = ?";
sqlParams.add(portfolioAccountDTO.getCurrencyCode());
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) { }
sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId()); if (portfolioAccountDTO.getDepositType() != null) {
} sql += " and sa.deposit_type_enum = ?";
sqlParams.add(portfolioAccountDTO.getDepositType().shortValue());
accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR }
break;
} if (portfolioAccountDTO.isExcludeOverDraftAccounts()) {
sql += " and sa.allow_overdraft = false";
return accounts; }
}
if (portfolioAccountDTO.getClientId() == null && portfolioAccountDTO.getGroupId() != null) {
private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> { sql += " and sa.group_id = ? ";
sqlParams.add(portfolioAccountDTO.getGroupId());
private final String schemaSql; }
PortfolioSavingsAccountMapper() { accounts = this.jdbcTemplate.query(sql, this.savingsAccountMapper, sqlParams.toArray()); // NOSONAR
break;
final StringBuilder sqlBuilder = new StringBuilder(400); case CURRENT:
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, "); sql = "select " + this.currentAccountMapper.schema() + " where ";
sqlBuilder.append("c.id as clientId, c.display_name as clientName, "); if (portfolioAccountDTO.getClientId() != null) {
sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); sql += " ca.client_id = ? and ca.status_enum in (?) ";
sqlBuilder.append("sp.id as productId, sp.name as productName, "); sqlParams.add(portfolioAccountDTO.getClientId());
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); sqlParams.add(defaultAccountStatus);
sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,"); } else {
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, "); sql += " ca.status_enum in (?) ";
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); sqlParams.add(defaultAccountStatus);
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); }
sqlBuilder.append("from m_savings_account sa "); if (portfolioAccountDTO.getCurrencyCode() != null) {
sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); sql += " and ca.currency_code = ?";
sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); sqlParams.add(portfolioAccountDTO.getCurrencyCode());
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 "); accounts = this.jdbcTemplate.query(sql, this.currentAccountMapper, sqlParams.toArray()); // NOSONAR
break;
this.schemaSql = sqlBuilder.toString(); }
}
return accounts;
public String schema() { }
return this.schemaSql;
} private static final class PortfolioSavingsAccountMapper implements RowMapper<PortfolioAccountData> {
@Override private final String schemaSql;
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
PortfolioSavingsAccountMapper() {
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String externalId = rs.getString("externalId"); 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, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("sp.id as productId, sp.name as productName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final String clientName = rs.getString("clientName"); sqlBuilder.append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits,");
sqlBuilder.append("sa.currency_multiplesof as inMultiplesOf, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final String productName = rs.getString("productName"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_savings_account sa ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); 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 ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_staff s ON s.id = sa.field_officer_id ");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); this.schemaSql = sqlBuilder.toString();
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); }
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, public String schema() {
currencyNameCode); return this.schemaSql;
}
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, currency); @Override
} public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
}
final Long id = rs.getLong("id");
private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> { final String accountNo = rs.getString("accountNo");
final String externalId = rs.getString("externalId");
private final String schemaSql;
final Long groupId = JdbcSupport.getLong(rs, "groupId");
PortfolioLoanAccountMapper() { final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
final StringBuilder sqlBuilder = new StringBuilder(400); final String clientName = rs.getString("clientName");
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, "); final Long productId = rs.getLong("productId");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, "); final String productName = rs.getString("productName");
sqlBuilder.append("lp.id as productId, lp.name as productName, ");
sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, "); final Long fieldOfficerId = rs.getLong("fieldOfficerId");
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,"); final String fieldOfficerName = rs.getString("fieldOfficerName");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, "); final String currencyCode = rs.getString("currencyCode");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); final String currencyName = rs.getString("currencyName");
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); final String currencyNameCode = rs.getString("currencyNameCode");
sqlBuilder.append("from m_loan la "); final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id "); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
sqlBuilder.append("join m_currency curr on curr.code = la.currency_code "); final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
sqlBuilder.append("left join m_client c ON c.id = la.client_id "); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
sqlBuilder.append("left join m_group g ON g.id = la.group_id "); currencyNameCode);
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
this.schemaSql = sqlBuilder.toString(); fieldOfficerId, fieldOfficerName, currency);
} }
}
public String schema() {
return this.schemaSql; private static final class PortfolioLoanAccountMapper implements RowMapper<PortfolioAccountData> {
}
private final String schemaSql;
@Override
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { PortfolioLoanAccountMapper() {
final Long id = rs.getLong("id"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String accountNo = rs.getString("accountNo"); sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, ");
final String externalId = rs.getString("externalId"); sqlBuilder.append("c.id as clientId, c.display_name as clientName, ");
sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("lp.id as productId, lp.name as productName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
final String clientName = rs.getString("clientName"); sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
sqlBuilder.append("la.total_overpaid_derived as totalOverpaid, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final String productName = rs.getString("productName"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
sqlBuilder.append("from m_loan la ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); 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 ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); this.schemaSql = sqlBuilder.toString();
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); }
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid"); public String schema() {
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, return this.schemaSql;
currencyNameCode); }
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, @Override
fieldOfficerId, fieldOfficerName, currency, amtForTransfer); 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");
private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> { final String externalId = rs.getString("externalId");
private String schemaSql; final Long groupId = JdbcSupport.getLong(rs, "groupId");
private final DatabaseSpecificSQLGenerator sqlGenerator; final String groupName = rs.getString("groupName");
final Long clientId = JdbcSupport.getLong(rs, "clientId");
PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) { final String clientName = rs.getString("clientName");
this.sqlGenerator = sqlGenerator;
} final Long productId = rs.getLong("productId");
final String productName = rs.getString("productName");
public String schema() {
final StringBuilder amountQueryString = new StringBuilder(400); final Long fieldOfficerId = rs.getLong("fieldOfficerId");
amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +"); final String fieldOfficerName = rs.getString("fieldOfficerName");
amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); final String currencyCode = rs.getString("currencyCode");
amountQueryString.append(" SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) as total_in_advance_derived"); final String currencyName = rs.getString("currencyName");
amountQueryString.append(" from m_loan ml INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id"); final String currencyNameCode = rs.getString("currencyNameCode");
amountQueryString.append(" where ml.id=? and ml.loan_status_id = 300"); final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
amountQueryString.append(" and mr.duedate >= " + sqlGenerator.currentBusinessDate() + " group by ml.id having"); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
amountQueryString.append(" (SUM(COALESCE(mr.principal_completed_derived, 0)) + "); final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + "); final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + "); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid "); currencyNameCode);
final StringBuilder sqlBuilder = new StringBuilder(400); return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
sqlBuilder.append("la.id as id, la.account_no as accountNo, la.external_id as externalId, "); fieldOfficerId, fieldOfficerName, currency, amtForTransfer);
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, "); private static final class PortfolioLoanAccountRefundByTransferMapper implements RowMapper<PortfolioAccountData> {
sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, "); private String schemaSql;
sqlBuilder.append(amountQueryString.toString()); private final DatabaseSpecificSQLGenerator sqlGenerator;
sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, "); PortfolioLoanAccountRefundByTransferMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol "); this.sqlGenerator = sqlGenerator;
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 "); public String schema() {
sqlBuilder.append("left join m_client c ON c.id = la.client_id "); final StringBuilder amountQueryString = new StringBuilder(400);
sqlBuilder.append("left join m_group g ON g.id = la.group_id "); amountQueryString.append("(select (SUM(COALESCE(mr.principal_completed_derived, 0)) +");
sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id "); amountQueryString.append("SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
this.schemaSql = sqlBuilder.toString(); 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");
return this.schemaSql; 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)) + ");
@Override amountQueryString.append(" SUM(COALESCE(mr.interest_completed_derived, 0)) + ");
public PortfolioAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { amountQueryString.append("SUM(COALESCE(mr.fee_charges_completed_derived, 0)) + ");
amountQueryString.append("SUM(COALESCE(mr.penalty_charges_completed_derived, 0))) > 0) as totalOverpaid ");
final Long id = rs.getLong("id");
final String accountNo = rs.getString("accountNo"); final StringBuilder sqlBuilder = new StringBuilder(400);
final String externalId = rs.getString("externalId"); 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, ");
final Long groupId = JdbcSupport.getLong(rs, "groupId"); sqlBuilder.append("g.id as groupId, g.display_name as groupName, ");
final String groupName = rs.getString("groupName"); sqlBuilder.append("lp.id as productId, lp.name as productName, ");
final Long clientId = JdbcSupport.getLong(rs, "clientId"); sqlBuilder.append("s.id as fieldOfficerId, s.display_name as fieldOfficerName, ");
final String clientName = rs.getString("clientName"); sqlBuilder.append("la.currency_code as currencyCode, la.currency_digits as currencyDigits,");
sqlBuilder.append("la.currency_multiplesof as inMultiplesOf, ");
final Long productId = rs.getLong("productId"); sqlBuilder.append(amountQueryString.toString());
final String productName = rs.getString("productName"); sqlBuilder.append(", ");
sqlBuilder.append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ");
final Long fieldOfficerId = rs.getLong("fieldOfficerId"); sqlBuilder.append("curr.display_symbol as currencyDisplaySymbol ");
final String fieldOfficerName = rs.getString("fieldOfficerName"); sqlBuilder.append("from m_loan la ");
sqlBuilder.append("join m_product_loan lp ON la.product_id = lp.id ");
final String currencyCode = rs.getString("currencyCode"); sqlBuilder.append("join m_currency curr on curr.code = la.currency_code ");
final String currencyName = rs.getString("currencyName"); sqlBuilder.append("left join m_client c ON c.id = la.client_id ");
final String currencyNameCode = rs.getString("currencyNameCode"); sqlBuilder.append("left join m_group g ON g.id = la.group_id ");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); sqlBuilder.append("left join m_staff s ON s.id = la.loan_officer_id ");
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); this.schemaSql = sqlBuilder.toString();
final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol, return this.schemaSql;
currencyNameCode); }
return new PortfolioAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, @Override
fieldOfficerId, fieldOfficerName, currency, amtForTransfer); 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");
@Override final String externalId = rs.getString("externalId");
public PortfolioAccountData retrieveOneByPaidInAdvance(Long accountId, Integer accountTypeId) {
// TODO Auto-generated method stub final Long groupId = JdbcSupport.getLong(rs, "groupId");
Object[] sqlParams = new Object[] { accountId, accountId }; final String groupName = rs.getString("groupName");
PortfolioAccountData accountData = null; final Long clientId = JdbcSupport.getLong(rs, "clientId");
// String currencyCode = null; final String clientName = rs.getString("clientName");
try {
String sql = null; final Long productId = rs.getLong("productId");
// final PortfolioAccountType accountType = final String productName = rs.getString("productName");
// PortfolioAccountType.fromInt(accountTypeId);
final Long fieldOfficerId = rs.getLong("fieldOfficerId");
sql = "select " + this.accountRefundByTransferMapper.schema() + " where la.id = ?"; final String fieldOfficerName = rs.getString("fieldOfficerName");
/*
* if (currencyCode != null) { sql += " and la.currency_code = ?"; sqlParams = new Object[] {accountId , final String currencyCode = rs.getString("currencyCode");
* accountId,currencyCode }; } final String currencyName = rs.getString("currencyName");
*/ final String currencyNameCode = rs.getString("currencyNameCode");
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
accountData = this.jdbcTemplate.queryForObject(sql, this.accountRefundByTransferMapper, sqlParams); final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
final Integer inMulitplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
} catch (final EmptyResultDataAccessException e) { final BigDecimal amtForTransfer = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverpaid");
throw new AccountTransferNotFoundException(accountId, e); final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMulitplesOf, currencyDisplaySymbol,
} currencyNameCode);
return accountData; 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.service; package org.apache.fineract.portfolio.account.service;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromAccountTypeParamName; 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.fromClientIdParamName;
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName; import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName; import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.account.PortfolioAccountType; import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants; import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException; import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException; import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService { public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private final StandingInstructionDataValidator standingInstructionDataValidator; private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler; private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository; private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository; private final StandingInstructionRepository standingInstructionRepository;
@Transactional @Transactional
@Override @Override
public CommandProcessingResult create(final JsonCommand command) { public CommandProcessingResult create(final JsonCommand command) {
this.standingInstructionDataValidator.validateForCreate(command); this.standingInstructionDataValidator.validateForCreate(command);
final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName); final Integer fromAccountTypeId = command.integerValueSansLocaleOfParameterNamed(fromAccountTypeParamName);
final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId); final PortfolioAccountType fromAccountType = PortfolioAccountType.fromInt(fromAccountTypeId);
final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName); final Integer toAccountTypeId = command.integerValueSansLocaleOfParameterNamed(toAccountTypeParamName);
final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId); final PortfolioAccountType toAccountType = PortfolioAccountType.fromInt(toAccountTypeId);
final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName); final Long fromClientId = command.longValueOfParameterNamed(fromClientIdParamName);
Long standingInstructionId = null; Long standingInstructionId = null;
try { try {
if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) { if (isSavingsToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler final AccountTransferDetails standingInstruction = this.standingInstructionAssembler
.assembleSavingsToSavingsTransfer(command); .assembleSavingsToSavingsTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) { } else if (isSavingsToLoanAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToLoanTransfer(command);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) { } else if (isLoanToSavingsAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command);
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleLoanToSavingsTransfer(command); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
this.accountTransferDetailRepository.saveAndFlush(standingInstruction); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId(); } else if (isCurrentToCurrentAccountTransfer(fromAccountType, toAccountType)) {
final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToCurrentTransfer(command);
} this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} catch (final JpaSystemException | DataIntegrityViolationException dve) { standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
final Throwable throwable = dve.getMostSpecificCause(); } else if (isCurrentToSavingsAccountTransfer(fromAccountType, toAccountType)) {
handleDataIntegrityIssues(command, throwable, dve); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToSavingsTransfer(command);
return CommandProcessingResult.empty(); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId) } else if (isSavingsToCurrentAccountTransfer(fromAccountType, toAccountType)) {
.withClientId(fromClientId); final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleSavingsToCurrentTransfer(command);
return builder.build(); this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
} standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
} else if (isCurrentToLoanAccountTransfer(fromAccountType, toAccountType)) {
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) { final AccountTransferDetails standingInstruction = this.standingInstructionAssembler.assembleCurrentToLoanTransfer(command);
if (realCause.getMessage().contains("name")) { this.accountTransferDetailRepository.saveAndFlush(standingInstruction);
final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName); standingInstructionId = standingInstruction.accountTransferStandingInstruction().getId();
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name", }
"Standinginstruction with name `" + name + "` already exists", "name", name); } catch (final JpaSystemException | DataIntegrityViolationException dve) {
} final Throwable throwable = dve.getMostSpecificCause();
log.error("Error occured.", dve); handleDataIntegrityIssues(command, throwable, dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue", return CommandProcessingResult.empty();
"Unknown data integrity issue with resource: " + realCause.getMessage()); }
} final CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder().withEntityId(standingInstructionId)
.withClientId(fromClientId);
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { return builder.build();
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount(); }
}
private void handleDataIntegrityIssues(final JsonCommand command, Throwable realCause, final NonTransientDataAccessException dve) {
private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) { if (realCause.getMessage().contains("name")) {
return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount(); final String name = command.stringValueOfParameterNamed(StandingInstructionApiConstants.nameParamName);
} throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, }
final PortfolioAccountType toAccountType) { log.error("Error occured.", dve);
return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount(); throw ErrorHandler.getMappable(dve, "error.msg.client.unknown.data.integrity.issue",
} "Unknown data integrity issue with resource: " + realCause.getMessage());
}
@Override
public CommandProcessingResult update(final Long id, final JsonCommand command) { private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
this.standingInstructionDataValidator.validateForUpdate(command); return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id) }
.orElseThrow(() -> new StandingInstructionNotFoundException(id));
final Map<String, Object> actualChanges = standingInstructionsForUpdate.update(command); private boolean isSavingsToLoanAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).with(actualChanges).build(); return fromAccountType.isSavingsAccount() && toAccountType.isLoanAccount();
} }
@Override private boolean isSavingsToSavingsAccountTransfer(final PortfolioAccountType fromAccountType,
public CommandProcessingResult delete(final Long id) { final PortfolioAccountType toAccountType) {
AccountTransferStandingInstruction standingInstructionsForUpdate = this.standingInstructionRepository.findById(id).orElseThrow(); return fromAccountType.isSavingsAccount() && toAccountType.isSavingsAccount();
// update the "deleted" and "name" properties of the standing }
// instruction
standingInstructionsForUpdate.delete(); private boolean isCurrentToCurrentAccountTransfer(final PortfolioAccountType fromAccountType,
final PortfolioAccountType toAccountType) {
final Map<String, Object> actualChanges = new HashMap<>(); return fromAccountType.isCurrentAccount() && toAccountType.isCurrentAccount();
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue()); }
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
} 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.starter; package org.apache.fineract.portfolio.account.starter;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler; import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository; import org.apache.fineract.portfolio.account.data.AccountTransfersDataValidator;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository; import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler; import org.apache.fineract.portfolio.account.domain.AccountTransferAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper; import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.mapper.AccountTransfersMapper;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.PortfolioAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl; import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionHistoryReadPlatformServiceImpl;
import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformServiceImpl;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.account.service.StandingInstructionWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.springframework.context.annotation.Bean; import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.context.annotation.Configuration; import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@Configuration import org.springframework.context.annotation.Configuration;
public class AccountConfiguration { import org.springframework.jdbc.core.JdbcTemplate;
@Bean @Configuration
@ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class) public class AccountConfiguration {
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate); @Bean
} @ConditionalOnMissingBean(AccountAssociationsReadPlatformService.class)
public AccountAssociationsReadPlatformService accountAssociationsReadPlatformService(JdbcTemplate jdbcTemplate) {
@Bean return new AccountAssociationsReadPlatformServiceImpl(jdbcTemplate);
@ConditionalOnMissingBean(AccountTransfersReadPlatformService.class) }
public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, @Bean
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator, @ConditionalOnMissingBean(AccountTransfersReadPlatformService.class)
DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper, public AccountTransfersReadPlatformService accountTransfersReadPlatformService(JdbcTemplate jdbcTemplate,
SqlValidator sqlValidator) { ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, ColumnValidator columnValidator,
portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator); DatabaseSpecificSQLGenerator sqlGenerator, AccountTransfersMapper accountTransfersMapper, PaginationHelper paginationHelper,
} SqlValidator sqlValidator) {
return new AccountTransfersReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
@Bean portfolioAccountReadPlatformService, columnValidator, sqlGenerator, accountTransfersMapper, paginationHelper, sqlValidator);
@ConditionalOnMissingBean(AccountTransfersWritePlatformService.class) }
public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler, @Bean
AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler, @ConditionalOnMissingBean(AccountTransfersWritePlatformService.class)
SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler, public AccountTransfersWritePlatformService accountTransfersWritePlatformService(
LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService, AccountTransfersDataValidator accountTransfersDataValidator, AccountTransferAssembler accountTransferAssembler,
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, AccountTransferRepository accountTransferRepository, SavingsAccountAssembler savingsAccountAssembler,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, SavingsAccountDomainService savingsAccountDomainService, LoanAssembler loanAccountAssembler,
FineractProperties fineractProperties) { LoanAccountDomainService loanAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties); CurrentAccountTransactionRepository currentAccountTransactionRepository,
} AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
@Bean accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
@ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class) loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate, gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
DatabaseSpecificSQLGenerator sqlGenerator) { currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator); }
}
@Bean
@Bean @ConditionalOnMissingBean(PortfolioAccountReadPlatformService.class)
@ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class) public PortfolioAccountReadPlatformService portfolioAccountReadPlatformService(JdbcTemplate jdbcTemplate,
public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate, DatabaseSpecificSQLGenerator sqlGenerator) {
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { return new PortfolioAccountReadPlatformServiceImpl(jdbcTemplate, sqlGenerator);
return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper); }
}
@Bean
@Bean @ConditionalOnMissingBean(StandingInstructionHistoryReadPlatformService.class)
@ConditionalOnMissingBean(StandingInstructionReadPlatformService.class) public StandingInstructionHistoryReadPlatformService standingInstructionHistoryReadPlatformService(JdbcTemplate jdbcTemplate,
public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate, ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService, return new StandingInstructionHistoryReadPlatformServiceImpl(jdbcTemplate, columnValidator, sqlGenerator, paginationHelper);
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService, }
DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { @Bean
return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService, @ConditionalOnMissingBean(StandingInstructionReadPlatformService.class)
portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper); public StandingInstructionReadPlatformService standingInstructionReadPlatformService(JdbcTemplate jdbcTemplate,
} ClientReadPlatformService clientReadPlatformService, OfficeReadPlatformService officeReadPlatformService,
PortfolioAccountReadPlatformService portfolioAccountReadPlatformService,
@Bean DropdownReadPlatformService dropdownReadPlatformService, ColumnValidator columnValidator,
@ConditionalOnMissingBean(StandingInstructionWritePlatformService.class) DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
public StandingInstructionWritePlatformService standingInstructionWritePlatformService( return new StandingInstructionReadPlatformServiceImpl(jdbcTemplate, clientReadPlatformService, officeReadPlatformService,
StandingInstructionDataValidator standingInstructionDataValidator, StandingInstructionAssembler standingInstructionAssembler, portfolioAccountReadPlatformService, dropdownReadPlatformService, columnValidator, sqlGenerator, paginationHelper);
AccountTransferDetailRepository accountTransferDetailRepository, StandingInstructionRepository standingInstructionRepository) { }
return new StandingInstructionWritePlatformServiceImpl(standingInstructionDataValidator, standingInstructionAssembler,
accountTransferDetailRepository, standingInstructionRepository); @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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.data; package org.apache.fineract.portfolio.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName; 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.bankNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName; 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.closedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName; 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.paymentTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName; 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.routingCodeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName; 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.transactionAccountNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType; import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class DepositAccountTransactionDataValidator { public class DepositAccountTransactionDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList( private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName, DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName, transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName, DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData, DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName)); DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown"));
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName)); DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName, Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName, DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName)); bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>( private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName)); Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
@Autowired @Autowired
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) { public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
} }
public void validate(final JsonCommand command, DepositAccountType depositAccountType) { public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName()); .resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element); final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull(); baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element); final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount(); baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
// Validate all string payment detail fields for max length // Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName)); routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) { for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50); .notExceedingLengthOf(50);
} }
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateDepositAmountUpdate(final JsonCommand command) { public void validateDepositAmountUpdate(final JsonCommand command) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS); DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element); final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull(); baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element); .extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName) baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount(); .value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateActivation(final JsonCommand command) { public void validateActivation(final JsonCommand command) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS); SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); .resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element); final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull(); baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) { public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS); DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName()); .resource(depositAccountType.resourceName());
final JsonElement element = this.fromApiJsonHelper.parse(json); final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull(); baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) { public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
final String json = command.json(); final String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName()); .resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson(); final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element); final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull(); baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element); final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank() baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); .isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
if (onAccountClosureId != null) { if (onAccountClosureId != null) {
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId); final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
if (accountOnClosureType.isTransferToSavings()) { if (accountOnClosureType.isTransferToSavings()) {
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element); final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId) baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName, .cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode()); DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) { } else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId) baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close"); .failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
} }
} }
// Validate all string payment detail fields for max length // Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element); final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName, final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName)); routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) { for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50); .notExceedingLengthOf(50);
} }
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) { private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
// //
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors); 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.expectedFirstDepositOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName; 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.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.mandatoryRecommendedDepositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maturityInstructionIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
@@ -230,6 +232,16 @@ public class DepositAccountAssembler {
interestRate = product.nominalAnnualInterestRate(); 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; SavingsCompoundingInterestPeriodType interestCompoundingPeriodType = null;
final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName); final Integer interestPeriodTypeValue = command.integerValueOfParameterNamed(interestCompoundingPeriodTypeParamName);
if (interestPeriodTypeValue != null) { if (interestPeriodTypeValue != null) {
@@ -1,75 +1,75 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
*/ */
package org.apache.fineract.portfolio.savings.domain; package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Entity @Entity
@Table(name = "m_fd_base_interest_rate_effective") @Table(name = "m_cbs_base_interest_rate_effective")
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public class FdBaseInterestRateEffective { public class FdBaseInterestRateEffective {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "fd_type", length = 20) @Column(name = "fd_type", length = 20)
private String fdType; private String fdType;
@Column(name = "version_label") @Column(name = "version_label")
private String versionLabel; private String versionLabel;
@Column(name = "effective_from_date") @Column(name = "effective_from_date")
private LocalDate effectiveFromDate; private LocalDate effectiveFromDate;
@Column(name = "effective_to_date") @Column(name = "effective_to_date")
private LocalDate effectiveToDate; private LocalDate effectiveToDate;
@Column(name = "is_active") @Column(name = "is_active")
private Boolean isActive; private Boolean isActive;
@Column(name = "remarks") @Column(name = "remarks")
private String remarks; private String remarks;
@Column(name = "created_by_id") @Column(name = "created_by_id")
private Long createdById; private Long createdById;
@Column(name = "created_date") @Column(name = "created_date")
private LocalDate createdDate; private LocalDate createdDate;
public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks, public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks,
Long createdById) { Long createdById) {
FdBaseInterestRateEffective e = new FdBaseInterestRateEffective(); FdBaseInterestRateEffective e = new FdBaseInterestRateEffective();
e.fdType = fdType; e.fdType = fdType;
e.versionLabel = versionLabel; e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate; e.effectiveFromDate = effectiveFromDate;
e.isActive = true; e.isActive = true;
e.remarks = remarks; e.remarks = remarks;
e.createdById = createdById; e.createdById = createdById;
e.createdDate = LocalDate.now(); e.createdDate = LocalDate.now();
return e; return e;
} }
public void deactivate(LocalDate toDate) { public void deactivate(LocalDate toDate) {
this.isActive = false; this.isActive = false;
this.effectiveToDate = toDate; this.effectiveToDate = toDate;
} }
} }
@@ -1,76 +1,76 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
*/ */
package org.apache.fineract.portfolio.savings.domain; package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Entity @Entity
@Table(name = "m_fd_base_interest_rate_slab_history") @Table(name = "m_cbs_base_interest_rate_slab_history")
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public class FdBaseInterestRateSlabHistory { public class FdBaseInterestRateSlabHistory {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "effective_id") @Column(name = "effective_id")
private Long effectiveId; private Long effectiveId;
@Column(name = "sr_no") @Column(name = "sr_no")
private Integer srNo; private Integer srNo;
@Column(name = "description") @Column(name = "description")
private String description; private String description;
@Column(name = "period_type_enum") @Column(name = "period_type_enum")
private Integer periodTypeEnum; private Integer periodTypeEnum;
@Column(name = "from_period") @Column(name = "from_period")
private Integer fromPeriod; private Integer fromPeriod;
@Column(name = "to_period") @Column(name = "to_period")
private Integer toPeriod; private Integer toPeriod;
@Column(name = "annual_interest_rate") @Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate; private BigDecimal annualInterestRate;
@Column(name = "senior_citizen_rate") @Column(name = "senior_citizen_rate")
private BigDecimal seniorCitizenRate; private BigDecimal seniorCitizenRate;
@Column(name = "created_date") @Column(name = "created_date")
private LocalDate createdDate; private LocalDate createdDate;
public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum, public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) { Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) {
FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory(); FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory();
s.effectiveId = effectiveId; s.effectiveId = effectiveId;
s.srNo = srNo; s.srNo = srNo;
s.description = description; s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1; s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod; s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod; s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate; s.annualInterestRate = annualInterestRate;
s.seniorCitizenRate = seniorCitizenRate; s.seniorCitizenRate = seniorCitizenRate;
s.createdDate = LocalDate.now(); s.createdDate = LocalDate.now();
return s; return s;
} }
} }
@@ -1,333 +1,356 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.domain; package org.apache.fineract.portfolio.savings.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent; 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.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService { public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
@Autowired @Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository, final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService, final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context, final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) { final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository; this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper; this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService; this.configurationDomainService = configurationDomainService;
this.context = context; this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository; this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService; this.businessEventNotifierService = businessEventNotifierService;
} }
@Transactional @Transactional
@Override @Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser(); context.authenticatedUser();
account.validateForAccountBlock(); account.validateForAccountBlock();
account.validateForDebitBlock(); account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd(); .isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) { if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType()); throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
} }
final Set<Long> existingTransactionIds = new HashSet<>(); final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null; final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) { if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else { } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
} }
Integer accountType = null; Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType); paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID(); UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(), final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString()); backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64; final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate(); final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd, account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else { } else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(), account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals); postReversals);
} }
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null; List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
} }
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(), account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal); saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) { if (backdatedTxnsAllowedTill) {
// Update transactions separately // Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
} }
this.savingsAccountRepository.save(account); this.savingsAccountRepository.save(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(), if (!transactionBooleanValues.isSkipAccountingPostings()) {
backdatedTxnsAllowedTill); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal)); }
return withdrawal;
} businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
@Transactional }
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, @Transactional
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, @Override
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) { public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
savingsAccountTransactionType, backdatedTxnsAllowedTill); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
} 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, @Override
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
context.authenticatedUser(); final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
account.validateForAccountBlock(); final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
account.validateForCreditBlock(); final boolean skipAccountingPostings) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
// Global configurations return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
.isSavingsInterestPostingAtCurrentPeriodEnd(); }
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
if (isRegularTransaction && !account.allowDeposit()) { final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType()); final boolean isAccountTransfer, final boolean isRegularTransaction,
} final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
boolean isInterestTransfer = false; return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
final Set<Long> existingTransactionIds = new HashSet<>(); savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
final Set<Long> existingReversedTransactionIds = new HashSet<>(); }
if (backdatedTxnsAllowedTill) { private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
} else { final boolean isAccountTransfer, final boolean isRegularTransaction,
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
} final boolean skipAccountingPostings) {
context.authenticatedUser();
Integer accountType = null; account.validateForAccountBlock();
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, account.validateForCreditBlock();
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID(); // Global configurations
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill, final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
relaxingDaysConfigForPivotDate, refNo.toString()); .isSavingsInterestPostingAtCurrentPeriodEnd();
final LocalDate postInterestOnDate = null; final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final MathContext mc = MathContext.DECIMAL64; final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
final LocalDate today = DateUtils.getBusinessLocalDate(); throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); }
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { boolean isInterestTransfer = false;
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, final Set<Long> existingTransactionIds = new HashSet<>();
postInterestOnDate, backdatedTxnsAllowedTill, postReversals); final Set<Long> existingReversedTransactionIds = new HashSet<>();
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, if (backdatedTxnsAllowedTill) {
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
saveTransactionToGenerateTransactionId(deposit); }
if (backdatedTxnsAllowedTill) { Integer accountType = null;
// Update transactions separately final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); paymentDetail, null, accountType);
} UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
this.savingsAccountRepository.saveAndFlush(account); relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); final MathContext mc = MathContext.DECIMAL64;
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit; final LocalDate today = DateUtils.getBusinessLocalDate();
} boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
@Transactional account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
@Override postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate, } else {
Boolean lienAllowed) { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
Money.of(account.getCurrency(), amount), lienAllowed); }
}
saveTransactionToGenerateTransactionId(deposit);
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate, if (backdatedTxnsAllowedTill) {
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) { // Update transactions separately
final DateTimeFormatter fmt = null; saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
final PaymentDetail paymentDetail = null; }
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true; this.savingsAccountRepository.saveAndFlush(account);
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, if (!skipAccountingPostings) {
savingsAccountTransactionType, backdatedTxnsAllowedTill); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
} }
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds, return deposit;
Set<Long> existingReversedTransactionIds) { }
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); @Transactional
} @Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { Boolean lienAllowed) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction); return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
return transaction.getId(); Money.of(account.getCurrency(), amount), lienAllowed);
} }
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) { @Override
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions); public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
} final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds, final PaymentDetail paymentDetail = null;
Set<Long> existingReversedTransactionIds) { final boolean isAccountTransfer = false;
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); final boolean isRegularTransaction = true;
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
} return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds, }
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), Set<Long> existingReversedTransactionIds) {
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); existingTransactionIds.addAll(account.findExistingTransactionIds());
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
} }
@Transactional private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
@Override this.savingsAccountTransactionRepository.saveAndFlush(transaction);
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds, return transaction.getId();
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { }
final boolean isAccountTransfer = false; private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
} }
@Override private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions, Set<Long> existingReversedTransactionIds) {
boolean backdatedTxnsAllowedTill) { existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService }
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>(); final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
final Set<Long> existingReversedTransactionIds = new HashSet<>(); existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
if (backdatedTxnsAllowedTill) { }
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else { @Transactional
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @Override
} public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
List<SavingsAccountTransaction> newTransactions = new ArrayList<>(); final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
SavingsAccountTransaction reversal = null;
final boolean isAccountTransfer = false;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null; postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { }
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid(); @Override
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet); public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
account.undoTransaction(savingsAccountTransaction); boolean backdatedTxnsAllowedTill) {
if (postReversals) {
newTransactions.add(reversal); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
} .isSavingsInterestPostingAtCurrentPeriodEnd();
} final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
boolean isInterestTransfer = false; final boolean postReversals = true;
LocalDate postInterestOnDate = null; final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate today = DateUtils.getBusinessLocalDate(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { if (backdatedTxnsAllowedTill) {
if (savingsAccountTransaction.isPostInterestCalculationRequired() updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) { } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, }
postInterestOnDate, backdatedTxnsAllowedTill, postReversals); List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
} else { SavingsAccountTransaction reversal = null;
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
} for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill, reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
relaxingDaysConfigForPivotDate, "savingsaccount"); chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false); reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.activateAccountBasedOnBalance(); account.undoTransaction(savingsAccountTransaction);
} if (postReversals) {
this.savingsAccountRepository.save(account); newTransactions.add(reversal);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig()); }
this.savingsAccountTransactionRepository.saveAll(newTransactions); }
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
boolean isInterestTransfer = false;
return reversal; 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.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
private final CurrentProductRepository currentProductRepository; private final CurrentProductRepository currentProductRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final JournalEntryRepository journalEntryRepository; private final JournalEntryRepository journalEntryRepository;
private final DenominationBreakdownWriter denominationBreakdownWriter;
@Transactional @Transactional
@Override @Override
@@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount"); final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "deposit"); validatePositiveAmount(amount, accountId, "deposit");
this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT");
final String refNo = command.stringValueOfParameterNamed("accountNumber"); final String refNo = command.stringValueOfParameterNamed("accountNumber");
@@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
currentAccountTransactionRepository.saveAndFlush(txn); 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.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount)); account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
currentAccountRepository.save(account); currentAccountRepository.save(account);
@@ -135,6 +143,14 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount)); account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
currentAccountRepository.save(account); 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) // Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal"); 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.SavingsProductFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; 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.FacilityChargeAutoApplyService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl; 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.DepositAccountPreMatureCalculationPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl; 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.DepositAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService; import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
@@ -267,7 +269,8 @@ public class SavingsConfiguration {
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository, DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository, ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
WorkingDaysRepositoryWrapper workingDaysRepository, WorkingDaysRepositoryWrapper workingDaysRepository,
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
DenominationBreakdownWriter denominationBreakdownWriter
) { ) {
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper, return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
@@ -276,7 +279,8 @@ public class SavingsConfiguration {
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService, journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService, chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository, accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository); configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
denominationBreakdownWriter);
} }
@Bean @Bean
@@ -424,7 +428,8 @@ public class SavingsConfiguration {
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService, StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService, GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) { ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
DenominationBreakdownWriter denominationBreakdownWriter) {
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper, return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator, staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService, savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
@@ -432,7 +437,7 @@ public class SavingsConfiguration {
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService, chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository, depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService, standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
errorHandler, facilityChargeAutoApplyService); errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
} }
@Bean @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/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/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0299_seed_indian_states.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> </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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings; package org.apache.fineract.portfolio.savings;
public class SavingsTransactionBooleanValues { public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer; private final boolean isAccountTransfer;
private final boolean isRegularTransaction; private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee; private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer; private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck; 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) { public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this.isAccountTransfer = isAccountTransfer; this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false);
this.isRegularTransaction = isRegularTransaction; }
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer; public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck; final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck,
} final boolean skipAccountingPostings) {
this.isAccountTransfer = isAccountTransfer;
public boolean isAccountTransfer() { this.isRegularTransaction = isRegularTransaction;
return this.isAccountTransfer; this.isApplyWithdrawFee = isApplyWithdrawFee;
} this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
public boolean isRegularTransaction() { this.skipAccountingPostings = skipAccountingPostings;
return this.isRegularTransaction; }
}
public boolean isAccountTransfer() {
public boolean isApplyWithdrawFee() { return this.isAccountTransfer;
return this.isApplyWithdrawFee; }
}
public boolean isRegularTransaction() {
public boolean isInterestTransfer() { return this.isRegularTransaction;
return this.isInterestTransfer; }
}
public boolean isApplyWithdrawFee() {
public boolean isExceptionForBalanceCheck() { return this.isApplyWithdrawFee;
return this.isExceptionForBalanceCheck; }
}
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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.data; package org.apache.fineract.portfolio.savings.data;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.SavingsApiConstants;
public class SavingsAccountConstant extends SavingsApiConstants { public class SavingsAccountConstant extends SavingsApiConstants {
/** /**
* These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to * 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. * get response parameters to match those of request parameters.
*/ */
protected static final Set<String> SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, protected static final Set<String> SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName, dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName,
externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName,
nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName,
interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName,
// withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, // withdrawalFeeAmountParamName, withdrawalFeeTypeParamName,
withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName, withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName,
overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName, overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName,
maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName,
withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication, withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication,
// CBS Account fields // CBS Account fields
"accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName, "accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName,
specialCategoryParamName, weakerSectionParamName, specialCategoryParamName, weakerSectionParamName,
guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName, guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName,
guardianClientIdParamName, minorDateOfBirthParamName, guardianClientIdParamName, minorDateOfBirthParamName,
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName, accountFacilityConfigParamName, jointHoldersParamName,
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName));
/** /**
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to * 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. * 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, protected static final Set<String> SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName, dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName,
retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction, retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction,
lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName)); 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_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_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, protected static final Set<String> SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName,
receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure)); 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, protected static final Set<String> SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>(
feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); 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)); 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") @Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
private Boolean autoDebitFacility = 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) @jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY)
private SavingsAccountFacilityConfig facilityConfig; private SavingsAccountFacilityConfig facilityConfig;
@@ -4041,4 +4047,16 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
public Boolean getAutoDebitFacility() { return autoDebitFacility; } public Boolean getAutoDebitFacility() { return autoDebitFacility; }
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.service; package org.apache.fineract.portfolio.savings.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService { public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues, BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill); boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill); boolean backdatedTxnsAllowedTill);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds, SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
boolean backdatedTxnsAllowedTill); BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill); void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill); SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
} 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"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
Licensed to the Apache Software Foundation (ASF) under one Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file or more contributor license agreements. See the NOTICE file
distributed with this work for additional information distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the KIND, either express or implied. See the License for the
specific language governing permissions and limitations specific language governing permissions and limitations
under the License. under the License.
--> -->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> 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 --> <!-- 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/2001_add_savings_accrual_job.xml" relativeToChangelogFile="true" />
<include file="parts/2002_add_savings_accrual_permission.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/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/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/2005_add_savings_product_classification_code_values.xml" relativeToChangelogFile="true" />
<include file="parts/2006_add_period_frequency_options.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/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/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/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/2010_add_savings_account_cbs_fields.xml" relativeToChangelogFile="true" />
<include file="parts/2011_add_facility_charge_mapping.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/2012_add_current_account_permissions.xml" relativeToChangelogFile="true" />
<include file="parts/2013_add_missing_current_account_columns.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/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
</databaseChangeLog> <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>