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 4ed00ec142b..a22cf3dafda 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 static InteropAccountData build(SavingsAccount account) { 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 dfdd0c5e2a9..dd1106b6f91 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 static InteropTransactionData build(SavingsAccountTransaction transaction 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 f3b67961028..e0746693705 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.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 InteropIdentifierAccountResponseData getAccountByIdentifier(@NotNull Inte 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 InteropIdentifierAccountResponseData registerAccountIdentifier(@NotNull I 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 InteropIdentifierAccountResponseData deleteAccountIdentifier(@NotNull Int 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 InteropQuoteResponseData createQuote(@NotNull JsonCommand command) { 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 InteropTransferResponseData prepareTransfer(@NotNull JsonCommand command) 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 InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) 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 InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) 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 InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) 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 InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) 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 InteropTransferResponseData commitTransfer(@NotNull JsonCommand command) } 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 b4ca5f24d04..2bb682a5dbe 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.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.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 String retrieveOne(@PathParam("accountId") @Parameter(description = "acco @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) { - - 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); - } + @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) { - return SavingsAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges); + return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo); } @PUT @@ -263,19 +225,26 @@ public String update(@PathParam("accountId") @Parameter(description = "accountId @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); - } - - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingsAccount(accountId).withJson(apiRequestBodyAsJson) - .build(); + return updateSavingAccount(accountId, null, apiRequestBodyAsJson, commandParam); + } - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + @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) { - return toApiJsonSerializer.serialize(result); + return updateSavingAccount(null, externalId, apiRequestBodyAsJson, commandParam); } @PUT @@ -389,6 +358,170 @@ public String handleCommands(@PathParam("accountId") @Parameter(description = "a @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 String handleCommands(@PathParam("accountId") @Parameter(description = "a 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 String delete(@PathParam("accountId") @Parameter(description = "accountId 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 65a5c5381a1..5f54f53ba18 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 @@ private PostSavingsAccountsRequest() {} 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 abf8051de72..317c6b03b07 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.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 DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav 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 DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav this.savingsHelper = new SavingsHelper(accountTransfersReadPlatformService); this.context = context; this.paymentDetailAssembler = paymentDetailAssembler; + this.externalIdFactory = externalIdFactory; } /** @@ -323,10 +327,10 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm 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 SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm 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 d6d385ac696..b07cd1569f3 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.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 @@ protected FixedDepositAccount() { } 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 static FixedDepositAccount createNewApplicationForSubmittal(final Client } 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 d26e2c3712a..f21fce256e5 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 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 @@ protected RecurringDepositAccount() { } 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 static RecurringDepositAccount createNewApplicationForSubmittal(final Cli } 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 static RecurringDepositAccount createNewActivatedAccount(final Client cli } 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 RecurringDepositAccount reInvest(BigDecimal depositAmount) { 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 716edcbf5d1..f9e1d7975f5 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.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 @@ protected SavingsAccount() { } 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 static SavingsAccount createNewApplicationForSubmittal(final Client clien } 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 @@ protected SavingsAccount(final Client client, final Group group, final SavingsPr } 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 List getSavingsAccountTransactionsWithPivotCon return this.savingsAccountTransactions; } - public String getExternalId() { + public ExternalId getExternalId() { return externalId; } @@ -1662,10 +1665,13 @@ public void modifyApplication(final JsonCommand command, final Map 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 7b7c238695a..3a406335698 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 @@ 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 @@ private void loadLazyCollections(Page accounts) { } } + 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 b425da10df4..4a39f445405 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 @@ 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 @@ List retrieveAllSavingsDataForInterestPosting(boolean backda 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 e3835b87933..e6679f6162f 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.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.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 SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte 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 SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext conte 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 List getAccountsIdsByStatusPaged(Integer status, int pageSize, Long 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 00000000000..8a068e2a7e4 --- /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); + } +}