From 7956c8d333be7cd5bdb9d9a67f424bd57892ae43 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 29 Oct 2024 21:26:14 +1000 Subject: [PATCH 1/2] Added better logging for Dibs provider. Added handling the webhook "payment checkout completed" callback to make it work on headless solution. --- src/DibsEasyCheckout.cs | 35 +++++++++++++--- ...e.CheckoutHandlers.DibsEasyCheckout.csproj | 2 +- ...ErrorResponse.cs => BadRequestResponse.cs} | 2 +- src/Models/InternalServerErrorResponse.cs | 16 +++++++ .../Webhooks/PaymentCheckoutCompleted.cs | 23 ++++++++++ .../Webhooks/PaymentCheckoutCompletedData.cs | 10 +++++ src/Service/DibsRequest.cs | 42 ++++++++++++++----- src/Service/DibsService.cs | 19 +++++---- 8 files changed, 122 insertions(+), 27 deletions(-) rename src/Models/{ErrorResponse.cs => BadRequestResponse.cs} (86%) create mode 100644 src/Models/InternalServerErrorResponse.cs create mode 100644 src/Models/Webhooks/PaymentCheckoutCompleted.cs create mode 100644 src/Models/Webhooks/PaymentCheckoutCompletedData.cs diff --git a/src/DibsEasyCheckout.cs b/src/DibsEasyCheckout.cs index 64d4988..6164824 100644 --- a/src/DibsEasyCheckout.cs +++ b/src/DibsEasyCheckout.cs @@ -13,6 +13,8 @@ using Dynamicweb.Rendering; using System; using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading; namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout @@ -219,7 +221,28 @@ private OutputResult StateOk(Order order) order.TransactionStatus = "Failed"; throw new Exception("Dibs payment failed."); } + var paymentId = Context.Current.Request["paymentId"]; + + try + { + using var reader = new StreamReader(Context.Current.Request.InputStream, Encoding.UTF8); + string inputStreamRawData = reader.ReadToEndAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!string.IsNullOrWhiteSpace(inputStreamRawData)) + LogEvent(order, $"Read the input stream data: {inputStreamRawData}"); + + PaymentCheckoutCompleted checkoutCompleted = Converter.Deserialize(inputStreamRawData); + if (checkoutCompleted?.Data?.PaymentId is string pId) + paymentId = pId; + } + catch (Exception ex) + { + LogError(order, $"Error during processing input stream to get the webhook data: {ex.Message}"); + } + + if (string.IsNullOrEmpty(paymentId)) + throw new Exception("Payment id is empty."); + UpdateOrderInfo(paymentId, order); return PassToCart(order); @@ -239,7 +262,7 @@ private void UpdateOrderInfo(string paymentId, Order order) { LogEvent(order, "Getting payment from Dibs by paymentId"); - var service = new DibsService(GetSecretKey(), GetApiUrl()); + var service = new DibsService(order, GetSecretKey(), GetApiUrl()); DibsPaymentResponse paymentResponse = service.GetPayment(paymentId); if (paymentResponse != null) @@ -314,7 +337,7 @@ private void UpdateOrderInfo(string paymentId, Order order) private void ChargePayment(Order order, string paymentId, long chargeAmount) { - var service = new DibsService(GetSecretKey(), GetApiUrl()); + var service = new DibsService(order, GetSecretKey(), GetApiUrl()); CapturePaymentResponse charge = service.CapturePayment(paymentId, chargeAmount); if (!string.IsNullOrWhiteSpace(charge?.ChargeId)) @@ -493,7 +516,7 @@ private string Refund(Order order) } long refundAmount = PriceHelper.ConvertToPIP(order.Currency, order.CaptureAmount); - var service = new DibsService(GetSecretKey(), GetApiUrl()); + var service = new DibsService(order, GetSecretKey(), GetApiUrl()); RefundPaymentResponse returnResponse = service.RefundPayment(order.TransactionNumber, refundAmount); LogEvent(order, "Remote return id: {0}", returnResponse.RefundId); @@ -534,7 +557,7 @@ private bool Cancel(Order order) return false; } - var service = new DibsService(GetSecretKey(), GetApiUrl()); + var service = new DibsService(order, GetSecretKey(), GetApiUrl()); service.CancelPayment(order.TransactionNumber, order.Price.PricePIP); LogEvent(order, "Order has been cancelled."); return true; @@ -617,8 +640,8 @@ private OutputResult RenderPaymentForm(Order order, Template formTemplate, bool string baseUrl = GetBaseUrl(order, headless); string approvetUrl = GetApprovetUrl(baseUrl); - var service = new DibsService(GetSecretKey(), GetApiUrl()); - var newPayment = service.CreatePayment(order, new() + var service = new DibsService(order, GetSecretKey(), GetApiUrl()); + var newPayment = service.CreatePayment(new() { BaseUrl = baseUrl, PrefillCustomerAddress = PrefillCustomerAddress, diff --git a/src/Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.csproj b/src/Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.csproj index 16ab632..921dacf 100644 --- a/src/Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.csproj +++ b/src/Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.csproj @@ -1,6 +1,6 @@  - 10.6.2 + 10.6.3 1.0.0.0 Nets Easy Checkout Nets Easy Checkout diff --git a/src/Models/ErrorResponse.cs b/src/Models/BadRequestResponse.cs similarity index 86% rename from src/Models/ErrorResponse.cs rename to src/Models/BadRequestResponse.cs index aa31743..b93ad89 100644 --- a/src/Models/ErrorResponse.cs +++ b/src/Models/BadRequestResponse.cs @@ -4,7 +4,7 @@ namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Models; [DataContract] -internal sealed class ErrorResponse +internal sealed class BadRequestResponse { [DataMember(Name = "errors")] public Dictionary> Errors { get; set; } diff --git a/src/Models/InternalServerErrorResponse.cs b/src/Models/InternalServerErrorResponse.cs new file mode 100644 index 0000000..504b436 --- /dev/null +++ b/src/Models/InternalServerErrorResponse.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Models; + +[DataContract] +internal sealed class InternalServerErrorResponse +{ + [DataMember(Name = "message")] + public string Message { get; set; } + + [DataMember(Name = "Code")] + public string Code { get; set; } + + [DataMember(Name = "Source")] + public string Source { get; set; } +} diff --git a/src/Models/Webhooks/PaymentCheckoutCompleted.cs b/src/Models/Webhooks/PaymentCheckoutCompleted.cs new file mode 100644 index 0000000..9de4243 --- /dev/null +++ b/src/Models/Webhooks/PaymentCheckoutCompleted.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Models; + +//see: https://developer.nexigroup.com/nexi-checkout/en-EU/api/webhooks/#checkout-completed +[DataContract] +internal sealed class PaymentCheckoutCompleted +{ + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "merchantId")] + public int MerchantId { get; set; } + + [DataMember(Name = "timestamp")] + public string Timestamp { get; set; } + + [DataMember(Name = "event")] + public string Event { get; set; } + + [DataMember(Name = "data")] + public PaymentCheckoutCompletedData Data { get; set; } +} diff --git a/src/Models/Webhooks/PaymentCheckoutCompletedData.cs b/src/Models/Webhooks/PaymentCheckoutCompletedData.cs new file mode 100644 index 0000000..b5c3bf1 --- /dev/null +++ b/src/Models/Webhooks/PaymentCheckoutCompletedData.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Models; + +[DataContract] +internal sealed class PaymentCheckoutCompletedData +{ + [DataMember(Name = "paymentId")] + public string PaymentId { get; set; } +} diff --git a/src/Service/DibsRequest.cs b/src/Service/DibsRequest.cs index 8e4f677..697c9be 100644 --- a/src/Service/DibsRequest.cs +++ b/src/Service/DibsRequest.cs @@ -1,5 +1,6 @@ using Dynamicweb.Core; using Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Models; +using Dynamicweb.Ecommerce.Orders; using System; using System.Collections.Generic; using System.Linq; @@ -16,7 +17,7 @@ namespace Dynamicweb.Ecommerce.CheckoutHandlers.DibsEasyCheckout.Service; /// internal static class DibsRequest { - public static string SendRequest(string apiUrl, string secretKey, CommandConfiguration configuration) + public static string SendRequest(Order order, string apiUrl, string secretKey, CommandConfiguration configuration) { using (var messageHandler = GetMessageHandler()) { @@ -46,26 +47,37 @@ ApiCommand.CreatePayment or { using (HttpResponseMessage response = requestTask.GetAwaiter().GetResult()) { - string data = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + Log(order, $"Remote server response: HttpStatusCode = {response.StatusCode}, HttpStatusDescription = {response.ReasonPhrase}"); + string responseText = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + Log(order, $"Remote server ResponseText: {responseText}"); if (!response.IsSuccessStatusCode) { - var errorResponse = Converter.Deserialize(data); - if (errorResponse?.Errors?.Any() is true) + if (response.StatusCode is HttpStatusCode.BadRequest) { - var errorMessage = new StringBuilder(); - foreach ((string propertyName, IEnumerable errors) in errorResponse.Errors) + var errorResponse = Converter.Deserialize(responseText); + if (errorResponse?.Errors?.Any() is true) { - string errorsText = string.Join(", ", errors); - errorMessage.AppendLine($"{propertyName}: {errorsText}"); + var errorMessage = new StringBuilder(); + foreach ((string propertyName, IEnumerable errors) in errorResponse.Errors) + { + string errorsText = string.Join(", ", errors); + errorMessage.AppendLine($"{propertyName}: {errorsText}"); + } + throw new Exception(errorMessage.ToString()); } - throw new Exception(errorMessage.ToString()); + } + else if (response.StatusCode is HttpStatusCode.InternalServerError) + { + var errorResponse = Converter.Deserialize(responseText); + if (!string.IsNullOrEmpty(errorResponse?.Message)) + throw new Exception($"Error code: {errorResponse.Code}. Error source: {errorResponse.Source}. Error message: {errorResponse.Message}"); } - throw new Exception($"Unhandled exception. Operation failed: {response.ReasonPhrase}"); + throw new Exception($"Unhandled exception. Operation failed: {response.ReasonPhrase}. Response text: {responseText}"); } - return data; + return responseText; } } catch (HttpRequestException requestException) @@ -101,4 +113,12 @@ private static string GetCommandLink(string baseAddress, ApiCommand command, str string GetCommandLink(string gateway) => $"{baseAddress}/v1/{gateway}"; } + + private static void Log(Order order, string message) + { + if (order is null) + return; + + Services.OrderDebuggingInfos.Save(order, message, typeof(DibsEasyCheckout).FullName, DebuggingInfoType.Undefined); + } } diff --git a/src/Service/DibsService.cs b/src/Service/DibsService.cs index eafdebb..62ef710 100644 --- a/src/Service/DibsService.cs +++ b/src/Service/DibsService.cs @@ -21,17 +21,20 @@ internal sealed class DibsService public string SecretKey { get; set; } - public DibsService(string secretKey, string apiUrl) + public Order Order { get; set; } + + public DibsService(Order order, string secretKey, string apiUrl) { + Order = order; SecretKey = secretKey; ApiUrl = apiUrl; } - public CreatePaymentResponse CreatePayment(Order order, CreatePaymentParameters parameters) + public CreatePaymentResponse CreatePayment(CreatePaymentParameters parameters) { - PaymentRequest request = ConvertOrder(order, parameters); + PaymentRequest request = ConvertOrder(Order, parameters); - string response = DibsRequest.SendRequest(ApiUrl, SecretKey, new() + string response = DibsRequest.SendRequest(Order, ApiUrl, SecretKey, new() { CommandType = ApiCommand.CreatePayment, Data = request @@ -43,7 +46,7 @@ public CreatePaymentResponse CreatePayment(Order order, CreatePaymentParameters public DibsPaymentResponse GetPayment(string paymentId) { - string response = DibsRequest.SendRequest(ApiUrl, SecretKey, new() + string response = DibsRequest.SendRequest(Order, ApiUrl, SecretKey, new() { CommandType = ApiCommand.GetPayment, OperatorId = paymentId @@ -54,7 +57,7 @@ public DibsPaymentResponse GetPayment(string paymentId) public CapturePaymentResponse CapturePayment(string paymentId, long amount) { - string response = DibsRequest.SendRequest(ApiUrl, SecretKey, new() + string response = DibsRequest.SendRequest(Order, ApiUrl, SecretKey, new() { CommandType = ApiCommand.CapturePayment, OperatorId = paymentId, @@ -69,7 +72,7 @@ public CapturePaymentResponse CapturePayment(string paymentId, long amount) public RefundPaymentResponse RefundPayment(string paymentId, long amount) { - string response = DibsRequest.SendRequest(ApiUrl, SecretKey, new() + string response = DibsRequest.SendRequest(Order, ApiUrl, SecretKey, new() { CommandType = ApiCommand.RefundPayment, OperatorId = paymentId, @@ -84,7 +87,7 @@ public RefundPaymentResponse RefundPayment(string paymentId, long amount) public void CancelPayment(string paymentId, long amount) { - string response = DibsRequest.SendRequest(ApiUrl, SecretKey, new() + string response = DibsRequest.SendRequest(Order, ApiUrl, SecretKey, new() { CommandType = ApiCommand.CancelPayment, OperatorId = paymentId, From 0ac999217ea33acaca4487bdcc76ff1caf1cf072 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 29 Oct 2024 21:43:43 +1000 Subject: [PATCH 2/2] Small fix. --- src/DibsEasyCheckout.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/DibsEasyCheckout.cs b/src/DibsEasyCheckout.cs index 6164824..338c3fe 100644 --- a/src/DibsEasyCheckout.cs +++ b/src/DibsEasyCheckout.cs @@ -229,11 +229,13 @@ private OutputResult StateOk(Order order) using var reader = new StreamReader(Context.Current.Request.InputStream, Encoding.UTF8); string inputStreamRawData = reader.ReadToEndAsync().ConfigureAwait(false).GetAwaiter().GetResult(); if (!string.IsNullOrWhiteSpace(inputStreamRawData)) + { LogEvent(order, $"Read the input stream data: {inputStreamRawData}"); - PaymentCheckoutCompleted checkoutCompleted = Converter.Deserialize(inputStreamRawData); - if (checkoutCompleted?.Data?.PaymentId is string pId) - paymentId = pId; + PaymentCheckoutCompleted checkoutCompleted = Converter.Deserialize(inputStreamRawData); + if (checkoutCompleted?.Data?.PaymentId is string pId) + paymentId = pId; + } } catch (Exception ex) {