This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 8fa1c4571 [FINERACT-1760] Client transaction by transaction external ID
8fa1c4571 is described below

commit 8fa1c4571a634ab1705ad20fd7aa738c47dde69b
Author: taskain7 <task...@gmail.com>
AuthorDate: Fri Jul 21 10:20:10 2023 +0200

    [FINERACT-1760] Client transaction by transaction external ID
---
 .../api/ClientChargesApiResourceSwagger.java       |   2 +
 .../client/api/ClientTransactionsApiResource.java  | 145 +++++++++++++++++++--
 .../client/data/ClientApiCollectionConstants.java  |   2 +-
 .../portfolio/client/domain/ClientTransaction.java |  14 +-
 .../client/domain/ClientTransactionRepository.java |   3 +
 .../client/exception/ClientNotFoundException.java  |   5 +
 .../ClientTransactionNotFoundException.java        |   7 +
 .../ClientChargeWritePlatformServiceImpl.java      |   7 +-
 .../ClientTransactionReadPlatformService.java      |   4 +
 .../ClientTransactionReadPlatformServiceImpl.java  |   9 ++
 .../client/ClientTransactionTest.java              |  20 ++-
 .../integrationtests/common/ClientHelper.java      |  35 ++++-
 12 files changed, 229 insertions(+), 24 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResourceSwagger.java
index cf9da1fb5..0769c0b38 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResourceSwagger.java
@@ -162,6 +162,8 @@ final class ClientChargesApiResourceSwagger {
         public String dateFormat;
         @Schema(example = "01 September 2015")
         public String transactionDate;
+        @Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002")
+        public String externalId;
     }
 
     @Schema(description = "PostClientsClientIdChargesChargeIdResponse")
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientTransactionsApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientTransactionsApiResource.java
index f70e8c1a3..27584344b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientTransactionsApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientTransactionsApiResource.java
@@ -43,6 +43,7 @@ import 
org.apache.fineract.commands.service.CommandWrapperBuilder;
 import 
org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
 import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+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;
@@ -51,7 +52,9 @@ import org.apache.fineract.infrastructure.core.service.Page;
 import org.apache.fineract.infrastructure.core.service.SearchParameters;
 import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
 import org.apache.fineract.portfolio.client.data.ClientTransactionData;
+import org.apache.fineract.portfolio.client.domain.ClientTransaction;
 import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
+import 
org.apache.fineract.portfolio.client.exception.ClientTransactionNotFoundException;
 import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
 import 
org.apache.fineract.portfolio.client.service.ClientTransactionReadPlatformService;
 import org.springframework.stereotype.Component;
