diff --git a/src/Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.csproj b/src/Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.csproj index 4623490..50c8e09 100644 --- a/src/Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.csproj +++ b/src/Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.csproj @@ -24,6 +24,16 @@ true true + + + + + + + + + + diff --git a/src/Hash.cs b/src/Hash.cs new file mode 100644 index 0000000..061126b --- /dev/null +++ b/src/Hash.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow; + +internal static class Hash +{ + public static string ByteArrayToHexString(byte[] bytes) + { + var result = new StringBuilder(); + foreach (byte b in bytes) + { + result.Append(b.ToString("x2")); + } + + return result.ToString(); + } + + public static string ComputeHash(string key, Stream message) + { + var encoding = new UTF8Encoding(); + var byteKey = encoding.GetBytes(key); + + using (HMACSHA256 hmac = new HMACSHA256(byteKey)) + { + var hashedBytes = hmac.ComputeHash(message); + return ByteArrayToHexString(hashedBytes); + } + } + + public static string ComputeHash(string key, string message) + { + var encoding = new UTF8Encoding(); + var byteKey = encoding.GetBytes(key); + + using (HMACSHA256 hmac = new HMACSHA256(byteKey)) + { + var messageBytes = encoding.GetBytes(message); + var hashedBytes = hmac.ComputeHash(messageBytes); + + return ByteArrayToHexString(hashedBytes); + } + } +} diff --git a/src/Models/CardLinkUrl.cs b/src/Models/CardLinkUrl.cs new file mode 100644 index 0000000..afa1c5d --- /dev/null +++ b/src/Models/CardLinkUrl.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models; + +[DataContract] +internal sealed class CardLinkUrl +{ + [DataMember(Name = "url")] + public string Url { get; set; } +} diff --git a/src/Models/Frontend/CallbackError.cs b/src/Models/Frontend/CallbackError.cs new file mode 100644 index 0000000..6cb4dfd --- /dev/null +++ b/src/Models/Frontend/CallbackError.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models.Frontend; + +//This is a model for ajax-interactions with our templates only +[DataContract] +internal sealed class CallbackError +{ + [DataMember(Name = "errorMessage")] + public string ErrorMessage { get; set; } +} diff --git a/src/Models/Frontend/CreateCardRequestData.cs b/src/Models/Frontend/CreateCardRequestData.cs new file mode 100644 index 0000000..32a634d --- /dev/null +++ b/src/Models/Frontend/CreateCardRequestData.cs @@ -0,0 +1,35 @@ +using System.Runtime.Serialization; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models.Frontend; + +//This is a model for ajax-interactions with our templates only +[DataContract] +internal sealed class CreateCardRequestData +{ + [DataMember(Name = "agreementId")] + public string AgreementId { get; set; } + + [DataMember(Name = "brandingId")] + public string BrandingId { get; set; } + + [DataMember(Name = "languageCode")] + public string LanguageCode { get; set; } + + [DataMember(Name = "paymentMethods")] + public string PaymentMethods { get; set; } + + [DataMember(Name = "googleAnalyticsTrackingId")] + public string GoogleAnalyticsTrackingId { get; set; } + + [DataMember(Name = "googleAnalyticsClientId")] + public string GoogleAnalyticsClientId { get; set; } + + [DataMember(Name = "receiptUrl")] + public string ReceiptUrl { get; set; } + + [DataMember(Name = "cancelUrl")] + public string CancelUrl { get; set; } + + [DataMember(Name = "callbackUrl")] + public string СallbackUrl { get; set; } +} diff --git a/src/Models/Error.cs b/src/Models/ServiceError.cs similarity index 84% rename from src/Models/Error.cs rename to src/Models/ServiceError.cs index 4340460..41f0fe4 100644 --- a/src/Models/Error.cs +++ b/src/Models/ServiceError.cs @@ -2,7 +2,8 @@ namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models; -internal sealed class Error +[DataContract] +internal sealed class ServiceError { [DataMember(Name = "message")] public string Message { get; set; } diff --git a/src/QuickPayPaymentWindow.cs b/src/QuickPayPaymentWindow.cs index 14e71ff..ddf54f2 100644 --- a/src/QuickPayPaymentWindow.cs +++ b/src/QuickPayPaymentWindow.cs @@ -2,6 +2,8 @@ using Dynamicweb.Configuration; using Dynamicweb.Core; using Dynamicweb.Ecommerce.Cart; +using Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models; +using Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Models.Frontend; using Dynamicweb.Ecommerce.Orders; using Dynamicweb.Ecommerce.Orders.Gateways; using Dynamicweb.Ecommerce.Prices; @@ -15,7 +17,6 @@ using System.Data; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Text; using System.Threading; @@ -92,7 +93,7 @@ public string PostModeSelection /// /// Gets or sets path to template that renders before user will be redirected to Quick Pay service /// - [AddInParameter("Post template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{PostTemplateFolder}; infoText=The Post template is used to post data to QuickPay when the render mode is Render template or Render inline form.;")] + [AddInParameter("Post template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{PostTemplateFolder}; infoText=The Post template is used to post data to QuickPay when the render mode is Render template or Render inline form. Please note, that in the case of \"save new card\" the template is for informational purposes only (not for interacting with the Quickpay Form).")] public string PostTemplate { get => TemplateHelper.GetTemplateName(postTemplate); @@ -156,30 +157,34 @@ public string ErrorTemplate #region Form properties + private static string[] SupportedLanguages { get; set; } = + [ + "da", + "de", + "en", + "es", + "fi", + "fo", + "fr", + "it", + "kl", + "nb", + "nl", + "nn", + "no", + "pl", + "pt", + "ru", + "se", + "sv" + ]; + private static string LanguageCode { get { string currentLanguageCode = Environment.ExecutingContext.GetCulture(true).TwoLetterISOLanguageName; - string[] supportedLanguageCodes = - [ - "da", - "de", - "es", - "fo", - "fi", - "fr", - "kl", - "it", - "nl", - "pl", - "pt", - "ru", - "sv", - "nb", - "nn" - ]; - if (!supportedLanguageCodes.Contains(currentLanguageCode)) + if (!SupportedLanguages.Contains(currentLanguageCode)) return "en"; else { @@ -262,52 +267,41 @@ public override OutputResult BeginCheckout(Order order, CheckoutParameters param } if (order.DoSaveCardToken || !string.IsNullOrEmpty(cardName) || order.IsRecurringOrderTemplate) - return CreateCard(cardName, order, headless, receiptUrl, cancelUrl); - else { - var formValues = new Dictionary + if (postMode is PostModes.Template) { - {"version", "v10"}, - {"merchant_id", Merchant.Trim()}, - {"agreement_id", Agreement.Trim()}, - {"order_id", order.Id}, - {"language", LanguageCode}, - {"amount", order.Price.PricePIP.ToString()}, - {"currency", order.Price.Currency.Code}, - {"continueurl", receiptUrl ?? ContinueUrl(order)}, - {"cancelurl", cancelUrl ?? CancelUrl(order)}, - {"callbackurl", CallbackUrl(order, headless)}, - {"autocapture", AutoCapture ? "1" : "0"}, - {"autofee", AutoFee ? "1" : "0"}, - {"payment_methods", PaymentMethods}, - {"branding_id", Branding}, - {"google_analytics_tracking_id", GoogleAnalyticsTracking}, - {"google_analytics_client_id", GoogleAnalyticsClient} - }; + var cardTemplate = new Template(TemplateHelper.GetTemplatePath(PostTemplate, PostTemplateFolder)); + GetTemplateHelper().SetCardTemplateTags(cardTemplate); - formValues.Add("checksum", ComputeHash(ApiKey, GetMacString(formValues))); + return new ContentOutputResult + { + Content = Render(order, cardTemplate) + }; + } - switch (postMode) - { - case PostModes.Auto: - LogEvent(order, "Autopost to QuickPay"); - return GetSubmitFormResult("https://payment.quickpay.net", formValues); + return CreateCard(order, headless, receiptUrl, cancelUrl); + } - case PostModes.Template: - LogEvent(order, "Render template"); + QuickpayTemplateHelper templateHelper = GetTemplateHelper(); + switch (postMode) + { + case PostModes.Auto: + LogEvent(order, "Autopost to QuickPay"); + return GetSubmitFormResult("https://payment.quickpay.net", templateHelper.GetQuickpayFormValues(ApiKey)); - var formTemplate = new Template(TemplateHelper.GetTemplatePath(PostTemplate, PostTemplateFolder)); - foreach (var formValue in formValues) - formTemplate.SetTag(string.Format("QuickPayPaymentWindow.{0}", formValue.Key), formValue.Value); + case PostModes.Template: + LogEvent(order, "Render template"); - return new ContentOutputResult { Content = formTemplate.Output() }; + var formTemplate = new Template(TemplateHelper.GetTemplatePath(PostTemplate, PostTemplateFolder)); + templateHelper.SetQuickpayFormTemplateTags(ApiKey, formTemplate); - default: - var errorMessage = string.Format("Unhandled post mode: '{0}'", postMode); - LogError(order, errorMessage); - return PrintErrorTemplate(order, errorMessage); + return new ContentOutputResult { Content = formTemplate.Output() }; + + default: + var errorMessage = string.Format("Unhandled post mode: '{0}'", postMode); + LogError(order, errorMessage); + return PrintErrorTemplate(order, errorMessage); - } } } catch (ThreadAbortException ex) @@ -319,6 +313,26 @@ public override OutputResult BeginCheckout(Order order, CheckoutParameters param LogError(order, ex, "Unhandled exception with message: {0}", ex.Message); return PrintErrorTemplate(order, ex.Message); } + + QuickpayTemplateHelper GetTemplateHelper() => new() + { + Agreement = Agreement.Trim(), + AutoCapture = AutoCapture, + AutoFee = AutoFee, + Branding = Converter.ToInt32(Branding), + CallbackUrl = CallbackUrl(order, headless), + CancelUrl = cancelUrl ?? CancelUrl(order), + ContinueUrl = receiptUrl ?? ContinueUrl(order), + GoogleAnalyticsClient = GoogleAnalyticsClient, + GoogleAnalyticsTracking = GoogleAnalyticsTracking, + LanguageCode = LanguageCode, + Merchant = Merchant.Trim(), + Order = order, + PaymentMethods = PaymentMethods, + ReceiptUrl = receiptUrl, + AvailableLanguages = SupportedLanguages, + AvailablePaymentMethods = GetCardTypes(false, true) + }; } /// @@ -336,6 +350,8 @@ public override OutputResult HandleRequest(Order order) { case "Ok": return StateOk(order); + case "CreateCard": + return HandleCreateCard(order); case "CardSaved": return StateCardSaved(order); case "Cancel": @@ -389,6 +405,46 @@ private OutputResult StateOk(Order order) return PrintErrorTemplate(order, errorMessage); } + private OutputResult HandleCreateCard(Order order) + { + try + { + using var streamReader = new StreamReader(Context.Current.Request.InputStream); + string jsonData = streamReader.ReadToEndAsync().GetAwaiter().GetResult(); + + if (string.IsNullOrEmpty(jsonData)) + ThrowException("Callback failed with message: data is empty."); + + CreateCardRequestData requestData; + try + { + requestData = Converter.Deserialize(jsonData); + } + catch + { + ThrowException("Callback failed with message: data has wrong format."); + } + + string paymentLink = CreateCard(order, requestData); + var cardLinkUrl = new CardLinkUrl { Url = paymentLink }; + + return EndRequest(Converter.Serialize(cardLinkUrl)); + } + catch (Exception ex) + { + LogError(order, ex.Message); + var callbackError = new CallbackError { ErrorMessage = ex.Message }; + + return EndRequest(Converter.Serialize(callbackError)); + } + + void ThrowException(string errorMessage) + { + LogError(order, errorMessage); + throw new Exception(errorMessage); + } + } + private OutputResult StateCardSaved(Order order) { try @@ -803,80 +859,95 @@ private ContentOutputResult PrintErrorTemplate(Order order, string errorMessage, }; } - private OutputResult CreateCard(string cardName, Order order, bool headless, string receiptUrl, string cancelUrl) + private OutputResult CreateCard(Order order, bool headless, string receiptUrl, string cancelUrl) { - try + var requestData = new CreateCardRequestData { - string errorMessage = "Error happened during creating QuickPay card"; + AgreementId = Agreement, + LanguageCode = LanguageCode, + ReceiptUrl = receiptUrl, + CancelUrl = cancelUrl, + СallbackUrl = CallbackUrl(order, headless), + PaymentMethods = PaymentMethods, + GoogleAnalyticsTrackingId = GoogleAnalyticsTracking, + GoogleAnalyticsClientId = GoogleAnalyticsClient, + BrandingId = Branding + }; - if (string.IsNullOrEmpty(cardName)) - cardName = order.Id; + return new RedirectOutputResult { RedirectUrl = CreateCard(order, requestData) }; + } - var request = new QuickPayRequest(ApiKey, order); - var response = Converter.Deserialize>(request.SendRequest(new() - { - CommandType = ApiService.CreateCard - })); - LogEvent(order, "QuickPay Card created"); + private string CreateCard(Order order, CreateCardRequestData requestData) + { + string errorMessage = "Error happened during creating QuickPay card"; + + string cardName = order.SavedCardDraftName; + if (string.IsNullOrEmpty(cardName)) + cardName = order.Id; + + var request = new QuickPayRequest(ApiKey, order); + var response = Converter.Deserialize>(request.SendRequest(new() + { + CommandType = ApiService.CreateCard + })); + LogEvent(order, "QuickPay Card created"); + + if (response.ContainsKey("id")) + { + var cardID = Converter.ToString(response["id"]); + string continueUrl = string.IsNullOrWhiteSpace(requestData.ReceiptUrl) ? CardSavedUrl(order, cardID, cardName) : requestData.ReceiptUrl; + string cancelUrl = string.IsNullOrWhiteSpace(requestData.CancelUrl) ? CancelUrl(order) : requestData.CancelUrl; + string callbackUrl = string.IsNullOrWhiteSpace(requestData.СallbackUrl) ? CallbackUrl(order) : requestData.СallbackUrl; - if (response.ContainsKey("id")) + if (!string.IsNullOrEmpty(cardID)) { - var cardID = Converter.ToString(response["id"]); - if (!string.IsNullOrEmpty(cardID)) + var parameters = new Dictionary { - var parameters = new Dictionary - { - ["agreement_id"] = Agreement.Trim(), - ["language"] = LanguageCode, - ["continueurl"] = receiptUrl ?? CardSavedUrl(order, cardID, cardName), - ["cancelurl"] = cancelUrl ?? CancelUrl(order), - ["callbackurl"] = CallbackUrl(order, headless), - ["payment_methods"] = PaymentMethods, - ["google_analytics_tracking_id"] = GoogleAnalyticsTracking, - ["google_analytics_client_id"] = GoogleAnalyticsClient - }; + ["agreement_id"] = requestData.AgreementId.Trim(), + ["language"] = requestData.LanguageCode, + ["continueurl"] = continueUrl, + ["cancelurl"] = cancelUrl, + ["callbackurl"] = callbackUrl, + ["payment_methods"] = requestData.PaymentMethods, + ["google_analytics_tracking_id"] = requestData.GoogleAnalyticsTrackingId, + ["google_analytics_client_id"] = requestData.GoogleAnalyticsClientId + }; - if (Converter.ToInt32(Branding) is int brandingId && brandingId > 0) - parameters["branding_id"] = brandingId; + if (Converter.ToInt32(requestData.BrandingId) is int brandingId && brandingId > 0) + parameters["branding_id"] = brandingId; - response = Converter.Deserialize>(request.SendRequest(new() - { - CommandType = ApiService.GetCardLink, - OperatorId = cardID, - Parameters = parameters - })); - LogEvent(order, "QuickPay Card authorize link received"); + response = Converter.Deserialize>(request.SendRequest(new() + { + CommandType = ApiService.GetCardLink, + OperatorId = cardID, + Parameters = parameters + })); + LogEvent(order, "QuickPay Card authorize link received"); - if (response.ContainsKey("url")) - { - Services.Orders.Save(order); - string redirectUrl = Converter.ToString(response["url"]); - return new RedirectOutputResult { RedirectUrl = redirectUrl }; - } - else - { - errorMessage = string.Format("Bad QuickPay response on getting payment url. Response text{0}", response.ToString()); - LogError(order, errorMessage); - } + if (response.ContainsKey("url")) + { + Services.Orders.Save(order); + return Converter.ToString(response["url"]); } else { - errorMessage = string.Format("QuickPay response doesn't contains value for card id. Response text:{0}", response.ToString()); - LogError(order, "QuickPay response doesn't contains value for card id. Response text:{0}", response.ToString()); + errorMessage = string.Format("Bad QuickPay response on getting payment url. Response text{0}", response.ToString()); + LogError(order, errorMessage); } } else { - errorMessage = string.Format("Bad QuickPay response on creating card. Response text{0}", response.ToString()); - LogError(order, "Bad QuickPay response on creating card. Response text{0}", response.ToString()); + errorMessage = string.Format("QuickPay response doesn't contains value for card id. Response text:{0}", response.ToString()); + LogError(order, "QuickPay response doesn't contains value for card id. Response text:{0}", response.ToString()); } - return PrintErrorTemplate(order, errorMessage); } - catch (Exception ex) + else { - LogError(order, ex, ex.Message); - return PrintErrorTemplate(order, ex.Message); + errorMessage = string.Format("Bad QuickPay response on creating card. Response text{0}", response.ToString()); + LogError(order, "Bad QuickPay response on creating card. Response text{0}", response.ToString()); } + + throw new Exception(errorMessage); } private void ProcessPayment(Order order, string savedCardToken, bool isRawToken = false) @@ -1051,7 +1122,7 @@ private CheckedData CheckData(Order order, string responseText, long transaction { string responseOrderId = Converter.ToString(quickpayResponse[orderIdKey]); if (!string.IsNullOrEmpty(responseOrderId) && !responseOrderId.Equals(order.Id, StringComparison.OrdinalIgnoreCase)) - return GetErrorResult($"The order id returned from callback does not match with the order id set on the order: Callback: '{responseOrderId}', order: '{order.Id}'") + return GetErrorResult($"The order id returned from callback does not match with the order id set on the order: Callback: '{responseOrderId}', order: '{order.Id}'"); } LogEvent( @@ -1071,7 +1142,7 @@ private CheckedData CheckData(Order order, string responseText, long transaction if (doCheckSum) { - var calculatedHash = ComputeHash(PrivateKey, responseText); + var calculatedHash = Hash.ComputeHash(PrivateKey, responseText); var callbackCheckSum = Context.Current.Request.Headers["QuickPay-Checksum-Sha256"]; if (!calculatedHash.Equals(callbackCheckSum, StringComparison.CurrentCultureIgnoreCase)) @@ -1370,68 +1441,11 @@ private Dictionary GetCardTypes(bool recurringOnly, bool transla return translate ? cardTypes.ToDictionary(x => x.Key, y => y.Value) : cardTypes; } - private string GetMacString(IDictionary formValues) - { - var excludeList = new List { "MAC" }; - var keysSorted = formValues.Keys.ToArray(); - Array.Sort(keysSorted, StringComparer.Ordinal); - - var message = new StringBuilder(); - foreach (string key in keysSorted) - { - if (excludeList.Contains(key)) - { - continue; - } - - if (message.Length > 0) - { - message.Append(" "); - } - - var value = formValues[key]; - message.Append(value); - } - - return message.ToString(); - } - - private string ByteArrayToHexString(byte[] bytes) + private StreamOutputResult EndRequest(string json) => new StreamOutputResult { - var result = new StringBuilder(); - foreach (byte b in bytes) - { - result.Append(b.ToString("x2")); - } - - return result.ToString(); - } - - private string ComputeHash(string key, Stream message) - { - var encoding = new System.Text.UTF8Encoding(); - var byteKey = encoding.GetBytes(key); - - using (HMACSHA256 hmac = new HMACSHA256(byteKey)) - { - var hashedBytes = hmac.ComputeHash(message); - return ByteArrayToHexString(hashedBytes); - } - } - - private string ComputeHash(string key, string message) - { - var encoding = new System.Text.UTF8Encoding(); - var byteKey = encoding.GetBytes(key); - - using (HMACSHA256 hmac = new HMACSHA256(byteKey)) - { - var messageBytes = encoding.GetBytes(message); - var hashedBytes = hmac.ComputeHash(messageBytes); - - return ByteArrayToHexString(hashedBytes); - } - } + ContentStream = new MemoryStream(Encoding.UTF8.GetBytes(json ?? string.Empty)), + ContentType = "application/json" + }; #endregion diff --git a/src/QuickPayRequest.cs b/src/QuickPayRequest.cs index e7efb80..7905376 100644 --- a/src/QuickPayRequest.cs +++ b/src/QuickPayRequest.cs @@ -82,7 +82,7 @@ ApiService.DeleteCard or if (!response.IsSuccessStatusCode) { - var error = Converter.Deserialize(responseText); + var error = Converter.Deserialize(responseText); if (error.ErrorCode > 0 || !string.IsNullOrWhiteSpace(error.Message)) { string errorMessage = error.ErrorCode > 0 @@ -111,7 +111,11 @@ HttpContent GetContent() { Dictionary parameters = configuration.Parameters?.ToDictionary(x => x.Key, y => configuration.Parameters[y.Key]?.ToString() ?? string.Empty, StringComparer.OrdinalIgnoreCase); - return new FormUrlEncodedContent(parameters ?? new()); + if (configuration.CommandType is ApiService.AuthorizePayment) + return new FormUrlEncodedContent(parameters ?? new()); + + string content = parameters?.Any() is true ? Converter.Serialize(parameters) : string.Empty; + return new StringContent(content, Encoding.UTF8, "application/json"); } } diff --git a/src/QuickpayTemplateHelper.cs b/src/QuickpayTemplateHelper.cs new file mode 100644 index 0000000..c60c1b6 --- /dev/null +++ b/src/QuickpayTemplateHelper.cs @@ -0,0 +1,151 @@ +using Dynamicweb.Ecommerce.Orders; +using Dynamicweb.Rendering; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow; + +/// +/// The class to help set template tags and create form values for Quickpay Form +/// +internal sealed class QuickpayTemplateHelper +{ + public Order Order { get; set; } + + public string Merchant { get; set; } + + public string Agreement { get; set; } + + public string LanguageCode { get; set; } + + public string[] AvailableLanguages { get; set; } + + public string ReceiptUrl { get; set; } + + public string CancelUrl { get; set; } + + public string CallbackUrl { get; set; } + + public string ContinueUrl { get; set; } + + public bool AutoCapture { get; set; } + + public bool AutoFee { get; set; } + + public string PaymentMethods { get; set; } + + public Dictionary AvailablePaymentMethods { get; set; } + + public int Branding { get; set; } + + public string GoogleAnalyticsTracking { get; set; } + + public string GoogleAnalyticsClient { get; set; } + + + /// + /// Gets values for Quickpay Form: https://learn.quickpay.net/tech-talk/payments/form/ + /// + /// The api key + public Dictionary GetQuickpayFormValues(string apiKey) + { + var quickpayValues = GetCommonValues(); + quickpayValues["version"] = "v10"; + quickpayValues["merchant_id"] = Merchant.Trim(); + quickpayValues["order_id"] = Order.Id; + quickpayValues["amount"] = Order.Price.PricePIP.ToString(); + quickpayValues["currency"] = Order.Price.Currency.Code; + quickpayValues["autocapture"] = AutoCapture ? "1" : "0"; + quickpayValues["autofee"] = AutoFee ? "1" : "0"; + quickpayValues["checksum"] = Hash.ComputeHash(apiKey, GetMacString(quickpayValues)); + + return quickpayValues; + } + + /// + /// Sets tags for Quickpay Form template: https://learn.quickpay.net/tech-talk/payments/form/ + /// + /// The api key + /// Quickpay Form template (see: Post.cshtml as example) + public void SetQuickpayFormTemplateTags(string apiKey, Template template) + { + Dictionary formValues = GetQuickpayFormValues(apiKey); + SetTemplateTags(template, formValues); + } + + /// + /// Sets tags for Card template (see: Card.cshtml as example) + /// + /// Card template + public void SetCardTemplateTags(Template template) + { + var quickpayValues = GetCommonValues(); + quickpayValues["receipturl"] = ReceiptUrl; + + var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + Dictionary availableLanguages = AvailableLanguages.ToDictionary( + code => code, + code => cultures.FirstOrDefault(culture => culture.TwoLetterISOLanguageName.Equals(code, StringComparison.OrdinalIgnoreCase))?.DisplayName ?? string.Empty + ); + quickpayValues["availableLanguages"] = GetStringValue(availableLanguages); + quickpayValues["availablePaymentMethods"] = GetStringValue(AvailablePaymentMethods); + + SetTemplateTags(template, quickpayValues); + + string GetStringValue(Dictionary data) => string.Join(',', data.Select(pair => $"{pair.Key}|{pair.Value}")); + } + + private void SetTemplateTags(Template template, Dictionary values) + { + foreach ((string key, string value) in values) + template.SetTag(string.Format("QuickPayPaymentWindow.{0}", key), value); + } + + /// + /// Gets common values for both QuickPay Form and Card + /// + private Dictionary GetCommonValues() + { + var values = new Dictionary + { + ["agreement_id"] = Agreement.Trim(), + ["language"] = LanguageCode, + ["continueurl"] = ContinueUrl, + ["cancelurl"] = CancelUrl, + ["callbackurl"] = CallbackUrl, + ["payment_methods"] = PaymentMethods, + ["google_analytics_tracking_id"] = GoogleAnalyticsTracking, + ["google_analytics_client_id"] = GoogleAnalyticsClient + }; + + if (Branding > 0) + values["branding_id"] = Branding.ToString(); + + return values; + } + + private string GetMacString(IDictionary formValues) + { + var excludeList = new List { "MAC" }; + var keysSorted = formValues.Keys.ToArray(); + Array.Sort(keysSorted, StringComparer.Ordinal); + + var message = new StringBuilder(); + foreach (string key in keysSorted) + { + if (excludeList.Contains(key)) + continue; + + if (message.Length > 0) + message.Append(" "); + + var value = formValues[key]; + message.Append(value); + } + + return message.ToString(); + } +} diff --git a/src/SavedCardInformation.cs b/src/SavedCardInformation.cs new file mode 100644 index 0000000..d06ae85 --- /dev/null +++ b/src/SavedCardInformation.cs @@ -0,0 +1,23 @@ +using Dynamicweb.Core; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow; + +internal sealed class SavedCardInformation +{ + public string Id { get; set; } + + public string Name { get; set; } + + public string PaymentUrl { get; set; } + + public SavedCardInformation(string id, string name, string paymentUrl) + { + Ensure.NotNullOrEmpty(id, nameof(id)); + Ensure.NotNullOrEmpty(name, nameof(name)); + Ensure.NotNullOrEmpty(paymentUrl, nameof(paymentUrl)); + + Id = id; + Name = name; + PaymentUrl = paymentUrl; + } +} diff --git a/src/Updates/Card.cshtml b/src/Updates/Card.cshtml new file mode 100644 index 0000000..8b15904 --- /dev/null +++ b/src/Updates/Card.cshtml @@ -0,0 +1,125 @@ +@using System.Collections.Generic +@using Dynamicweb.Rendering +@inherits RazorTemplateBase> + +
+ + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/src/Updates/InlineForm.cshtml b/src/Updates/InlineForm.cshtml new file mode 100644 index 0000000..209f4ce --- /dev/null +++ b/src/Updates/InlineForm.cshtml @@ -0,0 +1,78 @@ +
+

