diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts index 84ad4ec..93c8024 100644 --- a/samples/build.gradle.kts +++ b/samples/build.gradle.kts @@ -4,7 +4,7 @@ plugins { description = "Kody Java gRPC Client Sample" -val kodyClientVersion by extra("0.0.3") +val kodyClientVersion by extra("0.0.5") val protobufVersion by extra("4.27.3") val grpcVersion by extra("1.66.0") diff --git a/samples/src/main/java/cli/PaymentInput.java b/samples/src/main/java/cli/PaymentInput.java index 083df4d..a95ac12 100644 --- a/samples/src/main/java/cli/PaymentInput.java +++ b/samples/src/main/java/cli/PaymentInput.java @@ -1,8 +1,20 @@ package cli; +import com.kodypay.grpc.pay.v1.PaymentMethodType; + public class PaymentInput { private long amount; + private boolean showTips; + private PaymentMethodType paymentMethodType; + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } public boolean isShowTips() { return showTips; @@ -12,13 +24,11 @@ public void setShowTips(boolean showTips) { this.showTips = showTips; } - private boolean showTips; - - public long getAmount() { - return amount; + public PaymentMethodType getPaymentMethodType() { + return paymentMethodType; } - public void setAmount(long amount) { - this.amount = amount; + public void setPaymentMethodType(PaymentMethodType paymentMethodType) { + this.paymentMethodType = paymentMethodType; } } diff --git a/samples/src/main/java/common/PaymentClient.java b/samples/src/main/java/common/PaymentClient.java index 2cd10d2..fa30777 100644 --- a/samples/src/main/java/common/PaymentClient.java +++ b/samples/src/main/java/common/PaymentClient.java @@ -4,17 +4,11 @@ import com.kodypay.grpc.ecom.v1.GetPaymentsResponse.Response.PaymentDetails; import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc.KodyEcomPaymentsServiceStub; import com.kodypay.grpc.ecom.v1.PaymentInitiationRequest.ExpirySettings; -import com.kodypay.grpc.pay.v1.CancelRequest; -import com.kodypay.grpc.pay.v1.CancelResponse; -import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc; -import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc.KodyPayTerminalServiceStub; -import com.kodypay.grpc.pay.v1.PayRequest; -import com.kodypay.grpc.pay.v1.PayResponse; +import com.kodypay.grpc.pay.v1.*; import com.kodypay.grpc.pay.v1.PaymentDetailsRequest; -import com.kodypay.grpc.pay.v1.PaymentStatus; -import com.kodypay.grpc.pay.v1.Terminal; -import com.kodypay.grpc.pay.v1.TerminalsRequest; -import com.kodypay.grpc.pay.v1.TerminalsResponse; +import com.kodypay.grpc.pay.v1.RefundRequest; +import com.kodypay.grpc.pay.v1.RefundResponse; +import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc.KodyPayTerminalServiceStub; import com.kodypay.grpc.sdk.common.PageCursor; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -72,11 +66,7 @@ private Metadata getMetadata() { return metadata; } - public CompletableFuture sendPayment(String terminalId, BigDecimal amount, java.util.function.Consumer onPending) { - return sendPayment(terminalId, amount, false, onPending); - } - - public CompletableFuture sendPayment(String terminalId, BigDecimal amount, boolean showTips, java.util.function.Consumer onPending) { + public CompletableFuture sendPayment(String terminalId, BigDecimal amount, boolean showTips, PaymentMethodType paymentMethodType, java.util.function.Consumer onPending) { LOG.debug("sendPayment: storeId={}, amount={}, terminalId={} (address: {})", payStoreId, amount, terminalId, inetSocketAddress); CompletableFuture future = new CompletableFuture<>(); @@ -85,6 +75,7 @@ public CompletableFuture sendPayment(String terminalId, BigDecimal .setAmount(amount.toPlainString()) .setTerminalId(terminalId) .setShowTips(showTips) + .setPaymentMethod(PaymentMethod.newBuilder().setPaymentMethodType(paymentMethodType).build()) .build(), new StreamObserver<>() { PayResponse response; @@ -154,6 +145,42 @@ public void onCompleted() { return future; } + public CompletableFuture requestOnlineRefund(String paymentId, long amount) { + LOG.debug("requestOnlineRefund: storeId={}, paymentId={}, amount={} (address: {})", payStoreId, paymentId, amount, inetSocketAddress); + + CompletableFuture future = new CompletableFuture<>(); + ecomServiceStub.refund(com.kodypay.grpc.ecom.v1.RefundRequest.newBuilder() + .setStoreId(payStoreId.toString()) + .setPaymentId(paymentId) + .setAmount(String.valueOf(amount)) + .build(), new StreamObserver<>() { + com.kodypay.grpc.ecom.v1.RefundResponse response; + + @Override + public void onNext(com.kodypay.grpc.ecom.v1.RefundResponse res) { + response = res; + com.kodypay.grpc.ecom.v1.RefundResponse.RefundStatus refundStatus = response.getStatus(); + LOG.debug("requestRefund: response={}", response); + if (refundStatus == com.kodypay.grpc.ecom.v1.RefundResponse.RefundStatus.FAILED) { + LOG.error("requestRefund: Failed to request refund, status={}, message={}", refundStatus, response); + } + } + + @Override + public void onError(Throwable e) { + LOG.error("requestOnlineRefund: error requesting online refund, message={}, stack={}", e.getMessage(), e); + future.completeExceptionally(e); + } + + @Override + public void onCompleted() { + LOG.debug("requestOnlineRefund: complete"); + future.complete(response); + } + }); + return future; + } + public CompletableFuture cancelPayment(BigDecimal amount, String terminalId, String orderId) { LOG.debug("cancelPayment: storeId={}, amount={}, terminalId={}, orderId={}", payStoreId, amount, terminalId, orderId); @@ -191,6 +218,42 @@ public void onCompleted() { return future; } + public CompletableFuture requestRefund(BigDecimal amount, String orderId) { + LOG.debug("requestRefund: storeId={}, amount={}, orderId={}", payStoreId, amount, orderId); + + CompletableFuture future = new CompletableFuture<>(); + terminalServiceStub.refund(RefundRequest.newBuilder() + .setStoreId(payStoreId.toString()) + .setAmount(amount.toPlainString()) + .setOrderId(orderId) + .build(), new StreamObserver<>() { + RefundResponse response; + + @Override + public void onNext(RefundResponse res) { + response = res; + RefundResponse.RefundStatus refundStatus = response.getStatus(); + LOG.debug("requestRefund: response={}", response); + if (refundStatus == RefundResponse.RefundStatus.FAILED) { + LOG.error("requestRefund: Failed to request refund, status={}, message={}", refundStatus, response); + } + } + + @Override + public void onError(Throwable e) { + LOG.error("requestRefund: Failed to request refund, message={}, stack={}", e.getMessage(), e); + future.completeExceptionally(e); + } + + @Override + public void onCompleted() { + LOG.debug("requestRefund: complete"); + future.complete(response); + } + }); + return future; + } + public CompletableFuture getDetails(String orderId) { LOG.debug("getDetails: storeId={}, orderId={}", payStoreId, orderId); diff --git a/samples/src/main/java/ecom/EcomAsyncJavaClient.java b/samples/src/main/java/ecom/EcomAsyncJavaClient.java index 3cfad58..fbe88e5 100644 --- a/samples/src/main/java/ecom/EcomAsyncJavaClient.java +++ b/samples/src/main/java/ecom/EcomAsyncJavaClient.java @@ -1,10 +1,11 @@ package ecom; -import cli.EcomPaymentInput; import cli.Command; +import cli.EcomPaymentInput; import cli.PaymentCommand; -import com.kodypay.grpc.ecom.v1.*; import com.kodypay.grpc.ecom.v1.GetPaymentsResponse.Response.PaymentDetails; +import com.kodypay.grpc.ecom.v1.PaymentInitiationResponse; +import com.kodypay.grpc.ecom.v1.RefundResponse; import common.CurrencyEnum; import common.PaymentClient; import org.jline.reader.LineReader; @@ -44,10 +45,10 @@ public CompletableFuture sendPaymentAsync(long amount String paymentReference = generatePaymentReference(); String orderId = generateOrderId(); String currency = CurrencyEnum.HKD.name(); - return sendPaymentAsync(amount, paymentReference, currency, orderId); + return sendPaymentAsync(paymentReference, amount, currency, orderId); } - private CompletableFuture sendPaymentAsync(long amount, String currency, String paymentReference, String orderId) { + private CompletableFuture sendPaymentAsync(String paymentReference, long amount, String currency, String orderId) { LOG.info("Initiating payment for amount: {}", amount); return client.sendOnlinePayment(paymentReference, amount, currency, orderId, "returnUrl") @@ -57,6 +58,16 @@ private CompletableFuture sendPaymentAsync(long amoun }); } + CompletableFuture requestRefundAsync(String paymentId, long amount) { + LOG.info("Request refund for amount: {}", amount); + + return client.requestOnlineRefund(paymentId, amount) + .thenApply(res -> { + LOG.info("Requested Online Refund - Response: {}", res); + return res; + }); + } + public CompletableFuture> getPayments() { LOG.info("Get payments"); @@ -87,7 +98,7 @@ public SendPaymentCommand() { @Override public void execute() { - asyncClient.sendPaymentAsync(input.getAmount(), input.getCurrency(), input.getPaymentReference(), input.getOrderId()); + asyncClient.sendPaymentAsync(input.getPaymentReference(), input.getAmount(), input.getCurrency(), input.getOrderId()); } @Override diff --git a/samples/src/main/java/ecom/EcomBlockingJavaClient.java b/samples/src/main/java/ecom/EcomBlockingJavaClient.java index c7598c3..643bca5 100644 --- a/samples/src/main/java/ecom/EcomBlockingJavaClient.java +++ b/samples/src/main/java/ecom/EcomBlockingJavaClient.java @@ -1,29 +1,54 @@ package ecom; +import com.kodypay.grpc.ecom.v1.GetPaymentsResponse.Response.PaymentDetails; +import com.kodypay.grpc.ecom.v1.PaymentInitiationResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class EcomBlockingJavaClient { + private static final Logger LOG = LoggerFactory.getLogger(EcomBlockingJavaClient.class.getName()); private final EcomAsyncJavaClient asyncClient; public EcomBlockingJavaClient() { asyncClient = new EcomAsyncJavaClient(); } - public void sendOnlinePaymentBlocking(long amountStr) throws ExecutionException, InterruptedException, TimeoutException { - asyncClient.sendPaymentAsync(amountStr).get(1, TimeUnit.MINUTES); + public PaymentInitiationResponse sendOnlinePaymentBlocking(long amountStr) throws ExecutionException, InterruptedException, TimeoutException { + return asyncClient.sendPaymentAsync(amountStr).get(1, TimeUnit.MINUTES); + } + + public void requestOnlineRefund(String paymentId, long amountStr) { + asyncClient.requestRefundAsync(paymentId, amountStr).thenAccept(it -> LOG.info("Requested refund response: {}", it)); } - public void getPaymentsBlocking() throws ExecutionException, InterruptedException, TimeoutException { - asyncClient.getPayments().get(1, TimeUnit.MINUTES); + public List getPaymentsBlocking() throws ExecutionException, InterruptedException, TimeoutException { + return asyncClient.getPayments().get(1, TimeUnit.MINUTES); } public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException { EcomBlockingJavaClient ecomBlockingJavaClient = new EcomBlockingJavaClient(); long amountInPence = 100; - ecomBlockingJavaClient.getPaymentsBlocking(); - ecomBlockingJavaClient.sendOnlinePaymentBlocking(amountInPence); + Optional payment; + PaymentInitiationResponse paymentResponse = ecomBlockingJavaClient.sendOnlinePaymentBlocking(amountInPence); + + // Wait for payment to be complete before refunding it + do { + LOG.info("Waiting for online payment to complete"); + Thread.sleep(5000); + payment = ecomBlockingJavaClient + .getPaymentsBlocking() + .stream() + .filter(c -> c.getPaymentId().equals(paymentResponse.getResponse().getPaymentId())) + .findFirst(); + } while (payment.isEmpty() || payment.stream().allMatch(e -> e.getStatus() == PaymentDetails.PaymentStatus.PENDING)); + + ecomBlockingJavaClient.requestOnlineRefund(paymentResponse.getResponse().getPaymentUrl(), amountInPence); } } diff --git a/samples/src/main/java/terminal/TerminalJavaClient.java b/samples/src/main/java/terminal/TerminalJavaClient.java index 3a23a30..1eb17d6 100644 --- a/samples/src/main/java/terminal/TerminalJavaClient.java +++ b/samples/src/main/java/terminal/TerminalJavaClient.java @@ -3,12 +3,11 @@ import cli.Command; import cli.PaymentCommand; import cli.PaymentInput; -import com.kodypay.grpc.pay.v1.PayResponse; -import com.kodypay.grpc.pay.v1.PaymentStatus; -import com.kodypay.grpc.pay.v1.Terminal; +import com.kodypay.grpc.pay.v1.*; import common.PaymentClient; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.completer.EnumCompleter; import org.jline.reader.impl.completer.StringsCompleter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,24 +43,24 @@ public TerminalJavaClient() { client = new PaymentClient(address, storeId, apiKey); } - public PayResponse sendPayment(String amountStr, boolean showTips) throws ExecutionException, InterruptedException, TimeoutException { + public PayResponse sendPayment(String amountStr, boolean showTips, PaymentMethodType paymentMethodType) throws ExecutionException, InterruptedException, TimeoutException { LOG.info("Sending payment for amount: {} to terminal: {}", amountStr, exTerminalId); BigDecimal amount = new BigDecimal(amountStr); - CompletableFuture response = client.sendPayment(exTerminalId, amount, showTips, orderId -> { + CompletableFuture response = client.sendPayment(exTerminalId, amount, showTips, paymentMethodType, orderId -> { LOG.info("onPending: orderId={}", orderId); // optionally cancel payment after delay Executor delayed = CompletableFuture.delayedExecutor(30L, TimeUnit.SECONDS); CompletableFuture.supplyAsync(() -> { LOG.info("Cancelling payment: {} = {}", amount, orderId); - return cancelPaymentAsync(amountStr, orderId); + return cancelPayment(amountStr, orderId); }, delayed); }); response.thenAccept(res -> LOG.info("Sent Payment: {}, {}", amount, dump(res))); return response.get(timeout, TimeUnit.MINUTES); } - public CompletableFuture cancelPaymentAsync(String amountStr, String orderId) { + public CompletableFuture cancelPayment(String amountStr, String orderId) { LOG.info("Cancel payment: {} with amount: {}", orderId, amountStr); BigDecimal amount = new BigDecimal(amountStr); @@ -72,6 +71,21 @@ public CompletableFuture cancelPaymentAsync(String amountStr, Str return response; } + public RefundResponse requestRefund(String amountStr, String orderId) throws ExecutionException, InterruptedException, TimeoutException { + Executor delayed = CompletableFuture.delayedExecutor(5L, TimeUnit.SECONDS); + + CompletableFuture refundResponseFuture = CompletableFuture.supplyAsync(() -> { + LOG.info("Requesting refund for amount: {} for orderId: {}", amountStr, orderId); + + BigDecimal amount = new BigDecimal(amountStr); + return client.requestRefund(amount, orderId).toCompletableFuture().join(); + }, delayed); + + refundResponseFuture.thenAccept(res -> LOG.info("Refunded payment: {} = {}", orderId, dump(res))); + + return refundResponseFuture.get(timeout, TimeUnit.MINUTES); + } + public PayResponse getDetails(String orderId) throws ExecutionException, InterruptedException, TimeoutException { LOG.info("Get payment details for: {}", orderId); @@ -103,18 +117,22 @@ public static void main(String[] args) throws Exception { String amountStr = "3.14"; //Set to true to show tips on the terminal - boolean isShowTips = false; + boolean isShowTips = true; + PaymentMethodType paymentMethodType = PaymentMethodType.CARD; // ALIPAY, WECHAT listTerminals(); - var orderId = terminalClient.sendPayment(amountStr, isShowTips).getOrderId(); + var orderId = terminalClient.sendPayment(amountStr, isShowTips, paymentMethodType).getOrderId(); LOG.info("Completed order: {}", orderId); var status = terminalClient.getDetails(orderId).getStatus(); LOG.info("Payment status: {}", status); + var refundStatus = terminalClient.requestRefund(amountStr, orderId).getStatus(); + LOG.info("Refund status: {}", refundStatus); + if (status == PaymentStatus.PENDING) { - terminalClient.cancelPaymentAsync(amountStr, orderId).thenAccept(cancel -> { + terminalClient.cancelPayment(amountStr, orderId).thenAccept(cancel -> { LOG.info("Order is cancelled? {}", cancel == PaymentStatus.CANCELLED); }); } @@ -140,6 +158,14 @@ private static String dump(PayResponse msg) { }).collect(Collectors.joining(", ")); } + private static String dump(RefundResponse msg) { + return msg.getAllFields().entrySet().stream().map(it -> { + var key = it.getKey().getName(); + var value = it.getValue().toString().replace('\n', ' '); + return key + "=" + value; + }).collect(Collectors.joining(", ")); + } + public static class SendPaymentCommand implements PaymentCommand { private static final PaymentInput input = new PaymentInput(); @@ -151,11 +177,14 @@ public void execute() { listTerminals(); String orderId; try { - orderId = terminalClient.sendPayment(String.format("%.2f", (float) input.getAmount() / 100), input.isShowTips()).getOrderId(); + orderId = terminalClient.sendPayment(String.format("%.2f", (float) input.getAmount() / 100), input.isShowTips(), input.getPaymentMethodType()).getOrderId(); LOG.info("Completed order: {}", orderId); var status = terminalClient.getDetails(orderId).getStatus(); LOG.info("Payment status: {}", status); + + var refundStatus = terminalClient.requestRefund(String.format("%.2f", (float) input.getAmount() / 100), orderId).getStatus(); + LOG.info("Refund status: {}", refundStatus); } catch (Exception e) { throw new RuntimeException(e); } @@ -168,6 +197,10 @@ public void gatherInput() { LineReader booleanReader = LineReaderBuilder.builder().completer(new StringsCompleter("true", "false")).build(); input.setShowTips(Boolean.parseBoolean(booleanReader.readLine("\n Do you want to enable Terminal to show Tips (true/false): "))); + + LineReader paymentMethodTypeReader = LineReaderBuilder.builder() + .completer(new EnumCompleter(PaymentMethodType.class)).build(); + input.setPaymentMethodType(PaymentMethodType.valueOf(paymentMethodTypeReader.readLine("\nChoose payment methode type (CARD, ALIPAY, WECHAT): "))); } }