diff --git a/src/CyberSource.cs b/src/CyberSource.cs
index 768d639..88eaf75 100644
--- a/src/CyberSource.cs
+++ b/src/CyberSource.cs
@@ -1,1285 +1,1036 @@
-using Dynamicweb.Core;
-using Dynamicweb.Ecommerce.Cart;
+using Dynamicweb.Ecommerce.Cart;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Helpers;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Service;
using Dynamicweb.Ecommerce.Orders;
using Dynamicweb.Ecommerce.Orders.Gateways;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
+using Dynamicweb.Frontend;
using Dynamicweb.Rendering;
using Dynamicweb.Security.UserManagement;
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net;
-using System.Security.Cryptography.X509Certificates;
-using System.Web;
-using System.Threading.Tasks;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Security.Cryptography;
-using System.Text;
-using Dynamicweb.Content.Items.Annotations;
-
-namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource
-{
- ///
- /// CyberSource Checkout Handler
- ///
- [AddInName("CyberSource")]
- [AddInDescription("Payment system, http://www.cybersource.com")]
- public class CyberSource : CheckoutHandlerWithStatusPage, IParameterOptions, IRemoteCapture, ISavedCard, IRecurring, ICheckAuthorizationStatus
- {
- internal enum WorkModes { Test, Production }
- internal enum WindowModes { Redirect, Embedded }
- internal enum TransactionTypes { ZeroAuthorization, Authorization, Sale }
-
- private static string[] supportedCountryCodes;
- private static string[] supportedCurrencyCodes;
-
- internal WorkModes workMode = WorkModes.Test;
- internal WindowModes windowMode = WindowModes.Redirect;
- private TransactionTypes transactionType = TransactionTypes.Sale;
- private string decline_AVS_Flag;
- private const string FormTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Payment";
- private const string CancelTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Cancel";
- private const string ErrorTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Error";
-
- private static Dictionary CardTypes = new Dictionary
- {
- {"001", "Visa"},
- {"002", "MasterCard, Eurocard"},
- {"003", "American Express"},
- {"004", "Discover"},
- {"005", "Diners Club"},
- {"006", "Carte Blanche"},
- {"007", "JCB"},
- {"014", "EnRoute"},
- {"021", "JAL"},
- {"024", "Maestro (UK Domestic)"},
- {"031", "Delta, Global Collect"},
- {"033", "Visa Electron"},
- {"034", "Dankort"},
- {"036", "Carte Bleu"},
- {"037", "Carta Si"},
- {"042", "Maestro (International)"},
- {"043", "GE Money UK card"}
- };
- private string paymentTemplate;
- private string cancelTemplate;
- private string errorTemplate;
-
- static CyberSource()
- {
- List cultures = new List();
- foreach (var r in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
- {
- try
- {
- cultures.Add(new RegionInfo(r.Name));
- }
- catch
- {
-
- }
- }
- supportedCountryCodes = cultures.Select(x => x.TwoLetterISORegionName.ToUpper()).Distinct().ToArray();
- supportedCurrencyCodes = cultures.Select(x => x.ISOCurrencySymbol.ToUpper()).Distinct().ToArray();
- }
-
- ///
- /// Default constructor
- ///
- public CyberSource()
- {
- ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
- ErrorTemplate = "eCom7/CheckoutHandler/CyberSource/Error/checkouthandler_error.html";
- CancelTemplate = "eCom7/CheckoutHandler/CyberSource/Cancel/checkouthandler_cancel.html";
- PaymentTemplate = "eCom7/CheckoutHandler/CyberSource/Payment/Payment.html";
- }
-
- #region Addin parameters
-
- [AddInParameter("Merchant id"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is the name of your sandbox account;")]
- public string MerchantId { get; set; }
-
- [AddInParameter("Profile id"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is a security key generated in the CyberSource Business Center under: Tools & Settings > Profiles > Security;")]
- public string ProfileId { get; set; }
+using System.Threading;
- [AddInParameter("Access key"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is the public component of the security key;")]
- public string AccessKey { get; set; }
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource;
- [AddInParameter("Secret key"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;TextArea=true;infoText=This is the secret component of the security key;")]
- public string SecretKey { get; set; }
-
- [AddInParameter("Certificate"), AddInParameterEditor(typeof(FileManagerEditor), "NewGUI=true;allowBrowse=true;folder=System;showfullpath=true;infoText=The certificate should be uploaded to the Dynamicweb File Archive;")]
- public string CertificateFile { get; set; }
-
- [AddInParameter("Transaction type")]
- [AddInParameterEditor(typeof(RadioParameterEditor), "")]
- public string TransactionType
- {
- get { return transactionType.ToString(); }
- set { Enum.TryParse(value, out transactionType); }
- }
+///
+/// CyberSource Checkout Handler
+///
+[AddInName("CyberSource")]
+[AddInDescription("Payment system, http://www.cybersource.com")]
+public class CyberSource : CheckoutHandlerWithStatusPage, IParameterOptions, IRemoteCapture, ISavedCard, IRecurring, ICheckAuthorizationStatus
+{
+ private const string FormTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Payment";
+ private const string CancelTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Cancel";
+ private const string ErrorTemplateFolder = "eCom7/CheckoutHandler/CyberSource/Error";
- [AddInParameter("Forced tokenization"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=;Forces the token to be saved on order or card for logged in users who have not chosen \"Save card\";")]
- public bool ForceTokenization { get; set; }
+ private static HashSet SupportedCountryCodes { get; set; }
+ private static HashSet SupportedCurrencyCodes { get; set; }
- [AddInParameter("Payment template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{FormTemplateFolder}")]
- public string PaymentTemplate
+ private static Dictionary CardTypes = new()
+ {
+ {"001", "Visa"},
+ {"002", "MasterCard, Eurocard"},
+ {"003", "American Express"},
+ {"004", "Discover"},
+ {"005", "Diners Club"},
+ {"006", "Carte Blanche"},
+ {"007", "JCB"},
+ {"014", "EnRoute"},
+ {"021", "JAL"},
+ {"024", "Maestro (UK Domestic)"},
+ {"031", "Delta, Global Collect"},
+ {"033", "Visa Electron"},
+ {"034", "Dankort"},
+ {"036", "Carte Bleu"},
+ {"037", "Carta Si"},
+ {"042", "Maestro (International)"},
+ {"043", "GE Money UK card"}
+ };
+
+ static CyberSource()
+ {
+ List cultures = new List();
+ foreach (CultureInfo cultureInfo in CultureInfo.GetCultures(CultureTypes.SpecificCultures))
{
- get
+ try
{
- return TemplateHelper.GetTemplateName(paymentTemplate);
+ cultures.Add(new(cultureInfo.Name));
}
- set => paymentTemplate = value;
- }
-
- [AddInParameter("Cancel template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{CancelTemplateFolder}")]
- public string CancelTemplate
- {
- get
+ catch
{
- return TemplateHelper.GetTemplateName(cancelTemplate);
}
- set => cancelTemplate = value;
}
- [AddInParameter("Error template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{ErrorTemplateFolder}")]
- public string ErrorTemplate
- {
- get
- {
- return TemplateHelper.GetTemplateName(errorTemplate);
- }
- set => errorTemplate = value;
- }
+ SupportedCountryCodes = cultures.Select(regionInfo => regionInfo.TwoLetterISORegionName.ToUpper()).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ SupportedCurrencyCodes = cultures.Select(regionInfo => regionInfo.ISOCurrencySymbol.ToUpper()).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ }
- [AddInParameter("Work Mode"), AddInParameterEditor(typeof(RadioParameterEditor), "")]
- public string WorkMode
- {
- get { return workMode.ToString(); }
- set { Enum.TryParse(value, out workMode); }
- }
+ ///
+ /// Default constructor
+ ///
+ public CyberSource()
+ {
+ ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
+ ErrorTemplate = "eCom7/CheckoutHandler/CyberSource/Error/checkouthandler_error.html";
+ CancelTemplate = "eCom7/CheckoutHandler/CyberSource/Cancel/checkouthandler_cancel.html";
+ PaymentTemplate = "eCom7/CheckoutHandler/CyberSource/Payment/Payment.html";
+ }
- [AddInParameter("Window Mode"), AddInParameterEditor(typeof(RadioParameterEditor), "Explanation=Select if the payment window should redirect or if it should be embedded;")]
- public string WindowMode
- {
- get { return windowMode.ToString(); }
- set { Enum.TryParse(value, out windowMode); }
- }
+ #region Addin parameters
+ [AddInParameter("Merchant id"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is the name of your sandbox account;")]
+ public string MerchantId { get; set; }
- [AddInParameter("Review AVS Codes"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;Explanation=Cybersource supports AVS (Address Verification System) validation;Hint=Should contain the AVS codes you want to receive an AVS validation for;")]
- public string Decline_AVS_Flag
- {
- get { return string.IsNullOrEmpty(decline_AVS_Flag) ? "N" : decline_AVS_Flag; }
- set { decline_AVS_Flag = value; }
- }
+ [AddInParameter("Profile id"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is a security key generated in the CyberSource Business Center under: Tools & Settings > Profiles > Security;")]
+ public string ProfileId { get; set; }
- [AddInParameter("Ignore AVS Result"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=When Ignore AVS results is checked, you will receive no AVS declines;")]
- public bool Ignore_AVS_Result { get; set; } = false;
+ [AddInParameter("Access key"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=This is the public component of the security key;")]
+ public string AccessKey { get; set; }
- [AddInParameter("Approve AVS Code"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true; Explanation=Cybersource supports AVS (Address Verification System) validation;Hint=Should contain a comma-separated list of AVS codes which will permit the transaction to be approved;")]
- public string Result_AVS_Flag { get; set; }
- #endregion
+ [AddInParameter("Secret key"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;TextArea=true;infoText=This is the secret component of the security key;")]
+ public string SecretKey { get; set; }
- ///
- /// Gets options according to behavior mode
- ///
- ///
- /// Key-value pairs of settings
- public IEnumerable GetParameterOptions(string parameterName)
- {
- try
- {
- switch (parameterName)
- {
- case "Work Mode":
- return new List() {
- new ("Test", WorkModes.Test.ToString() ){Hint = "Choose Test to simulate payment transactions without involving real money transfers"},
- new ("Production",WorkModes.Production.ToString() ){Hint = "Choose Production when you are ready to go live"}
- };
- case "Window Mode":
- return new List()
- {
- new("Redirect",WindowModes.Redirect.ToString()),
- new("Embedded",WindowModes.Embedded.ToString())
- };
- case "Transaction type":
- return new List() {
- new( "Authorization (zero amount)",TransactionTypes.ZeroAuthorization.ToString())
- {
- Hint = "All transactions are zero authorized. " +
- "Capture is performed through AX or similar and you can carry out account " +
- "verification checks to check the validity of a Visa/MasterCard Debit or credit card"
- },
- new("Authorization (order amount)",TransactionTypes.Authorization.ToString())
- {
- Hint = " The order is authorized at AuthorizeNET and then you can " +
- "manually authorize from ecommerce backend order list. This is used for usual transactions"
- },
- new("Sale",TransactionTypes.Sale.ToString())
- {
- Hint = "The amount is sent for authorization, and if approved, is automatically submitted for settlement"
- }
- };
- default:
- throw new ArgumentException(string.Format("Unknown dropdown name: '{0}'", parameterName));
- }
- }
- catch (System.Threading.ThreadAbortException)
- {
- return null;
- }
- catch (Exception ex)
- {
- LogError(null, ex, "Unhandled exception with message: {0}", ex.Message);
- return null;
- }
- }
+ [AddInParameter("Certificate"), AddInParameterEditor(typeof(FileManagerEditor), "NewGUI=true;allowBrowse=true;folder=System;showfullpath=true;infoText=The certificate for REST API, which should be uploaded to the Dynamicweb File Archive;")]
+ public string CertificateFile { get; set; }
-
-
+ [AddInParameter("Certificate password"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=The password to read certificate;")]
+ public string CertificatePassword { get; set; }
- ///
- /// Send capture request to transaction service
- ///
- /// Order to be captured
- /// Response from transaction service
- OrderCaptureInfo IRemoteCapture.Capture(Order order)
- {
- try
- {
- var errorMessage = string.Empty;
- if (order == null || string.IsNullOrEmpty(order.Id))
- {
- errorMessage = "No valid Order object set";
- }
- else if (string.IsNullOrWhiteSpace(order.TransactionNumber))
- {
- errorMessage = "No transaction number set on the order";
- }
+ private TransactionTypes transactionType = TransactionTypes.Sale;
- var certPath = GetCertificateFilePath();
- if (string.IsNullOrWhiteSpace(certPath))
- {
- errorMessage = "No certificate not found";
- }
+ [AddInParameter("Transaction type")]
+ [AddInParameterEditor(typeof(RadioParameterEditor), "")]
+ public string TransactionType
+ {
+ get => transactionType.ToString();
+ set => Enum.TryParse(value, out transactionType);
+ }
- if (!string.IsNullOrEmpty(errorMessage))
- {
- LogEvent(order, errorMessage);
- return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, errorMessage);
- }
+ [AddInParameter("Forced tokenization"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=;Forces the token to be saved on order or card for logged in users who have not chosen \"Save card\";")]
+ public bool ForceTokenization { get; set; }
- var request = new
- {
- clientReferenceInformation = new
- {
- code = order.Id
- },
- orderInformation = new
- {
- amountDetails = new
- {
- totalAmount = GetTransactionAmount(order),
- currency = order.Price.Currency.Code
- }
- },
- };
+ private string paymentTemplate;
- var requestJson = Converter.Serialize(request);
- var url = $"https://{(workMode == WorkModes.Production ? "api" : "apitest")}.cybersource.com/pts/v2/payments/{order.TransactionNumber}/captures";
- var response = CallCyberSourceAPI(requestJson, url, order);
- if (response.Result.StatusCode == HttpStatusCode.Created)
- {
- LogEvent(order, "Capture successful", DebuggingInfoType.CaptureResult);
- return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, "Capture successful");
- }
- else
- {
- string responseJson = Converter.Serialize(response);
- string message = $"Remote Capture failed. Response: {responseJson}";
- LogEvent(order, message, DebuggingInfoType.CaptureResult);
- return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, message);
- }
- }
- catch (System.Threading.ThreadAbortException)
- {
- return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, "System.Threading.ThreadAbortException");
- }
- catch (Exception ex)
- {
- var message = string.Format("Remote capture failed with the message: {0}", ex.Message);
- LogError(order, ex, message);
+ [AddInParameter("Payment template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{FormTemplateFolder}")]
+ public string PaymentTemplate
+ {
+ get => TemplateHelper.GetTemplateName(paymentTemplate);
+ set => paymentTemplate = value;
+ }
- return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, message);
- }
- }
+ private string cancelTemplate;
- [Obsolete()]
- ///
- /// Starts order checkout procedure
- ///
- /// Order to be checked out
- /// String representation of template output
- public override string StartCheckout(Order order)
- {
- try
- {
- string errorMessage;
- if (!ValidateOrderFields(order, out errorMessage))
- {
- return OnError(order, errorMessage);
- }
+ [AddInParameter("Cancel template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{CancelTemplateFolder}")]
+ public string CancelTemplate
+ {
+ get => TemplateHelper.GetTemplateName(cancelTemplate);
+ set => cancelTemplate = value;
+ }
- bool isIFrameMode = windowMode == WindowModes.Embedded;
+ private string errorTemplate;
- Dictionary form;
- string gatewayUrl;
+ [AddInParameter("Error template"), AddInParameterEditor(typeof(TemplateParameterEditor), $"folder=Templates/{ErrorTemplateFolder}")]
+ public string ErrorTemplate
+ {
+ get => TemplateHelper.GetTemplateName(errorTemplate);
+ set => errorTemplate = value;
+ }
- if (order.IsRecurringOrderTemplate || !String.IsNullOrWhiteSpace(GetSavedCardName(order)))
- {
- gatewayUrl = transactionType == TransactionTypes.ZeroAuthorization ? GetCreateCardGatewayUrl(isIFrameMode) : GetGatewayUrl(isIFrameMode);
- form = PrepareCreateCardRequest(order);
- }
- else
- {
- gatewayUrl = GetGatewayUrl(isIFrameMode);
- if (transactionType == TransactionTypes.Sale)
- {
- form = PrepareSaleRequest(order);
- }
- else
- {
- form = PrepareAuthorizationRequest(order);
- }
- }
+ private WorkModes workMode = WorkModes.Test;
- if (isIFrameMode)
- {
- return RenderPaymentFrame(order, gatewayUrl, form);
- }
- else
- {
- SubmitForm(gatewayUrl, form);
- }
- }
- catch (System.Threading.ThreadAbortException)
- {
- return string.Empty;
- }
- catch (Exception ex)
- {
- LogError(order, ex, "Unhandled exception with message: {0}", ex.Message);
- return OnError(order, ex.Message);
- }
+ [AddInParameter("Work Mode"), AddInParameterEditor(typeof(RadioParameterEditor), "")]
+ public string WorkMode
+ {
+ get => workMode.ToString();
+ set => Enum.TryParse(value, out workMode);
+ }
- return string.Empty;
- }
+ private WindowModes windowMode = WindowModes.Redirect;
+ [AddInParameter("Window Mode"), AddInParameterEditor(typeof(RadioParameterEditor), "Explanation=Select if the payment window should redirect or if it should be embedded;")]
+ public string WindowMode
+ {
+ get => windowMode.ToString();
+ set => Enum.TryParse(value, out windowMode);
+ }
- private string RenderPaymentFrame(Orders.Order order, string gatewayUrl, Dictionary form)
- {
- if (string.IsNullOrWhiteSpace(PaymentTemplate))
- {
- LogError(order, "Embedded payment template not set");
- return OnError(order, "Embedded payment template not set");
- }
+ private string declineAVSFlag;
- // Get template
- var formTemplate = new Template(TemplateHelper.GetTemplatePath(PaymentTemplate, FormTemplateFolder));
+ [AddInParameter("Review AVS Codes"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;Explanation=Cybersource supports AVS (Address Verification System) validation;Hint=Should contain the AVS codes you want to receive an AVS validation for;")]
+ public string Decline_AVS_Flag
+ {
+ get => string.IsNullOrEmpty(declineAVSFlag) ? "N" : declineAVSFlag;
+ set => declineAVSFlag = value;
+ }
- // Render tags
- formTemplate.SetTag("CyberSource.HostedPaymentURL", gatewayUrl);
+ [AddInParameter("Ignore AVS Result"), AddInParameterEditor(typeof(YesNoParameterEditor), "infoText=When Ignore AVS results is checked, you will receive no AVS declines;")]
+ public bool Ignore_AVS_Result { get; set; } = false;
- formTemplate.SetTag("CyberSource.CancelURL", GetCancelUrl(order));
+ [AddInParameter("Approve AVS Code"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true; Explanation=Cybersource supports AVS (Address Verification System) validation;Hint=Should contain a comma-separated list of AVS codes which will permit the transaction to be approved;")]
+ public string Result_AVS_Flag { get; set; }
- var loopTemplate = formTemplate.GetLoop("CyberSourceFields");
- foreach (var field in form)
- {
- loopTemplate.SetTag("CyberSource.FieldName", field.Key);
- loopTemplate.SetTag("CyberSource.FieldValue", field.Value);
- loopTemplate.CommitLoop();
- }
+ #endregion
- // Render and return
- return this.Render(order, formTemplate);
- }
- [Obsolete()]
- public override string Redirect(Order order)
- {
- LogEvent(order, "Redirected to CyberSource CheckoutHandler");
- string result;
+ private string GetHost()
+ {
+ string apiType = workMode is WorkModes.Production ? "api" : "apitest";
+
+ return $"{apiType}.cybersource.com";
+ }
- switch (Dynamicweb.Context.Current.Request["cmd"])
+ ///
+ /// Gets options according to behavior mode
+ ///
+ ///
+ /// Key-value pairs of settings
+ public IEnumerable GetParameterOptions(string parameterName)
+ {
+ try
+ {
+ switch (parameterName)
{
- case "Accept":
- result = ValidateAVSCode(order);
- if (result != null)
+ case "Work Mode":
+ return new List
{
- return result;
- }
- return StateOk(order);
- case "CardSaved":
- result = ValidateAVSCode(order);
- if (result != null)
+ new("Test", WorkModes.Test.ToString()) { Hint = "Choose Test to simulate payment transactions without involving real money transfers" },
+ new("Production", WorkModes.Production.ToString()) { Hint = "Choose Production when you are ready to go live" }
+ };
+
+ case "Window Mode":
+ return new List
{
- return result;
- }
- return StateCardSaved(order);
- case "Cancel":
- return StateCancel(order);
- case "IFrameError":
- return StateIFrameError(order);
+ new("Redirect", WindowModes.Redirect.ToString()),
+ new("Embedded", WindowModes.Embedded.ToString())
+ };
+
+ case "Transaction type":
+ return new List
+ {
+ new("Authorization (zero amount)", TransactionTypes.ZeroAuthorization.ToString())
+ {
+ Hint = "All transactions are zero authorized. " +
+ "Capture is performed through AX or similar and you can carry out account " +
+ "verification checks to check the validity of a Visa/MasterCard Debit or credit card"
+ },
+ new("Authorization (order amount)", TransactionTypes.Authorization.ToString())
+ {
+ Hint = " The order is authorized at AuthorizeNET and then you can " +
+ "manually authorize from ecommerce backend order list. This is used for usual transactions"
+ },
+ new("Sale",TransactionTypes.Sale.ToString())
+ {
+ Hint = "The amount is sent for authorization, and if approved, is automatically submitted for settlement"
+ }
+ };
+
default:
- Context.Current.Response.End();
- return null;
+ throw new ArgumentException(string.Format("Unknown dropdown name: '{0}'", parameterName));
}
}
-
- private string ValidateAVSCode(Order order)
+ catch (ThreadAbortException)
{
- string transact = Context.Current.Request["transaction_id"];
- string avsResult = Context.Current.Request["auth_avs_code"];
- string avsResultRaw = Context.Current.Request["auth_avs_code_raw"];
- var resultCodesAllowed = new List();
- if (!string.IsNullOrWhiteSpace(Result_AVS_Flag))
- {
- resultCodesAllowed.AddRange(Result_AVS_Flag.Replace(' ', ',').Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
- }
- LogEvent(order, "CyberSource response: avs_code: '{0}', avs_code_raw: '{1}'.", avsResult, avsResultRaw);
-
- if (!string.IsNullOrEmpty(avsResult) && resultCodesAllowed.Any() && !resultCodesAllowed.Contains(avsResult))
- {
- LogEvent(order, "Transaction {0} not approved.", transact);
- return OnError(order, string.Format("Transaction {0} not approved. {1} (code={2})"
- , transact, Context.Current.Request["message"], avsResult), windowMode == WindowModes.Embedded);
- }
-
return null;
}
-
- private bool ValidateOrderFields(Order order, out string errorMessage)
+ catch (Exception ex)
{
- if (!supportedCurrencyCodes.Any(x => x == order.CurrencyCode))
- {
- errorMessage = $"Only {string.Join(",", supportedCurrencyCodes)} currency codes is allowed. Order currency: {order.CurrencyCode}";
- return false;
- }
-
- if (string.IsNullOrWhiteSpace(order.CustomerCountryCode))
- {
- errorMessage = "Required customer country code";
- return false;
- }
-
- if (!supportedCountryCodes.Any(x => x == order.CustomerCountryCode))
- {
- errorMessage = $"Only {string.Join(",", supportedCountryCodes)} country codes is supported. Order country code: {order.CustomerCountryCode}";
- return false;
- }
-
- errorMessage = string.Empty;
-
- return true;
+ LogError(null, ex, "Unhandled exception with message: {0}", ex.Message);
+ return null;
}
+ }
- private string StateCancel(Order order)
+ ///
+ /// Send capture request to transaction service
+ ///
+ /// Order to be captured
+ /// Response from transaction service
+ OrderCaptureInfo IRemoteCapture.Capture(Order order)
+ {
+ try
{
- LogEvent(order, "State cancel");
- string calculatedSignature;
- if (windowMode != WindowModes.Embedded && !Security.ValidateResponseSignation(AccessKey, SecretKey, out calculatedSignature))
+ string errorMessage = string.Empty;
+ if (order is null || string.IsNullOrEmpty(order.Id))
+ errorMessage = "No valid Order object set";
+ else if (string.IsNullOrWhiteSpace(order.TransactionNumber))
+ errorMessage = "No transaction number set on the order";
+
+ string certPath = Helper.GetCertificateFilePath(CertificateFile);
+ if (string.IsNullOrWhiteSpace(certPath))
+ errorMessage = "Certificate for REST API is not found";
+
+ if (!string.IsNullOrEmpty(errorMessage))
{
- LogError(order, "The signature returned from callback does not match: {0}, calculated: {1}", Dynamicweb.Context.Current.Request["signature"], calculatedSignature);
- return OnError(order, "Wrong signature");
+ LogEvent(order, errorMessage);
+ return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, errorMessage);
}
- order.TransactionStatus = "Cancelled";
- Services.Orders.Save(order);
- CheckoutDone(order);
-
- var cancelTemplate = new Template(TemplateHelper.GetTemplatePath(CancelTemplate, CancelTemplateFolder));
- var orderRenderer = new Dynamicweb.Ecommerce.Frontend.Renderer();
- orderRenderer.RenderOrderDetails(cancelTemplate, order, true);
+ var service = new CyberSourceService(GetHost(), MerchantId, CertificateFile, CertificatePassword);
+ CaptureResponse response = service.Capture(order, order.TransactionNumber);
+ LogEvent(order, "Capture successful", DebuggingInfoType.CaptureResult);
- return cancelTemplate.Output();
+ return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, "Capture successful");
}
-
- private string StateIFrameError(Order order)
+ catch (ThreadAbortException)
{
- return OnError(order, Dynamicweb.Context.Current.Request["ErrorMessage"]);
+ return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, "System.Threading.ThreadAbortException");
}
-
- private string StateOk(Order order)
+ catch (Exception ex)
{
- LogEvent(order, "State ok");
+ string message = string.Format("Remote capture failed with the message: {0}", ex.Message);
+ LogEvent(order, message, DebuggingInfoType.CaptureResult);
+ LogError(order, ex, message);
- if (!order.Complete)
- {
- return ProcessOrder(order);
- }
-
- PassToCart(order);
-
- return null;
+ return new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Failed, message);
}
+ }
-
- private string StateCardSaved(Order order)
+ ///
+ /// Starts order checkout procedure
+ ///
+ /// Order to be checked out
+ public override OutputResult BeginCheckout(Order order, CheckoutParameters parameters)
+ {
+ try
{
- LogEvent(order, "CyberSource Card Authorized successfully");
-
- var cardName = HttpUtility.UrlDecode(Context.Current.Request["CardTokenName"]);
- if (string.IsNullOrEmpty(cardName))
- {
- cardName = order.Id;
- }
-
- if (Context.Current.Request["reason_code"] == "100")
- {
- var requestCardType = Context.Current.Request["req_card_type"];
- var subscribtionId = Context.Current.Request["payment_token"];
- var cardType = CardTypes.Keys.Any(key => key == requestCardType) ? CardTypes[requestCardType] : String.Format("Unrecognized card type - {0}", requestCardType);
- var cardNubmer = order.TransactionCardNumber = Context.Current.Request["req_card_number"].ToUpper();
-
- order.TransactionCardType = cardType;
- string transactionId = Dynamicweb.Context.Current.Request["transaction_id"];
- var user = UserManagementServices.Users.GetUserById(order.CustomerAccessUserId);
- if (user != null)
- {
- var savedCard = Services.PaymentCard.CreatePaymentCard(user.ID, order.PaymentMethodId, cardName, cardType, cardNubmer, subscribtionId);
- order.SavedCardId = savedCard.ID;
- }
- else
- {
- order.TransactionToken = subscribtionId;
- }
-
- int decimals = order.Currency.Rounding == null || order.Currency.Rounding.Id == string.Empty ? 2 : order.Currency.Rounding.Decimals;
- if (!order.IsRecurringOrderTemplate && transactionType == TransactionTypes.ZeroAuthorization)
- {
- order.TransactionAmount = 0.00;
- order.TransactionStatus = "Succeeded";
- order.TransactionCardType = cardType;
- order.TransactionCardNumber = cardNubmer;
- order.TransactionType = "Zero authorization";
- string msg = "Zero authorization succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
- else if (transactionType == TransactionTypes.Sale)
- {
- order.TransactionAmount = Math.Round(order.Price.Price, decimals);
- order.TransactionStatus = "Succeeded";
- string msg = "Capture succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
- else if (transactionType == TransactionTypes.Authorization)
- {
- order.TransactionType = "Zero authorization";
- order.TransactionAmount = Math.Round(order.Price.Price, decimals);
- order.TransactionStatus = "Succeeded";
- string msg = "Authorization succeeded";
- LogEvent(order, msg);
- }
+ string errorMessage;
+ if (!ValidateOrderFields(order, out errorMessage))
+ return OnError(order, errorMessage);
- Services.Orders.Save(order);
+ bool isIFrameMode = windowMode is WindowModes.Embedded;
- LogEvent(order, "Saved Card created");
+ Dictionary form;
+ string gatewayUrl;
- SetOrderComplete(order, transactionId);
+ if (order.IsRecurringOrderTemplate || !string.IsNullOrWhiteSpace(GetSavedCardName(order)))
+ {
+ gatewayUrl = transactionType is TransactionTypes.ZeroAuthorization
+ ? GetCreateCardGatewayUrl(isIFrameMode)
+ : GetGatewayUrl(isIFrameMode);
- LogEvent(order, "Create Card successful and order completed");
+ form = PrepareCreateCardRequest(order);
}
else
{
- LogError(order, string.Format("Create card failed. Decision: '{0}' messageId: '{1}', messageText: '{2}'", Context.Current.Request["decision"], Context.Current.Request["reason_code"], Context.Current.Request["message"]));
+ gatewayUrl = GetGatewayUrl(isIFrameMode);
+ form = transactionType is TransactionTypes.Sale
+ ? PrepareSaleRequest(order)
+ : PrepareAuthorizationRequest(order);
}
- CheckoutDone(order);
- if (!order.Complete)
- {
- return OnError(order, "Some error happened on creating saved card.", windowMode == WindowModes.Embedded);
- }
+ if (isIFrameMode)
+ return RenderPaymentFrame(order, gatewayUrl, form);
- if (windowMode != WindowModes.Embedded)
- {
- PassToCart(order);
- }
- else
- {
- Context.Current.Response.Write(string.Format("", GetBaseUrl(order)));
- Context.Current.Response.End();
- }
- return null;
+ return GetSubmitFormResult(gatewayUrl, form);
}
-
- private string ProcessOrder(Order order)
+ catch (ThreadAbortException)
{
- bool orderWasCompleted = order.Complete;
-
- try
- {
- bool errorOccured = false;
- string calculatedSignature;
+ return NoActionOutputResult.Default;
+ }
+ catch (Exception ex)
+ {
+ LogError(order, ex, "Unhandled exception with message: {0}", ex.Message);
- if (!Security.ValidateResponseSignation(AccessKey, SecretKey, out calculatedSignature))
- {
- errorOccured = true;
- LogError(order, "The signature returned from callback does not match: {0}, calculated: {1}", Dynamicweb.Context.Current.Request["signature"], calculatedSignature);
- }
+ return OnError(order, ex.Message);
+ }
+ }
- string transactionId = Dynamicweb.Context.Current.Request["transaction_id"];
- if (string.IsNullOrEmpty(transactionId))
- {
- errorOccured = true;
- LogEvent(order, "No transaction number sent to callback");
- }
+ private OutputResult RenderPaymentFrame(Order order, string gatewayUrl, Dictionary form)
+ {
+ if (string.IsNullOrWhiteSpace(PaymentTemplate))
+ {
+ LogError(order, "Embedded payment template not set");
- if (Dynamicweb.Context.Current.Request["reason_code"] != "100")
- {
- errorOccured = true;
- LogEvent(order, "Transaction {0} not approved. CyberSource response: messageId: '{1}', messageText: '{2}'.", transactionId, Dynamicweb.Context.Current.Request["auth_response"], Dynamicweb.Context.Current.Request["message"]);
+ return OnError(order, "Embedded payment template not set");
+ }
- return OnError(order, string.Format("Transaction {0} not approved. {1}", transactionId, Dynamicweb.Context.Current.Request["message"]), windowMode == WindowModes.Embedded);
- }
+ // Get template
+ var formTemplate = new Template(TemplateHelper.GetTemplatePath(PaymentTemplate, FormTemplateFolder));
- string amount = Dynamicweb.Context.Current.Request["req_amount"];
- int decimals = order.Currency.Rounding == null || order.Currency.Rounding.Id == string.Empty ? 2 : order.Currency.Rounding.Decimals;
+ // Render tags
+ formTemplate.SetTag("CyberSource.HostedPaymentURL", gatewayUrl);
+ formTemplate.SetTag("CyberSource.CancelURL", GetCancelUrl(order));
- if (errorOccured)
- {
- LogError(order, "At least one validation error exists - exiting callback routine.");
- order.TransactionStatus = "Failed";
- Services.Orders.Save(order);
- }
- else
- {
- LogEvent(order, "Payment succeeded with transaction number {0}", transactionId);
- var requestCardType = Dynamicweb.Context.Current.Request["req_card_type"];
- var cardType = CardTypes.Keys.Any(key => key == requestCardType) ? CardTypes[requestCardType] : String.Format("Unrecognized card type - {0}", requestCardType);
+ Template loopTemplate = formTemplate.GetLoop("CyberSourceFields");
+ foreach ((string key, string value) in form)
+ {
+ loopTemplate.SetTag("CyberSource.FieldName", key);
+ loopTemplate.SetTag("CyberSource.FieldValue", value);
+ loopTemplate.CommitLoop();
+ }
- order.TransactionAmount = Math.Round(order.Price.Price, decimals);
- order.TransactionStatus = "Succeeded";
- order.TransactionCardType = cardType;
- order.TransactionCardNumber = HideCardNumber(Dynamicweb.Context.Current.Request["req_card_number"]);
+ return new ContentOutputResult
+ {
+ Content = Render(order, formTemplate)
+ };
+ }
- if (transactionType == TransactionTypes.Sale)
- {
- string msg = "Capture succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
- else if (transactionType == TransactionTypes.ZeroAuthorization)
- {
- order.TransactionAmount = 0.00;
- order.TransactionType = "Zero authorization";
- string msg = "Zero authorization succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
- SetOrderComplete(order, transactionId);
- }
- }
- finally
- {
- if (!orderWasCompleted && order.Complete)
- {
- CheckoutDone(order);
+ public override OutputResult HandleRequest(Order order)
+ {
+ LogEvent(order, "Redirected to CyberSource CheckoutHandler");
- if (windowMode != WindowModes.Embedded)
- {
- PassToCart(order);
- }
- else
- {
- Context.Current.Response.Write(string.Format("", GetBaseUrl(order)));
- Context.Current.Response.End();
- }
- }
- }
- return null;
+ switch (Context.Current.Request["cmd"])
+ {
+ case "Accept":
+ if (ValidateAVSCode(order) is ContentOutputResult errorAcceptResult)
+ return errorAcceptResult;
+ return StateOk(order);
+ case "CardSaved":
+ if (ValidateAVSCode(order) is ContentOutputResult errorCardSavedResult)
+ return errorCardSavedResult;
+ return StateCardSaved(order);
+ case "Cancel":
+ return StateCancel(order);
+ case "IFrameError":
+ return StateIFrameError(order);
+ default:
+ return ContentOutputResult.Empty;
}
+ }
+
+ private OutputResult ValidateAVSCode(Order order)
+ {
+ string transact = Context.Current.Request["transaction_id"];
+ string avsResult = Context.Current.Request["auth_avs_code"];
+ string avsResultRaw = Context.Current.Request["auth_avs_code_raw"];
- private string OnError(Orders.Order order, string message, bool isIFrameError = false)
+ var resultCodesAllowed = new List();
+ if (!string.IsNullOrWhiteSpace(Result_AVS_Flag))
{
- if (windowMode == WindowModes.Embedded && isIFrameError)
- {
- Context.Current.Response.Write(string.Format("", GetBaseUrl(order), HttpUtility.UrlEncode(message)));
- Context.Current.Response.End();
- }
+ string formattedResult = Result_AVS_Flag.Replace(' ', ',');
+ resultCodesAllowed.AddRange(formattedResult.Split(',', StringSplitOptions.RemoveEmptyEntries));
+ }
- order.TransactionAmount = 0;
- order.TransactionStatus = "Failed";
- order.Errors.Add(message);
- Services.Orders.Save(order);
+ LogEvent(order, "CyberSource response: avs_code: '{0}', avs_code_raw: '{1}'.", avsResult, avsResultRaw);
- Services.Orders.DowngradeToCart(order);
- order.CartV2StepIndex = 0;
- order.TransactionStatus = string.Empty;
- Common.Context.SetCart(order);
+ if (!string.IsNullOrEmpty(avsResult) && resultCodesAllowed.Any() && !resultCodesAllowed.Contains(avsResult))
+ {
+ LogEvent(order, "Transaction {0} not approved.", transact);
- if (string.IsNullOrWhiteSpace(ErrorTemplate))
- {
- PassToCart(order);
- }
+ string message = Context.Current.Request["message"];
+ return OnError(order, $"Transaction {transact} not approved. {message} (code={avsResult})", windowMode is WindowModes.Embedded);
+ }
+
+ return NoActionOutputResult.Default;
+ }
- var errorTemplate = new Template(TemplateHelper.GetTemplatePath(ErrorTemplate, ErrorTemplateFolder));
- errorTemplate.SetTag("CheckoutHandler:ErrorMessage", message);
+ private bool ValidateOrderFields(Order order, out string errorMessage)
+ {
+ string supportedCodes = string.Join(",", SupportedCurrencyCodes);
- return Render(order, errorTemplate);
+ if (!SupportedCurrencyCodes.Any(code => code.Equals(order.CurrencyCode, StringComparison.OrdinalIgnoreCase)))
+ {
+ errorMessage = $"Only {supportedCodes} currency codes is allowed. Order currency: {order.CurrencyCode}";
+ return false;
}
- private Dictionary PrepareAuthorizationRequest(Order order, string token = "")
+ if (string.IsNullOrWhiteSpace(order.CustomerCountryCode))
{
- return PrepareRequest(order, "authorization", token);
+ errorMessage = "Required customer country code";
+ return false;
}
- private Dictionary PrepareSaleRequest(Order order, string token = "")
+ if (!SupportedCountryCodes.Any(code => code.Equals(order.CustomerCountryCode, StringComparison.OrdinalIgnoreCase)))
{
- return PrepareRequest(order, "sale", token);
+ errorMessage = $"Only {supportedCodes} country codes is supported. Order country code: {order.CustomerCountryCode}";
+ return false;
}
- private Dictionary PrepareCreateCardRequest(Order order)
+ errorMessage = string.Empty;
+
+ return true;
+ }
+
+ private OutputResult StateCancel(Order order)
+ {
+ LogEvent(order, "State cancel");
+ string calculatedSignature;
+ if (windowMode is not WindowModes.Embedded && !SecurityHelper.ValidateResponseSignation(AccessKey, SecretKey, out calculatedSignature))
{
- if (transactionType == TransactionTypes.Sale)
- {
- return PrepareRequest(order, "sale,create_payment_token", "");
- }
- else if (transactionType == TransactionTypes.Authorization)
- {
- return PrepareRequest(order, "authorization,create_payment_token", "");
- }
- else
- {
- return PrepareRequest(order, "create_payment_token", "");
- }
+ string signature = Context.Current.Request["signature"];
+ LogError(order, "The signature returned from callback does not match: {0}, calculated: {1}", signature, calculatedSignature);
+ return OnError(order, "Wrong signature");
}
- #region Request building
+ order.TransactionStatus = "Cancelled";
+ Services.Orders.Save(order);
+ CheckoutDone(order);
- private Dictionary PrepareRequest(Order order, string requestTransactionType, string token)
+ var cancelTemplate = new Template(TemplateHelper.GetTemplatePath(CancelTemplate, CancelTemplateFolder));
+
+ return new ContentOutputResult
{
- var customerName = Converter.ToString(order.CustomerName).Trim();
+ Content = Render(order, cancelTemplate)
+ };
+ }
+
+ private OutputResult StateIFrameError(Order order)
+ {
+ string errorMessage = Context.Current.Request["ErrorMessage"];
- var firstName = GetCustomerFirstName(order, customerName);
- var lastName = GetCustomerLastName(order, customerName);
+ return OnError(order, errorMessage);
+ }
- string amount = transactionType == TransactionTypes.ZeroAuthorization ? "0.00" : GetTransactionAmount(order);
- var requestParameters = new Dictionary
- {
- {"profile_id", ProfileId},
- {"access_key", AccessKey},
- {"transaction_uuid", Guid.NewGuid().ToString()},
- {"signed_date_time", DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")},
- {"unsigned_field_names", ""},
- {"locale", GetLanguageCode()},
-
- {"transaction_type", requestTransactionType},
- {"payment_method", "card"},
- {"reference_number", !order.IsRecurringOrderTemplate ? order.Id : order.CustomerAccessUserId.ToString()},
- {"amount", amount},
- {"currency", order.Price.Currency.Code},
- {"override_custom_cancel_page", GetCancelUrl(order)},
- {"override_custom_receipt_page", GetAcceptUrl(order)},
- {"businessRules_declineAVSFlags", Decline_AVS_Flag },
- {"businessRules_ignoreAVSResult", Ignore_AVS_Result.ToString().ToLower() }
- };
+ private OutputResult StateOk(Order order)
+ {
+ LogEvent(order, "State ok");
+
+ if (!order.Complete)
+ return ProcessOrder(order);
+
+ return PassToCart(order);
+ }
- if (!string.IsNullOrWhiteSpace(token))
+ private OutputResult StateCardSaved(Order order)
+ {
+ LogEvent(order, "CyberSource Card Authorized successfully");
+
+ string cardName = WebUtility.UrlDecode(Context.Current.Request["CardTokenName"]);
+ if (string.IsNullOrEmpty(cardName))
+ cardName = order.Id;
+
+ if (Context.Current.Request["reason_code"] == "100")
+ {
+ string requestCardType = Context.Current.Request["req_card_type"];
+ string subscribtionId = Context.Current.Request["payment_token"];
+ string cardType = CardTypes.Keys.Any(key => key.Equals(requestCardType, StringComparison.OrdinalIgnoreCase))
+ ? CardTypes[requestCardType]
+ : $"Unrecognized card type - {requestCardType}";
+
+ string cardNubmer = Context.Current.Request["req_card_number"].ToUpper();
+ order.TransactionCardNumber = cardNubmer;
+
+ order.TransactionCardType = cardType;
+ string transactionId = Context.Current.Request["transaction_id"];
+ User user = UserManagementServices.Users.GetUserById(order.CustomerAccessUserId);
+ if (user is not null)
{
- requestParameters.Add("payment_token", token);
+ PaymentCardToken savedCard = Services.PaymentCard.CreatePaymentCard(user.ID, order.PaymentMethodId, cardName, cardType, cardNubmer, subscribtionId);
+ order.SavedCardId = savedCard.ID;
}
else
+ order.TransactionToken = subscribtionId;
+
+ int decimals = string.IsNullOrEmpty(order.Currency.Rounding?.Id)
+ ? 2
+ : order.Currency.Rounding.Decimals;
+
+ if (!order.IsRecurringOrderTemplate && transactionType is TransactionTypes.ZeroAuthorization)
{
- requestParameters = requestParameters.Union(
- new Dictionary
- {
- {"bill_to_forename", firstName},
- {"bill_to_surname", lastName},
- {"bill_to_email", Converter.ToString(order.CustomerEmail)},
- {"bill_to_phone", Converter.ToString(order.CustomerPhone)},
- {"bill_to_company_name", Converter.ToString(order.CustomerCompany)},
- {"bill_to_address_line1", Converter.ToString(order.CustomerAddress)},
- {"bill_to_address_line2", Converter.ToString(order.CustomerAddress2)},
- {"bill_to_address_city", Converter.ToString(order.CustomerCity)},
- {"bill_to_address_state", Converter.ToString(order.CustomerRegion)},
- {"bill_to_address_postal_code", Converter.ToString(order.CustomerZip)},
- {"bill_to_address_country", Converter.ToString(order.CustomerCountryCode)},
-
- }).ToDictionary(x => x.Key, x => x.Value);
+ order.TransactionAmount = 0.00;
+ order.TransactionStatus = "Succeeded";
+ order.TransactionCardType = cardType;
+ order.TransactionCardNumber = cardNubmer;
+ order.TransactionType = "Zero authorization";
+ string msg = "Zero authorization succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
- bool useBillInfoForDelivery = string.IsNullOrEmpty(order.DeliveryAddress);
- if (useBillInfoForDelivery)
+ else if (transactionType is TransactionTypes.Sale)
{
- requestParameters.Add("ship_to_forename", firstName);
- requestParameters.Add("ship_to_surname", lastName);
- requestParameters.Add("ship_to_email", Converter.ToString(order.CustomerEmail));
- requestParameters.Add("ship_to_phone", Converter.ToString(order.CustomerPhone));
- requestParameters.Add("ship_to_company_name", Converter.ToString(order.CustomerCompany));
- requestParameters.Add("ship_to_address_line1", Converter.ToString(order.CustomerAddress));
- requestParameters.Add("ship_to_address_line2", Converter.ToString(order.CustomerAddress2));
- requestParameters.Add("ship_to_address_city", Converter.ToString(order.CustomerCity));
- requestParameters.Add("ship_to_address_state", Converter.ToString(order.CustomerRegion));
- requestParameters.Add("ship_to_address_postal_code", Converter.ToString(order.CustomerZip));
- requestParameters.Add("ship_to_address_country", Converter.ToString(order.CustomerCountryCode));
+ order.TransactionAmount = Math.Round(order.Price.Price, decimals);
+ order.TransactionStatus = "Succeeded";
+ string msg = "Capture succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
- else
+ else if (transactionType is TransactionTypes.Authorization)
{
- requestParameters.Add("ship_to_forename", string.IsNullOrWhiteSpace(order.DeliveryFirstName) ? firstName : order.DeliveryFirstName);
- requestParameters.Add("ship_to_surname", string.IsNullOrWhiteSpace(order.DeliverySurname) ? lastName : order.DeliverySurname);
- requestParameters.Add("ship_to_email", Converter.ToString(order.DeliveryEmail));
- requestParameters.Add("ship_to_phone", Converter.ToString(order.DeliveryPhone));
- requestParameters.Add("ship_to_company_name", Converter.ToString(order.DeliveryCompany));
- requestParameters.Add("ship_to_address_line1", Converter.ToString(order.DeliveryAddress));
- requestParameters.Add("ship_to_address_line2", Converter.ToString(order.DeliveryAddress2));
- requestParameters.Add("ship_to_address_city", Converter.ToString(order.DeliveryCity));
- requestParameters.Add("ship_to_address_state", Converter.ToString(order.DeliveryRegion));
- requestParameters.Add("ship_to_address_postal_code", Converter.ToString(order.DeliveryZip));
- requestParameters.Add("ship_to_address_country", supportedCountryCodes.Any(x => x == order.DeliveryCountryCode) ? order.DeliveryCountryCode : string.Empty);
+ order.TransactionType = "Zero authorization";
+ order.TransactionAmount = Math.Round(order.Price.Price, decimals);
+ order.TransactionStatus = "Succeeded";
+ string msg = "Authorization succeeded";
+ LogEvent(order, msg);
}
- requestParameters = requestParameters.Where(x => !string.IsNullOrEmpty(x.Value)).ToDictionary(x => x.Key, x => x.Value);
- var signedFieldNames = string.Join(",", requestParameters.Keys) + ",signed_field_names";
- requestParameters.Add("signed_field_names", signedFieldNames);
- requestParameters.Add("signature", Security.Sign(requestParameters, SecretKey));
-
- return requestParameters;
+ Services.Orders.Save(order);
+ LogEvent(order, "Saved Card created");
+ SetOrderComplete(order, transactionId);
+ LogEvent(order, "Create Card successful and order completed");
}
+ else
+ LogError(order, string.Format("Create card failed. Decision: '{0}' messageId: '{1}', messageText: '{2}'", Context.Current.Request["decision"], Context.Current.Request["reason_code"], Context.Current.Request["message"]));
- private static string GetCustomerLastName(Order order, string customerName)
- {
- string lastName = order.CustomerSurname;
- var delimeterPosition = customerName.IndexOf(' ');
- if (string.IsNullOrWhiteSpace(lastName))
- {
- lastName = delimeterPosition > -1 ? customerName.Substring(delimeterPosition + 1) : customerName;
- }
+ CheckoutDone(order);
+ if (!order.Complete)
+ return OnError(order, "Some error happened on creating saved card.", windowMode == WindowModes.Embedded);
- return lastName;
- }
+ if (windowMode is not WindowModes.Embedded)
+ return PassToCart(order);
- private static string GetCustomerFirstName(Order order, string customerName)
+ return new ContentOutputResult
{
- var firstName = order.CustomerFirstName;
- var delimeterPosition = customerName.IndexOf(' ');
- if (string.IsNullOrWhiteSpace(firstName))
- {
- firstName = delimeterPosition > -1 ? customerName.Substring(0, delimeterPosition) : customerName;
- }
- return firstName;
- }
+ Content = $""
+ };
+ }
- #endregion
+ private OutputResult ProcessOrder(Order order)
+ {
+ bool orderWasCompleted = order.Complete;
- ///
- /// This demonstrates what a generic API request helper method would look like.
- ///
- /// Request to send to API endpoint<
- /// Task
- public async Task CallCyberSourceAPI(string request, string resource, Order order)
+ try
{
- var client = new HttpClient();
+ bool errorOccured = false;
+ string calculatedSignature;
- var jwtToken = GenerateJWT(request, "POST", order);
+ if (!SecurityHelper.ValidateResponseSignation(AccessKey, SecretKey, out calculatedSignature))
+ {
+ errorOccured = true;
+ LogError(order, "The signature returned from callback does not match: {0}, calculated: {1}", Context.Current.Request["signature"], calculatedSignature);
+ }
- LogEvent(order, "JWT token created", jwtToken);
+ string transactionId = Context.Current.Request["transaction_id"];
+ if (string.IsNullOrEmpty(transactionId))
+ {
+ errorOccured = true;
+ LogEvent(order, "No transaction number sent to callback");
+ }
- StringContent content = new StringContent(request);
- content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
+ if (Context.Current.Request["reason_code"] != "100")
+ {
+ errorOccured = true;
+ string messageId = Context.Current.Request["auth_response"];
+ string messageText = Context.Current.Request["message"];
+ LogEvent(order, "Transaction {0} not approved. CyberSource response: messageId: '{1}', messageText: '{2}'.", transactionId, messageId, messageText);
- var response = await client.PostAsync(resource, content);
- return response;
- }
+ return OnError(order, $"Transaction {transactionId} not approved. {messageText}", windowMode is WindowModes.Embedded);
+ }
- ///
- /// This method demonstrates the creation of the JWT Authentication credential
- /// Takes Request Paylaod and Http method(GET/POST) as input.
- ///
- /// Value from which to generate JWT
- /// The HTTP Verb that is needed for generating the credential
- /// String containing the JWT Authentication credential
- public string GenerateJWT(string request, string method, Order order)
- {
- string digest;
- string token = "TOKEN_PLACEHOLDER";
+ string amount = Context.Current.Request["req_amount"];
+ int decimals = string.IsNullOrEmpty(order.Currency.Rounding?.Id)
+ ? 2
+ : order.Currency.Rounding.Decimals;
- try
+ if (errorOccured)
{
- // Generate the hash for the payload
- using (SHA256 sha256Hash = SHA256.Create())
- {
- byte[] payloadBytes = sha256Hash.ComputeHash(Encoding.ASCII.GetBytes(request));
- digest = Convert.ToBase64String(payloadBytes);
- }
-
- // Create the JWT payload (aka claimset / JWTBody)
- string jwtBody = "0";
+ LogError(order, "At least one validation error exists - exiting callback routine.");
+ order.TransactionStatus = "Failed";
+ Services.Orders.Save(order);
+ }
+ else
+ {
+ LogEvent(order, "Payment succeeded with transaction number {0}", transactionId);
+ string requestCardType = Context.Current.Request["req_card_type"];
+ string cardType = CardTypes.Keys.Any(key => key.Equals(requestCardType, StringComparison.OrdinalIgnoreCase))
+ ? CardTypes[requestCardType]
+ : $"Unrecognized card type - {requestCardType}";
+
+ order.TransactionAmount = Math.Round(order.Price.Price, decimals);
+ order.TransactionStatus = "Succeeded";
+ order.TransactionCardType = cardType;
+ order.TransactionCardNumber = HideCardNumber(Context.Current.Request["req_card_number"]);
- if (method.Equals("POST"))
+ if (transactionType is TransactionTypes.Sale)
{
- jwtBody = "{\n\"digest\":\"" + digest + "\", \"digestAlgorithm\":\"SHA-256\", \"iat\":\"" + DateTime.Now.ToUniversalTime().ToString("r") + "\"}";
+ string msg = "Capture succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
- else if (method.Equals("GET"))
+ else if (transactionType is TransactionTypes.ZeroAuthorization)
{
- jwtBody = "{\"iat\":\"" + DateTime.Now.ToUniversalTime().ToString("r") + "\"}";
+ order.TransactionAmount = 0.00;
+ order.TransactionType = "Zero authorization";
+ string msg = "Zero authorization succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
+ SetOrderComplete(order, transactionId);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogError(order, ex, $"Unhandled exception: {ex.Message}");
+ }
+
+ if (!orderWasCompleted && order.Complete)
+ {
+ CheckoutDone(order);
+
+ if (windowMode is not WindowModes.Embedded)
+ return PassToCart(order);
+
+ return new ContentOutputResult
+ {
+ Content = $""
+ };
+ }
+ return ContentOutputResult.Empty;
+ }
- // P12 certificate public key is sent in the header and the private key is used to sign the token
- X509Certificate2 x5Cert = new X509Certificate2(GetCertificateFilePath(), MerchantId, X509KeyStorageFlags.MachineKeySet);
+ private OutputResult OnError(Order order, string message, bool isIFrameError = false)
+ {
+ if (windowMode is WindowModes.Embedded && isIFrameError)
+ {
+ return new ContentOutputResult
+ {
+ Content = $""
+ };
+ }
- // Extracting Public Key from .p12 file
- string x5cPublicKey = Convert.ToBase64String(x5Cert.RawData);
+ order.TransactionAmount = 0;
+ order.TransactionStatus = "Failed";
+ order.Errors.Add(message);
+ Services.Orders.Save(order);
- // Extracting Private Key from .p12 file
- var privateKey = x5Cert.GetRSAPrivateKey();
+ Services.Orders.DowngradeToCart(order);
+ order.TransactionStatus = string.Empty;
+ Common.Context.SetCart(order);
- // Extracting serialNumber
- string serialNumber = null;
- string serialNumberPrefix = "SERIALNUMBER=";
+ if (string.IsNullOrWhiteSpace(ErrorTemplate))
+ return PassToCart(order);
- string principal = x5Cert.Subject;
+ var errorTemplate = new Template(TemplateHelper.GetTemplatePath(ErrorTemplate, ErrorTemplateFolder));
+ errorTemplate.SetTag("CheckoutHandler:ErrorMessage", message);
- int beg = principal.IndexOf(serialNumberPrefix);
- if (beg >= 0)
- {
- int x5cBase64List = principal.IndexOf(",", beg);
- if (x5cBase64List == -1)
- {
- x5cBase64List = principal.Length;
- }
+ return new ContentOutputResult
+ {
+ Content = Render(order, errorTemplate)
+ };
+ }
- serialNumber = principal.Substring(serialNumberPrefix.Length, x5cBase64List - serialNumberPrefix.Length);
- }
+ private Dictionary PrepareAuthorizationRequest(Order order, string token = "") => PrepareRequest(order, "authorization", token);
- // Create the JWT Header custom fields
- var x5cList = new List()
- {
- x5cPublicKey
- };
- var cybsHeaders = new Dictionary()
- {
- { "v-c-merchant-id", MerchantId },
- { "x5c", x5cList }
- };
+ private Dictionary PrepareSaleRequest(Order order, string token = "") => PrepareRequest(order, "sale", token);
- // JWT token is Header plus the Body plus the Signature of the Header & Body
- // Here the Jose-JWT helper library (https://github.com/dvsekhvalnov/jose-jwt) is used create the JWT
- token = Jose.JWT.Encode(jwtBody, privateKey, Jose.JwsAlgorithm.RS256, cybsHeaders);
- }
- catch (Exception ex)
- {
- LogError(order, ex, "JWT token create failed");
- }
+ private Dictionary PrepareCreateCardRequest(Order order) => transactionType switch
+ {
+ TransactionTypes.Sale => PrepareRequest(order, "sale,create_payment_token", ""),
+ TransactionTypes.Authorization => PrepareRequest(order, "authorization,create_payment_token", ""),
+ _ => PrepareRequest(order, "create_payment_token", "")
+ };
- return token;
- }
+ #region Request building
- private string GetSavedCardName(Order order)
+ private Dictionary PrepareRequest(Order order, string requestTransactionType, string token)
+ {
+ string customerName = order.CustomerName?.Trim() ?? "";
+ string firstName = Helper.GetCustomerFirstName(order, customerName);
+ string lastName = Helper.GetCustomerLastName(order, customerName);
+ string amount = transactionType is TransactionTypes.ZeroAuthorization
+ ? "0.00"
+ : Helper.GetTransactionAmount(order);
+
+ var requestParameters = new Dictionary
{
- return !string.IsNullOrWhiteSpace(order.SavedCardDraftName) ? order.SavedCardDraftName : (order.DoSaveCardToken || order.IsRecurringOrderTemplate || ForceTokenization ? order.Id : "");
- }
+ ["profile_id"] = ProfileId,
+ ["access_key"] = AccessKey,
+ ["transaction_uuid"] = Guid.NewGuid().ToString(),
+ ["signed_date_time"] = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ ["unsigned_field_names"] = "",
+ ["locale"] = GetLanguageCode(),
+ ["transaction_type"] = requestTransactionType,
+ ["payment_method"] = "card",
+ ["reference_number"] = !order.IsRecurringOrderTemplate ? order.Id : order.CustomerAccessUserId.ToString(),
+ ["amount"] = amount,
+ ["currency"] = order.Price.Currency.Code,
+ ["override_custom_cancel_page"] = GetCancelUrl(order),
+ ["override_custom_receipt_page"] = GetAcceptUrl(order),
+ ["businessRules_declineAVSFlags"] = Decline_AVS_Flag,
+ ["businessRules_ignoreAVSResult"] = Ignore_AVS_Result.ToString().ToLower()
+ };
- private string GetAcceptUrl(Order order)
+ if (!string.IsNullOrWhiteSpace(token))
{
- var cardName = GetSavedCardName(order);
- return string.Format("{0}&cmd={1}{2}", GetBaseUrl(order), (order.IsRecurringOrderTemplate || !string.IsNullOrWhiteSpace(cardName)) ? "CardSaved" : "Accept",
- !string.IsNullOrWhiteSpace(cardName) ? string.Format("&CardTokenName={0}", HttpUtility.UrlEncode(cardName)) : "");
+ requestParameters.Add("payment_token", token);
}
-
- private string GetCancelUrl(Order order)
+ else
{
- return string.Format("{0}&cmd=Cancel", GetBaseUrl(order));
+ requestParameters = requestParameters.Union(new Dictionary
+ {
+ ["bill_to_forename"] = firstName,
+ ["bill_to_surname"] = lastName,
+ ["bill_to_email"] = order.CustomerEmail ?? "",
+ ["bill_to_phone"] = order.CustomerPhone ?? "",
+ ["bill_to_company_name"] = order.CustomerCompany ?? "",
+ ["bill_to_address_line1"] = order.CustomerAddress ?? "",
+ ["bill_to_address_line2"] = order.CustomerAddress2 ?? "",
+ ["bill_to_address_city"] = order.CustomerCity ?? "",
+ ["bill_to_address_state"] = order.CustomerRegion ?? "",
+ ["bill_to_address_postal_code"] = order.CustomerZip ?? "",
+ ["bill_to_address_country"] = order.CustomerCountryCode ?? ""
+ }).ToDictionary(param => param.Key, param => param.Value);
}
- private string GetLanguageCode()
+ bool useBillInfoForDelivery = string.IsNullOrEmpty(order.DeliveryAddress);
+ if (useBillInfoForDelivery)
{
- var currentLanguageCode = Dynamicweb.Environment.ExecutingContext.GetCulture(true).TwoLetterISOLanguageName;
- return currentLanguageCode;
+ requestParameters.Add("ship_to_forename", firstName);
+ requestParameters.Add("ship_to_surname", lastName);
+ requestParameters.Add("ship_to_email", order.CustomerEmail ?? "");
+ requestParameters.Add("ship_to_phone", order.CustomerPhone ?? "");
+ requestParameters.Add("ship_to_company_name", order.CustomerCompany ?? "");
+ requestParameters.Add("ship_to_address_line1", order.CustomerAddress ?? "");
+ requestParameters.Add("ship_to_address_line2", order.CustomerAddress2 ?? "");
+ requestParameters.Add("ship_to_address_city", order.CustomerCity ?? "");
+ requestParameters.Add("ship_to_address_state", order.CustomerRegion ?? "");
+ requestParameters.Add("ship_to_address_postal_code", order.CustomerZip ?? "");
+ requestParameters.Add("ship_to_address_country", order.CustomerCountryCode ?? "");
+ }
+ else
+ {
+ requestParameters.Add("ship_to_forename", string.IsNullOrWhiteSpace(order.DeliveryFirstName) ? firstName : order.DeliveryFirstName);
+ requestParameters.Add("ship_to_surname", string.IsNullOrWhiteSpace(order.DeliverySurname) ? lastName : order.DeliverySurname);
+ requestParameters.Add("ship_to_email", order.DeliveryEmail ?? "");
+ requestParameters.Add("ship_to_phone", order.DeliveryPhone ?? "");
+ requestParameters.Add("ship_to_company_name", order.DeliveryCompany ?? "");
+ requestParameters.Add("ship_to_address_line1", order.DeliveryAddress ?? "");
+ requestParameters.Add("ship_to_address_line2", order.DeliveryAddress2 ?? "");
+ requestParameters.Add("ship_to_address_city", order.DeliveryCity ?? "");
+ requestParameters.Add("ship_to_address_state", order.DeliveryRegion ?? "");
+ requestParameters.Add("ship_to_address_postal_code", order.DeliveryZip ?? "");
+ requestParameters.Add("ship_to_address_country", SupportedCountryCodes.Any(code => code.Equals(order.DeliveryCountryCode, StringComparison.OrdinalIgnoreCase))
+ ? order.DeliveryCountryCode
+ : string.Empty
+ );
}
- #region Gateway URLs
+ requestParameters = requestParameters
+ .Where(param => !string.IsNullOrEmpty(param.Value))
+ .ToDictionary(param => param.Key, param => param.Value);
- private string GetGatewayUrl(bool isIFrameMode)
- {
- if (isIFrameMode)
- {
- if (workMode == WorkModes.Production)
- {
- return "https://secureacceptance.cybersource.com/embedded/pay";
- }
- return "https://testsecureacceptance.cybersource.com/embedded/pay";
- }
+ string signedFieldNames = string.Join(",", requestParameters.Keys) + ",signed_field_names";
+ requestParameters.Add("signed_field_names", signedFieldNames);
+ requestParameters.Add("signature", SecurityHelper.Sign(requestParameters, SecretKey));
- if (workMode == WorkModes.Production)
- {
- return "https://secureacceptance.cybersource.com/pay";
- }
- return "https://testsecureacceptance.cybersource.com/pay";
- }
+ return requestParameters;
+ }
- private string GetCreateCardGatewayUrl(bool isIFrameMode)
- {
- if (isIFrameMode)
- {
- if (workMode == WorkModes.Production)
- {
- return "https://secureacceptance.cybersource.com/embedded/token/create";
- }
- return "https://testsecureacceptance.cybersource.com/embedded/token/create";
- }
+ #endregion
- if (workMode == WorkModes.Production)
- {
- return "https://secureacceptance.cybersource.com/token/create";
- }
- return "https://testsecureacceptance.cybersource.com/token/create";
- }
+ private string GetSavedCardName(Order order)
+ {
+ string fallbackCardName = order.DoSaveCardToken || order.IsRecurringOrderTemplate || ForceTokenization
+ ? order.Id
+ : string.Empty;
+
+ return !string.IsNullOrWhiteSpace(order.SavedCardDraftName)
+ ? order.SavedCardDraftName
+ : fallbackCardName;
+ }
- #endregion
+ private string GetAcceptUrl(Order order)
+ {
+ string cardName = GetSavedCardName(order);
+ string command = order.IsRecurringOrderTemplate || !string.IsNullOrWhiteSpace(cardName)
+ ? "CardSaved"
+ : "Accept";
+ string queryString = !string.IsNullOrWhiteSpace(cardName)
+ ? $"&CardTokenName={WebUtility.UrlEncode(cardName)}"
+ : string.Empty;
+
+ return $"{GetBaseUrl(order)}&cmd={command}{queryString}";
+ }
+
+ private string GetCancelUrl(Order order) => $"{GetBaseUrl(order)}&cmd=Cancel";
+
+ private string GetLanguageCode() => Environment.ExecutingContext.GetCulture(true).TwoLetterISOLanguageName;
- private string GetCertificateFilePath()
+ #region Gateway URLs
+
+ private string GetGatewayUrl(bool isIFrameMode)
+ {
+ if (isIFrameMode)
{
- if (string.IsNullOrWhiteSpace(CertificateFile))
- {
- return string.Empty;
- }
- var path = Context.Current.Server.MapPath(string.Format("/Files/{0}", CertificateFile));
- if (File.Exists(path))
- {
- return path;
- }
- return string.Empty;
+ if (workMode is WorkModes.Production)
+ return "https://secureacceptance.cybersource.com/embedded/pay";
+ return "https://testsecureacceptance.cybersource.com/embedded/pay";
}
- private string GetTransactionAmount(Order order)
+ if (workMode is WorkModes.Production)
+ return "https://secureacceptance.cybersource.com/pay";
+ return "https://testsecureacceptance.cybersource.com/pay";
+ }
+
+ private string GetCreateCardGatewayUrl(bool isIFrameMode)
+ {
+ if (isIFrameMode)
{
- int decimals = order.Currency.Rounding == null || order.Currency.Rounding.Id == "" ? 2 : order.Currency.Rounding.Decimals;
- string amount = Math.Round(order.Price.Price, decimals).ToString("0.00", System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
- return amount;
+ if (workMode is WorkModes.Production)
+ return "https://secureacceptance.cybersource.com/embedded/token/create";
+ return "https://testsecureacceptance.cybersource.com/embedded/token/create";
}
- #region ISavedCard interface
+ if (workMode is WorkModes.Production)
+ return "https://secureacceptance.cybersource.com/token/create";
+ return "https://testsecureacceptance.cybersource.com/token/create";
+ }
+
+ #endregion
+ #region ISavedCard interface
- public void DeleteSavedCard(int savedCardID)
+ public void DeleteSavedCard(int savedCardID)
+ {
+ //not supported due new cybersouyrce api
+ }
+
+ ///
+ /// Directs checkout handler to use saved card
+ ///
+ /// Order that should be processed using saved card information
+ /// Empty string, if operation succeeded, otherwise string template with exception mesage
+ public string UseSavedCard(Order order)
+ {
+ /*PassToCart part doesn't work because of changes in Redirect behavior.
+ * We need to return RedirectOutputResult as OutputResult, and handle output result to make it work.
+ * It means, that we need to change ISavedCard.UseSavedCard method, probably create new one (with OutputResult as returned type)
+ * To make it work (temporarily), we use Response.Redirect here
+ */
+
+ try
{
- //not supported due new cybersouyrce api
- }
+ if (UseSavedCardInternal(order) is RedirectOutputResult redirectResult)
+ RedirectToCart(redirectResult);
- public string UseSavedCard(Orders.Order order)
+ if (!order.Complete)
+ return ProcessError("Some error happened on creating payment using saved card");
+
+ return string.Empty;
+ }
+ catch (ThreadAbortException)
{
- try
- {
- UseSavedCardInternal(order);
- if (!order.Complete)
- {
- LogEvent(order, "Order not complete on using saved card", DebuggingInfoType.UseSavedCard);
- return OnError(order, "Some error happened on creating payment using saved card");
- }
- return string.Empty;
- }
- catch (System.Threading.ThreadAbortException)
- {
- return string.Empty;
- }
- catch (Exception ex)
- {
- LogEvent(order, ex.Message, DebuggingInfoType.UseSavedCard);
- return OnError(order, ex.Message);
- }
+ return string.Empty;
}
-
- public bool SavedCardSupported(Orders.Order order)
+ catch (Exception ex)
{
- return !string.IsNullOrWhiteSpace(GetCertificateFilePath());
+ return ProcessError(ex.Message);
}
- private async void UseSavedCardInternal(Orders.Order order)
+ string ProcessError(string errorMessage)
{
- var savedCard = Services.PaymentCard.GetById(order.SavedCardId);
- if (savedCard == null || order.CustomerAccessUserId != savedCard.UserID)
- {
- throw new Exception("Token is incorrect.");
- }
+ LogEvent(order, $"Order not complete on using saved card. Error: {errorMessage}", DebuggingInfoType.UseSavedCard);
+ OutputResult errorResult = OnError(order, $"Some error happened on creating payment using saved card: {errorMessage}");
+ if (errorResult is ContentOutputResult contentErrorResult)
+ return contentErrorResult.Content;
+ if (errorResult is RedirectOutputResult redirectErrorResult)
+ RedirectToCart(redirectErrorResult);
- var certPath = GetCertificateFilePath();
- if (string.IsNullOrWhiteSpace(certPath))
- {
- LogError(order, "No certificate not found");
- return;
- }
+ return string.Empty;
+ }
+ }
- LogEvent(order, "Using saved card({0}) with id: {1}", savedCard.Identifier, savedCard.ID);
+ public bool SavedCardSupported(Order order)
+ {
+ string certPath = Helper.GetCertificateFilePath(CertificateFile);
- if (order.IsRecurringOrderTemplate)
- {
- SetOrderComplete(order);
- LogEvent(order, "Recurring order template created");
- CheckoutDone(order);
+ return !string.IsNullOrWhiteSpace(certPath);
+ }
- if (!order.Complete)
- {
- LogError(order, "Some error happened on creating saved card.");
- }
- PassToCart(order);
- }
- else
- {
- string request = PreparePaymentRequest(order, savedCard);
+ private OutputResult UseSavedCardInternal(Order order)
+ {
+ PaymentCardToken savedCard = Services.PaymentCard.GetById(order.SavedCardId);
+ if (savedCard is null || order.CustomerAccessUserId != savedCard.UserID)
+ throw new Exception("Token is incorrect.");
- var url = $"https://{(workMode == WorkModes.Production ? "api" : "apitest")}.cybersource.com/pts/v2/payments";
- var response = CallCyberSourceAPI(request, url, order);
- var responseContent = await response.Result.Content.ReadAsStringAsync();
- var responseJson = Converter.Deserialize>(responseContent);
+ string certPath = Helper.GetCertificateFilePath(CertificateFile);
+ if (string.IsNullOrWhiteSpace(certPath))
+ throw new Exception("Certificate for REST API is not found");
- if (response.Result.StatusCode == HttpStatusCode.Created)
- {
- var transactionId = Converter.ToString(responseJson["id"]);
+ LogEvent(order, "Using saved card({0}) with id: {1}", savedCard.Identifier, savedCard.ID);
- LogEvent(order, "Transaction succeeded with transaction number {0}", transactionId);
+ if (order.IsRecurringOrderTemplate)
+ {
+ SetOrderComplete(order);
+ LogEvent(order, "Recurring order template created");
+ CheckoutDone(order);
- int decimals = order.Currency.Rounding == null || order.Currency.Rounding.Id == "" ? 2 : order.Currency.Rounding.Decimals;
- order.TransactionAmount = Math.Round(order.Price.Price, decimals);
- order.TransactionStatus = "Succeeded";
- order.TransactionCardType = savedCard.CardType;
- order.TransactionCardNumber = savedCard.Identifier;
- if (transactionType == TransactionTypes.Sale)
- {
- string msg = "Capture succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
- else if (transactionType == TransactionTypes.ZeroAuthorization)
- {
- order.TransactionAmount = 0.00;
- order.TransactionType = "Zero authorization";
- string msg = "Zero authorization succeeded";
- LogEvent(order, msg);
- order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
- }
+ if (!order.Complete)
+ throw new Exception("Some error happened on creating saved card.");
- SetOrderComplete(order, transactionId);
+ return PassToCart(order);
+ }
- LogEvent(order, "Order completed");
+ var service = new CyberSourceService(GetHost(), MerchantId, CertificateFile, CertificatePassword);
+ PaymentResponse response = service.CreatePayment(order, savedCard);
- CheckoutDone(order);
+ string transactionId = response.Id;
+ LogEvent(order, "Transaction succeeded with transaction number {0}", transactionId);
- LogEvent(order, "Recurring successful");
- }
- else
- {
- var errorMessage = $"Payment using saved card information failed. Response: {responseContent}";
- LogError(order, errorMessage);
- return;
- }
- }
+ int decimals = string.IsNullOrEmpty(order.Currency.Rounding?.Id)
+ ? 2
+ : order.Currency.Rounding.Decimals;
- if (order.RecurringOrderId <= 0)
- {
- PassToCart(order);
- }
+ order.TransactionAmount = Math.Round(order.Price.Price, decimals);
+ order.TransactionStatus = "Succeeded";
+ order.TransactionCardType = savedCard.CardType;
+ order.TransactionCardNumber = savedCard.Identifier;
+ if (transactionType is TransactionTypes.Sale)
+ {
+ string msg = "Capture succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
-
- private string PreparePaymentRequest(Order order, PaymentCardToken savedCard)
+ else if (transactionType is TransactionTypes.ZeroAuthorization)
{
- var customerName = Converter.ToString(order.CustomerName).Trim();
- var firstName = GetCustomerFirstName(order, customerName);
- var lastName = GetCustomerLastName(order, customerName);
- var request = new
- {
- clientReferenceInformation = new
- {
- code = order.Id
- },
- paymentInformation = new
- {
- legacyToken = new
- {
- id = savedCard.Token
- }
- },
- orderInformation = new
- {
- amountDetails = new
- {
- totalAmount = GetTransactionAmount(order),
- currency = order.Price.Currency.Code
- }
- },
- billTo = new
- {
- firstName,
- lastName,
- address1 = Converter.ToString(order.CustomerAddress),
- locality = Converter.ToString(order.CustomerCity),
- administrativeArea = Converter.ToString(order.CustomerRegion),
- postalCode = Converter.ToString(order.CustomerZip),
- country = Converter.ToString(order.CustomerCountryCode),
- email = Converter.ToString(order.CustomerEmail),
- phoneNumber = Converter.ToString(order.CustomerPhone)
- }
- };
-
- var requestJson = Converter.Serialize(request);
- return requestJson;
+ order.TransactionAmount = 0.00;
+ order.TransactionType = "Zero authorization";
+ string msg = "Zero authorization succeeded";
+ LogEvent(order, msg);
+ order.CaptureInfo = new OrderCaptureInfo(OrderCaptureInfo.OrderCaptureState.Success, msg);
}
- #endregion
+ SetOrderComplete(order, transactionId);
+ LogEvent(order, "Order completed");
+ CheckoutDone(order);
+ LogEvent(order, "Recurring successful");
+
+ if (order.RecurringOrderId <= 0)
+ return PassToCart(order);
+
+ return NoActionOutputResult.Default;
+ }
+
+ ///
+ /// A temporary method to maintain previous behavior. Redirects to cart by Response.Redirect. Please remove it when the needed changes will be done.
+ ///
+ private void RedirectToCart(RedirectOutputResult redirectResult) => Context.Current.Response.Redirect(redirectResult.RedirectUrl, redirectResult.IsPermanent);
+
+ #endregion
- #region IRecurring
+ #region IRecurring
- public void Recurring(Order order, Order initialOrder)
+ public void Recurring(Order order, Order initialOrder)
+ {
+ if (order is null)
+ return;
+
+ try
{
- if (order != null)
- {
- try
- {
- UseSavedCardInternal(order);
- LogEvent(order, "Recurring succeeded");
- }
- catch (System.Threading.ThreadAbortException)
- {
- }
- catch (Exception ex)
- {
- LogEvent(order, "Recurring order failed for {0} (based on {1}). The payment failed with the message: {2}",
- DebuggingInfoType.RecurringError, order.Id, initialOrder.Id, ex.Message);
- }
- }
+ UseSavedCardInternal(order);
+ LogEvent(order, "Recurring succeeded");
}
-
- public bool RecurringSupported(Order order)
+ catch (ThreadAbortException)
+ {
+ }
+ catch (Exception ex)
{
- return true;
+ LogEvent(order, "Recurring order failed for {0} (based on {1}). The payment failed with the message: {2}",
+ DebuggingInfoType.RecurringError, order.Id, initialOrder.Id, ex.Message);
}
+ }
- #endregion
+ public bool RecurringSupported(Order order) => true;
- #region ICheckAuthorizationStatus
+ #endregion
- public AuthorizationStatus CheckAuthorizationStatus(Order order)
+ #region ICheckAuthorizationStatus
+
+ public AuthorizationStatus CheckAuthorizationStatus(Order order)
+ {
+ if (string.Equals(order.TransactionStatus, "Succeeded", StringComparison.OrdinalIgnoreCase))
{
- if (order.TransactionStatus == "Succeeded")
- {
- return order.TransactionType == "Zero authorization" && order.CaptureInfo.State == OrderCaptureInfo.OrderCaptureState.Success ?
- AuthorizationStatus.AuthorizedZeroAmount : AuthorizationStatus.AuthorizedFullAmount;
- }
- else
- {
- return AuthorizationStatus.NotAuthorized;
- }
+ return string.Equals(order.TransactionType, "Zero authorization", StringComparison.OrdinalIgnoreCase) && order.CaptureInfo.State is OrderCaptureInfo.OrderCaptureState.Success
+ ? AuthorizationStatus.AuthorizedZeroAmount
+ : AuthorizationStatus.AuthorizedFullAmount;
}
-
- #endregion
-
+ return AuthorizationStatus.NotAuthorized;
}
+
+ #endregion
}
diff --git a/src/Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.csproj b/src/Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.csproj
index cfc20d0..d91a778 100644
--- a/src/Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.csproj
+++ b/src/Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.csproj
@@ -1,6 +1,6 @@
- 10.0.3
+ 10.11.2
1.0.0.0
CyberSource
"Payment system, http://www.cybersource.com"
@@ -14,17 +14,34 @@
Copyright © 2023 Dynamicweb Software A/S
- net7.0
+ net8.0
true
true
true
true
true
snupkg
+ cybersource-logo.png
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
diff --git a/src/Helpers/Helper.cs b/src/Helpers/Helper.cs
new file mode 100644
index 0000000..ffdde93
--- /dev/null
+++ b/src/Helpers/Helper.cs
@@ -0,0 +1,52 @@
+using Dynamicweb.Core.Helpers;
+using Dynamicweb.Ecommerce.Orders;
+using System;
+using System.IO;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Helpers;
+
+internal static class Helper
+{
+ public static string GetCertificateFilePath(string certificateFile)
+ {
+ if (string.IsNullOrWhiteSpace(certificateFile))
+ return string.Empty;
+
+ string path = FilePathHelper.GetAbsolutePath(certificateFile);
+ if (File.Exists(path))
+ return path;
+
+ return string.Empty;
+ }
+
+ public static string GetCustomerLastName(Order order, string customerName)
+ {
+ string lastName = order.CustomerSurname;
+ int delimeterPosition = customerName.IndexOf(' ');
+ if (string.IsNullOrWhiteSpace(lastName))
+ lastName = delimeterPosition > -1 ? customerName.Substring(delimeterPosition + 1) : customerName;
+
+ return lastName;
+ }
+
+ public static string GetCustomerFirstName(Order order, string customerName)
+ {
+ string firstName = order.CustomerFirstName;
+ int delimeterPosition = customerName.IndexOf(' ');
+ if (string.IsNullOrWhiteSpace(firstName))
+ firstName = delimeterPosition > -1 ? customerName.Substring(0, delimeterPosition) : customerName;
+
+ return firstName;
+ }
+
+ public static string GetTransactionAmount(Order order)
+ {
+ int decimals = order.Currency.Rounding is null || string.IsNullOrEmpty(order.Currency.Rounding.Id)
+ ? 2
+ : order.Currency.Rounding.Decimals;
+
+ string amount = Math.Round(order.Price.Price, decimals).ToString("0.00", System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
+
+ return amount;
+ }
+}
diff --git a/src/Helpers/SecurityHelper.cs b/src/Helpers/SecurityHelper.cs
new file mode 100644
index 0000000..97737fd
--- /dev/null
+++ b/src/Helpers/SecurityHelper.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Security.Cryptography;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Helpers;
+
+internal static class SecurityHelper
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// ToDo: Params property hasn't been defined in new Dynamicweb.Context class.
+ public static bool ValidateResponseSignation(string publicKey, string secretKey, out string signature)
+ {
+ var parameters = Context.Current.Request.Params;
+ return ValidateResponseSignation(parameters, publicKey, secretKey, out signature);
+ }
+
+ ///
+ /// Validates transaction signature
+ ///
+ /// Transaction parameters
+ /// public key
+ /// secret key
+ /// signature
+ /// Boolean result of validating transaction signature
+ public static bool ValidateResponseSignation(NameValueCollection parameters, string publicKey, string secretKey, out string signature)
+ {
+ var transactionSignature = parameters["signature"];
+ var signedFieldNames = parameters["signed_field_names"].Split(',');
+
+ var dataToSign = new List();
+ foreach (string signedFieldName in signedFieldNames)
+ {
+ dataToSign.Add(signedFieldName + "=" + parameters[signedFieldName]);
+ }
+ signature = Sign(string.Join(",", dataToSign), secretKey).Replace("\n", string.Empty);
+
+ return transactionSignature.Equals(signature);
+ }
+
+ ///
+ /// Signs parameters with secret key
+ ///
+ /// set of key value pairs
+ /// key that is used for encription
+ /// Encrypted string
+ public static string Sign(Dictionary parameters, string secretKey)
+ {
+ return Sign(BuildSignation(parameters), secretKey);
+ }
+
+ private static string Sign(string data, string secretKey)
+ {
+ var encoding = new System.Text.UTF8Encoding();
+ var keyBytes = encoding.GetBytes(secretKey);
+
+ using (var hmacsha256 = new HMACSHA256(keyBytes))
+ {
+ var messageBytes = encoding.GetBytes(data);
+ return Convert.ToBase64String(hmacsha256.ComputeHash(messageBytes));
+ }
+ }
+
+ private static string BuildSignation(IDictionary parameters)
+ {
+ var signedFieldNames = parameters["signed_field_names"].Split(',');
+ var dataToSign = new List();
+
+ foreach (string signedFieldName in signedFieldNames)
+ {
+ dataToSign.Add(signedFieldName + "=" + parameters[signedFieldName]);
+ }
+
+ return string.Join(",", dataToSign);
+ }
+}
diff --git a/src/Models/Request/CaptureRequestData.cs b/src/Models/Request/CaptureRequestData.cs
new file mode 100644
index 0000000..ab201ab
--- /dev/null
+++ b/src/Models/Request/CaptureRequestData.cs
@@ -0,0 +1,18 @@
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request;
+
+///
+/// Request to capture the payment
+/// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_capture_capture-a-payment
+///
+[DataContract]
+internal sealed class CaptureRequestData
+{
+ [DataMember(Name = "clientReferenceInformation")]
+ public ClientReferenceInformation ClientReferenceInformation { get; set; }
+
+ [DataMember(Name = "orderInformation")]
+ public OrderInformation OrderInformation { get; set; }
+}
\ No newline at end of file
diff --git a/src/Models/Request/Common/AmountDetails.cs b/src/Models/Request/Common/AmountDetails.cs
new file mode 100644
index 0000000..ff630cb
--- /dev/null
+++ b/src/Models/Request/Common/AmountDetails.cs
@@ -0,0 +1,20 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class AmountDetails
+{
+ ///
+ /// Grand total for the order. This value cannot be negative. You can include a decimal point (.), but no other special characters.
+ /// CyberSource truncates the amount to the correct number of decimal places.
+ ///
+ [DataMember(Name = "totalAmount")]
+ public string TotalAmount { get; set; }
+
+ ///
+ /// Currency used for the order. Use the three-character ISO Standard Currency Codes.
+ ///
+ [DataMember(Name = "currency")]
+ public string Currency { get; set; }
+}
diff --git a/src/Models/Request/Common/BillTo.cs b/src/Models/Request/Common/BillTo.cs
new file mode 100644
index 0000000..4955bdd
--- /dev/null
+++ b/src/Models/Request/Common/BillTo.cs
@@ -0,0 +1,68 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class BillTo
+{
+ ///
+ /// Customer’s first name. This name must be the same as the name on the card.
+ ///
+ [DataMember(Name = "firstName")]
+ public string FirstName { get; set; }
+
+ ///
+ /// Customer’s last name. This name must be the same as the name on the card.
+ ///
+ [DataMember(Name = "lastName")]
+ public string LastName { get; set; }
+
+ ///
+ /// Payment card billing street address as it appears on the credit card issuer’s records.
+ ///
+ [DataMember(Name = "address1")]
+ public string Address1 { get; set; }
+
+ ///
+ /// Payment card billing city.
+ ///
+ [DataMember(Name = "locality")]
+ public string Locality { get; set; }
+
+ ///
+ /// State or province of the billing address. Use the State, Province, and Territory Codes for the United States and Canada.
+ ///
+ [DataMember(Name = "administrativeArea")]
+ public string AdministrativeArea { get; set; }
+
+ ///
+ /// Postal code for the billing address. The postal code must consist of 5 to 9 digits.
+ /// When the billing country is the U.S., the 9-digit postal code must follow this format:
+ /// [5 digits][dash][4 digits]
+ /// Example 12345-6789
+ /// When the billing country is Canada, the 6-digit postal code must follow this format:
+ /// [alpha][numeric][alpha][space][numeric][alpha][numeric]
+ /// Example A1B 2C3
+ ///
+ [DataMember(Name = "postalCode")]
+ public string PostalCode { get; set; }
+
+ ///
+ /// Payment card billing country. Use the two-character ISO Standard Country Codes.
+ ///
+ [DataMember(Name = "country")]
+ public string Country { get; set; }
+
+ ///
+ /// Customer's email address, including the full domain name.
+ ///
+ [DataMember(Name = "email")]
+ public string Email { get; set; }
+
+ ///
+ /// Customer’s phone number.
+ /// It is recommended that you include the country code when the order is from outside the U.S.
+ ///
+ [DataMember(Name = "phoneNumber")]
+ public string PhoneNumber { get; set; }
+}
\ No newline at end of file
diff --git a/src/Models/Request/Common/ClientReferenceInformation.cs b/src/Models/Request/Common/ClientReferenceInformation.cs
new file mode 100644
index 0000000..efaab1e
--- /dev/null
+++ b/src/Models/Request/Common/ClientReferenceInformation.cs
@@ -0,0 +1,13 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class ClientReferenceInformation
+{
+ ///
+ /// Merchant-generated order reference or tracking number. It is recommended that you send a unique value for each transaction so that you can perform meaningful searches for the transaction.
+ ///
+ [DataMember(Name = "code")]
+ public string Code { get; set; }
+}
diff --git a/src/Models/Request/Common/LegacyToken.cs b/src/Models/Request/Common/LegacyToken.cs
new file mode 100644
index 0000000..fc628c0
--- /dev/null
+++ b/src/Models/Request/Common/LegacyToken.cs
@@ -0,0 +1,14 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class LegacyToken
+{
+ ///
+ /// Unique identifier for the legacy Secure Storage token used in the transaction.
+ /// When you include this value in your request, many of the fields that are normally required for an authorization or credit become optional.
+ ///
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+}
diff --git a/src/Models/Request/Common/OrderInformation.cs b/src/Models/Request/Common/OrderInformation.cs
new file mode 100644
index 0000000..2d0fd40
--- /dev/null
+++ b/src/Models/Request/Common/OrderInformation.cs
@@ -0,0 +1,13 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class OrderInformation
+{
+ [DataMember(Name = "amountDetails")]
+ public AmountDetails AmountDetails { get; set; }
+
+ [DataMember(Name = "billTo", EmitDefaultValue = false)]
+ public BillTo BillTo { get; set; }
+}
diff --git a/src/Models/Request/Common/PaymentInformation.cs b/src/Models/Request/Common/PaymentInformation.cs
new file mode 100644
index 0000000..41760d2
--- /dev/null
+++ b/src/Models/Request/Common/PaymentInformation.cs
@@ -0,0 +1,10 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+
+[DataContract]
+internal sealed class PaymentInformation
+{
+ [DataMember(Name = "legacyToken")]
+ public LegacyToken LegacyToken { get; set; }
+}
\ No newline at end of file
diff --git a/src/Models/Request/Error/CybersourceError.cs b/src/Models/Request/Error/CybersourceError.cs
new file mode 100644
index 0000000..0269d98
--- /dev/null
+++ b/src/Models/Request/Error/CybersourceError.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Error;
+
+internal sealed class CybersourceError
+{
+ [DataMember(Name = "submitTimeUtc")]
+ public string SubmitTimeUtc { get; set; }
+
+ [DataMember(Name = "status")]
+ public string Status { get; set; }
+
+ [DataMember(Name = "reason")]
+ public string Reason { get; set; }
+
+ [DataMember(Name = "message")]
+ public string Message { get; set; }
+
+ [DataMember(Name = "details")]
+ public IEnumerable Details { get; set; }
+}
diff --git a/src/Models/Request/Error/ErrorDetail.cs b/src/Models/Request/Error/ErrorDetail.cs
new file mode 100644
index 0000000..c3a3470
--- /dev/null
+++ b/src/Models/Request/Error/ErrorDetail.cs
@@ -0,0 +1,13 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Error;
+
+[DataContract]
+internal sealed class ErrorDetail
+{
+ [DataMember(Name = "field")]
+ public string Field { get; set; }
+
+ [DataMember(Name = "reason")]
+ public string Reason { get; set; }
+}
diff --git a/src/Models/Request/PaymentRequestData.cs b/src/Models/Request/PaymentRequestData.cs
new file mode 100644
index 0000000..7cefe39
--- /dev/null
+++ b/src/Models/Request/PaymentRequestData.cs
@@ -0,0 +1,21 @@
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Common;
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request;
+
+///
+/// A payment authorizes the amount for the transaction. There are a number of supported payment features, such as E-commerce and Card Present - Credit Card/Debit Card, Echeck, e-Wallets, Level II/III Data, etc.
+/// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_payments_process-a-payment
+///
+[DataContract]
+internal sealed class PaymentRequestData
+{
+ [DataMember(Name = "clientReferenceInformation")]
+ public ClientReferenceInformation ClientReferenceInformation { get; set; }
+
+ [DataMember(Name = "paymentInformation")]
+ public PaymentInformation PaymentInformation { get; set; }
+
+ [DataMember(Name = "orderInformation")]
+ public OrderInformation OrderInformation { get; set; }
+}
\ No newline at end of file
diff --git a/src/Models/Response/CaptureResponse.cs b/src/Models/Response/CaptureResponse.cs
new file mode 100644
index 0000000..a13f64e
--- /dev/null
+++ b/src/Models/Response/CaptureResponse.cs
@@ -0,0 +1,23 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+
+///
+/// Response for payment operation
+/// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_capture_capture-a-payment_responsefielddescription_201
+///
+[DataContract]
+internal sealed class CaptureResponse
+{
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+
+ [DataMember(Name = "reconciliationId")]
+ public string ReconciliationId { get; set; }
+
+ [DataMember(Name = "status")]
+ public string Status { get; set; }
+
+ [DataMember(Name = "processorInformation")]
+ public ProcessorInformation ProcessorInformation { get; set; }
+}
diff --git a/src/Models/Response/PaymentResponse.cs b/src/Models/Response/PaymentResponse.cs
new file mode 100644
index 0000000..349c64e
--- /dev/null
+++ b/src/Models/Response/PaymentResponse.cs
@@ -0,0 +1,23 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+
+///
+/// Response for payment operation
+/// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_payments_create-a-payment-order-request_responsefielddescription_201_clientReferenceInformation
+///
+[DataContract]
+internal sealed class PaymentResponse
+{
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+
+ [DataMember(Name = "reconciliationId")]
+ public string ReconciliationId { get; set; }
+
+ [DataMember(Name = "status")]
+ public string Status { get; set; }
+
+ [DataMember(Name = "riskInformation")]
+ public RiskInformation RiskInformation { get; set; }
+}
diff --git a/src/Models/Response/ProcessorInformation.cs b/src/Models/Response/ProcessorInformation.cs
new file mode 100644
index 0000000..5de8c51
--- /dev/null
+++ b/src/Models/Response/ProcessorInformation.cs
@@ -0,0 +1,13 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+
+[DataContract]
+internal sealed class ProcessorInformation
+{
+ [DataMember(Name = "networkTransactionId")]
+ public string NetworkTransactionId { get; set; }
+
+ [DataMember(Name = "responseDetails")]
+ public string ResponseDetails { get; set; }
+}
diff --git a/src/Models/Response/RiskInformation.cs b/src/Models/Response/RiskInformation.cs
new file mode 100644
index 0000000..07db707
--- /dev/null
+++ b/src/Models/Response/RiskInformation.cs
@@ -0,0 +1,16 @@
+using System.Runtime.Serialization;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+
+[DataContract]
+internal sealed class RiskInformation
+{
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+
+ [DataMember(Name = "fraudDecision")]
+ public string FraudDecision { get; set; }
+
+ [DataMember(Name = "fraudDecisionReason")]
+ public string FraudDecisionReason { get; set; }
+}
diff --git a/src/Security.cs b/src/Security.cs
deleted file mode 100644
index e46e837..0000000
--- a/src/Security.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using Dynamicweb.Ecommerce.Orders;
-using Dynamicweb.Logging;
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Security.Cryptography;
-
-namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource
-{
- internal class Security
- {
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// ToDo: Params property hasn't been defined in new Dynamicweb.Context class.
- public static bool ValidateResponseSignation(string publicKey, string secretKey, out string signature)
- {
- var parameters = Context.Current.Request.Params;
- return ValidateResponseSignation(parameters, publicKey, secretKey, out signature);
- }
-
- ///
- /// Validates transaction signature
- ///
- /// Transaction parameters
- /// public key
- /// secret key
- /// signature
- /// Boolean result of validating transaction signature
- public static bool ValidateResponseSignation(NameValueCollection parameters, string publicKey, string secretKey, out string signature)
- {
- var transactionSignature = parameters["signature"];
- var signedFieldNames = parameters["signed_field_names"].Split(',');
-
- var dataToSign = new List();
- foreach (string signedFieldName in signedFieldNames)
- {
- dataToSign.Add(signedFieldName + "=" + parameters[signedFieldName]);
- }
- signature = Sign(string.Join(",", dataToSign), secretKey).Replace("\n", string.Empty);
-
- return transactionSignature.Equals(signature);
- }
-
- ///
- /// Signs parameters with secret key
- ///
- /// set of key value pairs
- /// key that is used for encription
- /// Encrypted string
- public static string Sign(Dictionary parameters, string secretKey)
- {
- return Sign(BuildSignation(parameters), secretKey);
- }
-
- private static string Sign(string data, string secretKey)
- {
- var encoding = new System.Text.UTF8Encoding();
- var keyBytes = encoding.GetBytes(secretKey);
-
- using (var hmacsha256 = new HMACSHA256(keyBytes))
- {
- var messageBytes = encoding.GetBytes(data);
- return Convert.ToBase64String(hmacsha256.ComputeHash(messageBytes));
- }
- }
-
- private static string BuildSignation(IDictionary parameters)
- {
- var signedFieldNames = parameters["signed_field_names"].Split(',');
- var dataToSign = new List();
-
- foreach (string signedFieldName in signedFieldNames)
- {
- dataToSign.Add(signedFieldName + "=" + parameters[signedFieldName]);
- }
-
- return string.Join(",", dataToSign);
- }
- }
-}
diff --git a/src/Service/ApiCommand.cs b/src/Service/ApiCommand.cs
new file mode 100644
index 0000000..67dbdd9
--- /dev/null
+++ b/src/Service/ApiCommand.cs
@@ -0,0 +1,21 @@
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Service;
+
+///
+/// REST API Commands
+///
+internal enum ApiCommand
+{
+ ///
+ /// Create a payment operation to authorize the amount for the transaction.
+ /// POST /pts/v2/payments
+ /// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_payments_process-a-payment
+ ///
+ CreatePayment,
+
+ ///
+ /// Captures payment. Include the payment ID in the POST request to capture the payment amount.
+ /// POST /pts/v2/payments/{operatorId}/captures
+ /// See: https://developer.cybersource.com/api-reference-assets/index.html#payments_capture_capture-a-payment
+ ///
+ CapturePayment,
+}
diff --git a/src/Service/CommandConfiguration.cs b/src/Service/CommandConfiguration.cs
new file mode 100644
index 0000000..6a4969e
--- /dev/null
+++ b/src/Service/CommandConfiguration.cs
@@ -0,0 +1,19 @@
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Service;
+
+internal sealed class CommandConfiguration
+{
+ ///
+ /// Cyber source command. See operation urls in and
+ ///
+ public ApiCommand CommandType { get; set; }
+
+ ///
+ /// Command operator id, like https://.../pts/v2/payments/{OperatorId}/captures
+ ///
+ public string OperatorId { get; set; }
+
+ ///
+ /// Data to serialize
+ ///
+ public object Data { get; set; }
+}
diff --git a/src/Service/CyberSourceRequest.cs b/src/Service/CyberSourceRequest.cs
new file mode 100644
index 0000000..0b4cf29
--- /dev/null
+++ b/src/Service/CyberSourceRequest.cs
@@ -0,0 +1,220 @@
+using Dynamicweb.Core;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Helpers;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request.Error;
+using Dynamicweb.Ecommerce.Orders;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Service;
+
+///
+/// Sends request to CyberSource and gets response.
+///
+internal sealed class CyberSourceRequest
+{
+ public string MerchantId { get; set; }
+
+ public string CertificateFile { get; set; }
+
+ public string CertificatePassword { get; set; }
+
+ public CyberSourceRequest(string merchantId, string certificateFile, string certificatePassword)
+ {
+ MerchantId = merchantId;
+ CertificateFile = certificateFile;
+ CertificatePassword = certificatePassword;
+ }
+
+ public string SendRequest(Order order, string host, CommandConfiguration configuration)
+ {
+ using (HttpMessageHandler messageHandler = GetMessageHandler())
+ {
+ using var client = new HttpClient(messageHandler);
+
+ client.Timeout = new TimeSpan(0, 0, 0, 90);
+ client.DefaultRequestHeaders.Add("Host", host);
+
+ UriBuilder baseAddress = GetBaseAddress(host);
+ client.BaseAddress = new Uri(baseAddress.ToString());
+
+ HttpMethod method = configuration.CommandType switch
+ {
+ ApiCommand.CreatePayment or
+ ApiCommand.CapturePayment => HttpMethod.Post,
+ _ => throw new NotSupportedException($"Unknown operation was used. The operation code: {configuration.CommandType}.")
+ };
+
+ string data = Converter.Serialize(configuration.Data);
+ string jwtToken = GenerateJWT(order, method, data);
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
+
+ string apiCommand = GetCommandLink(baseAddress, configuration.CommandType, configuration.OperatorId);
+ Task requestTask = method switch
+ {
+ _ when method == HttpMethod.Post => client.PostAsync(apiCommand, GetContent(data)),
+ _ => throw new NotSupportedException($"Unknown http method was used: {method.ToString()}.")
+ };
+
+ try
+ {
+ using (HttpResponseMessage response = requestTask.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(responseText);
+ if (string.IsNullOrEmpty(errorResponse.Status))
+ throw new Exception($"Unhandled exception. Operation failed: '{response.ReasonPhrase}'. Response text: '{responseText}'");
+
+ string errorMessage = $"Operation failed. Status: '{errorResponse.Status}'. Reason: '{errorResponse.Reason}'. Message: '{errorResponse.Message}'.";
+ if (response.StatusCode is HttpStatusCode.BadRequest)
+ {
+ if (errorResponse.Details?.Any() is true)
+ {
+ var detailsMessage = new StringBuilder();
+ foreach (ErrorDetail detail in errorResponse.Details)
+ detailsMessage.AppendLine($"{detail.Field}: {detail.Reason}");
+
+ errorMessage += $" Details: '{detailsMessage.ToString()}'";
+ }
+ }
+ throw new Exception(errorMessage);
+ }
+
+ return responseText;
+ }
+ }
+ catch (HttpRequestException requestException)
+ {
+ throw new Exception($"An error occurred during CyberSource request. Error code: {requestException.StatusCode}");
+ }
+ }
+
+ HttpMessageHandler GetMessageHandler() => new HttpClientHandler
+ {
+ AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
+ };
+
+ HttpContent GetContent(string content) => new StringContent(content, Encoding.UTF8, "application/json");
+ }
+
+ private UriBuilder GetBaseAddress(string host) => new UriBuilder(Uri.UriSchemeHttps, host);
+
+ private string GetCommandLink(UriBuilder baseAddress, ApiCommand command, string operatorId)
+ {
+ return command switch
+ {
+ ApiCommand.CreatePayment => GetCommandLink("payments"),
+ ApiCommand.CapturePayment => GetCommandLink($"payments/{operatorId}/captures"),
+ _ => throw new NotSupportedException($"The api command is not supported. Command: {command}")
+ };
+
+ string GetCommandLink(string gateway)
+ {
+ baseAddress.Path = $"pts/v2/{gateway}";
+ return baseAddress.ToString();
+ }
+ }
+
+ private void Log(Order order, string message)
+ {
+ if (order is null)
+ return;
+
+ Services.OrderDebuggingInfos.Save(order, message, typeof(CyberSource).FullName, DebuggingInfoType.Undefined);
+ }
+
+ ///
+ /// This method demonstrates the creation of the JWT Authentication credential
+ /// Takes Request Payload and Http method(GET/POST) as input.
+ /// This code is an example from: https://github.com/CyberSource/cybersource-rest-samples-csharp/blob/master/Source/Samples/Authentication/StandAloneJWT.cs
+ ///
+ /// Value from which to generate JWT
+ /// The HTTP Verb that is needed for generating the credential
+ /// String containing the JWT Authentication credential
+ private string GenerateJWT(Order order, HttpMethod method, string data)
+ {
+ string digest;
+ string token = "TOKEN_PLACEHOLDER";
+
+ try
+ {
+ // Generate the hash for the payload
+ using (SHA256 sha256Hash = SHA256.Create())
+ {
+ byte[] payloadBytes = sha256Hash.ComputeHash(Encoding.ASCII.GetBytes(data));
+ digest = Convert.ToBase64String(payloadBytes);
+ }
+
+ // Create the JWT payload (aka claimset / JWTBody)
+ string jwtBody = "0";
+
+ if (method == HttpMethod.Post)
+ jwtBody = "{\n\"digest\":\"" + digest + "\", \"digestAlgorithm\":\"SHA-256\", \"iat\":\"" + DateTime.Now.ToUniversalTime().ToString("r") + "\"}";
+ else if (method == HttpMethod.Get)
+ jwtBody = "{\"iat\":\"" + DateTime.Now.ToUniversalTime().ToString("r") + "\"}";
+
+ string certificatePath = Helper.GetCertificateFilePath(CertificateFile);
+ if (string.IsNullOrEmpty(certificatePath))
+ throw new Exception("Certificate for REST API is not found");
+
+ // P12 certificate public key is sent in the header and the private key is used to sign the token
+ X509Certificate2 x5Cert = new X509Certificate2(certificatePath, CertificatePassword, X509KeyStorageFlags.MachineKeySet);
+
+ // Extracting Public Key from .p12 file
+ string x5cPublicKey = Convert.ToBase64String(x5Cert.RawData);
+
+ // Extracting Private Key from .p12 file
+ var privateKey = x5Cert.GetRSAPrivateKey();
+
+ // Extracting serialNumber
+ string serialNumber = null;
+ string serialNumberPrefix = "SERIALNUMBER=";
+
+ string principal = x5Cert.Subject;
+
+ int beg = principal.IndexOf(serialNumberPrefix);
+ if (beg >= 0)
+ {
+ int x5cBase64List = principal.IndexOf(",", beg);
+ if (x5cBase64List == -1)
+ x5cBase64List = principal.Length;
+
+ serialNumber = principal.Substring(serialNumberPrefix.Length, x5cBase64List - serialNumberPrefix.Length);
+ }
+
+ // Create the JWT Header custom fields
+ var x5cList = new List()
+ {
+ x5cPublicKey
+ };
+
+ var cybsHeaders = new Dictionary()
+ {
+ { "v-c-merchant-id", MerchantId },
+ { "x5c", x5cList }
+ };
+
+ // JWT token is Header plus the Body plus the Signature of the Header & Body
+ // Here the Jose-JWT helper library (https://github.com/dvsekhvalnov/jose-jwt) is used create the JWT
+ token = Jose.JWT.Encode(jwtBody, privateKey, Jose.JwsAlgorithm.RS256, cybsHeaders);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("JWT token create failed", ex);
+ }
+
+ return token;
+ }
+}
diff --git a/src/Service/CyberSourceService.cs b/src/Service/CyberSourceService.cs
new file mode 100644
index 0000000..31ab46f
--- /dev/null
+++ b/src/Service/CyberSourceService.cs
@@ -0,0 +1,106 @@
+using Dynamicweb.Core;
+using Dynamicweb.Ecommerce.Cart;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Helpers;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Request;
+using Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Models.Response;
+using Dynamicweb.Ecommerce.Orders;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Service;
+
+internal sealed class CyberSourceService
+{
+ public CyberSourceRequest Request { get; }
+
+ public string BaseAddress { get; }
+
+ public CyberSourceService(string baseAddress, string merchantId, string certificateFile, string certificatePassword)
+ {
+ Request = new(merchantId, certificateFile, certificatePassword);
+ BaseAddress = baseAddress;
+ }
+
+ public PaymentResponse CreatePayment(Order order, PaymentCardToken savedCard)
+ {
+ PaymentRequestData requestData = PreparePaymentRequest(order, savedCard);
+ var configuration = new CommandConfiguration
+ {
+ CommandType = ApiCommand.CreatePayment,
+ Data = requestData
+ };
+
+ string response = Request.SendRequest(order, BaseAddress, configuration);
+ return Converter.Deserialize(response);
+ }
+
+ public CaptureResponse Capture(Order order, string transactionNumber)
+ {
+ var captureRequestData = new CaptureRequestData
+ {
+ ClientReferenceInformation = new()
+ {
+ Code = order.Id
+ },
+ OrderInformation = new()
+ {
+ AmountDetails = new()
+ {
+ Currency = order.Price.Currency.Code,
+ TotalAmount = Helper.GetTransactionAmount(order)
+ }
+ }
+ };
+
+ var configuration = new CommandConfiguration
+ {
+ CommandType = ApiCommand.CapturePayment,
+ OperatorId = transactionNumber,
+ Data = captureRequestData
+ };
+
+ string response = Request.SendRequest(order, BaseAddress, configuration);
+ return Converter.Deserialize(response);
+ }
+
+ private PaymentRequestData PreparePaymentRequest(Order order, PaymentCardToken savedCard)
+ {
+ string customerName = order.CustomerName?.Trim() ?? string.Empty;
+ string firstName = Helper.GetCustomerFirstName(order, customerName);
+ string lastName = Helper.GetCustomerLastName(order, customerName);
+
+ return new()
+ {
+ ClientReferenceInformation = new()
+ {
+ Code = order.Id
+ },
+ PaymentInformation = new()
+ {
+ LegacyToken = new()
+ {
+ Id = savedCard.Token
+ }
+ },
+ OrderInformation = new()
+ {
+ AmountDetails = new()
+ {
+ TotalAmount = Helper.GetTransactionAmount(order),
+ Currency = order.Price.Currency.Code
+ },
+ BillTo = new()
+ {
+ FirstName = firstName,
+ LastName = lastName,
+ Address1 = Converter.ToString(order.CustomerAddress),
+ Locality = Converter.ToString(order.CustomerCity),
+ AdministrativeArea = Converter.ToString(order.CustomerRegion),
+ PostalCode = Converter.ToString(order.CustomerZip),
+ Country = Converter.ToString(order.CustomerCountryCode),
+ Email = Converter.ToString(order.CustomerEmail),
+ PhoneNumber = Converter.ToString(order.CustomerPhone)
+ }
+ }
+ };
+ }
+
+}
diff --git a/src/TransactionTypes.cs b/src/TransactionTypes.cs
new file mode 100644
index 0000000..2be335d
--- /dev/null
+++ b/src/TransactionTypes.cs
@@ -0,0 +1,8 @@
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource;
+
+internal enum TransactionTypes
+{
+ ZeroAuthorization,
+ Authorization,
+ Sale
+}
diff --git a/src/Updates/CyberSourceUpdateProvider.cs b/src/Updates/CyberSourceUpdateProvider.cs
new file mode 100644
index 0000000..e15fc96
--- /dev/null
+++ b/src/Updates/CyberSourceUpdateProvider.cs
@@ -0,0 +1,32 @@
+using Dynamicweb.Updates;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Updates;
+
+public sealed class CyberSourceUpdateProvider : UpdateProvider
+{
+ private static Stream GetResourceStream(string name)
+ {
+ string resourceName = $"Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource.Updates.{name}";
+
+ return Assembly.GetAssembly(typeof(CyberSourceUpdateProvider)).GetManifestResourceStream(resourceName);
+ }
+
+ public override IEnumerable GetUpdates()
+ {
+ return new List()
+ {
+ new FileUpdate("7c7b5833-f67f-4eb7-88bb-a7460e538f4f", this, "/Files/Templates/eCom7/CheckoutHandler/CyberSource/Cancel/checkouthandler_cancel.html", () => GetResourceStream("checkouthandler_cancel.html")),
+ new FileUpdate("1d7c25a8-cf3d-4f9a-a85e-c82e3219cba5", this, "/Files/Templates/eCom7/CheckoutHandler/CyberSource/Error/checkouthandler_error.html", () => GetResourceStream("checkouthandler_error.html")),
+ new FileUpdate("541895f6-0e7f-4ad4-9bd7-333aa86dfa07", this, "/Files/Templates/eCom7/CheckoutHandler/CyberSource/Payment/Payment.html", () => GetResourceStream("Payment.html"))
+ };
+ }
+
+ /*
+ * IMPORTANT!
+ * Use a generated GUID string as id for an update
+ * - Execute command in C# interactive window: Guid.NewGuid().ToString()
+ */
+}
\ No newline at end of file
diff --git a/src/Updates/Payment.html b/src/Updates/Payment.html
new file mode 100644
index 0000000..d8f68fd
--- /dev/null
+++ b/src/Updates/Payment.html
@@ -0,0 +1,24 @@
+
+
+
+ :
+ :
+ :
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Updates/checkouthandler_cancel.html b/src/Updates/checkouthandler_cancel.html
new file mode 100644
index 0000000..cc82980
--- /dev/null
+++ b/src/Updates/checkouthandler_cancel.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ .
+
+
\ No newline at end of file
diff --git a/src/Updates/checkouthandler_error.html b/src/Updates/checkouthandler_error.html
new file mode 100644
index 0000000..2cf3061
--- /dev/null
+++ b/src/Updates/checkouthandler_error.html
@@ -0,0 +1,17 @@
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+ .
+
+
+
\ No newline at end of file
diff --git a/src/WindowModes.cs b/src/WindowModes.cs
new file mode 100644
index 0000000..9372a1f
--- /dev/null
+++ b/src/WindowModes.cs
@@ -0,0 +1,7 @@
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource;
+
+internal enum WindowModes
+{
+ Redirect,
+ Embedded
+}
\ No newline at end of file
diff --git a/src/WorkModes.cs b/src/WorkModes.cs
new file mode 100644
index 0000000..945cd31
--- /dev/null
+++ b/src/WorkModes.cs
@@ -0,0 +1,7 @@
+namespace Dynamicweb.Ecommerce.CheckoutHandlers.CyberSource;
+
+internal enum WorkModes
+{
+ Test,
+ Production
+}
\ No newline at end of file
diff --git a/src/cybersource-logo.png b/src/cybersource-logo.png
new file mode 100644
index 0000000..598d72b
Binary files /dev/null and b/src/cybersource-logo.png differ