Skip to content

Added the ability to do repeat payments #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Merchello.Plugin.Payments.SagePay/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public static class ExtendedDataKeys
// Stores the 3DSecure url
public static string ThreeDSecureUrl = "ThreeDSecureUrl";

// Stores the vendor transaction reference code
public static string SagepayVendorTxCode = "SagepayVendorTxCode";

/// Stores the authorisation code
public static string SagepayTxAuthNo = "SagepayTxAuthNo";


}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public HttpResponseMessage SuccessPayment(Guid invoiceKey, Guid paymentKey, stri


[HttpGet]
public HttpResponseMessage AbortPayment(Guid invoiceKey, Guid paymentKey, string crypt)
public HttpResponseMessage AbortPayment(Guid invoiceKey, Guid paymentKey, string crypt = "")
{
var invoiceService = _merchelloContext.Services.InvoiceService;
var paymentService = _merchelloContext.Services.PaymentService;
Expand Down Expand Up @@ -193,18 +193,23 @@ public HttpResponseMessage PaypalCallback(Guid invoiceKey, Guid paymentKey)
{
IPayPalNotificationRequest payPalNotificationRequest = new SagePayDirectIntegration(_directProcessor.Settings).GetPayPalNotificationRequest();

// Query merchello for associated invoice and payment objects
var payment = _merchelloContext.Services.PaymentService.GetByKey(paymentKey);
var invoice = _merchelloContext.Services.InvoiceService.GetByKey(invoiceKey);

var cancelUrl = payment.ExtendedData.GetValue(Constants.ExtendedDataKeys.CancelUrl);

if (payPalNotificationRequest.Status != ResponseStatus.OK)
{
//var ex = new Exception(string.Format("Invalid payment status. Detail: {0}", paymentResult.StatusDetail));
//LogHelper.Error<SagePayApiController>("Sagepay error processing payment.", ex);
return ShowError(payPalNotificationRequest.StatusDetail);
LogHelper.Error<SagePayApiController>("Sagepay error processing payment.", new System.Exception(payPalNotificationRequest.StatusDetail));
var cancelResponse = Request.CreateResponse(HttpStatusCode.Moved);
cancelResponse.Headers.Location = new Uri(cancelUrl.Replace("%INVOICE%", invoice.Key.ToString().EncryptWithMachineKey()));
return cancelResponse;
}

// Query merchello for associated invoice and payment objects
var invoice = _merchelloContext.Services.InvoiceService.GetByKey(invoiceKey);
var payment = _merchelloContext.Services.PaymentService.GetByKey(paymentKey);

if (invoice == null || payment == null || invoice.CustomerKey == null)
if (invoice == null || payment == null)
{
var ex = new NullReferenceException(string.Format("Invalid argument exception. Arguments: invoiceKey={0}, paymentKey={1}", invoiceKey, paymentKey));
LogHelper.Error<SagePayApiController>("Payment not authorized.", ex);
Expand Down Expand Up @@ -232,7 +237,9 @@ public HttpResponseMessage PaypalCallback(Guid invoiceKey, Guid paymentKey)
sagePayResponseValues = HttpUtility.ParseQueryString(responseString);
if (sagePayResponseValues["Status"] != "OK")
{
return ShowError(sagePayResponseValues["StatusDetail"]);
// This is almost certainly caused by the user cancelling the paypal payment. Abort the payment
LogHelper.Error<SagePayApiController>("Payment not authorized.", new NullReferenceException(string.Format("Payment Invalid. Arguments: invoiceKey={0}, paymentKey={1}, exception={2}", invoiceKey, paymentKey, sagePayResponseValues["StatusDetail"])));
return AbortPayment(invoiceKey, paymentKey);
}


Expand Down Expand Up @@ -271,6 +278,8 @@ public HttpResponseMessage PaypalCallback(Guid invoiceKey, Guid paymentKey)
return ShowError(captureResult.Payment.Exception.Message);
}

Notification.Trigger("OrderConfirmation", new Merchello.Core.Gateways.Payment.PaymentResult(Attempt<Merchello.Core.Models.IPayment>.Succeed(payment), invoice, true), new[] { invoice.BillToEmail });

// Redirect to ReturnUrl (with token replacement for an alternative means of order retrieval)
var returnUrl = payment.ExtendedData.GetValue(Constants.ExtendedDataKeys.ReturnUrl);
var response = Request.CreateResponse(HttpStatusCode.Moved);
Expand Down
38 changes: 38 additions & 0 deletions src/Merchello.Plugin.Payments.SagePay/Models/RepeatDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Merchello.Plugin.Payments.SagePay.Models
{
public class RepeatDetails
{
/// <summary>
/// The security key from the transaction to repeat
/// </summary>
public string RelatedSecurityKey { get; set; }

/// <summary>
/// The id from the original transaction
/// </summary>
public string RelatedVpsTxId { get; set; }

/// <summary>
/// The original vendor transaction code
/// </summary>
public string RelatedVendorTxCode { get; set; }

/// <summary>
/// The original auth code
/// </summary>
public string RelatedTxAuthNo { get; set; }

/// <summary>
/// The customer's CV2 code (optional)
/// </summary>
public string CV2 { get; set; }


}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Merchello.Core.Gateways.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Merchello.Plugin.Payments.SagePay.Models
{
public static class RepeatDetailsExtensions
{
public static ProcessorArgumentCollection AsProcessorArgumentCollection(this RepeatDetails repeatDetails)
{
return new ProcessorArgumentCollection()
{
{ "relatedSecurityKey", repeatDetails.RelatedSecurityKey },
{ "relatedVpsTxId", repeatDetails.RelatedVpsTxId },
{ "relatedVendorTxCode", repeatDetails.RelatedVendorTxCode },
{ "relatedTxAuthNo", repeatDetails.RelatedTxAuthNo },
{ "cv2", repeatDetails.CV2 ?? "" }
};
}

public static RepeatDetails AsRepeatDetails(this ProcessorArgumentCollection args)
{
return new RepeatDetails()
{
RelatedSecurityKey = args.ArgValue("relatedSecurityKey"),
RelatedVpsTxId = args.ArgValue("relatedVpsTxId"),
RelatedVendorTxCode = args.ArgValue("relatedVendorTxCode"),
RelatedTxAuthNo = args.ArgValue("relatedTxAuthNo"),
CV2 = args.ArgValue("cv2")
};
}


private static string ArgValue(this ProcessorArgumentCollection args, string key)
{
return args.ContainsKey(key) ? args[key] : string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,39 @@ public SagePayDirectPaymentProcessor(SagePayProcessorSettings settings)
public IPaymentResult InitializePayment(IInvoice invoice, IPayment payment, ProcessorArgumentCollection args)
{
try
{
var sagePayDirectIntegration = new SagePayAPIIntegration(Settings);
{
var sagePayDirectIntegration = new SagePayDirectIntegration(Settings);

// See if this is a repeat or a new card
if (args.ContainsKey("relatedSecurityKey"))
{
IRepeatRequest repeatRequest = sagePayDirectIntegration.RepeatRequest();

SetSagePayRepeatData(repeatRequest, invoice, payment, args.AsRepeatDetails());
ICaptureResult repeatResult = sagePayDirectIntegration.DoRepeat(repeatRequest, false);

if (repeatResult.Status == ResponseStatus.OK)
{
payment.Collected = true;
payment.Authorized = true;
GatewayProviderService service = new GatewayProviderService();
service.ApplyPaymentToInvoice(payment.Key, invoice.Key, Core.AppliedPaymentType.Debit, "SagePay: capture authorized", invoice.Total);

payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagePaySecurityKey, repeatResult.SecurityKey);
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagePayTransactionCode, repeatResult.VpsTxId);
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagepayTxAuthNo, repeatResult.TxAuthNo.ToString());
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagepayVendorTxCode, repeatRequest.VendorTxCode);

return new PaymentResult(Attempt<IPayment>.Succeed(payment), invoice, true);
}
else
{
return new PaymentResult(Attempt<IPayment>.Fail(payment, new Exception(repeatResult.StatusDetail)), invoice, true);
}

}


var request = sagePayDirectIntegration.DirectPaymentRequest();

var creditCard = args.AsCreditCard();
Expand Down Expand Up @@ -65,6 +96,15 @@ public IPaymentResult InitializePayment(IInvoice invoice, IPayment payment, Proc
{
values.Add("TxType", "PAYMENT");
}
else if (property.Name == "Amount")
{
// If amount has no decimal place, the property.getvalue method adds lots of zeros
var amount = property.GetValue(request).ToString();
var amountDec = decimal.Parse(amount);

values.Add(property.Name, amountDec.ToString("n2"));

}
else
{
values.Add(property.Name, property.GetValue(request).ToString());
Expand Down Expand Up @@ -108,11 +148,16 @@ public IPaymentResult InitializePayment(IInvoice invoice, IPayment payment, Proc
IDirectPaymentResult result = sagePayDirectIntegration.ProcessDirectPaymentRequest(request, string.Format("https://{0}.sagepay.com/gateway/service/vspdirect-register.vsp", Settings.Environment));



if (result.Status == ResponseStatus.OK)
{
payment.Collected = true;
payment.Authorized = true;
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagePaySecurityKey, result.SecurityKey);
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagePayTransactionCode, result.VpsTxId);
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagepayTxAuthNo, result.TxAuthNo.ToString());
payment.ExtendedData.SetValue(Constants.ExtendedDataKeys.SagepayVendorTxCode, request.VendorTxCode);

GatewayProviderService service = new GatewayProviderService();
service.ApplyPaymentToInvoice(payment.Key, invoice.Key, Core.AppliedPaymentType.Debit, "SagePay: capture authorized", invoice.Total);
return new PaymentResult(Attempt<IPayment>.Succeed(payment), invoice, true);
Expand Down Expand Up @@ -260,5 +305,34 @@ private void SetSagePayApiData(IDirectPayment request, IInvoice invoice, IPaymen

}


private void SetSagePayRepeatData(IRepeatRequest request, IInvoice invoice, IPayment payment, RepeatDetails repeat)
{
// Get Merchello data
//TODO - what if there is no shipping info? e.g. Classes only - Get from billing?
var shipmentLineItem = invoice.ShippingLineItems().FirstOrDefault();
var shipment = shipmentLineItem.ExtendedData.GetShipment<InvoiceLineItem>();
var shippingAddress = shipment.GetDestinationAddress();
var billingAddress = invoice.GetBillingAddress();

// SagePay details
request.VpsProtocol = Settings.ProtocolVersion;
request.TransactionType = Settings.TransactionType;
request.Vendor = Settings.VendorName;
request.VendorTxCode = SagePayAPIIntegration.GetNewVendorTxCode();
request.Amount = payment.Amount;
request.Currency = invoice.CurrencyCode();
request.Description = "Goods from " + Settings.VendorName;
request.RelatedSecurityKey = repeat.RelatedSecurityKey;
request.RelatedVpsTxId = repeat.RelatedVpsTxId;
request.RelatedVendorTxCode = repeat.RelatedVendorTxCode;
request.RelatedTxAuthNo = Convert.ToInt32(repeat.RelatedTxAuthNo);

// Delivery details are optional for repeat transactions, it will use the original details if they are not supplied
// It would be expected to use the delivery details from merchello anyway so we won't supply them here


}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public SagePayDirectIntegration(SagePayProcessorSettings settings) : base(settin
_settings = settings;
}

public IDirectPayment DirectPaymentRequest()
public IRepeatRequest RepeatRequest()
{
IDirectPayment request = new DataObject();
IRepeatRequest request = new DataObject();
return request;
}

Expand All @@ -44,6 +44,24 @@ public IThreeDAuthRequest ThreeDAuthRequest()
return request;
}



public ICaptureResult DoRepeat(IRepeatRequest request, bool deferred)
{
if (deferred)
request.TransactionType = TransactionType.REPEATDEFERRED;
else
request.TransactionType = TransactionType.REPEAT;

if (request.Cv2 == null)
{
request.Cv2 = "";
}

RequestQueryString = BuildQueryString(request, ProtocolMessage.REPEAT_REQUEST, _settings.ProtocolVersion);
ResponseQueryString = ProcessWebRequestToSagePay(string.Format("https://{0}.sagepay.com/gateway/service/repeat.vsp", _settings.Environment), RequestQueryString);
ICaptureResult result = ConvertToCaptureResult(ResponseQueryString);
return result;
}

}
}