@@ -122,7 +125,8 @@ public class ClientTransactionsApiResource {
     @Consumes({ MediaType.APPLICATION_JSON })
     @Produces({ MediaType.APPLICATION_JSON })
     @Operation(summary = "List Client Transactions", description = "The list 
capability of client transaction can support pagination."
-            + "\n\n" + "Example Requests:\n\n" + 
"clients/189/transactions\n\n" + "clients/189/transactions?offset=10&limit=50")
+            + "\n\n" + "Example Requests:\n\n" + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions\n\n"
+            + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?offset=10&limit=50")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.GetClientsClientIdTransactionsResponse.class)))
 })
     public String retrieveAllClientTransactions(
@@ -131,9 +135,11 @@ public class ClientTransactionsApiResource {
             @QueryParam("limit") @Parameter(description = "limit") final 
Integer limit) {
         
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
 
-        Long clientId = resolveClientId(clientExternalId);
+        ExternalId clientExtId = ExternalIdFactory.produce(clientExternalId);
+
+        Long clientId = resolveClientId(clientExtId);
         if (Objects.isNull(clientId)) {
-            throw new ClientNotFoundException();
+            throw new ClientNotFoundException(clientExtId);
         }
 
         return getAllClientTransactions(clientId, uriInfo, offset, limit);
@@ -143,8 +149,9 @@ public class ClientTransactionsApiResource {
     @Path("external-id/{clientExternalId}/transactions/{transactionId}")
     @Consumes({ MediaType.APPLICATION_JSON })
     @Produces({ MediaType.APPLICATION_JSON })
-    @Operation(summary = "Retrieve a Client Transaction", description = 
"Example Requests:\n" + "clients/1/transactions/1\n" + "\n" + "\n"
-            + "clients/1/transactions/1?fields=id,officeName")
+    @Operation(summary = "Retrieve a Client Transaction", description = 
"Example Requests:\n"
+            + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions/1\n" + 
"\n" + "\n"
+            + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions/1?fields=id,officeName")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.GetClientsClientIdTransactionsTransactionIdResponse.class)))
 })
     public String retrieveClientTransaction(
@@ -154,10 +161,11 @@ public class ClientTransactionsApiResource {
 
         
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
 
-        Long clientId = resolveClientId(clientExternalId);
+        ExternalId clientExtId = ExternalIdFactory.produce(clientExternalId);
 
+        Long clientId = resolveClientId(clientExtId);
         if (Objects.isNull(clientId)) {
-            throw new ClientNotFoundException();
+            throw new ClientNotFoundException(clientExtId);
         }
 
         return getClientTransaction(clientId, transactionId, uriInfo);
@@ -176,9 +184,119 @@ public class ClientTransactionsApiResource {
             @QueryParam("command") @Parameter(description = "command") final 
String commandParam,
             @Parameter(hidden = true) final String apiRequestBodyAsJson) {
 
-        Long clientId = resolveClientId(clientExternalId);
+        ExternalId clientExtId = ExternalIdFactory.produce(clientExternalId);
+
+        Long clientId = resolveClientId(clientExtId);
+        if (Objects.isNull(clientId)) {
+            throw new ClientNotFoundException(clientExtId);
+        }
+
+        return undoTransaction(clientId, transactionId, commandParam, 
apiRequestBodyAsJson);
+    }
+
+    @GET
+    @Path("{clientId}/transactions/external-id/{transactionExternalId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(summary = "Retrieve a Client Transaction", description = 
"Example Requests:\n"
+            + 
"clients/1/transactions/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854\n" + 
"\n" + "\n"
+            + 
"clients/1/transactions/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854?fields=id,officeName")
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.GetClientsClientIdTransactionsTransactionIdResponse.class)))
 })
+    public String retrieveClientTransaction(@PathParam("clientId") 
@Parameter(description = "clientId") final Long clientId,
+            @PathParam("transactionExternalId") @Parameter(description = 
"transactionExternalId") final String transactionExternalId,
+            @Context final UriInfo uriInfo) {
+
+        
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
+
+        ExternalId transactionExtId = 
ExternalIdFactory.produce(transactionExternalId);
+
+        Long transactionId = resolveTransactionId(transactionExtId);
+
+        if (Objects.isNull(transactionId)) {
+            throw new ClientTransactionNotFoundException(clientId, 
transactionExtId);
+        }
+
+        return getClientTransaction(clientId, transactionId, uriInfo);
+    }
+
+    @GET
+    
@Path("external-id/{clientExternalId}/transactions/external-id/{transactionExternalId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(summary = "Retrieve a Client Transaction", description = 
"Example Requests:\n"
+            + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854\n"
+            + "\n" + "\n"
+            + 
"clients/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854?fields=id,officeName")
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.GetClientsClientIdTransactionsTransactionIdResponse.class)))
 })
+    public String retrieveClientTransaction(
+            @PathParam("clientExternalId") @Parameter(description = 
"clientExternalId") final String clientExternalId,
+            @PathParam("transactionExternalId") @Parameter(description = 
"transactionExternalId") final String transactionExternalId,
+            @Context final UriInfo uriInfo) {
+
+        
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME);
+
+        ExternalId clientExtId = ExternalIdFactory.produce(clientExternalId);
+        Long clientId = resolveClientId(clientExtId);
+        if (Objects.isNull(clientId)) {
+            throw new ClientNotFoundException(clientExtId);
+        }
+
+        ExternalId transactionExtId = 
ExternalIdFactory.produce(transactionExternalId);
+        Long transactionId = resolveTransactionId(transactionExtId);
+        if (Objects.isNull(transactionId)) {
+            throw new ClientTransactionNotFoundException(clientId, 
transactionExtId);
+        }
+
+        return getClientTransaction(clientId, transactionId, uriInfo);
+    }
+
+    @POST
+    @Path("{clientId}/transactions/external-id/{transactionExternalId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(summary = "Undo a Client Transaction", description = "Undoes a 
Client Transaction")
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.PostClientsClientIdTransactionsTransactionIdResponse.class)))
 })