All transactions are secure and encrypted. Credit card information is never stored

+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/src/Updates/Post.html b/src/Updates/Post.html new file mode 100644 index 0000000..d7ad42f --- /dev/null +++ b/src/Updates/Post.html @@ -0,0 +1,20 @@ +
+ + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/src/Updates/QuickPayUpdateProvider.cs b/src/Updates/QuickPayUpdateProvider.cs new file mode 100644 index 0000000..7490858 --- /dev/null +++ b/src/Updates/QuickPayUpdateProvider.cs @@ -0,0 +1,32 @@ +using Dynamicweb.Updates; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Updates; + +public class QuickPayUpdateProvider : UpdateProvider +{ + private static Stream GetResourceStream(string name) + { + string resourceName = $"Dynamicweb.Ecommerce.CheckoutHandlers.QuickPayPaymentWindow.Updates.{name}"; + + return Assembly.GetAssembly(typeof(QuickPayUpdateProvider)).GetManifestResourceStream(resourceName); + } + + public override IEnumerable GetUpdates() + { + return new List() + { + new FileUpdate("ab0730a8-f5fa-4427-80b2-2c7635ab2c5c", this, "/Files/Templates/eCom7/CheckoutHandler/QuickPayPaymentWindow/Post/Card.cshtml", () => GetResourceStream("Card.cshtml")), + new FileUpdate("fdc187af-6c9e-417a-9671-848a8c3a8a0d", this, "/Files/Templates/eCom7/CheckoutHandler/QuickPayPaymentWindow/Post/InlineForm.cshtml", () => GetResourceStream("InlineForm.cshtml")), + new FileUpdate("c30f6547-1722-4cc1-a581-03ddcdf97540", this, "/Files/Templates/eCom7/CheckoutHandler/QuickPayPaymentWindow/Post/Post.html", () => GetResourceStream("Post.html")), + }; + } + + /* + * IMPORTANT! + * Use a generated GUID string as id for an update + * - Execute command in C# interactive window: Guid.NewGuid().ToString() + */ +}