Hi,
You can take a look at PaymentMethodViewModelResolver and CheckoutController. At the time of checkout you will create a new payment method instance (based on the selection of customer) and the pass it to CheckoutController. CheckoutController itself calls PaymenService.Process to let the payment method does it works.
/Q
That's the thing that PaymentMethodViewModelResolver returns proper Payment Method and CheckoutController calls PaymentService's method ProcessPayment with that Payment Method. But ProcessPayment method never calls Payment Gateway as seen in the code in my initial question. ProcessPayment just adds Payment instance to the order.
Finally, I got Authorize.NET to work.
There are several reasons why it didn't work:
1. A payment gateway is called by running Cart Checkout Workflow, but it has one condition - Payment should be with status Pending.
Unfortunately if you are looking into Quicksilver's implementation it looks confusing. First, in CheckoutController's action - Purchase it calls PaymentService's method ProcessPayment:
https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L416
try { _paymentService.ProcessPayment(checkoutViewModel.Payment.PaymentMethod); } catch (PreProcessException) { ModelState.AddModelError("PaymentMethod", _localizationService.GetString("/Checkout/Payment/Errors/PreProcessingFailure")); }
And internally this method adds a new OrderForm to the Cart, calls IPaymentOption's PreProcess method which creates Payment, adds returned Payment to OrderForm and calls IPaymentOption's PostProcess method. But it never calls any action which will process actual payment: https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Payment/Services/PaymentService.cs#L23
public void ProcessPayment(IPaymentOption method) { var cart = _cartHelper(Mediachase.Commerce.Orders.Cart.DefaultName).Cart; if (!cart.OrderForms.Any()) { cart.OrderForms.AddNew(); } var payment = method.PreProcess(cart.OrderForms[0]); if (payment == null) { throw new PreProcessException(); } cart.OrderForms[0].Payments.Add(payment); cart.AcceptChanges(); method.PostProcess(cart.OrderForms[0]); }
But in the CheckoutController's method Finish it calls Cart Checkout Workflow:
https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L518
_cartService.RunWorkflow(OrderGroupWorkflowManager.CartCheckOutWorkflowName);
But it still doesn't work and Process Payment Activity of the workflow doesn't process payment. After debugging decompiled code, I found that Payment has already Processed status so that it doesn't process this Payment. And then I found that default implementation of IPaymentOption for Credit Card handling - GenericCreditCardPaymentMethod sets status to Processed in PostProcess method: https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Payment/PaymentMethods/GenericCreditCardPaymentMethod.cs#L201
public bool PostProcess(OrderForm orderForm) { var card = orderForm.Payments.ToArray().FirstOrDefault(x => x.PaymentType == PaymentType.CreditCard); if (card == null) return false; card.Status = PaymentStatus.Processed.ToString(); card.AcceptChanges(); return true; }
By deleting the code and keeping only "return true", I got further but stopped at next issue.
2. In the CheckoutController's Finish method there is a check if total order amount is same as Processed Payment amount. As I removed the code which sets Processed status, it now throws exception:
https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L513
PurchaseOrder purchaseOrder = null; string emailAddress = null; OrderForm orderForm = _cartService.GetOrderForms().First(); decimal totalProcessedAmount = orderForm.Payments.Where(x => x.Status.Equals(PaymentStatus.Processed.ToString())).Sum(x => x.Amount); if (totalProcessedAmount != orderForm.Total) { throw new InvalidOperationException("Wrong amount"); }
So I removed it also but got another exception.
3. Now Cart Checkout Workflow started to process my Payment, but it threw an exception that Billing address is not set. Looking at the CheckoutController's Purchase method I found that it is actually setting Billing address: https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L407
SaveBillingAddress(checkoutViewModel);
Again after debugging decompiled code I found that AuthorizePaymentGateway expects Payment class instance to have BillingAddressId set. So after setting BillingAddressId in the GenericCreditCardPaymentMethod's PreProcess method, it started to work and I could successfully buy a product using Authorize.NET. Here are final PreProcess and PostProcess methods:
public Payment PreProcess(OrderForm orderForm) { if (orderForm == null) throw new ArgumentNullException(nameof(orderForm)); if (!ValidateData()) return null; var payment = new CreditCardPayment { CardType = CardType, PaymentMethodId = PaymentMethodId, PaymentMethodName = "Authorize", OrderFormId = orderForm.OrderFormId, OrderGroupId = orderForm.OrderGroupId, Amount = orderForm.Total, CreditCardNumber = CreditCardNumber, CreditCardSecurityCode = CreditCardSecurityCode, ExpirationMonth = ExpirationMonth, ExpirationYear = ExpirationYear, Status = PaymentStatus.Pending.ToString(), CustomerName = CreditCardName, TransactionType = TransactionType.Authorization.ToString(), BillingAddressId = orderForm.BillingAddressId }; return payment; } public bool PostProcess(OrderForm orderForm) { return true; }
4. Also to be able to handle different Credit Cards, GenericCreditCardPaymentMethod's ValidateCreditCardSecurityCode and ValidateCreditCardNumber methods has to be changed. For ValidateCreditCardSecurityCode allow 3-4 digits long security code:
private string ValidateCreditCardSecurityCode() { if (string.IsNullOrEmpty(CreditCardSecurityCode)) { return _localizationService.GetString("/Checkout/Payment/Methods/CreditCard/Empty/CreditCardSecurityCode"); } if (!Regex.IsMatch(CreditCardSecurityCode, "^[0-9]{3,4}$")) { return _localizationService.GetString("/Checkout/Payment/Methods/CreditCard/ValidationErrors/CreditCardSecurityCode"); } return null; }
And for ValidateCreditCardNumber remove check for the last digit to be '4'. Optionally you will want to add more accurate credit card validation, but payment provider should handle it anyway and will do it properly. So I just removed false check and now it looks like this:
private string ValidateCreditCardNumber() { if (string.IsNullOrEmpty(CreditCardNumber)) { return _localizationService.GetString("/Checkout/Payment/Methods/CreditCard/Empty/CreditCardNumber"); } return null; }
It seems that Quicksilver's payment handling is not finished and it confuses a lot when you start working with it. PaymentService's ProcessPayment method's naming is confusing - developer expects it to handle payment, but it just adds Payment DTO to OrderForm and that causes IPaymentOption's PostProcess method to execute wrong actions - as it would execute after payment.
Thanks for your feedback and taking the time for such a detailed response. We will look into your suggestions on how to improve the quicksilver payment provider experience. We are also looking into improving the payment provider system as they were not designed with MVC in mind when they were introduced many years ago.
The structure in QuickSilver seems a bit wrong as you states, as it runs both the PreProcess and PostProcess regardless of the orderflow. We are looking at implementing Klarna Invoice and the DIBS paymentproviders from EPiServers own example codes, althogh not made for MVC. For DIBS the handling was a bit simpler since the PostProcess just returned true and we could just run the PaymentService and after the CheckoutViewModel was initialized in the Purchase method of the CheckoutController we could do a redirect to a DIBs form page which then handled the communication with DIBS, proceesed the payment and the redirected back to the Finish method of the Checkout page.
For Klarna it's bit more difficult though and we are still working on a good way to keep the code for the payment handling separated from the checkout code in a good way.
What we are testing is to replace the IPaymentOption interface with one that also has a Process method (bool Process(Payment payment, Cart cart);). Then we have updated the PaymentService so it hase three separate methods for PreProcess, Process and PostProcess. That way we get more control over when and where different parts of the payment is executed and all of these metod code can be put in their own PaymentMethodClasses and be kept out of the checkoutflow. This will affect the DIBSpayment aswell and we can keep it cleaner. But as I said we're trying this out now and it might introduce some other issues aswell.
I have to add the Authorize.NET payment gateway into my solution which is based on Quicksilver. I configured payment gateway properly in Commerce Manager.
In the checkout controller, PaymentMethodId of my Authorize.NET payment option is set on the GenericCreditCardPaymentMethod instance:
https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L142
And then in the Purchase action it calls PaymentService's method - ProcessPayment with selected payment method:
https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Checkout/Controllers/CheckoutController.cs#L416
But when running the code, nothing happens - Authorize.NET is not called.
Then I looked into the PaymentService's implementation:
And I do not see the Payment Gateway to be instantiated and called.
As I understood from the method signatures, Payment Method class (for example, GenericCreditCardPaymentMethod) is responsible for preparing Payment information in the PreProcess method and update payment status in the PostProcess method, but all the processing reside in the PaymentService. But it seems that it's not.
Should I implement myself the Payment Gateway call? From where should it be called?
Should I create my own Payment Method class and call Payment Gateway in PreProcess method?