+    public String undoClientTransaction(@PathParam("clientId") 
@Parameter(description = "clientId") final Long clientId,
+            @PathParam("transactionExternalId") @Parameter(description = 
"transactionExternalId") final String transactionExternalId,
+            @QueryParam("command") @Parameter(description = "command") final 
String commandParam,
+            @Parameter(hidden = true) final String apiRequestBodyAsJson) {
+
+        ExternalId transactionExtId = 
ExternalIdFactory.produce(transactionExternalId);
+        Long transactionId = resolveTransactionId(transactionExtId);
+        if (Objects.isNull(transactionId)) {
+            throw new ClientTransactionNotFoundException(clientId, 
transactionExtId);
+        }
+
+        return undoTransaction(clientId, transactionId, commandParam, 
apiRequestBodyAsJson);
+    }
+
+    @POST
+    
@Path("external-id/{clientExternalId}/transactions/external-id/{transactionExternalId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(summary = "Undo a Client Transaction", description = "Undoes a 
Client Transaction")
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
ClientTransactionsApiResourceSwagger.PostClientsClientIdTransactionsTransactionIdResponse.class)))
 })
+    public String undoClientTransaction(
+            @PathParam("clientExternalId") @Parameter(description = 
"clientExternalId") final String clientExternalId,
+            @PathParam("transactionExternalId") @Parameter(description = 
"transactionExternalId") final String transactionExternalId,
+            @QueryParam("command") @Parameter(description = "command") final 
String commandParam,
+            @Parameter(hidden = true) final String apiRequestBodyAsJson) {
+
+        ExternalId clientExtId = ExternalIdFactory.produce(clientExternalId);
+
+        Long clientId = resolveClientId(clientExtId);
         if (Objects.isNull(clientId)) {
-            throw new ClientNotFoundException();
+            throw new ClientNotFoundException(clientExtId);
+        }
+
+        ExternalId transactionExtId = 
ExternalIdFactory.produce(transactionExternalId);
+        Long transactionId = resolveTransactionId(transactionExtId);
+        if (Objects.isNull(transactionId)) {
+            throw new ClientTransactionNotFoundException(clientId, 
transactionExtId);
         }
 
         return undoTransaction(clientId, transactionId, commandParam, 
apiRequestBodyAsJson);
@@ -214,8 +332,13 @@ public class ClientTransactionsApiResource {
         }
     }
 
-    private Long resolveClientId(String clientExternalId) {
-        return 
clientReadPlatformService.retrieveClientIdByExternalId(ExternalIdFactory.produce(clientExternalId));
+    private Long resolveClientId(ExternalId clientExternalId) {
+        return 
clientReadPlatformService.retrieveClientIdByExternalId(clientExternalId);
+    }
+
+    private Long resolveTransactionId(ExternalId transactionExternalId) {
+        ClientTransaction clientTransaction = 
clientTransactionReadPlatformService.retrieveTransactionByExternalId(transactionExternalId);
+        return !Objects.isNull(clientTransaction) ? clientTransaction.getId() 
: null;
     }
 
     private boolean is(final String commandParam, final String commandValue) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java
index 2c7638101..330afe3e6 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java
@@ -78,6 +78,6 @@ public class ClientApiCollectionConstants extends 
ClientApiConstants {
 
     protected static final Set<String> 
CLIENT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new 
HashSet<>(Arrays.asList(amountParamName,
             transactionDateParamName, dateFormatParamName, localeParamName, 
paymentTypeIdParamName, transactionAccountNumberParamName,
-            checkNumberParamName, routingCodeParamName, 
receiptNumberParamName, bankNumberParamName));
+            checkNumberParamName, routingCodeParamName, 
receiptNumberParamName, bankNumberParamName, externalIdParamName));
 
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransaction.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransaction.java
index b940e030b..858375861 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransaction.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransaction.java
@@ -39,6 +39,7 @@ import java.util.Set;
 import org.apache.fineract.accounting.glaccount.domain.GLAccount;
 import org.apache.fineract.infrastructure.core.data.EnumOptionData;
 import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
