From 781f3ae78c0ad8d8b698a7e68595514a66e46491 Mon Sep 17 00:00:00 2001 From: "abraham.menyhart" Date: Tue, 29 Aug 2023 13:22:09 +0200 Subject: [PATCH] FINERACT-1760: savings account externalId support --- .../data/InteropAccountData.java | 6 +- .../data/InteropTransactionData.java | 4 +- .../service/InteropServiceImpl.java | 41 +- .../api/SavingsAccountsApiResource.java | 367 ++++++++++++------ .../SavingsAccountsApiResourceSwagger.java | 2 + .../domain/DepositAccountAssembler.java | 16 +- .../savings/domain/FixedDepositAccount.java | 5 +- .../domain/RecurringDepositAccount.java | 9 +- .../savings/domain/SavingsAccount.java | 20 +- .../domain/SavingsAccountAssembler.java | 13 +- .../domain/SavingsAccountRepository.java | 6 +- .../SavingsAccountRepositoryWrapper.java | 4 + .../SavingsAccountReadPlatformService.java | 3 + ...SavingsAccountReadPlatformServiceImpl.java | 12 +- .../SavingsAccountsExternalIdTest.java | 166 ++++++++ 15 files changed, 508 insertions(+), 166 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountsExternalIdTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java index 4ed00ec14..a22cf3daf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropAccountData.java @@ -111,9 +111,9 @@ public class InteropAccountData extends CommandProcessingResult { SavingsProduct product = account.savingsProduct(); SavingsAccountSubStatusEnum subStatus = SavingsAccountSubStatusEnum.fromInt(account.getSubStatus()); - return new InteropAccountData(account.getExternalId(), product.getId().toString(), product.getName(), product.getShortName(), - account.getCurrency().getCode(), account.getAccountBalance(), account.getWithdrawableBalance(), account.getStatus(), - subStatus, account.getAccountType(), account.depositAccountType(), account.getActivationLocalDate(), + return new InteropAccountData(account.getExternalId().getValue(), product.getId().toString(), product.getName(), + product.getShortName(), account.getCurrency().getCode(), account.getAccountBalance(), account.getWithdrawableBalance(), + account.getStatus(), subStatus, account.getAccountType(), account.depositAccountType(), account.getActivationLocalDate(), calcStatusUpdateOn(account), account.getWithdrawnOnDate(), account.retrieveLastTransactionDate(), ids, account.getClient().getId()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java index dfdd0c5e2..dd1106b6f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/data/InteropTransactionData.java @@ -143,7 +143,7 @@ public class InteropTransactionData extends CommandProcessingResult { sb.append(SavingsEnumerations.transactionType(transactionType).getValue()); } - return new InteropTransactionData(savingsAccount.getId(), savingsAccount.getExternalId(), transactionId, transactionType, amount, - chargeAmount, currency, runningBalance, bookingDateTime, valueDateTime, sb.toString()); + return new InteropTransactionData(savingsAccount.getId(), savingsAccount.getExternalId().getValue(), transactionId, transactionType, + amount, chargeAmount, currency, runningBalance, bookingDateTime, valueDateTime, sb.toString()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java index f3b679610..e07466937 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java @@ -48,6 +48,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; @@ -267,7 +268,7 @@ public class InteropServiceImpl implements InteropService { throw new InteropAccountNotFoundException(idType, idValue, subIdOrType); } - return InteropIdentifierAccountResponseData.build(identifier.getId(), identifier.getAccount().getExternalId()); + return InteropIdentifierAccountResponseData.build(identifier.getId(), identifier.getAccount().getExternalId().getValue()); } @NotNull @@ -288,7 +289,7 @@ public class InteropServiceImpl implements InteropService { identifierRepository.saveAndFlush(identifier); - return InteropIdentifierAccountResponseData.build(identifier.getId(), savingsAccount.getExternalId()); + return InteropIdentifierAccountResponseData.build(identifier.getId(), savingsAccount.getExternalId().getValue()); } catch (final JpaSystemException | DataIntegrityViolationException dve) { handleInteropDataIntegrityIssues(idType, request.getAccountId(), dve.getMostSpecificCause(), dve); return InteropIdentifierAccountResponseData.empty(); @@ -309,7 +310,7 @@ public class InteropServiceImpl implements InteropService { throw new InteropAccountNotFoundException(idType, idValue, subIdOrType); } - String accountId = identifier.getAccount().getExternalId(); + String accountId = identifier.getAccount().getExternalId().getValue(); Long id = identifier.getId(); identifierRepository.delete(identifier); @@ -356,8 +357,8 @@ public class InteropServiceImpl implements InteropService { if (transactionType.isDebit()) { fee = savingsAccount.calculateWithdrawalFee(request.getAmount().getAmount()); if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), request.getAmount().getAmount().add(fee))) { - throw new InsufficientAccountBalanceException(savingsAccount.getExternalId(), savingsAccount.getWithdrawableBalance(), fee, - request.getAmount().getAmount()); + throw new InsufficientAccountBalanceException(savingsAccount.getExternalId().getValue(), + savingsAccount.getWithdrawableBalance(), fee, request.getAmount().getAmount()); } } else { fee = BigDecimal.ZERO; @@ -390,15 +391,15 @@ public class InteropServiceImpl implements InteropService { BigDecimal total = calculateTotalTransferAmount(request, savingsAccount); if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance(), total)) { - throw new InsufficientAccountBalanceException(savingsAccount.getExternalId(), savingsAccount.getWithdrawableBalance(), null, - total); + throw new InsufficientAccountBalanceException(savingsAccount.getExternalId().getValue(), + savingsAccount.getWithdrawableBalance(), null, total); } if (findTransaction(savingsAccount, transferCode, AMOUNT_HOLD.getValue()) != null) { - throw new InteropTransferAlreadyOnHoldException(savingsAccount.getExternalId(), transferCode); + throw new InteropTransferAlreadyOnHoldException(savingsAccount.getExternalId().getValue(), transferCode); } - PaymentDetail paymentDetail = instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, - null); + PaymentDetail paymentDetail = instance(findPaymentType(), savingsAccount.getExternalId().getValue(), null, getRoutingCode(), + transferCode, null); SavingsAccountTransaction holdTransaction = SavingsAccountTransaction.holdAmount(savingsAccount, savingsAccount.office(), paymentDetail, transactionDate, Money.of(savingsAccount.getCurrency(), total), DateUtils.getLocalDateTimeOfTenant(), getLoginUser(), false); @@ -427,7 +428,7 @@ public class InteropServiceImpl implements InteropService { String transferCode = request.getTransferCode(); if (findTransaction(savingsAccount, transferCode, (isDebit ? WITHDRAWAL : DEPOSIT).getValue()) != null) { - throw new InteropTransferAlreadyCommittedException(savingsAccount.getExternalId(), transferCode); + throw new InteropTransferAlreadyCommittedException(savingsAccount.getExternalId().getValue(), transferCode); } LocalDateTime transactionDateTime = DateUtils.getLocalDateTimeOfTenant(); @@ -439,17 +440,17 @@ public class InteropServiceImpl implements InteropService { if (isDebit) { SavingsAccountTransaction holdTransaction = findTransaction(savingsAccount, transferCode, AMOUNT_HOLD.getValue()); if (holdTransaction == null) { - throw new InteropTransferMissingException(savingsAccount.getExternalId(), transferCode); + throw new InteropTransferMissingException(savingsAccount.getExternalId().getValue(), transferCode); } BigDecimal totalTransferAmount = calculateTotalTransferAmount(request, savingsAccount); if (holdTransaction.getAmount().compareTo(totalTransferAmount) != 0) { - throw new InteropTransferMissingException(savingsAccount.getExternalId(), transferCode); + throw new InteropTransferMissingException(savingsAccount.getExternalId().getValue(), transferCode); } if (MathUtil.isLessThan(savingsAccount.getWithdrawableBalance().add(holdTransaction.getAmount()), totalTransferAmount)) { - throw new InsufficientAccountBalanceException(savingsAccount.getExternalId(), savingsAccount.getWithdrawableBalance(), null, - totalTransferAmount); + throw new InsufficientAccountBalanceException(savingsAccount.getExternalId().getValue(), + savingsAccount.getWithdrawableBalance(), null, totalTransferAmount); } if (holdTransaction.getReleaseIdOfHoldAmountTransaction() == null) { @@ -464,12 +465,12 @@ public class InteropServiceImpl implements InteropService { SavingsTransactionBooleanValues transactionValues = new SavingsTransactionBooleanValues(false, true, true, false, false); transaction = savingsAccountService.handleWithdrawal(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), - instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), + instance(findPaymentType(), savingsAccount.getExternalId().getValue(), null, getRoutingCode(), transferCode, null), transactionValues, backdatedTxnsAllowedTill); } else { transaction = savingsAccountService.handleDeposit(savingsAccount, fmt, transactionDate, request.getAmount().getAmount(), - instance(findPaymentType(), savingsAccount.getExternalId(), null, getRoutingCode(), transferCode, null), false, true, - backdatedTxnsAllowedTill); + instance(findPaymentType(), savingsAccount.getExternalId().getValue(), null, getRoutingCode(), transferCode, null), + false, true, backdatedTxnsAllowedTill); } String note = request.getNote(); @@ -506,7 +507,7 @@ public class InteropServiceImpl implements InteropService { savingsAccountRepository.save(savingsAccount); } else { - throw new InteropTransferMissingException(savingsAccount.getExternalId(), request.getTransferCode()); + throw new InteropTransferMissingException(savingsAccount.getExternalId().getValue(), request.getTransferCode()); } return InteropTransferResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, @@ -558,7 +559,7 @@ public class InteropServiceImpl implements InteropService { } private SavingsAccount validateAndGetSavingAccount(String accountId) { - SavingsAccount savingsAccount = savingsAccountRepository.findByExternalId(accountId); + SavingsAccount savingsAccount = savingsAccountRepository.findByExternalId(ExternalIdFactory.produce(accountId)); if (savingsAccount == null) { throw new SavingsAccountNotFoundException(accountId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java index b4ca5f24d..2bb682a5d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResource.java @@ -57,9 +57,11 @@ import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.UploadRequest; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; @@ -68,6 +70,7 @@ import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData; import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -186,65 +189,24 @@ public class SavingsAccountsApiResource { @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, @Context final UriInfo uriInfo) { - context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - - if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { - throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); - } - - final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId); - - final Set mandatoryResponseParameters = new HashSet<>(); - final SavingsAccountData savingsAccountTemplate = populateTemplateAndAssociations(accountId, savingsAccount, - staffInSelectedOfficeOnly, chargeStatus, uriInfo, mandatoryResponseParameters); - - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters(), - mandatoryResponseParameters); - return toApiJsonSerializer.serialize(settings, savingsAccountTemplate, - SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); + return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo); } - private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount, - final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo, - final Set mandatoryResponseParameters) { + @GET + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a savings application/account by external id", description = "Retrieves a savings application/account by external id\n\n" + + "Example Requests :\n" + "\n" + "savingsaccounts/external-id/ExternalId1\n" + "\n" + "\n" + + "savingsaccounts/external-id/ExternalId1?associations=all") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsAccountIdResponse.class))) }) + public String retrieveOne(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, + @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly, + @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, + @Context final UriInfo uriInfo) { - Collection transactions = null; - Collection charges = null; - - final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); - if (!associationParameters.isEmpty()) { - - if (associationParameters.contains("all")) { - associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); - } - - if (associationParameters.contains(SavingsApiConstants.transactions)) { - mandatoryResponseParameters.add(SavingsApiConstants.transactions); - final Collection currentTransactions = savingsAccountReadPlatformService - .retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT); - if (!CollectionUtils.isEmpty(currentTransactions)) { - transactions = currentTransactions; - } - } - - if (associationParameters.contains(SavingsApiConstants.charges)) { - mandatoryResponseParameters.add(SavingsApiConstants.charges); - final Collection currentCharges = savingsAccountChargeReadPlatformService - .retrieveSavingsAccountCharges(accountId, chargeStatus); - if (!CollectionUtils.isEmpty(currentCharges)) { - charges = currentCharges; - } - } - } - - SavingsAccountData templateData = null; - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - if (settings.isTemplate()) { - templateData = savingsAccountReadPlatformService.retrieveTemplate(savingsAccount.getClientId(), savingsAccount.getGroupId(), - savingsAccount.getSavingsProductId(), staffInSelectedOfficeOnly); - } - - return SavingsAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); + return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo); } @PUT @@ -263,19 +225,26 @@ public class SavingsAccountsApiResource { @Parameter(hidden = true) final String apiRequestBodyAsJson, @QueryParam("command") @Parameter(description = "command") final String commandParam) { - if (is(commandParam, "updateWithHoldTax")) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson).updateWithHoldTax(accountId) - .build(); - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - return toApiJsonSerializer.serialize(result); - } + return updateSavingAccount(accountId, null, apiRequestBodyAsJson, commandParam); + } - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingsAccount(accountId).withJson(apiRequestBodyAsJson) - .build(); + @PUT + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Modify a savings application | Modify savings account withhold tax applicability", description = "Modify a savings application:\n\n" + + "Savings application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc\n\n" + + "Modify savings account withhold tax applicability:\n\n" + + "Savings application's withhold tax can be modified when in 'Active' state. Once the application is activated, can modify the account withhold tax to post tax or vice-versa" + + "Showing request/response for 'Modify a savings application'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PutSavingsAccountsAccountIdResponse.class))) }) + public String update(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - - return toApiJsonSerializer.serialize(result); + return updateSavingAccount(null, externalId, apiRequestBodyAsJson, commandParam); } @PUT @@ -389,6 +358,170 @@ public class SavingsAccountsApiResource { @QueryParam("command") @Parameter(description = "command") final String commandParam, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return handleCommands(accountId, null, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve savings application | Undo approval savings application | Assign Savings Officer | Unassign Savings Officer | Reject savings application | Withdraw savings application | Activate a savings account | Close a savings account | Calculate Interest on Savings Account | Post Interest on Savings Account | Block Savings Account | Unblock Savings Account | Block Savings Account Credit transactions | Unblock Savings Account Credit transactions | Block Savings Account Debit transactions | Unblock Savings Account debit transactions", description = "Approve savings application:\n\n" + + "Approves savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Undo approval savings application:\n\n" + + "Will move 'approved' savings application back to 'Submitted and pending approval' state.\n\n" + "Assign Savings Officer:\n\n" + + "Allows you to assign Savings Officer for existing Savings Account.\n\n" + "Unassign Savings Officer:\n\n" + + "Allows you to unassign the Savings Officer.\n\n" + "Reject savings application:\n\n" + + "Rejects savings application so long as its in 'Submitted and pending approval' state.\n\n" + + "Withdraw savings application:\n\n" + + "Used when an applicant withdraws from the savings application. It must be in 'Submitted and pending approval' state.\n\n" + + "Activate a savings account:\n\n" + + "Results in an approved savings application being converted into an 'active' savings account.\n\n" + + "Close a savings account:\n\n" + + "Results in an Activated savings application being converted into an 'closed' savings account.\n" + "\n" + + "closedOnDate is closure date of savings account\n" + "\n" + + "withdrawBalance is a boolean description, true value of this field performs a withdrawal transaction with account's running balance.\n\n" + + "Mandatory Fields: dateFormat,locale,closedOnDate\n\n" + + "Optional Fields: note, withdrawBalance, paymentTypeId, accountNumber, checkNumber, routingCode, receiptNumber, bankNumber\n\n" + + "Calculate Interest on Savings Account:\n\n" + + "Calculates interest earned on a savings account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n" + + "Post Interest on Savings Account:\n\n" + + "Calculates and Posts interest earned on a savings account based on today's date and whether an interest posting or crediting event is due.\n\n" + + "Block Savings Account:\n\n" + "Blocks Savings account from all types of credit and debit transactions\n\n" + + "Unblock Savings Account:\n\n" + + "Unblock a blocked account. On unblocking account, user can perform debit and credit transactions\n\n" + + "Block Savings Account Credit transactions:\n\n" + + "Savings account will be blocked from all types of credit transactions.\n\n" + + "Unblock Savings Account Credit transactions:\n\n" + + "It unblocks the Saving account's credit operations. Now all types of credits can be transacted to Savings account\n\n" + + "Block Savings Account Debit transactions:\n\n" + "All types of debit operations from Savings account wil be blocked\n\n" + + "Unblock Savings Account debit transactions:\n\n" + + "It unblocks the Saving account's debit operations. Now all types of debits can be transacted from Savings account\n\n" + + "Showing request/response for 'Unassign Savings Officer'") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.PostSavingsAccountsAccountIdResponse.class))) }) + public String handleCommands(@PathParam("externalId") @Parameter(description = "externalId") final String externalId, + @QueryParam("command") @Parameter(description = "command") final String commandParam, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + return handleCommands(null, externalId, commandParam, apiRequestBodyAsJson); + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); + } + + @DELETE + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) }) + public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { + + return deleteSavingAccount(accountId, null); + } + + @DELETE + @Path("/external-id/{externalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) }) + public String delete(@PathParam("externalId") @Parameter(description = "externalId") final String externalId) { + + return deleteSavingAccount(null, externalId); + } + + @GET + @Path("downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getSavingsTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_ACCOUNT.toString(), officeId, staffId, dateFormat); + } + + @POST + @Path("uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload savings template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postSavingsTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_ACCOUNT.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return toApiJsonSerializer.serialize(importDocumentId); + } + + @GET + @Path("transactions/downloadtemplate") + @Produces("application/vnd.ms-excel") + public Response getSavingsTransactionTemplate(@QueryParam("officeId") final Long officeId, + @QueryParam("dateFormat") final String dateFormat) { + return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), officeId, null, dateFormat); + } + + @POST + @Path("transactions/uploadtemplate") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RequestBody(description = "Upload savings transaction template", content = { + @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) + public String postSavingsTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, + @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, + @FormDataParam("dateFormat") final String dateFormat) { + final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), + uploadedInputStream, fileDetail, locale, dateFormat); + return toApiJsonSerializer.serialize(importDocumentId); + } + + private String retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly, String chargeStatus, + UriInfo uriInfo) { + context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); + + if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { + throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); + } + + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId); + + final Set mandatoryResponseParameters = new HashSet<>(); + final SavingsAccountData savingsAccountTemplate = populateTemplateAndAssociations(accountId, savingsAccount, + staffInSelectedOfficeOnly, chargeStatus, uriInfo, mandatoryResponseParameters); + + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters(), + mandatoryResponseParameters); + return toApiJsonSerializer.serialize(settings, savingsAccountTemplate, + SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); + } + + private String updateSavingAccount(Long accountId, String externalId, String apiRequestBodyAsJson, String commandParam) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + + if (is(commandParam, "updateWithHoldTax")) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson).updateWithHoldTax(accountId) + .build(); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingsAccount(accountId).withJson(apiRequestBodyAsJson) + .build(); + + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return toApiJsonSerializer.serialize(result); + } + + private String handleCommands(Long accountId, String externalId, String commandParam, String apiRequestBodyAsJson) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); + String jsonApiRequest = apiRequestBodyAsJson; if (StringUtils.isBlank(jsonApiRequest)) { jsonApiRequest = "{}"; @@ -465,18 +598,9 @@ public class SavingsAccountsApiResource { return toApiJsonSerializer.serialize(result); } - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - - @DELETE - @Path("{accountId}") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Delete a savings application", description = "At present we support hard delete of savings application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the savings account.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.DeleteSavingsAccountsAccountIdResponse.class))) }) - public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) { + private String deleteSavingAccount(Long accountId, String externalId) { + ExternalId accountExternalId = ExternalIdFactory.produce(externalId); + accountId = getResolvedAccountId(accountId, accountExternalId); final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteSavingsAccount(accountId).build(); @@ -485,45 +609,58 @@ public class SavingsAccountsApiResource { return toApiJsonSerializer.serialize(result); } - @GET - @Path("downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getSavingsTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_ACCOUNT.toString(), officeId, staffId, dateFormat); + private Long getResolvedAccountId(Long accountId, ExternalId accountExternalId) { + Long resolvedAccountId = accountId; + if (resolvedAccountId == null) { + accountExternalId.throwExceptionIfEmpty(); + resolvedAccountId = savingsAccountReadPlatformService.retrieveAccountIdByExternalId(accountExternalId); + if (resolvedAccountId == null) { + throw new SavingsAccountNotFoundException(resolvedAccountId); + } + } + return resolvedAccountId; } - @POST - @Path("uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload savings template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postSavingsTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_ACCOUNT.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return toApiJsonSerializer.serialize(importDocumentId); - } + private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount, + final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo, + final Set mandatoryResponseParameters) { - @GET - @Path("transactions/downloadtemplate") - @Produces("application/vnd.ms-excel") - public Response getSavingsTransactionTemplate(@QueryParam("officeId") final Long officeId, - @QueryParam("dateFormat") final String dateFormat) { - return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), officeId, null, dateFormat); - } + Collection transactions = null; + Collection charges = null; - @POST - @Path("transactions/uploadtemplate") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @RequestBody(description = "Upload savings transaction template", content = { - @Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) }) - public String postSavingsTransactionTemplate(@FormDataParam("file") InputStream uploadedInputStream, - @FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale, - @FormDataParam("dateFormat") final String dateFormat) { - final Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.SAVINGS_TRANSACTIONS.toString(), - uploadedInputStream, fileDetail, locale, dateFormat); - return toApiJsonSerializer.serialize(importDocumentId); + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + if (!associationParameters.isEmpty()) { + + if (associationParameters.contains("all")) { + associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges)); + } + + if (associationParameters.contains(SavingsApiConstants.transactions)) { + mandatoryResponseParameters.add(SavingsApiConstants.transactions); + final Collection currentTransactions = savingsAccountReadPlatformService + .retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT); + if (!CollectionUtils.isEmpty(currentTransactions)) { + transactions = currentTransactions; + } + } + + if (associationParameters.contains(SavingsApiConstants.charges)) { + mandatoryResponseParameters.add(SavingsApiConstants.charges); + final Collection currentCharges = savingsAccountChargeReadPlatformService + .retrieveSavingsAccountCharges(accountId, chargeStatus); + if (!CollectionUtils.isEmpty(currentCharges)) { + charges = currentCharges; + } + } + } + + SavingsAccountData templateData = null; + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + if (settings.isTemplate()) { + templateData = savingsAccountReadPlatformService.retrieveTemplate(savingsAccount.getClientId(), savingsAccount.getGroupId(), + savingsAccount.getSavingsProductId(), staffInSelectedOfficeOnly); + } + + return SavingsAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java index 65a5c5381..5f54f53ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountsApiResourceSwagger.java @@ -230,6 +230,8 @@ final class SavingsAccountsApiResourceSwagger { public String dateFormat; @Schema(example = "01 March 2011") public String submittedOnDate; + @Schema(example = "123") + public String externalId; } @Schema(description = "PostSavingsAccountsResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index abf8051de..317c6b03b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -70,6 +70,7 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; @@ -119,6 +120,8 @@ public class DepositAccountAssembler { private final DepositProductAssembler depositProductAssembler; private final PaymentDetailAssembler paymentDetailAssembler; + private final ExternalIdFactory externalIdFactory; + @Autowired public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository, @@ -128,7 +131,7 @@ public class DepositAccountAssembler { final DepositProductAssembler depositProductAssembler, final RecurringDepositProductRepository recurringDepositProductRepository, final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context, - final PaymentDetailAssembler paymentDetailAssembler) { + final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; this.clientRepository = clientRepository; @@ -143,6 +146,7 @@ public class DepositAccountAssembler { this.savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); this.context = context; this.paymentDetailAssembler = paymentDetailAssembler; + this.externalIdFactory = externalIdFactory; } /** @@ -323,10 +327,10 @@ public class DepositAccountAssembler { prodTermAndPreClosure); FixedDepositAccount fdAccount = FixedDepositAccount.createNewApplicationForSubmittal(client, group, product, fieldOfficer, - accountNo, externalId, accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, - interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, - lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, - accountTermAndPreClosure, accountChart, withHoldTax); + accountNo, externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, + interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, + minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, + charges, accountTermAndPreClosure, accountChart, withHoldTax); accountTermAndPreClosure.updateAccountReference(fdAccount); fdAccount.validateDomainRules(); account = fdAccount; @@ -341,7 +345,7 @@ public class DepositAccountAssembler { prodRecurringDetail.recurringDetail()); RecurringDepositAccount rdAccount = RecurringDepositAccount.createNewApplicationForSubmittal(client, group, product, - fieldOfficer, accountNo, externalId, accountType, submittedOnDate, submittedBy, interestRate, + fieldOfficer, accountNo, externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, accountTermAndPreClosure, accountRecurringDetail, accountChart, withHoldTax); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index d6d385ac6..b07cd1569 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -43,6 +43,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -87,7 +88,7 @@ public class FixedDepositAccount extends SavingsAccount { } public static FixedDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, final SavingsProduct product, - final Staff fieldOfficer, final String accountNo, final String externalId, final AccountType accountType, + final Staff fieldOfficer, final String accountNo, final ExternalId externalId, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal interestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -110,7 +111,7 @@ public class FixedDepositAccount extends SavingsAccount { } private FixedDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, - final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, + final String accountNo, final ExternalId externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java index d26e2c371..f21fce256 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositAccount.java @@ -45,6 +45,7 @@ import java.util.Set; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -95,7 +96,7 @@ public class RecurringDepositAccount extends SavingsAccount { } public static RecurringDepositAccount createNewApplicationForSubmittal(final Client client, final Group group, - final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final String externalId, + final SavingsProduct product, final Staff fieldOfficer, final String accountNo, final ExternalId externalId, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal interestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -117,7 +118,7 @@ public class RecurringDepositAccount extends SavingsAccount { } public static RecurringDepositAccount createNewActivatedAccount(final Client client, final Group group, final SavingsProduct product, - final Staff fieldOfficer, final String accountNo, final String externalId, final AccountType accountType, + final Staff fieldOfficer, final String accountNo, final ExternalId externalId, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal interestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -139,7 +140,7 @@ public class RecurringDepositAccount extends SavingsAccount { } private RecurringDepositAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, - final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, + final String accountNo, final ExternalId externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -1116,7 +1117,7 @@ public class RecurringDepositAccount extends SavingsAccount { final InterestRateChart productChart = product.applicableChart(getClosedOnDate()); final DepositAccountInterestRateChart newChart = DepositAccountInterestRateChart.from(productChart); final String accountNumber = null; - final String externalId = this.externalId; + final ExternalId externalId = this.externalId; final AccountType accountType = AccountType.fromInt(this.accountType); final SavingsPostingInterestPeriodType postingPeriodType = SavingsPostingInterestPeriodType.fromInt(this.interestPostingPeriodType); final SavingsCompoundingInterestPeriodType compoundingPeriodType = SavingsCompoundingInterestPeriodType diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 716edcbf5..f9e1d7975 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -73,13 +73,16 @@ import java.util.Set; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; import org.apache.fineract.interoperation.domain.InteropIdentifier; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -141,7 +144,7 @@ public class SavingsAccount extends AbstractPersistableCustom { protected String accountNumber; @Column(name = "external_id", nullable = true) - protected String externalId; + protected ExternalId externalId; @ManyToOne(optional = true) @JoinColumn(name = "client_id", nullable = true) @@ -345,7 +348,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } public static SavingsAccount createNewApplicationForSubmittal(final Client client, final Group group, final SavingsProduct product, - final Staff fieldOfficer, final String accountNo, final String externalId, final AccountType accountType, + final Staff fieldOfficer, final String accountNo, final ExternalId externalId, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal interestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -367,7 +370,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff fieldOfficer, - final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, + final String accountNo, final ExternalId externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -383,7 +386,7 @@ public class SavingsAccount extends AbstractPersistableCustom { } protected SavingsAccount(final Client client, final Group group, final SavingsProduct product, final Staff savingsOfficer, - final String accountNo, final String externalId, final SavingsAccountStatusType status, final AccountType accountType, + final String accountNo, final ExternalId externalId, final SavingsAccountStatusType status, final AccountType accountType, final LocalDate submittedOnDate, final AppUser submittedBy, final BigDecimal nominalAnnualInterestRate, final SavingsCompoundingInterestPeriodType interestCompoundingPeriodType, final SavingsPostingInterestPeriodType interestPostingPeriodType, final SavingsInterestCalculationType interestCalculationType, @@ -461,7 +464,7 @@ public class SavingsAccount extends AbstractPersistableCustom { return this.savingsAccountTransactions; } - public String getExternalId() { + public ExternalId getExternalId() { return externalId; } @@ -1662,10 +1665,13 @@ public class SavingsAccount extends AbstractPersistableCustom { this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); } - if (command.isChangeInStringParameterNamed(SavingsApiConstants.externalIdParamName, this.externalId)) { + if (command.isChangeInStringParameterNamed(SavingsApiConstants.externalIdParamName, this.externalId.getValue())) { final String newValue = command.stringValueOfParameterNamed(SavingsApiConstants.externalIdParamName); actualChanges.put(SavingsApiConstants.externalIdParamName, newValue); - this.externalId = StringUtils.defaultIfEmpty(newValue, null); + this.externalId = ExternalIdFactory.produce(newValue); + if (this.externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + this.externalId = ExternalId.generate(); + } } if (command.isChangeInLongParameterNamed(SavingsApiConstants.clientIdParamName, clientId())) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java index ece7ef4c0..b541a2d1d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java @@ -57,6 +57,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; @@ -100,6 +101,7 @@ public class SavingsAccountAssembler { private final FromJsonHelper fromApiJsonHelper; private final JdbcTemplate jdbcTemplate; private final ConfigurationDomainService configurationDomainService; + private final ExternalIdFactory externalIdFactory; @Autowired public SavingsAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper, @@ -109,7 +111,7 @@ public class SavingsAccountAssembler { final SavingsAccountRepositoryWrapper savingsAccountRepository, final SavingsAccountChargeAssembler savingsAccountChargeAssembler, final FromJsonHelper fromApiJsonHelper, final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final JdbcTemplate jdbcTemplate, - final ConfigurationDomainService configurationDomainService) { + final ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) { this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper; this.savingsAccountTransactionDataSummaryWrapper = savingsAccountTransactionDataSummaryWrapper; this.clientRepository = clientRepository; @@ -122,6 +124,7 @@ public class SavingsAccountAssembler { savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); this.jdbcTemplate = jdbcTemplate; this.configurationDomainService = configurationDomainService; + this.externalIdFactory = externalIdFactory; } /** @@ -323,10 +326,10 @@ public class SavingsAccountAssembler { } final SavingsAccount account = SavingsAccount.createNewApplicationForSubmittal(client, group, product, fieldOfficer, accountNo, - externalId, accountType, submittedOnDate, submittedBy, interestRate, interestCompoundingPeriodType, - interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, - lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, allowOverdraft, - overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, maxAllowedLienLimit, lienAllowed, + externalIdFactory.create(externalId), accountType, submittedOnDate, submittedBy, interestRate, + interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, + minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges, + allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, maxAllowedLienLimit, lienAllowed, nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax); account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java index 93a1abe83..aaece884c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepository.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.domain; import jakarta.persistence.LockModeType; import java.util.List; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -65,5 +66,8 @@ public interface SavingsAccountRepository extends JpaRepository findByStatus(Integer status, Pageable pageable); - SavingsAccount findByExternalId(String externalId); + SavingsAccount findByExternalId(ExternalId externalId); + + @Query("SELECT sa.id FROM SavingsAccount sa WHERE sa.externalId = :externalId") + Long findIdByExternalId(@Param("externalId") ExternalId externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java index 7b7c23869..3a4063356 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountRepositoryWrapper.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.domain; import java.time.LocalDate; import java.util.List; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; import org.springframework.beans.factory.annotation.Autowired; @@ -175,4 +176,7 @@ public class SavingsAccountRepositoryWrapper { } } + public Long findIdByExternalId(final ExternalId externalId) { + return this.repository.findIdByExternalId(externalId); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java index b425da10d..4a39f4454 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformService.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.service; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.SearchParameters; import org.apache.fineract.portfolio.savings.DepositAccountType; @@ -68,4 +69,6 @@ public interface SavingsAccountReadPlatformService { Long maxSavingsId); List retrieveAllTransactionData(List refNo); + + Long retrieveAccountIdByExternalId(ExternalId externalId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index e3835b879..e6679f616 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -33,6 +33,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.Page; @@ -75,6 +76,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumD import org.apache.fineract.portfolio.savings.data.SavingsProductData; import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargesPaidByData; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum; import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException; @@ -119,6 +121,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final ColumnValidator columnValidator; private final SavingsAccountAssembler savingAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper; + @Autowired public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, final ClientReadPlatformService clientReadPlatformService, final GroupReadPlatformService groupReadPlatformService, @@ -127,7 +131,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final ChargeReadPlatformService chargeReadPlatformService, final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator, final SavingsAccountAssembler savingAccountAssembler, PaginationHelper paginationHelper, - DatabaseSpecificSQLGenerator sqlGenerator) { + DatabaseSpecificSQLGenerator sqlGenerator, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) { this.context = context; this.jdbcTemplate = jdbcTemplate; this.clientReadPlatformService = clientReadPlatformService; @@ -136,6 +140,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead this.staffReadPlatformService = staffReadPlatformService; this.dropdownReadPlatformService = dropdownReadPlatformService; this.sqlGenerator = sqlGenerator; + this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper; this.transactionTemplateMapper = new SavingsAccountTransactionTemplateMapper(); this.transactionsMapper = new SavingsAccountTransactionsMapper(); this.savingsAccountTransactionsForBatchMapper = new SavingsAccountTransactionsForBatchMapper(); @@ -1807,4 +1812,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead return new ArrayList<>(); } } + + @Override + public Long retrieveAccountIdByExternalId(final ExternalId externalId) { + return savingsAccountRepositoryWrapper.findIdByExternalId(externalId); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountsExternalIdTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountsExternalIdTest.java new file mode 100644 index 000000000..8a068e2a7 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountsExternalIdTest.java @@ -0,0 +1,166 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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.integrationtests; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.models.GetSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest; +import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.models.PostSavingsAccountsRequest; +import org.apache.fineract.client.models.PostSavingsAccountsResponse; +import org.apache.fineract.client.models.PutSavingsAccountsAccountIdRequest; +import org.apache.fineract.client.models.PutSavingsAccountsAccountIdResponse; +import org.apache.fineract.client.util.Calls; +import org.apache.fineract.integrationtests.client.IntegrationTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import retrofit2.Response; + +public class SavingsAccountsExternalIdTest extends IntegrationTest { + + private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountsExternalIdTest.class); + public static final String EXTERNAL_ID = UUID.randomUUID().toString(); + private final String dateFormat = "dd MMMM yyyy"; + private final String locale = "en"; + private final String formattedDate = LocalDate.now(ZoneId.systemDefault()).minusDays(5) + .format(DateTimeFormatter.ofPattern("dd MMM yyyy")); + + @Test + @Order(1) + void submitSavingsAccountsApplication() { + LOG.info("------------------------------ CREATING NEW SAVINGS ACCOUNT APPLICATION ---------------------------------------"); + PostSavingsAccountsRequest request = new PostSavingsAccountsRequest(); + request.setClientId(1); + request.setProductId(1); + request.setLocale(locale); + request.setDateFormat(dateFormat); + request.submittedOnDate(formattedDate); + request.setExternalId(EXTERNAL_ID); + + Response response = okR(fineract().savingsAccounts.submitApplication2(request)); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + } + + @Test + @Order(2) + void updateSavingsAccountWithExternalId() { + LOG.info("------------------------------ UPDATING SAVINGS ACCOUNT ---------------------------------------"); + PutSavingsAccountsAccountIdRequest request = new PutSavingsAccountsAccountIdRequest(); + request.setLocale(locale); + request.setNominalAnnualInterestRate(5.999); + Response response = okR(fineract().savingsAccounts.update21(EXTERNAL_ID, request, "")); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + } + + @Test + @Order(3) + void approveSavingsAccount() { + LOG.info("------------------------------ APPROVING SAVINGS ACCOUNT ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + request.dateFormat(dateFormat); + request.setLocale(locale); + request.setApprovedOnDate(formattedDate); + Response response = okR( + fineract().savingsAccounts.handleCommands7(EXTERNAL_ID, request, "approve")); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + } + + @Test + @Order(4) + void retrieveSavingsAccountWithExternalId() { + LOG.info("------------------------------ RETRIEVING SAVINGS ACCOUNT ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + request.dateFormat(dateFormat); + request.setLocale(locale); + request.setActivatedOnDate(formattedDate); + Response response = okR(fineract().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all")); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().getStatus().getCode()).isEqualTo("savingsAccountStatusType.approved"); + assertThat(response.body().getNominalAnnualInterestRate()).isEqualTo(5.999); + } + + @Test + @Order(5) + void undoApprovalSavingsAccountWithExternalId() { + LOG.info("------------------------------ UNDO APPROVAL SAVINGS ACCOUNT ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + Response response = okR( + fineract().savingsAccounts.handleCommands7(EXTERNAL_ID, request, "undoapproval")); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + } + + @Test + @Order(6) + void retrieveSavingsAccountWithExternalIdSecondTime() { + LOG.info("------------------------------ RETRIEVING SAVINGS ACCOUNT - SECOND TIME ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + request.dateFormat(dateFormat); + request.setLocale(locale); + request.setActivatedOnDate(formattedDate); + Response response = okR(fineract().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all")); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().getStatus().getCode()).isEqualTo("savingsAccountStatusType.submitted.and.pending.approval"); + } + + @Test + @Order(7) + void deleteSavingsAccountWithExternalId() { + LOG.info("------------------------------ DELETING SAVINGS ACCOUNT ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + request.dateFormat(dateFormat); + request.setLocale(locale); + request.setActivatedOnDate(formattedDate); + Response response = okR(fineract().savingsAccounts.delete20(EXTERNAL_ID)); + + assertThat(response.isSuccessful()).isTrue(); + assertThat(response.body()).isNotNull(); + } + + @Test + @Order(8) + void retrieveSavingsAccountWithExternalIdThirdTime() { + LOG.info("------------------------------ RETRIEVING SAVINGS ACCOUNT - THIRD TIME ---------------------------------------"); + PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest(); + request.dateFormat(dateFormat); + request.setLocale(locale); + request.setActivatedOnDate(formattedDate); + Response response = Calls + .executeU(fineract().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all")); + + assertThat(response.raw().code()).isEqualTo(404); + } +}