@@ -81,7 +82,7 @@ public class ClientTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     private boolean reversed;
 
     @Column(name = "external_id", length = 100, nullable = true, unique = true)
-    private String externalId;
+    private ExternalId externalId;
 
     /*
      * Deprecated since common Auditable fields were introduced. Columns and 
data left untouched to help migration.
@@ -98,9 +99,8 @@ public class ClientTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     protected ClientTransaction() {}
 
     public static ClientTransaction payCharge(final Client client, final 
Office office, PaymentDetail paymentDetail,
-            final LocalDate transactionDate, final Money amount, final String 
currencyCode) {
+            final LocalDate transactionDate, final Money amount, final String 
currencyCode, final ExternalId externalId) {
         final boolean isReversed = false;
-        final String externalId = null;
         return new ClientTransaction(client, office, paymentDetail, 
ClientTransactionType.PAY_CHARGE.getValue(), transactionDate, amount,
                 isReversed, externalId, currencyCode);
     }
@@ -108,14 +108,14 @@ public class ClientTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
     public static ClientTransaction waiver(final Client client, final Office 
office, final LocalDate transactionDate, final Money amount,
             final String currencyCode) {
         final boolean isReversed = false;
-        final String externalId = null;
+        final ExternalId externalId = ExternalId.empty();
         final PaymentDetail paymentDetail = null;
         return new ClientTransaction(client, office, paymentDetail, 
ClientTransactionType.WAIVE_CHARGE.getValue(), transactionDate, amount,
                 isReversed, externalId, currencyCode);
     }
 
     public ClientTransaction(Client client, Office office, PaymentDetail 
paymentDetail, Integer typeOf, LocalDate transactionDate,
-            Money amount, boolean reversed, String externalId, String 
currencyCode) {
+            Money amount, boolean reversed, ExternalId externalId, String 
currencyCode) {
 
         this.client = client;
         this.office = office;
@@ -229,4 +229,8 @@ public class ClientTransaction extends 
AbstractAuditableWithUTCDateTimeCustom {
         return this.submittedOnDate;
     }
 
+    public ExternalId getExternalId() {
+        return this.externalId;
+    }
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransactionRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransactionRepository.java
index f00498c30..5ddcdd49f 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransactionRepository.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientTransactionRepository.java
@@ -18,9 +18,12 @@
  */
 package org.apache.fineract.portfolio.client.domain;
 
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
 public interface ClientTransactionRepository extends 
JpaRepository<ClientTransaction, Long>, 
JpaSpecificationExecutor<ClientTransaction> {
 
+    ClientTransaction findByExternalId(ExternalId externalId);
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientNotFoundException.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientNotFoundException.java
index 26aad1be5..56e08b4ed 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientNotFoundException.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientNotFoundException.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.portfolio.client.exception;
 
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
 import org.springframework.dao.EmptyResultDataAccessException;
 
@@ -30,6 +31,10 @@ public class ClientNotFoundException extends 
AbstractPlatformResourceNotFoundExc
         super("error.msg.client.id.invalid", "Client with identifier " + id + 
" does not exist", id);
     }
 
+    public ClientNotFoundException(final ExternalId externalId) {
+        super("error.msg.client.id.invalid", "Client with identifier " + 
externalId + " does not exist", externalId);
+    }
+
     public ClientNotFoundException() {
         super("error.msg.client.not.found.with.basic.details", "Client not 
found with basic details.");
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientTransactionNotFoundException.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientTransactionNotFoundException.java
index 0f866d0e5..313b690d7 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientTransactionNotFoundException.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientTransactionNotFoundException.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.portfolio.client.exception;
 
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
 import org.springframework.dao.EmptyResultDataAccessException;
 
@@ -29,6 +30,12 @@ public class ClientTransactionNotFoundException extends 
AbstractPlatformDomainRu
                 clientId);
     }
 
+    public ClientTransactionNotFoundException(final Long clientId, final 
ExternalId transactionId) {
+        super("error.msg.client.transaction.not.found.exception",
+                "The Transaction with id `" + transactionId.getValue() + "` 
does not exist for a Client with id `" + clientId,
+                transactionId.getValue(), clientId);
+    }
+
     public ClientTransactionNotFoundException(Long clientId, Long 
transactionId, EmptyResultDataAccessException e) {
         super("error.msg.client.transaction.not.found.exception",
                 "The Transaction with id `" + transactionId + "` does not 
exist for a Client with id `" + clientId, transactionId, clientId,
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java
index dadec193c..baa51baa8 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java
@@ -35,9 +35,11 @@ import 
org.apache.fineract.infrastructure.core.data.ApiParameterError;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import 
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
 import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
 import 
org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import 
org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
@@ -136,6 +138,8 @@ public class ClientChargeWritePlatformServiceImpl 
implements ClientChargeWritePl
             final DateTimeFormatter fmt = 
DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
             final LocalDate transactionDate = 
command.localDateValueOfParameterNamed(ClientApiConstants.transactionDateParamName);
             final BigDecimal amountPaid = 
command.bigDecimalValueOfParameterNamed(ClientApiConstants.amountParamName);
+            final ExternalId transactionExternalId = ExternalIdFactory
+                    
.produce(command.stringValueOfParameterNamedAllowingNull(ClientApiConstants.externalIdParamName));
             final Money chargePaid = Money.of(clientCharge.getCurrency(), 
amountPaid);
 
             // Validate business rules for payment
@@ -149,7 +153,7 @@ public class ClientChargeWritePlatformServiceImpl 
implements ClientChargeWritePl
             final PaymentDetail paymentDetail = 
this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, 
changes);
 
             ClientTransaction clientTransaction = 
ClientTransaction.payCharge(client, client.getOffice(), paymentDetail, 
transactionDate,
-                    chargePaid, clientCharge.getCurrency().getCode());
+                    chargePaid, clientCharge.getCurrency().getCode(), 
transactionExternalId);
             this.clientTransactionRepository.saveAndFlush(clientTransaction);
 
             // update charge paid by associations
@@ -162,6 +166,7 @@ public class ClientChargeWritePlatformServiceImpl 
implements ClientChargeWritePl
             return new CommandProcessingResultBuilder() //
                     .withTransactionId(clientTransaction.getId().toString())//
                     .withEntityId(clientCharge.getId()) //
+                    
.withSubEntityId(clientTransaction.getId()).withSubEntityExternalId(clientTransaction.getExternalId())
                     
.withOfficeId(clientCharge.getClient().getOffice().getId()) //
                     .withClientId(clientCharge.getClient().getId()).build();
         } catch (final JpaSystemException | DataIntegrityViolationException 
dve) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformService.java
index 960f97b9c..3eb72cc2c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformService.java
@@ -19,9 +19,11 @@
 package org.apache.fineract.portfolio.client.service;
 
 import java.util.Collection;
+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.client.data.ClientTransactionData;
+import org.apache.fineract.portfolio.client.domain.ClientTransaction;
 import org.springframework.transaction.annotation.Transactional;
 
 public interface ClientTransactionReadPlatformService {
@@ -35,4 +37,6 @@ public interface ClientTransactionReadPlatformService {
     @Transactional(readOnly = true)
     ClientTransactionData retrieveTransaction(Long clientId, Long 
transactionId);
 
+    ClientTransaction retrieveTransactionByExternalId(ExternalId 
transactionExternalId);
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformServiceImpl.java
index fb2e795c9..1c82138c1 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionReadPlatformServiceImpl.java
@@ -25,6 +25,7 @@ import java.time.LocalDate;
 import java.util.Collection;
 import lombok.RequiredArgsConstructor;
 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.Page;
 import org.apache.fineract.infrastructure.core.service.PaginationHelper;
@@ -33,6 +34,8 @@ import 
org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
 import org.apache.fineract.organisation.monetary.data.CurrencyData;
 import org.apache.fineract.portfolio.client.data.ClientTransactionData;
 import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
+import org.apache.fineract.portfolio.client.domain.ClientTransaction;
+import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository;
 import org.apache.fineract.portfolio.client.domain.ClientTransactionType;
 import 
org.apache.fineract.portfolio.client.exception.ClientTransactionNotFoundException;
 import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
@@ -50,6 +53,7 @@ public class ClientTransactionReadPlatformServiceImpl 
implements ClientTransacti
     private final DatabaseSpecificSQLGenerator sqlGenerator;
     private final ClientTransactionMapper clientTransactionMapper = new 
ClientTransactionMapper();
     private final PaginationHelper paginationHelper;
+    private final ClientTransactionRepository clientTransactionRepository;
 
     private static final class ClientTransactionMapper implements 
RowMapper<ClientTransactionData> {
 
@@ -175,4 +179,9 @@ public class ClientTransactionReadPlatformServiceImpl 
implements ClientTransacti
         }
     }
 
+    @Override
+    public ClientTransaction retrieveTransactionByExternalId(ExternalId 
transactionExternalId) {
+        return 
clientTransactionRepository.findByExternalId(transactionExternalId);
+    }
+
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientTransactionTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientTransactionTest.java
index 925c5dbce..ff6f34f6d 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientTransactionTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientTransactionTest.java
@@ -26,6 +26,7 @@ import io.restassured.builder.ResponseSpecBuilder;
 import io.restassured.http.ContentType;
 import io.restassured.specification.RequestSpecification;
 import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
 import java.util.UUID;
 import 
org.apache.fineract.client.models.GetClientsClientIdTransactionsResponse;
 import 
org.apache.fineract.client.models.GetClientsClientIdTransactionsTransactionIdResponse;
@@ -68,6 +69,7 @@ public class ClientTransactionTest {
         final Integer clientChargeId1 = 
ClientHelper.addChargesForClient(requestSpec, responseSpec, clientId.intValue(),
                 
ClientHelper.getSpecifiedDueDateChargesClientAsJSON(chargeId.toString(), "29 
October 2011"));
         Assertions.assertNotNull(clientChargeId1);
+        String transactionExternalId = UUID.randomUUID().toString();
         final String clientChargePaidTransactionId1 = 
ClientHelper.payChargesForClients(requestSpec, responseSpec, 
clientId.intValue(),
                 clientChargeId1, ClientHelper.getPayChargeJSON("25 AUGUST 
2015", "10"));
         assertNotNull(clientChargePaidTransactionId1);
@@ -75,9 +77,10 @@ public class ClientTransactionTest {
         final Integer clientChargeId2 = 
ClientHelper.addChargesForClient(requestSpec, responseSpec, clientId.intValue(),
                 
ClientHelper.getSpecifiedDueDateChargesClientAsJSON(chargeId.toString(), "29 
October 2011"));
         Assertions.assertNotNull(clientChargeId2);
-        final String clientChargePaidTransactionId2 = 
ClientHelper.payChargesForClients(requestSpec, responseSpec, 
clientId.intValue(),
-                clientChargeId2, ClientHelper.getPayChargeJSON("25 AUGUST 
2015", "10"));
-        assertNotNull(clientChargePaidTransactionId2);
+        final String clientChargePaidTransactionExternalId = 
ClientHelper.payChargesForClientsTransactionExternalId(requestSpec,
+                responseSpec, clientId.intValue(), clientChargeId2,
+                ClientHelper.getPayChargeJSONWithExternalId("25 AUGUST 2015", 
"12", transactionExternalId));
+        assertNotNull(clientChargePaidTransactionExternalId);
 
         GetClientsClientIdTransactionsResponse 
allClientTransactionsByExternalId = clientHelper
                 .getAllClientTransactionsByExternalId(clientExternalId);
@@ -87,8 +90,17 @@ public class ClientTransactionTest {
                 .getClientTransactionByExternalId(clientExternalId, 
clientChargePaidTransactionId1);
         assertEquals(Integer.parseInt(clientChargePaidTransactionId1), 
clientTransactionByExternalId.getId());
 
+        GetClientsClientIdTransactionsTransactionIdResponse 
clientTransactionByTransactionExternalId = clientHelper
+                .getClientTransactionByTransactionExternalId(clientId, 
clientChargePaidTransactionExternalId);
+        assertNotNull(clientTransactionByTransactionExternalId);
+        assertEquals(BigDecimal.valueOf(12), 
clientTransactionByTransactionExternalId.getAmount().stripTrailingZeros());
+
         PostClientsClientIdTransactionsTransactionIdResponse 
undoTransactionResponse = clientHelper
-                .undoClientTransactionByExternalId(clientExternalId, 
clientChargePaidTransactionId2);
+                .undoClientTransactionByExternalId(clientExternalId, 
clientChargePaidTransactionId1);
         assertNotNull(undoTransactionResponse.getResourceId());
+
+        PostClientsClientIdTransactionsTransactionIdResponse 
undoTransactionResponse2 = clientHelper
+                .undoClientTransactionByTransactionExternalId(clientId, 
clientChargePaidTransactionExternalId);
+        assertNotNull(undoTransactionResponse2.getResourceId());
     }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
index 7d9fa2cab..4c18dbd71 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
@@ -638,6 +638,18 @@ public class ClientHelper extends IntegrationTest {
         return json;
     }
 
+    public static String getPayChargeJSONWithExternalId(final String date, 
String amount, String externalId) {
+        final HashMap<String, String> map = new HashMap<>();
+        map.put("locale", "en_GB");
+        map.put("dateFormat", Utils.DATE_FORMAT);
+        map.put("transactionDate", date);
+        map.put("amount", amount);
+        map.put("externalId", externalId);
+        String json = GSON.toJson(map);
+        log.info("{}", json);
+        return json;
+    }
+
     public static String getWaiveChargeJSON(final String amount, String 
clientChargeId) {
         final HashMap<String, String> map = new HashMap<>();
         map.put("locale", "en_GB");
@@ -752,6 +764,15 @@ public class ClientHelper extends IntegrationTest {
         return response.get("transactionId") != null ? 
response.get("transactionId").toString() : null;
     }
 
+    public static String payChargesForClientsTransactionExternalId(final 
RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, final Integer clientId, 
final Integer clientChargeId, final String json) {
+        log.info("--------------------------------- PAY CHARGES FOR CLIENT 
--------------------------------");
+        final String CHARGES_URL = "/fineract-provider/api/v1/clients/" + 
clientId + "/charges/" + clientChargeId + "?command=paycharge&"
+                + Utils.TENANT_IDENTIFIER;
+        final HashMap<?, ?> response = Utils.performServerPost(requestSpec, 
responseSpec, CHARGES_URL, json, "");
+        return response.get("subResourceExternalId") != null ? 
response.get("subResourceExternalId").toString() : null;
+    }
+
     public static String waiveChargesForClients(final RequestSpecification 
requestSpec, final ResponseSpecification responseSpec,
             final Integer clientId, final Integer clientChargeId, final String 
json) {
         log.info("--------------------------------- WAIVE CHARGES FOR CLIENT 
--------------------------------");
@@ -795,12 +816,22 @@ public class ClientHelper extends IntegrationTest {
 
     public GetClientsClientIdTransactionsTransactionIdResponse 
getClientTransactionByExternalId(final String externalId,
             final String transactionId) {
-        return 
ok(fineract().clientTransactions.retrieveClientTransaction1(externalId, 
Long.parseLong(transactionId)));
+        return 
ok(fineract().clientTransactions.retrieveClientTransaction2(externalId, 
Long.parseLong(transactionId)));
+    }
+
+    public GetClientsClientIdTransactionsTransactionIdResponse 
getClientTransactionByTransactionExternalId(final Long clientId,
+            final String transactionExternalId) {
+        return 
ok(fineract().clientTransactions.retrieveClientTransaction1(clientId, 
transactionExternalId));
     }
 
     public PostClientsClientIdTransactionsTransactionIdResponse 
undoClientTransactionByExternalId(final String externalId,
             final String transactionId) {
-        return 
ok(fineract().clientTransactions.undoClientTransaction1(externalId, 
Long.parseLong(transactionId), "undo"));
+        return 
ok(fineract().clientTransactions.undoClientTransaction2(externalId, 
Long.parseLong(transactionId), "undo"));
+    }
+
+    public PostClientsClientIdTransactionsTransactionIdResponse 
undoClientTransactionByTransactionExternalId(final Long clientId,
+            final String transactionExternalId) {
+        return 
ok(fineract().clientTransactions.undoClientTransaction1(clientId, 
transactionExternalId, "undo"));
     }
 
     public Workbook getClientEntityWorkbook(GlobalEntityType clientsEntity, 
String dateFormat) throws IOException {


Reply via email to